Past exam of the computer science course of the University of Oxford 2015 Compilers 3 a Solution 2026-10-31
Use the following explicit tree notation for the intermediate representation.
C(k) is a constant, R(r) a processor register, M(e) a word load from memory address e, ADD addition, and SHL(e,2) a left shift by two bits. MOVE(destination,source) assigns either a processor register or the computer memory word designated by M. Parentheses show complete tree structure, so these displays specify trees rather than already shared Directed acyclic graphs.Let be the constant base memory address of the global array
a, and the constant offset of local i from the frame pointer fp. Each integer occupies four bytes, and array indices are zero-based; a fixed nonzero lower bound can instead be absorbed into . Local t is represented by R(t). After replacing multiplication by four with a left shift, the three trees are:MOVE(R(t),
M(ADD(C(A),
SHL(M(ADD(C(A),
SHL(M(ADD(R(fp),C(I))),2))),2))))
MOVE(M(ADD(C(A),
SHL(M(ADD(C(A),
SHL(M(ADD(R(fp),C(I))),2))),2))),
M(ADD(C(A),SHL(M(ADD(R(fp),C(I))),2))))
MOVE(M(ADD(C(A),SHL(M(ADD(R(fp),C(I))),2))),
R(t))Every
M(ADD(R(fp),C(I))) loads i; the next enclosing M loads a[i]; the outermost M in the first tree loads a[a[i]]. Each array index is converted to a byte offset by a left shift of two bits. The address rule used in all three trees is The source language's array bounds checks are omitted as requested. Past exam of the computer science course of the University of Oxford 2015 Compilers 3 b Solution 2026-10-31
The useful repeated program expressions, nested inside one another, are the memory address of
a[i], its value, and the memory address of a[a[i]]. Materialize each maximal reusable tree at the level at which it is shared, using fresh temporary variables u, v, and w:MOVE(R(u), ADD(C(A),SHL(M(ADD(R(fp),C(I))),2)))
MOVE(R(v), M(R(u)))
MOVE(R(w), ADD(C(A),SHL(R(v),2)))
MOVE(R(t), M(R(w)))
MOVE(M(R(w)), R(v))
MOVE(M(R(u)), R(t))Thus
u is a saved memory address, v the old integer value of a[i], w the second saved memory address, and t the old value at that second location. The two large address trees occur both in loads and as store destinations. After extracting them, the repeated load of a[i] inside the second address and as the middle assignment's source becomes v. Extracting every smaller nested tree again would add no benefit.The relevant alias analysis facts are that local
i does not occupy any location written through global a, that frame pointer fp and base stay fixed, and that t,u,v,w are distinct processor registers or otherwise nonoverlapping temporary variables. There are no intervening calls, volatile memory accesses, or concurrent changes. Under these assumptions, loading i once remains valid even across the array stores. Before the middle store, no array location has yet changed, so both its target memory address and its source value can be computed and saved earlier.No assumption that the two array locations are distinct is required. The middle store may change
a[i] when its target coincides with it, but v is not loaded again afterwards: only the saved address u is needed for the final store. It would be unsound to retain an arbitrary array load across an aliasing store and then treat it as the new contents; that is not what these trees do.For a language with a true global array, a distinct local variable, and no arbitrary means of fabricating pointers to unrelated storage, these facts follow simply from declarations and the stack frame layout. Accesses must denote valid array elements. In an unrestricted low-level language with overlapping storage, arbitrary pointer arithmetic or unchecked out-of-range accesses, that conclusion requires extra guarantees. Omitting emitted array bounds checks does not itself prove that all memory addresses are valid. If denotes a word load, the saved quantities are
Let be the final output length, the number of emitted markers, and the number of pending handles. Each pending subtree contributes at least one future marker, soStore output from the low end and the stack from the high end of the same array. A pop frees one cell before the next write. For a fork, the two pushed children each account for at least one of the remaining markers. Thus the two regions never overlap.The loop invariant includes
PROCEDURE PreorderShared(t: Tree; VAR out: ARRAY OF INTEGER;
VAR count: INTEGER);
VAR top, cap: INTEGER; p: Tree;
BEGIN
cap := LEN(out); count := 0;
top := cap-1; out[top] := t;
WHILE top < cap DO
p := out[top]; INC(top);
IF p = 0 THEN
out[count] := 0; INC(count)
ELSE
out[count] := 1; INC(count);
DEC(top); out[top] := nodes[p-1].right;
DEC(top); out[top] := nodes[p-1].left
END
END
END PreorderShared;count <= top, with pending handles in out[top..cap) and final markers in out[0..count). The preceding accounting proves every intermediate write also respects array bounds. This uses the integer handle representation chosen in part (b). Simply putting a pointer into a genuinely Boolean-typed cell would not be valid Oberon.Implement staircase matrix search from the top-right corner. In the following Oberon-style code, The loop invariant is that every occurrence not yet returned lies inInitially this is the whole array. If , every entry of row in the candidate rectangle is smaller than , so increasing discards no possible answer. If , every entry of column in that rectangle is greater than , so decreasing is safe. These conclusions follow from monotonicity even when repeated values occur.
& denotes short-circuit conjunction; all guarded accesses respect array bounds.PROCEDURE Staircase(x: INTEGER; VAR i, j: INTEGER): BOOLEAN;
VAR r, c: INTEGER;
BEGIN
r := 0; c := N-1;
WHILE (r < M) & (c >= 0) DO
IF f[r,c] = x THEN i := r; j := c; RETURN TRUE
ELSIF f[r,c] < x THEN INC(r)
ELSE DEC(c)
END
END;
RETURN FALSE
END Staircase;The loop variant decreases on every unsuccessful iteration. If a dimension becomes empty, the loop invariant proves absence. For positive dimensions the path inspects at most entries; an initially empty dimension causes no inspection. Thus
Use a merge algorithm with one unread index in each run. Here Let denote the original two runs. The first loop invariant says and is the sorted merge of and ; moreover each emitted value is at most every unread value. Choosing the smaller head preserves that condition. Equal heads are taken from the first run, giving stable sorting within this merge. The loop variant is .
u is the module's array; the destination is disjoint from the source and lies within the array bounds:PROCEDURE Merge(a, b, c, d: INTEGER);
VAR i, j, k: INTEGER;
BEGIN
i := a; j := b; k := d;
WHILE (i < b) & (j < c) DO
IF u[i] <= u[j] THEN
u[k] := u[i]; i := i+1
ELSE
u[k] := u[j]; j := j+1
END;
k := k+1
END;
WHILE i < b DO
u[k] := u[i]; i := i+1; k := k+1
END;
WHILE j < c DO
u[k] := u[j]; j := j+1; k := k+1
END
END Merge;For each copying loop, the output remains the sorted merge of the consumed prefixes, at least one source run is exhausted, and only the indicated suffix remains. The respective loop variants are and . At exit both source runs are exhausted:The destination's multiset is the sum of the two source multisets.
Replace the existence-dependent bounds proof with explicit bounds tests. Neither input array is changed or copied.The loop invariant is , , and every common value of the original arrays lies in both remaining suffixes and . Equivalently neither discarded prefix contains a common value. Advancing the smaller current value preserves this for the same ordering reason as in part (a).
PROCEDURE commonOrNone(a, N, b, M);
VAR i, j: INTEGER;
BEGIN
i := 0; j := 0;
WHILE (i < N) AND (j < M) DO
IF a[i] = b[j] THEN RETURN Some(a[i])
ELSIF a[i] < b[j] THEN i := i + 1
ELSE j := j + 1
END
END;
RETURN None
END commonOrNone;Both array bounds are checked before any element is read. If the remaining heads are equal, all later entries are larger, so the returned value is the smallest. If either suffix becomes empty, the loop invariant proves there is no common value. The loop variant decreases on each unsuccessful comparison. The result is
Some(smallest common value) or None, with time and auxiliary space, including empty-input cases. A language without short-circuit operators can use nested tests; the guard here itself contains no array reads.The cyclic strict comparisons in the hint identify a head that cannot be the answer:Let be the smallest common value. The loop invariant is , , , and every discarded entry is not common to all three arrays. If , the entry cannot occur in the unprocessed part of , so discard it. The branch has the analogous justification. If neither comparison holds but the heads are not all equal, the hint guarantees , so discard .
PROCEDURE common3(a, N, b, M, c, L): INTEGER;
VAR i, j, k: INTEGER;
BEGIN
i := 0; j := 0; k := 0;
WHILE (a[i] # b[j]) OR (b[j] # c[k]) DO
IF a[i] < b[j] THEN i := i + 1
ELSIF b[j] < c[k] THEN j := j + 1
ELSE k := k + 1
END
END;
RETURN a[i]
END common3;A pointer at its target position cannot be advanced: for example and would imply . Thus all reads stay within array bounds. The loop variant decreases. At exit all heads equal . The cost is time and auxiliary space.
Assume every indicated segment lies within array bounds, and lengths are nonnegative. Use a temporary value of element type The first procedure even works for . For the second, disjointness ensures that each swap exchanges the two original entries at offset
T:PROCEDURE swap(i, j: INTEGER);
VAR x: T;
BEGIN
x := m[i]; m[i] := m[j]; m[j] := x
END swap;
PROCEDURE blockswap(i, j, n: INTEGER);
VAR t: INTEGER;
BEGIN
t := 0;
WHILE t < n DO
swap(i+t, j+t);
t := t + 1
END
END blockswap;t without disturbing any other pair. Its loop invariant says that offsets below t have been exchanged, offsets from t through retain their original values, and everything outside the two segments is unchanged. The loop variant proves algorithm termination. A block exchange uses swaps and auxiliary space. The stray wp printed after this source subpart is not an additional requirement.