Files
swift-mirror/docs/StackNestingProof.txt
John McCall 8d231d20c6 Rewrite StackNesting to be a non-iterative single-pass algorithm.
The previous algorithm was doing an iterative forward data flow analysis
followed by a reverse data flow analysis. I suspect the history here is that
it was a reverse analysis, and that didn't really work for infinite loops,
and so complexity accumulated.

The new algorithm is quite straightforward and relies on the allocations
being properly jointly post-dominated, just not nested. We simply walk
forward through the blocks in consistent-with-dominance order, maintaining
the stack of active allocations and deferring deallocations that are
improperly nested until we deallocate the allocations above it. The only
real subtlety is that we have to delay walking into dead-end regions until
we've seen all of the edges into them, so that we can know whether we have
a coherent stack state in them. If the state is incoherent, we need to
remove any deallocations of previous allocations because we cannot talk
correctly about what's on top of the stack.

The reason I'm doing this, besides it just being a simpler and hopefully
faster algorithm, is that modeling some of the uses of the async stack
allocator properly requires builtins that cannot just be semantically
reordered. That should be somewhat easier to handle with the new approach,
although really (1) we should not have runtime functions that need this and
(2) we're going to need a conservatively-correct solution that's different
from this anyway because hoisting allocations is *also* limited in its own
way.

I've attached a rather pedantic proof of the correctness of the algorithm.

The thing that concerns me most about the rewritten pass is that it isn't
actually validating joint post-dominance on input, so if you give it bad
input, it might be a little mystifying to debug the verifier failures.
2025-11-03 11:51:17 -08:00

4587 lines
196 KiB
Plaintext

This is a correctness proof of the algorithm adopted by the SIL StackNesting
utility in late 2025. It is a text file rather than Markdown because the
notation it uses is quite unfriendly to Markdown.
Repeating the text from the in-file comment:
We use a straightforward, single-pass algorithm:
enum AllocationStatus {
case allocated
case pendingDeallocation
case undeallocatable
}
struct State {
var stack = [(StackAllocationInst, AllocationStatus)]()
}
F.searchBlocksForJointPostDominance(initialState: State()) {
(block, state) in
for inst in block.instructions {
if inst.isStackAllocation {
state.stack.push((inst, .allocated))
} else if inst.isStackDeallocation {
let allocation = inst.stackAllocation
if state.stack.top == (A, .allocated) {
_ = state.stack.pop()
let builder = SILBuilder(insertAfter: I)
while !state.stack.isEmpty &&
state.stack.top!.1 == .pendingDeallocation {
builder.createStackDeallocation(for: state.stack.pop().0)
}
} else {
I.remove()
let curStatus = state.findStatus(A)
assert(curStatus != .pendingDeallocation)
if curStatus == .allocated) {
state.setStatus(A, .pendingDeallocation)
}
}
}
}
}
The expectation is that searchBlocksForJointPostDominance performs a
depth-first search, passing a state that reflects the current simple path
from the entry block that is being explored. However, there's a twist to this
from a standard DFS; see below.
For the most part, the value in `state` at the end of processing a block is
the initial value of `state` at the start of processing its successor as
visited by the DFS, but this also has a twist; see below.
The state consists of (1) an active stack of allocations which haven't yet
been deallocated on this path and (2) an active status for each allocation.
It has five invariants:
1. Allocations on the stack dominate all allocations above them.
2. Every allocation on the stack dominates the current point
being considered.
3. The top of the stack never has pending status.
4. If a stack item has undeallocatable status, every item
below it also has undeallocatable status.
5. If an allocation has pending status, there is an allocation
above it on the stack which has allocated status.
The twist about the search order and state relates to the non-coherence of
SIL's joint post-dominance requirement for stack allocations. Specifically,
SIL permits the current state of the stack to vary on different edges into a
dead-end region; this just means it is not permitted to pop any of those
deallocations from the stack. (More allocations can be pushed and popped, but
the existing ones become untouchable.) StackNesting therefore permits its
input to be non-coherent: whether an allocation has been deallocated is
allowed to vary across the entries to a dead-end region.
To handle this, the search delays considering edges into a dead-end region
until it has seen the last such edge. Furthermore, the initial state of the
destination block is set to a conservative merger of the final states of all
of the predecessors: if two states differ in any way, the contents of the
stack are pared back to the common prefix, and all allocations are placed in
the undeallocatable state.
Bear in mind that a depth-first search of a CFG corresponds to the selection
of a spanning tree T of that CFG, so when this proof talks about states under
a particular spanning tree, it really means "while performing a depth-first
search". The twist doesn't change that: from the DFS perspective, it just
delays the visitation of the successor block until later.
The twist *does* create a significantly stronger invariant in the algorithm,
however, because it reestablishes the property that the allocations currently
on the stack always dominate the current point. Without the twist, this can
be lost when the search explores a dead-end path.
(An earlier version of this algorithm simply allowed the DFS to continue into
dead-end blocks normally. It turns out that, when doing so, it's still
impossible to emit a deallocation at a point the allocation does not
dominate, because there is an invariant that every allocation with pending
status is beneath an allocation with allocated status that it dominates.
But that version of the algorithm could not always properly establish nesting
for deallocations in dead-end blocks, so it needed to be fixed by the
addition of MERGE operations.)
Induction principles
--------------------
This proof contains several proofs by structural induction over paths in an
implicit spanning tree T:
\forall P: point -> pred .
P(entry point) /\
(\forall x: point . \forall p: path .
(\exists p': path .
p in T /\
p ends at x /\
p = (p'..., c) /\
(\forall x': point . p' visits x' => P(x')))
=> P(x))
=> \forall x: point . P(x)
or over the SCC tree:
\forall P: point -> pred .
(\forall x: point .
(\forall y: point . SCC(y) is a proper ancestor of SCC(x) => P(y))
=> P(x))
=> \forall x: point . P(x)
These induction principles only actually work for reachable points --- they
are ultimately derived by inducting on the definition of reachability --- but
that's fine because we only care about the properties of reachable points.
Spanning tree path induction is useful because the state at a point is defined
inductively over the path to that point in some spanning tree T.
(Remember that the choice of a spanning tree T reflects the arbitrary
decision of which path the DFS takes to a block, which is ultimately proven
not to matter to the algorithm by the [tree-invariance] theorem.)
SCC tree induction is useful because certain properties are easier to build up
that way, especially properties that are affected by the "twist" for dead-end
regions.
Theory and notation
-------------------
The CFG is generally thought of as a directed graph of blocks, with
control-flow edges between them. However, in this proof, we deal mostly with
the point-graph, the directed graph of points (the gaps before and after
instructions). The edges in the point-graph are the instructions and control
flow edges of the function.
In this proof, we will consistently use "edge" to mean a control-flow edge
between blocks, and we will use "component" to refer to the things that act
as edges in the point-graph.
<I is the CFG point prior to an instruction I.
I> is the CFG point after an instruction I.
If I is immediately followed by J, then I> and <J are the same point. In the
point-graph, an instruction I acts as an edge from <I to I>.
<B is the start point of block B, which is the same point as <I,
where I is the first instruction in the block.
B> is the ending point of block B, which is the same point as I>,
where I is the last instruction in the block.
The entry point is the start point of the entry block.
X -> Y is a control flow edge from its source block X to its destination block
Y. In the point-graph, it acts as an edge from X> to <Y.
<K and K> are the CFG points before and after a component K. If K is an
instruction I, then <K = <I and K> = I>. If K is a control flow edge X -> Y,
then <K = X> and K> = <Y, which is probably a little confusing, sorry.
A path through the CFG (sometimes called a walk in other sources: we are not
assuming non-repetition) is a sequence of CFG points joined by components
(either instructions or control-flow edges). A path consisting of a single
point is called the empty path at that point. A simple path is a path that
does not visit the same point twice.
If the text talks about the path to some point in a spanning tree, without
clarifing the start point of the path, the path is implicitly from the
entry point.
A path is written as the sequence of the instructions and control-flow edges
that it passes through. There is an implicit requirement that the end point
of each part of this notation be the start point of the next, which must
be satisfied when concatenating paths.
As an example:
(P..., I, A -> B)
The path follows the subpath P (which must end at <I), then passes through I,
then passes through the control flow edge from block A (which I must be the
last instruction of) to B.
A point X is said to be live if there exists a path from X to an exit of the
function. A block B is said to be live if its start point is live. An SCC
is live if all of its blocks are live. If a point/block/SCC is not live, it
is dead-end.
The entry SCC is the SCC containing the entry block.
An edge is an internal edge of an SCC if its source and destination blocks are
both in that SCC. An edge is an entry edge of an SCC if its destination block
is in that SCC but its source block is not. All edges are either internal
edges or entry edges of some SCC.
A path is life-preserving if it does not pass through any entry edges
into dead-end regions. (Note that it can begin in a dead-end region;
it just can't enter a different one.)
A component is said to be in an SCC C if it is an instruction that's in
a block in that SCC or if it's an internal edge whose source and destination
blocks are in that SCC. Entry edges are not in any SCC.
The start blocks of an SCC are:
- for the entry SCC, the singleton set containing the entry block;
- for any other SCC, the set of all blocks that are the destination block
of some entry edge into the SCC.
The start points of an SCC are the set of the start points of the
start blocks of that SCC.
Points, blocks, and SCCs only ever refer to entities reachable from the entry
point, as if unreachable blocks simply didn't exist in the CFG. The algorithm
implicitly ignores unreachable code, and so shall we. (The implementation
does still has to account for them, since they're allowed to exist in SIL.)
MAP(S, F) applies F to the elements of S in order. It is a monoid morphism and
therefore respects concatenation, i.e.
MAP((), F) = ()
MAP((A..., B...), F) = (MAP(A, F)..., MAP(B, F)...)
FLAT_MAP(S, F) applies F to the elements of S in order and concatenates the
resulting sequences. It is a monoid morphism and therefore respects
concatenation, i.e.
FLAT_MAP((), F) = ()
FLAT_MAP((A..., B...), F) = (FLAT_MAP(A, F)..., FLAT_MAP(B, F)...)
EDGE_FLAT_MAP(S, F) applies F to the joining elements of S in order. For
example, on a CFG, it applies F to the components that the path passes
through. It is a monoid morphism and therefore respects concatenation, i.e.
EDGE_FLAT_MAP((), F) = ()
EDGE_FLAT_MAP((A..., B...), F) = (EDGE_FLAT_MAP(A, F)..., EDGE_FLAT_MAP(B, F)...)
All of the theorems below are implicitly quantified over an arbitrary spanning
tree T unless they're marked (tree-independent).
Formal definition of the algorithm
----------------------------------
The following definitions formalize the state changes of the algorithm:
Definition [state].
A algorithm state is a sequence of pairs of allocation instructions
and status. A status is one of "allocated", "pending", or
"undeallocatable".
An allocation A is said to be present (or contained by, or included in)
a state S if there is an element of S with A as its first element.
An allocation A is said that have status X in S if there is an element
of S with A as its first element and X as its second.
A state is well-formed if no allocations repeat in it and if the
allocations having undeallocatable status form a prefix of the sequence.
There are four state operations. It is a precondition for all of them
that the initial state S is well-formed.
STATE_PUSH(S, A) := (S..., (A, allocated))
It is a precondition that A is not already present in S.
STATE_POP(S, A) := PREFIX(S, I)
where I is the smallest index such that all elements at I and later in S,
except for the last, have pending status. It is a precondition that
the final element of S is (A, allocated).
STATE_PEND(S, A) := SET_STATUS(S, I, pending)
where I is the smallest index such that the allocation at index I of S
is A. It is a precondition A is not the final element of S and that
S gives A allocated status.
STATE_MERGE(S, PSS) := CONSERVATIVE_MERGE(PSS)
It is a precondition that S is an element of PSS.
SET_STATUS(S', I, X) produces a sequence S where LENGTH(S') = LENGTH(S),
where S[J][0] = S'[J][0], and where either
S[J][1] = X (if I = J)
S[J][1] = S'[J][1] (if I != J)
It is a precondition that I < LENGTH(S').
CONSERVATIVE_MERGE(PSS) produces a sequence S where
- LENGTH(S) is the maximum L such that
- for all S1 in PSS, L <= LENGTH(S1)
- for all S1 and S2 in PSS, and for all I' < L, S1[I'][0] = S2[I'][0]
- S[I][0] = S'[I][0], where S' may be any element of PSS
- S[I][1] = undeallocatable
The second rule is well-founded because I < LENGTH(S) and therefore
S1[I][0] = S2[I][0] for all S1 and S2 in PSS.
The empty stack is the state at the entry point:
STATE_EMPTY := ()
Definition [operation, operation sequence, state at a point,
predecessor state set].
All of these definitions are sensitive to a specific spanning tree T
because of the rules around entry edges into dead-end regions. In this
section, we will make the T parameter explicit, but most places in the
proof will leave it off.
An operation sequence is a non-empty sequence of states joined by
operations, e.g.
[ () ]
[ (), PUSH@A, ((A, allocated)) ]
[ (), PUSH@A, ((A, allocated)), POP@D, () ]
Like paths, we will usually not be explicit about states, because
they can be imputed from context and the operations; instead, we will
just write the operations, like so:
(PUSH@A)
When we need to be explicit about states, we will write the sequence in
square brackets [ ... ] rather than parentheses ( ... ).
Concatenation on operation sequences requires the end state of the first
sequence to match the start state of the second; it then just concatenates
the underlying sequences after discarding the start state from the second
sequence. Concatenation is associative, and the empty sequence (at the
right state) is both a left-identity and a right-identity; we will not
prove these properties.
The operations are:
- PUSH@A, where A is an allocation
- POP@D, where D is a deallocation
- PEND@D, where D is a deallocation
- MERGE@(X -> Y), where X -> Y is a conservative entry edge in T
An operation O changes state S' to state S (under T) if
S = OPERATION_APPLY(S', O, T)
where
OPERATION_APPLY(S', PUSH@A, T) := STATE_PUSH(S', A)
OPERATION_APPLY(S', POP@D, T) := STATE_POP(S', ALLOCATION(D))
OPERATION_APPLY(S', PEND@D, T) := STATE_PEND(S', ALLOCATION(D))
OPERATION_APPLY(S', MERGE@(X -> Y), T) := STATE_MERGE(S', PSS(SCC(Y), T))
Here ALLOCATION(D) is the allocation of a deallocation and SCC(Y) is the
SCC containing the block Y.
An operation O is said to be well-formed for a transition from S1 to S2
(under T) if
- S1 and S2 are well-formed states,
- O satisfies its component restriction (under T),
- S1 satisfies the preconditions of the corresponding state operation, and
- O changes S1 to S2 (under T).
An operation sequence OS is well-formed (under T) if
- each state in OS is well-formed and
- each operation triple [ S1, O, S2 ] in OS is well-formed (under T),
defined as O being well-formed for the transition from S1 to S2 (under T).
Given an initial path P in T, the tree-driven operation sequence
OS(P, T) is defined as OS_PATH(STATE_EMPTY, P, T).
Given a path P, the path-driven operation sequence OS_PATH(S, P, T)
corresponding to P (under T) and start with state S is derived
inductively:
OS_PATH(S, (), T) := [ S ]
OS_PATH(S, (P..., C), T) :=
(OS_PATH(S, P', T)..., OPERATION(S', SC, T)...)
where S' is the end state of OS_PATH(S, P', T).
OPERATION(S, C, T) is defined as follows:
- If C is an allocation A, then
OPERATION(S, C, T) := [ S, PUSH@A, STATE_PUSH(S, A) ]
- If C is a deallocation D of an allocation A, and A has allocated
status and is at the top of the stack in S, then
OPERATION(S, C, T) := [ S, POP@D, STATE_POP(S, A) ]
- If C is a deallocation D of an allocation A, and A has allocated
status and is not at the top of the stack in S, then
OPERATION(S, C, T) := [ S, PEND@D, STATE_PEND(S, A) ]
- If C is a conservative entry edge X -> Y into R, then
OPERATION(S, C, T) := [ S, MERGE@(X -> Y), STATE_MERGE(S, PSS(R, T)) ]
- Otherwise
OPERATION(S, C, T) := [ S ]
Note that the path must pass through an instruction or edge, not just reach
it, in order for the operation sequence to include the corresponding
operation.
The state at a point X (under T) is the end state of the operation
sequence corresponding to the path to X in T.
The start state of a block B (under T) is the state at <B (under T).
The end state of a block B (under T) is the state at B> (under T).
The start state of a path P (under T) is the state at the start
point of P (under T).
The end state of a path P (under T) is the state at the end point of P
(under T).
The predecessor state set PSS(C, T) of an SCC C is the set of end
states (under T) of all blocks B that are not in C but have an edge to a
block in C.
An edge is a conservative entry edge (under T) if it is an entry edge
into a dead-end region R where PSS(R, T) is not singleton.
A path is deallocatability-preserving (under T) if it does not pass through
any conservative entry edges (under T).
That OS(P, T) produces a well-formed operation sequence and satisfies the
preconditions on the state operations is proven by
[correctness-operation-sequence].
Note that OPERATION does not create an operation for every deallocation.
- If the allocation has allocated status, OPERATION always creates an
operation.
- If the allocation is not in S, or if it has pending status, OPERATION
silently ignores it. These cases are proven impossible by
[correctness-deallocation-preconditions].
- If the allocation has undeallocatable status, OPERATION silently
ignores it (in terms of state changes). This is okay because it can
only happen in dead-end regions, and since the algorithm will also
just delete the deallocation at these points.
Formal definitions
------------------
Axiom [block-single-entry].
If an initial path visits a point in block B, then it has a suffix that
starts at <B and does not pass through an edge.
Axiom [block-single-exit].
For every point X in block B, there exists a path from <B to X
that does not pass through an edge.
Axiom [block-straight-line].
If a path does not pass through an edge, it is a simple path, and it
is the unique such path from its start point to its end point.
Proofs of general utility
-------------------------
Definition [SCC ancestry].
These are all basic definitions and theorems in the theory of SCCs and are
presented without proof.
A is an ancestor of B if there is a path from some block in A to some block
in B. A is a proper ancestor of B if A is an ancestor of B and A != B.
A is a descendent of B if B is an ancestor of A. A is a proper descendent
of B if B is a propert ancestor of A.
A cannot be both a proper ancestor of B and a descendent of B.
A cannot be both an ancestor of B and a proper descendent of B.
If A is an ancestor of B, there is a path from the start point of any
block in A to any point in B. If A is a proper ancestor of B, there is a
path from any point in A to any point in B.
If A is a proper ancestor of B, there is no path from any point in B to any
point in A.
If X -> Y is an entry edge into the SCC of Y, the SCC of X is a proper
ancestor of the SCC of Y.
If P is a path to a point in a proper ancestor of A, then all points on P
are in SCCs that are proper ancestors of A.
If P is a path between two points in the same SCC, then all points on P
and components passed through by P are in that SCC.
End.
Definition [SCC entry prefix, entry suffix, entry path, entry point].
If P is a path that visits an SCC C,
- the SCC entry point of P for C is the first point on P that is in C,
- the SCC entry prefix of P for C is the shortest prefix of P that ends at
a point in C, and
- the SCC entry suffix of P for C is the longest suffix of P that begins at
a point in C.
P is an SCC entry path for C if it is an initial path that ends at a point
in C, but only the point at the end index of P is in C.
X is an SCC entry point of C if there exists an initial path P that visits
C such that X is the SCC entry point of P.
End.
Lemma [scc-non-entry-path]. (tree-independent)
If an initial path P to the start point of a block B is not an SCC entry
path for the SCC C containing B, then
P = (P_I..., M -> B)
where M -> B is an internal edge of C.
Pf.
Because P is not an SCC entry path for C, there exists a point on P that
is in C besides the end point. P is therefore not the empty path, and can
be partitioned before its last component:
P = (P_I..., K)
P_I passes through a point in C; let it be partitioned at such a point:
P_I = (P_II..., P_IF...)
Then (P_IF..., K) is a path from a point in C to a point in C, and
so every component it passes through must be in C, including K. The end
point of P is the start point of the block B, so K must be an edge to B:
P = (P_I..., M -> B)
and because M -> B is in C, it must be an internal edge of C.
Qed.
Lemma [scc-entry-point].
If P is a path that visits an SCC C, P_I is the SCC entry prefix of P for C,
and P_F is the entry suffix of P for C, then
P = (P_I..., P_F...)
Furthermore, the entry point of P for C is the same as the end point of P_I
and the start point of P_F.
Pf.
P_I is a prefix of P, so it ends at some position X in P.
P_F is a suffix of P, so it ends at some position Y in P.
The points at X and Y are in C by definition.
If X < Y, then the suffix of P start from X would be longer than P_F;
since it would also start from a point in C, this would contradict the
assumption that P_F is the longest such suffix of P.
If Y < X, then the prefix of P ending at Y would be shorter than P_I; since
it would also end at a point in C, this would contradict the assumption
that P_I is the shortest such prefix of P.
Therefore Y = X, and concatenating a prefix and a suffix of a sequence taken
at the same position always produces the original sequence.
The point of P at X is the entry point of P for C: if there were an earlier
point in P that was in C, then the prefix of P ending at the index of that
point would be shorter than P_I; since it would end at a point in C, that
would contradict the assumption that P_I is the shortest such prefix of P.
Qed.
Lemma [scc-entry-prefix-ancestry].
If P_I is the SCC entry prefix of some path P for C, then
- if P_I is an initial path, it is an entry path of C;
- P_I does not pass through any components of descendent SCCs of C;
- P_I visits only points in ancestor SCCs of C; and
- if P_I is not the empty path, it ends with an entry edge into C
and the points of P_I at all indexes except the end index are in proper
ancestor SCCs of C.
Pf.
Let X be the end point of P_I. By definition, X is in C.
Let X' be a point visited by P_I, and let C' be the SCC containing X'. Then
the suffix of P_I start at X' is a path from a point in C' to a point in
C, which makes C' an ancestor SCC of C by definition.
P_I does not pass through any component K of a descendent SCC C' of C:
Suppose it did; let
P_I = (PK_I..., K, PK_F...)
PK_F is a path from K> (a point in C') to X (a point in C), so C' must
be an ancestor SCC of C'; since it is both an ancestor and a descendent
of C, C' = C. Then since <K is also a point in C, and it is at an earlier
index of P than X, a prefix of P_I ending at <K would be a shorter prefix
of P that ends at a point in C, which would contradict the assumption
that P_I is the SCC entry prefix of P for C. So P_I cannot pass through
any such component.
If P_I is not the empty path:
P_I ends by passing through some component K. As previously argued,
K cannot be in C, but since K> is in C, K must be an entry edge M -> N
into C. The SCC containing M is a proper ancestor of C. Let X be a point
on P_I prior to this edge. Then the subpath of P_I from X to M> is a path
to a point in a proper ancestor SCC of C, which means X is in a proper
ancestor SCC of C.
If P_I is an initial path:
As previously argued, the end point of P_I is in C.
If P_I is the empty path, then there are no indexes in P except the final
index, so it is vacuously true that only the point at the end index of P
is in C.
If P_I is not the empty path, then as previously argued, all points of P_I
at indexes other than the end index are in proper ancestors of C, and
therefore not in C.
Therefore, in all cases, P_I is an entry path of C.
Qed.
Lemma [scc-entry-suffix-ancestry]. (tree-independent)
If P_F is the SCC entry suffix of some path P for C, then P_F visits only
points in descendent SCCs of C. Furthermore, if P ends at a point in C,
P_F visits only points in C.
Pf.
The start point of P_F is in C. Let X be a point visited by P_F; then
the prefix of P_F ending at X is a path from a point in C to a point in the
SCC containing X, which makes the SCC containing X a descendent SCC of C
by definition.
If P ends at a point in C:
Let X be a point visited by P_F. Then the suffix of P_F start at X
is a path from X to a point in C, which makes the SCC containing X
an ancestor SCC of C as well. Since it is both a descendent and an
ancestor of C, it must be C.
Qed.
Lemma [scc-entry-suffix-suffix]. (tree-independent)
If a path P has a partition like so
P = (P_I..., M -> N, P_F...)
where M -> N is an entry edge, and P_F' is the SCC entry suffix of P for
the SCC containing the end point of P, then P_F' is a suffix of P_F.
Pf.
Let C be the SCC containing the end point of P. By definition, P_F' is
the longest suffix of P that begins at a point in C. By the given
partition of P, all indexes in P correspond to points in either P_I or P_F.
Suppose that P_F' begins at an index into the P_I subpath of P, and let
P_I' be the suffix of P_I from that index. But then the start point of
P_I' is in C, so there exists a path P_FI from the end point of P to the
start point of P_I'. (P_I..., M -> N, P_F..., P_FI...) is a cycle
that begins and ends at a point in C, so all points on it must be in C.
In particular, M> and <N must both in the C, which contradicts the
assumption that M -> N is an entry edge.
Therefore P_F' must begin at an index into the P_F subpath of P, and so
P_F' is a suffix of P_F.
Qed.
Lemma [scc-entry-path-existence]. (tree-independent)
For every SCC C and spanning tree T, there exists (possibly non-uniquely) an
SCC entry path of C.
Pf.
(Recall that we universally assume reachability.)
Let X' be a point in C, and let P' be the path to X' in T. Then let P be the
SCC entry prefix of P' for C. P is an initial path because is a prefix of an
initial path. Therefore by [scc-entry-prefix-ancestry], P is an SCC entry
path of C.
Qed.
Lemma [scc-entry-path-ancestry]. (tree-independent)
If P is an SCC entry path of C,
- P is the SCC entry prefix of P for C,
- P visits only points in ancestor SCCs of C; and
- if P is not the empty path, it ends with an entry edge into C
and the points of P at all indexes except the end index are in proper
ancestor SCCs of C.
Pf.
Let P' be the SCC entry prefix of P for C. By definition, only the point at
the final index of P is in C, so P' is a prefix of P that must contain the
final index of P, which means P = P'.
Since P is the SCC entry prefix of P for C, the other properties are true
by [scc-entry-prefix-ancestry].
Qed.
Lemma [scc-entry-edge-partition]. (tree-independent)
If a path P contains an edge X -> Y that is an entry edge into SCC C,
then
P = (P_I.., X -> Y, P_F...)
where (P_I..., X -> Y) is the SCC entry prefix of P for C and P_F is the
SCC entry suffix of P for C. All points visited by P_I are in proper
ancestors of C.
Pf.
Let P be split around the edge as specified:
P = (P_I..., X -> Y, P_F...)
Let CX be the SCC containing X. Because X -> Y is an entry edge into C, CX
is a proper ancestor of C; since X> is a point in CX, all points on P_I are
in proper ancestors of C. Therefore the first index of P that is for a
point in C must be the index following the X -> Y edge, making
(P_I..., X -> Y)
the SCC entry prefix of P for C and P_F the SCC entry suffix of P for C.
Qed.
Lemma [scc-entry-path-partition]. (tree-independent)
If an initial path P visits an SCC C, and C is not the entry SCC, then
there uniquely exist M, N, P_I, and P_F such that
P = (P_I..., M -> N, P_F...)
such that (P_I..., M -> N) is the SCC entry prefix of P for C, P_F is the
SCC entry suffix of P for C, M -> N is an entry edge into C, and all points
visited by P_I are in proper ancestors of C.
Pf.
Let P_I' be the SCC entry prefix of P for C, and let P_F be the SCC entry
suffix of P for C. By [scc-entry-point],
P = (P_I'..., P_F...)
P_I' must be an initial path because P_I is. P_I' therefore cannot be the
empty path because otherwise the final point of P_I' would be both the
entry point and in C, and C is not the entry SCC by assumption. Therefore
[scc-entry-prefix-ancestry] tells us that P_I' can be further split:
P_I' = (P_I..., M -> N)
where M -> N is an entry edge into C and P_I contains only points in
proper ancestors of C.
By substitution,
P = (P_I..., M -> N, P_F...)
and P_I, M -> N, and P_F satisfy all the requirements of the lemma.
Qed.
Lemma [acyclic-scc-entry-point]. (tree-independent)
If X is a point in an acyclic SCC C, the entry point for C of all initial
paths to X is the start point of the block containing X.
Pf.
Let an initial path P to X be given, and let P be partitioned by its
entry point for C:
P = (P_I..., P_F...)
The entry point of P for C is the start point of P_F, which must be the
start point of some block in C. But an acyclic SCC contains only a single
block, and so this must be the block containing X.
Qed.
Lemma [no-cyclic-scc-deallocation]. (tree-independent)
A cyclic SCC C cannot contain a deallocation D for an allocation A that is
not in C.
Pf.
Let C, D, and A be given, and let P be a path to D>. The entry block is
never a cyclic SCC, so P must pass through exactly one entry edge of C:
P = (P_EX..., X -> Y, P_YD...)
such that X is not in C and Y is in C. Then P_YD lies entirely within C.
Because C is a cyclic SCC, there must exist a path P_DY from D> to <Y which
also lies entirely within C. Then
(P_EX..., X -> Y, P_YD..., P_DY..., P_YD...)
is a (non-simple) path that passes through two deallocations of A. This path
cannot pass through A between the deallocations because that portion of the
path lies entirely within C, and A is not in C. This violates the joint
post-dominance rule of deallocation.
Qed.
Lemma [no-scc-self-path-deallocation]. (tree-independent)
A path P from <X to <Y, where X and Y are two blocks in the same SCC, can
never pass through a deallocation of an allocation from a different SCC.
Pf.
If the SCC is not cyclic, then X and Y must be the same
block, and the only possible such path is empty and therefore
does not pass through anything. Otherwise, it is cyclic and
[no-cyclic-scc-deallocation] applies.
Qed.
Lemma [no-scc-self-path-history]. (tree-independent)
If P is a path from <X to <Y, where X and Y are two blocks in the same SCC
C, and A is an allocation from a different SCC, then AH(A, P) = ().
Pf.
P passes through only components of C, so it cannot pass through A,
which is not in C by assumption. By [no-scc-self-path-deallocation],
P also cannot pass through a deallocation of A. Therefore AH(A, P) = ().
Qed.
Lemma [implied-dominance]. (tree-independent)
For all points X, Y, and Z, if X dominates Z, and there is a path
P_YZ from Y to Z that does not pass through X, then X dominates Y.
Pf.
Let X, Y, Z, and P be given. Let P_Y be an arbitrary path from
the entry point to Y; then PZ := (P_Y..., P_YZ...) is a path from
the entry point to Z. X dominates Z, so all paths from the entry
point to Z pass through X, and that includes P_Z. The P_YZ subpath
of P_Z does not pass through X, so the P_Y subpath must. But that
means that all paths to Y pass through X, so X dominates Y.
Qed.
Lemma [same-block-dominance]. (tree-independent)
If a simple initial path to X passes through an instruction I in the
same block as X, then I dominates X.
Pf.
Let an arbitrary initial path P' to X be given. Let B be the block
containing X.
By [block-single-entry], P and P' have suffixes that start at <B and do
not pass through an edge.
P = (PI..., PF...)
P' = (PI'..., PF'...)
PF must pass through A:
P passes through A, so either PI or PF passes through A.
If PI passes through A, it can be partitioned around that:
PI = (PAI..., A, PAF...)
and then since PAI visits <A, a point in B, it has a suffix that starts
at <B by [block-single-entry]:
PAI = (PAII..., PAIF...)
Then altogether, P looks like this:
P = (PAII..., PAIF..., A, PAF..., PF...)
and since both PAIF and PF start at <B, and they are separated by at
least one concrete component on P, P must visit <B twice, which
contradicts the assumption that it is a simple path. So PI cannot
pass through A, and so PF must.
By [block-straight-line], PF = PF', so P' also passes through A.
Since P' was an arbitrary initial path to X, all such paths pass
through A, and so A dominates X.
Qed.
Lemma [live-ancestry]. (tree-independent)
If P is a path from X to Y, and Y is a live point, then X is a live
point, and P is a life-preserving path.
Pf.
Since Y is a live point, there exists a path PYF from Y to a function exit.
Then (P..., PYF...) is a path from X to a function exit, so by definition,
X is a live point.
Suppose that P passed through an entry edge M -> N into a dead-end region;
then there exist PI and PF such that
P = (PI..., M -> N, PF...)
But then (PF..., PYF...) is a path from <N to a function exit, contradicting
the assumption that N is in a dead-end region. So P cannot pass through
such an edge.
Qed.
Lemma [dead-end-region-ancestry]. (tree-independent)
If P is a path from X to Y, and X is in a dead-end region, then
Y is in a dead-end region.
Pf.
Suppose that Y was in a live SCC; then Y is a live point, and so by
[live-ancestry], X is also a live point, which contradicts the assumption
that X is in a dead-end region. Therefore Y is in a dead-end region.
Qed.
Lemma [dead-end-region-path]. (tree-independent)
If P is a life-preserving path, then the start point of P is in a
dead-end region R if and only if the end point of P is in R.
Pf.
Let X and Y be the start and end points of P.
If Y is in a dead-end region R', then X is in R':
Every point on P is in R', which we can prove by reverse structural
induction on P. Let P' be a suffix of P.
In the base case, P' is the empty path, and its start point, Y, is in R'
by assumption.
In the inductive case, P' := (K, P''...), and all points on P'' are in
R' by the induction hypothesis. This includes K>.
If K is a instruction, then <K is in the same block, and therefore the
same SCC, as K>, which means the start point of P' is in R'.
If K is an edge M -> N, then <N is in R', which is a dead-end region. K
cannot be an entry edge into a dead-end region because of the assumption
that P is life-preserving. So it must be an internal edge, which means M>
is in the same region as <N, which is R'. Therefore the start point of
P' is in R'.
In all cases, the start point of P' is in R, and by induction, the
start point of P is in R, and so X is in R.
If X is in R:
Y must be in a dead-end region R' by [dead-end-region-ancestry],
so X is in R' by the helper lemma above. But we assumed that X is in R,
so R = R', which means Y is in R.
If Y is in R:
X is in R by the helper lemma above.
Qed.
States and operations
---------------------
Lemma [push-cases].
If PUSH@A' is well-formed for the transition from S' to S, then for all
allocations A, exactly one of the following is true:
#top: A = A', S gives A allocated status, the last element of S is
(A, allocated), and S' does not contain A, or
#prefix: A != A', and S gives A status U if and only if S' gives A status U.
Pf.
Because PUSH@A' is well-formed for the transition from S' to S,
S = STATUS_PUSH(S', A') = (S'..., (A', allocated))
and A' is not contained in S'.
Let an allocation A be given.
If A = A':
(A, allocated) is the last element of S, which means by definition that
S gives A allocated status. A is not contained in S because A' is not.
Therefore #top is true. #prefix is not true because A = A'.
If A != A', then:
If A has status U in S':
(A, U) must be an element of S'. S' is a prefix of S, so (A, U)
must also be an element of S. Therefore A has status U in S.
If A has status U in S:
(A, U) must be an element of S, and S = (S'..., (A', allocated)),
so either (A, U) is an element of S' or (A, U) = (A', allocated).
But A != A', so the latter is impossible, and therefore (A, U)
is an element of S', which means S' gives A status U.
Therefore #prefix is true. #top is not true because A = A'.
Qed.
Lemma [push-preserves-preceding].
If PUSH@A is well-formed for the transition from S' to S, then
if S' gives A' some status U, S gives A' some status U.
Pf.
By [push-cases], one of the following is true for A':
If #top:
S' does not contain A, so this is a contradiction.
If #prefix:
Under the conditions of this case, since S' gives A status U, S gives A
status U.
Qed.
Lemma [push-allocated].
If PUSH@A is well-formed for the transition from S' to S, then
S gives A allocated status.
Pf.
By [push-cases], one of the following is true for A:
If #top:
Under the conditions of this case, S gives A allocated status.
If #prefix:
Under the conditions of this case, A != A, which is a contradiction.
Qed.
Lemma [pop-cases].
If POP@D is well-formed for the transition from S' to S, and A' is the
allocation deallocated by D, then
- S is a prefix of S'.
- (A', allocated) is the final element of S', and therefore A has allocated
status in S'.
- For all allocations A, if S gives A status U, S' gives A status U.
- For all allocations A, exactly one of the following is true:
#absent: A != A', and neither S' nor S contains A;
#top: A = A', S' gives A allocated status, and S does not contain A;
#popped: A != A', S' gives A pending status, the allocations in S' at all
indexes following A other than the final index have pending
status, and S does not contain A; or
#prefix: A != A', S' and S both contain A and give it the same status, and
if S' gives A pending status, there exists an index I' following A
other than the final index of S' such that the allocation at that
index has allocated status in S', and I' < LENGTH(S).
Pf.
Because POP@D is well-formed for the transition from S' to S, S' does not
repeat allocations, the undeallocatable allocations of S' are a prefix,
the final element of S' is (A', allocated), and
S = STATUS_POP(S', A') = PREFIX(S', I)
where I is the smallest index such that all elements at I and later in S',
except for the last, have pending status.
Let an allocation A be given such that S gives A status U; then
(A,U) is an element of S, which is a prefix of S', and so (A,U) is
an element of S', and so S' gives A status U.
A' is at index LENGTH(S') - 1 in S'. Because S' does not repeat
allocations, A' is not at any other index in S'.
Let an allocation A be given.
If A = A':
If A were in S, i.e. if A were in PREFIX(S', I), then I would have
to be LENGTH(S') because A cannot be at any other index of S' except
LENGTH(S') - 1. But then I' := LENGTH(S') - 1 would be a smaller
index where all elements at indexes >= I' but != LENGTH(S') - 1 have
pending status (because there are no such elements). So S does not
contain A. Therefore #top is true. None of the others is true because
A = A'.
If A != A', and S' does not contain A:
S is a prefix of S', so S also cannot contain A. Therefore [absent]
is true. #top is not true because A != A'. #popped and #prefix are
not true because S' does not contain A.
If A != A', S' contains A, and the index IA of A in S' is >= I:
By the construction of I, for all indexes J >= I, either
J = LENGTH(S') - 1 or the status of the allocation at index J of S' is
pending. IA >= I, so this is true of all indexes >= IA. IA cannot be
LENGTH(S') - 1, because the allocation there is A', and we already
know that A != A'; so the status of A (the allocation at index IA of S')
is pending. If S contained A, it would have to be at some index J;
since S is a prefix of S', it would be at index J of S', but that would
mean that A repeats in S'; therefore S does not contain A.
Therefore #popped is true. #absent is not true because S' contains A.
#top is not true because A != A'. #prefix is not true because S
does not contain A.
If A != A', S' contains A, and the index IA of A in S' is < I:
S = PREFIX(S', I), so an identical entry for A exists in S, which
means S both contains A and gives A the same status as S'.
Suppose that A has pending status in S':
By assumption, I is the least index such that all elements at I
and later in S', except for the last, having pending status.
There must exist an index > IA but != LENGTH(S') - 1 that does not
have pending status, or else IA would be a smaller such index.
Let I' be the least such index. I' < I by the conditions on I,
and so I' < LENGTH(S) = LENGTH(PREFIX(S', I)) = I. The element
at I' does not have pending status in S', and it does not have
undeallocatable status in S' because the undeallocatable allocations
are a prefix of S' but A, the element at AI, which is less than I'
by assumption, has pending status, not undeallocatable. Therefore
the element at I' in S' has allocated status.
Therefore #prefix is true. #absent is not true because S' contains A.
#top is not true because A != A'. #popped is not true because
the element at index I', which is >= IA and != LENGTH(S') - 1, has
allocated status.
Qed.
Lemma [pop-preserves-prefix].
If POP@D is well-formed for a transition from S' to S, and S gives
some allocation A a status U, then S' gives A status U.
Pf.
By [pop-cases], exactly one of the following is true for A:
#absent: S does not contain A, which is a contradiction.
#top: S does not contain A, which is a contradiction.
#popped: S does not contain A, which is a contradiction.
#prefix: S' give A the same status as S does.
Qed.
Lemma [pend-cases].
If PEND@D is well-formed for a transition from S' to S, and A' is the
allocation deallocated by D, then for all allocations A, then
- S contains A if and only if S' contains A; and
- exactly one of the following is true:
#changed: A = A', A is not the final allocation of either S' or S,
S' gives A allocated status, and S gives A pending status.
#kept: A != A', and S' and S give A the same status U.
#absent: A != A', and neither S' nor S contain A.
Pf.
Because O is part of a well-formed operation sequence, S' does not
repeat allocations, the undeallocatable allocations of S' are a prefix,
A' is not the final allocation of S', S' gives A' allocated status, and
S = STATUS_PEND(S', A') = SET_STATUS(S', I, pending)
where I is the smallest index such that the allocation at index I of S'
is A'.
Let an allocation A be given.
If S' contains A, then let AI be the index of A in S'; SET_STATUS does
not change the allocation sequence, so AI is also the index of A in
SET_STATUS(S', I, pending), so S contain A.
If S contains A, then let AI be the index of A in SET_STATUS(S', I, pending); SET_STATUS does not change the allocation sequence, so AI is also the index
of A in in S', so S' contains A.
If A = A':
By substitution, S' gives A allocated status, and A is not the final
allocation of S'. The sequence of allocations in the same in S' and
SET_STATUS(S', _, _), so A is also not the final allocation of S.
I is the smallest index of A in S', and it is unique because S' does
not repeat allocations; so the allocation at index I of
SET_STATUS(S', I, pending) is also A, and its status is pending
by the definition of SET_STATUS, and so S gives A pending status.
Therefore #changed is true. #kept and #absent are not true
because A = A'.
If A != A':
If S' contains A:
Let AI be the index of A in S'; S'[AI][0] = A. Let U = S'[AI][1].
By definition, since AI is index of A in S', S' gives A the status U.
By the axioms of SET_STATUS,
SET_STATUS(S', I, pending)[AI][0] = S'[AI][0]
so S contains A.
AI != I because the allocation at index I in S' is A', and A != A'
by assumption in this case. So by the axioms of SET_STATUS,
SET_STATUS(S', I, pending)[AI][1] = S'[AI][1] = U
So S gives A status U.
Therefore #kept is true. #changed is not true because
A != A', and #absent is not true because S' contains A.
If S' does not contain A:
Suppose S contained A; then let AI be the index of A in S, such that
S[AI][0] = A. But by the axioms of SET_STATUS,
SET_STATUS(S', I, pending)[AI][0] = S'[AI][0]
so if S[AI][0] = A, then S'[AI][0] = A, which contradicts the assumption
that S' does not contain A. So S does not contain A.
Therefore #absent is true. #changed is not true because A != A'.
#kept is not true because S' does not contain A.
Qed.
Lemma [pend-preserves-allocation-set].
If PEND@D is well-formed for a transition from S' to S, then
S contains some allocation A if and only if S' contains A.
Pf.
By [pend-cases], exactly one of the following is true for A:
#changed: S' gives A allocated status and S gives A pending status,
so both S' and S contain A.
#kept: S' and S both give A the same status U, so both contain A.
#absent: Neither S' nor S contain A.
Qed.
Lemma [conservative-merge-index].
If S = CONSERVATIVE_MERGE(PSS(R)), and I < LENGTH(S), and S' is the end
state of a block with an entry edge into R, then S[I][0] = S[I'][0].
Pf.
By the axioms of CONSERVATIVE_MERGE, there exists a state S'' in PSS(R)
such that S[I][0] = S''[I][0].
By the axioms of CONSERVATIVE_MERGE, LENGTH(S) is a number such that,
for all I' < LENGTH(S), for all S1 and S2 in PSS(R), S1[I'][0] = S2[I'][0].
I < LENGTH(S), and S' and S'' are in PSS(R), so S'[I][0] = S''[I][0].
By transitivity, S[I][0] = S'[I][0].
Qed.
Lemma [conservative-merge-predecessors].
If CONSERVATIVE_MERGE(PSS(R)) contains an allocation A, then for all
blocks B that have an entry edge into R, the state at B> contains A.
Pf.
Let S = CONSERVATIVE_MERGE(PSS(R)). Let I be the index of A in S;
that is, S[I][0] = A.
Let an arbitrary block B that has an entry edge into R be given,
and let S' be the state at B>. Then by [conservative-merge-index],
S'[I][0] = S[I][0] = A. Therefore S' contains A. Since the choice of
B was arbitrary, this is true for all such blocks.
Qed.
Lemma [merge-preserves-allocation-set-prefix].
If MERGE@(X -> Y) is well-formed for a transition from S' to S,
then if S contains some allocation A, and B is a block with an
entry edge into the SCC containing Y, the state at B> contains A.
In particular, the state at M> contains A.
Pf.
Let R be the SCC containing Y. By the well-formedness rules
S = STATE_MERGE(S', PSS(R)) = CONSERVATIVE_MERGE(PSS(R))
So since S contains A, by [conservative-merge-predecessors],
B> contains A.
M is a block with an entry edge into R (namely, X -> Y), so by
the previous conclusion, M> contains A.
Qed.
Allocation history
------------------
Definition [allocation history].
The allocation history AH(A,P) of an allocation A on a path P is the
sequence of allocations and deallocations of A that P passes through,
in order:
AH(A, P) = EDGE_FLAT_MAP(P, AH_ELEMENT(A))
where
AH_ELEMENT(A)(A)) = (A) (if the equality holds)
AH_ELEMENT(A)(D)) = (D) (if D is a deallocation of A)
AH_ELEMENT(A)(K)) = () (otherwise)
A simplified allocation history is a string over the set {a,b}.
If H is an allocation history, then AH_SIMPLIFY(H) produces a
simplified allocation history corresponding to H, defined as:
AH_SIMPLIFY(H) = MAP(H, AH_SIMPLIFY_ELEMENT)
where
AH_SIMPLIFY_ELEMENT(A) = a (if A is an allocation)
AH_SIMPLIFY_ELEMENT(D) = d (if D is a deallocation)
An allocation pattern is a regular expression over the terminals {a,d}.
An allocation pattern R is said to match a history H if AH_SIMPLIFY(H) is in
the language of R. (This implies a *complete* match.)
Then the joint post-dominance axioms can be directly restated as follows:
For all allocations A and initial paths P,
- /d.*/ may not match AH(A,P),
- /.*aa.*/ may not match AH(A,P),
- /.*dd.*/ may not match AH(A,P), and
- /.*a/ may not match AH(A,P) if P is also a terminating path.
This is equivalent to saying that /(ad)*a?/ must match AH(A,P), and more
specifically /(ad)*/ must match AH(A,P) if AH(A,P) is terminating.
An allocation pattern R1 is a refinement of an allocation pattern R2 if
the language of R1 is a subset of the language of R2.
A singleton allocation pattern is an allocation pattern that matches exactly
one simplified allocation history.
The instruction-final prefix of a path P is PREFIX(P, I), where I is
LENGTH(P) if P does not end in a edge and LENGTH(P) - 1 if it does.
A path P is an instruction-simple path if its instruction-final
prefix is a simple path.
End.
Lemma [history-concatenation]. (tree-independent)
AH(A, (P1..., P2...)) = (AH(A, P1)..., AH(A, P2)...)
Pf.
AH(A, (P1..., P2...)) = EDGE_FLAT_MAP((P1..., P2...), AH_ELEMENT(A))
= (EDGE_FLAT_MAP(P1, AH_ELEMENT(A))...,
EDGE_FLAT_MAP(P2, AH_ELEMENT(A))...)
= (AH(A, P1)..., AH(A, P2)...)
Qed.
Lemma [simplified-history-concatenation]. (tree-independent)
AH_SIMPLIFY((H1..., H2...)) = (AH_SIMPLIFY(H1)..., AH_SIMPLIFY(H2)...)
Pf.
AH_SIMPLIFY((H1..., H2...)) = MAP((H1..., H2...), AH_SIMPLIFY_ELEMENT)
= (MAP(H1, AH_SIMPLIFY_ELEMENT)...,
MAP(H2, AH_SIMPLIFY_ELEMENT)...)
= (AH_SIMPLIFY(H1...), AH_SIMPLIFY(H2...))
Qed.
Lemma [history-pattern-concatenation]. (tree-independent)
If R1 matches AH(A, P1) and R2 matches AH(A, P2), then the concatenation
expression /(R1)(R2)/ matches AH(A, (P1..., P2...)).
Pf.
Let S1 = AH_SIMPLIFY(AH(A, P1)) and let S2 = AH_SIMPLIFY(AH(A, P1)).
By the definition of matching, S1 is in the language of R1 and S2 is in the
language of R2.
In the theory of regular expressions, the concatenation /(R1)(R2)/
is defined as the expression whose language is the set
{ X where \exists X1,X2 . X = (X1..., X2...) /\
X1 is in the language of R1 /\
X2 is in the language of R2 }
Then (S1..., S2...) is in that set.
By [history-concatenation] and then [simplified-history-concatenation],
AH_SIMPLIFY(AH(A, (P1..., P2...))) = AH_SIMPLIFY((AH(A, P1)..., AH(A, P2)...))
= (AH_SIMPLIFY(AH(A, P1))...,
AH_SIMPLIFY(AH(A, P2))...)
= (S1..., S2...)
So AH_SIMPLIFY(AH(A, (P1..., P2...))) is in the language of /(R1)(R2)/,
and by definition that means /(R1)(R2)/ matches AH(A, (P1..., P2...)).
Qed.
Lemma [history-pattern-prefix-removal]. (tree-independent)
If S1 is a simplified allocation history, and R is an allocation pattern,
then there exists an allocation pattern R2 such that, for all simplified
allocation histories S2, R matches (S1..., S2...) if and only if R2 accepts S2.
Pf.
Let
D = (Q, {a,d}, δ, q0, F)
be a DFA corresponding to R. Let q1 = δ*(q0, S1). Then define
D2 := (Q, {a,d}, δ, q1, F)
and let R2 be a regular expression corresponding to D2.
Suppose that R matches (S1..., S2...); by construction, D accepts
(S1..., S2...). By the definition of DFA acceptance,
δ*(q0, (S1..., S2...))
is therefore some state q in F. But it is a well-known property of DFAs that
δ*(q0, (S1..., S2...)) = δ*(δ*(q0, S1), S2)
and substituting the definition of q1 gives
δ*(q0, (S1..., S2...)) = δ*(q1, S2)
which means that D2 accepts S2, and therefore R2 matches S2.
Suppose that R2 matches S2; by construction, D2 accepts S2, and by definition
δ*(q1, S2)
is some state q in F. By substitution
δ*(δ*(q0, S1), S2) = q
and thus
δ*(q0, (S1..., S2...)) = q
and therefore D accepts (and R matches) (S1..., S2...).
Qed.
Lemma [history-prefix-removal]. (tree-independent)
If R matches AH(A, (P1..., P2...)), and a singleton pattern R1 (with uniquely-
matched history H1) matches AH(A, P1), and R2 is constructed such that the
language of /(R1)(R2)/ is
{ S where (R matches S /\ S has a prefix H1) }
then R2 matches AH(A, P2).
Pf.
(Note that this would not be reliably true if R1 if were not a singleton
pattern.)
We are given that R1 matches AH(A, P1), which means that
AH_SIMPLIFY(AH(A, P1)) is in the language of R1. Since R1 is a singleton
allocation pattern, AH_SIMPLIFY(AH(A, P1)) = H1, the uniquely matched
history of R1.
Let S := AH_SIMPLIFY(AH(A, (P1..., P2...)))
By [history-concatenation] and [simplified-history-concatenation]
S = AH_SIMPLIFY((AH(A, P1)..., AH(A, P2)...))
= (AH_SIMPLIFY(AH(A, P1))..., AH_SIMPLIFY(AH(A, P2))...)
= (H1..., AH_SIMPLIFY(AH(A, P2))...)
We are given that R matches AH(A, (P1..., P2...)) (as an allocation history),
and therefore R matches S (as a simplified allocation history).
Then by the construction of R2, S is in the language of /(R1)(R2)/.
By the definition of matching a concatenation pattern, there must exist
strings X1 and X2 such that S = (X1..., X2...) where R1 matches X1 and
R2 matches X2. But R1 is a singleton pattern that only matches H1, so
X1 must be H1. Since we have both
S = (H1..., X2...) and
S = (H1..., AH_SIMPLIFY(AH(A, P2))...)
we must also have
X2 = AH_SIMPLIFY(AH(A, P2))
which means that R2 matches AH(A, P2).
Qed.
Lemma [history-path-narrowing]. (tree-independent)
If P = (P', X -> Y), then AH(A, P) = AH(A, P').
If P = (X -> Y, P'), then AH(A, P) = AH(A, P').
Pf.
In the first case, by substitution and [history-concatenation],
AH(A, P) = AH(A, (P', X -> Y)) = (AH(A, P')..., AH(A, (X -> Y))...)
but AH(A, (X -> Y)) = () because an edge is neither an allocation nor
a deallocation, so this concatenation simplifies to
AH(A, P) = AH(A, P')
In the second case, by substitution and [history-concatenation],
AH(A, P) = AH(A, (X -> Y, P1)) = (AH(A, (X -> Y))..., AH(A, P')...)
but AH(A, (X -> Y)) = () because an edge is neither an allocation nor
a deallocation, so this concatenation simplifies to
AH(A, P) = AH(A, P')
Qed.
Lemma [history-instruction-simple]. (tree-independent)
If P is an instruction-simple path, R matches AH(A, P) if and only if
R matches AH(A, P'), where P' is the instruction-final prefix of P.
Pf.
If P does not end in an edge: P = P', so this is trivially true.
If P does end in an edge:
P = (P'..., X -> Y), so by [history-concatenation],
AH(A, P) = AH(A, (P'..., X -> Y))
= (AH(A, P')..., AH(X -> Y)...)
= AH(A, P')
Qed.
Lemma [history-proper-ancestor]. (tree-independent)
If P is a path to a point in a proper ancestor SCC of some SCC C, and
A is an allocation in C, then // matches AH(A, P).
Pf.
Let P_I be an initial path to the start point of P, and let
P' = (P_I..., P...)
P' passes only through components in proper ancestor SCCs of C, so
it does not pass through A, and so its subpath P does not pass through
A. If P' passed through a deallocation D of A, then since it is an
initial path, it would also have to pass through A by the dominance
axioms, which contradicts the previous conclusion; therefore P' does
not pass through a deallocation of A, and so its subpath P does not
either. Therefore P does not pass through either an allocation or a
deallocation of A, which is to say that AH(A, P) = () and therefore
// matches AH(A, P).
Qed.
Lemma [cyclic-scc-history]. (tree-independent)
Let P be a reachable cyclic path. Then for all allocations A,
/((ad)*|(da)*)/ must match AH(A, P).
Pf.
Since P is reachable, there exists a path P_E from the entry
point to the start point of P. Then
P' := (P_E..., P...)
P'' := (P_E..., P..., P...)
are both well-formed initial paths. Now let an allocation A be
given.
AH(A, P') must match /(ad)*a?/ by the axioms. This constrains
AH(A, P) to match some suffix of that pattern, which (per standard
regular expression theory) means it must match a pattern that can
be expressed as the choice between patterns produced by taking
a suffix at each of the sequence positions in the pattern. This
produces /((ad)*a?|d(ad)*a?|a?|)/, which can be simplified to
/((ad)*a?|d(ad)*a?)/ because /a?/ and // are subsets of /(ad)*a?/.
But AH(A, P'') must also match /(ad)*a?/ by the axioms. Since P''
contains P twice sequentially, P cannot start and end with the
same kind of instruction, or else P'' will have that sequence.
We can restrict the pattern by "unrolling" each non-empty choice
to make it start and end with a terminal, adding back options
that this excluded:
First, forcing a terminal at the beginning of each clause:
/(ad)*a?/ => /ad(ad)*a?/ (adding back /a?/)
/a?/ => /a/ (adding back //)
// => //
/d(ad)*a?/ => /d(ad)*a?/
Next, forcing a terminal at the ending of each clause:
/ad(ad)*a?/ => /ad(ad)*a/ (adding back /ad(ad)*/)
/ad(ad)*/ => /ad(ad)*ad/ (adding back /ad/)
/ad/ => /ad/
/a/ => /a/
// => //
/d(ad)*a?/ => /d(ad)*a/ (adding back /d(ad)*/)
/d(ad)*/ => /d(ad)*ad/ (adding back /d/)
/d/ => /d/
Now we can exclude choices that begin and end with the same terminal.
/ad(ad)*a/ => excluded
/ad(ad)*ad/ => included
/ad/ => included
/a/ => excluded
// => included
/d(ad)*a/ => included
/d(ad)*ad/ => excluded
/d/ => excluded
And then reassemble the choice:
/(ad(ad)*ad|ad||d(ad)*a)/
which is equivalent to /((ad)*|(da)*)/.
Qed.
Lemma [history-reverse-concatenation]. (tree-independent)
If /(R1)(R2)/ matches AH(A, P), then there exist P1 and P2 such that
P = (P1..., P2...)
where R1 matches AH(A, P1) and R2 matches AH(A, P2).
Pf.
AH_SIMPLIFY(AH(A, P)) is in the language of /(R1)(R2)/.
By the definition of the language of a concatenation regular expression,
there exist simplified allocation histories S1 and S2 where
AH_SIMPLIFY(AH(A, P)) = (S1..., S2...)
such that R1 matches S1 and R2 matches S2.
Let
H1 := PREFIX(AH(A, P), LENGTH(S1))
H2 := SUFFIX(AH(A, P), LENGTH(S1))
Then
H := (H1..., H2...)
= (PREFIX(AH(A, P), LENGTH(S1))...,
SUFFIX(AH(A, P), LENGTH(S1))...)
= AH(A, P)
AH_SIMPLIFY is a MAP operation, which preserves length.
LENGTH(H1) = LENGTH(PREFIX(AH(A, P), LENGTH(S1)))
= LENGTH(S1)
LENGTH(H2) = LENGTH(SUFFIX(AH(A, P), LENGTH(S1)))
= LENGTH(AH(A, P)) - LENGTH(S1)
= LENGTH(AH_SIMPLIFY(AH(A, P))) - LENGTH(S1)
= LENGTH((S1..., S2...)) - LENGTH(S1)
= LENGTH(S1) + LENGTH(S2) - LENGTH(S1)
= LENGTH(S2)
So S1 = AH_SIMPLIFY(H1) and S2 = AH_SIMPLIFY(H2).
By substitution and the definition of matching an allocation history,
R1 matches H1 and R2 matches H2.
AH(A,_) is a PATH_FLAT_MAP, so for all P1 and P2,
AH(A,(P1..., P2...)) = (AH(A,P1)..., AH(A,P2)...)
So since AH(A,P) = (H1..., H2...), there must be paths P1 and P2
such that P = (P1, P2), AH(A, P1) = H1, and AH(A, P2) = H2.
Then by substitution, R1 matches AH(A, P1) and R2 matches AH(A, P2).
Qed.
Lemma [history-reverse-partition]. (tree-independent)
Let R2 = /a/ or /d/. Then if /(R1)(R2)(R3)/ matches AH(A, P), then
there exist P_I and P_F such that
P = (P_I..., K, P_F...)
and where R1 matches AH(A, P_I), R3 matches AH(A, P_F), and
K is either A (if R2 = /a/) or a deallocation of A (if R2 = /d/).
Pf.
By [history-reverse-concatenation], there exist P1 and P2' such that
P = (P1..., P2'...)
and where R1 matches P1 and /(R2)(R3)/ matches P2'. Applying it again,
there exist P2 and P3 such that
P2' = (P2..., P3...)
and where R2 matches P2 and /R3/ matches P3.
If R2 = /a/, then AH(A, P2) must be (A); let K = A.
If R2 = /d/, then AH(A, P2) must be (D) for some deallocation D of A;
let K = D.
K is on P2, so there exist PK1 and PK2 such that
P2 = (PK1..., K, PK3...)
Then by substitution
P = (P1..., P2'...)
= (P1..., P2..., P3...)
= (P1..., PK1..., K, PK3..., P3...)
So let P_I = (P1..., PK1...) and P_F = (PK3..., P3...); then
P = (P_I..., K, P_F...)
By substitution
AH(A, P2) = AH(A, (PK1..., K, PK3...))
= (AH(A, PK1)..., AH(A, (K))..., AH(A, PK3)...)
But we know that AH(A, P2) = AH(A, (K)), so AH(A, PK1) = ()
and AH(A, PK3) = ().
Therefore
AH(A, A_I) = AH(A, (P1..., PK1...))
= (AH(A, P1)..., AH(A, PK1)...)
= (AH(A, P1)..., ()...)
= AH(A, P1)
and therefore R1 matches AH(A, A_I).
Similarly
AH(A, A_F) = AH(A, (PK3..., P3...))
= (AH(A, PK3)..., AH(A, P3)...)
= (()..., AH(A, P3)...)
= AH(A, P3)
and therefore R3 matches AH(A, A_F).
Qed.
Proof of the algorithm
----------------------
Lemma [operation-sequence-concatenation-path]. (tree-independent)
If PXY is the path in T from X to Y, PYZ is the path in T from Y to Z,
SX is the state at X under T, and SY is the end state of
OS_PATH(SX, PXY, T), then
OS_PATH(SX, (PXY..., PYZ...), T) = (OS_PATH(SX, PXY, T)...,
OS_PATH(SY, PYZ, T)...)
Pf.
By induction on PYZ.
For the base case, PYZ is an empty path. By the definiton of OS_PATH
OS_PATH(SY, PYZ, T) = [ SY ]
Then consider
(OS_PATH(SX, PXY, T)..., OS_PATH(SY, PYZ, T)...)
Since SY is the end state of OS_PATH(SX, PXY, T), this concatenation is
proper and simplifies to OS_PATH(SX, PXY, T). Meanwhile, by substitution
OS_PATH(SX, (PXY..., PYZ...), T) = OS_PATH(SX, (PXY..., ()...), T)
= OS_PATH(SX, PXY, T)
So the equality holds by transitivity.
For the inductive case, PYZ = (PYZ'..., K), and by the induction hypothesis
OS_PATH(SX, (PXY..., PYZ'...), T) = (OS_PATH(SX, PXY, T)...,
OS_PATH(SY, PYZ', T)...)
Let SZ be the end state of OS_PATH(SX, (PXY..., PYZ'...), T). By this
equality, and the properties of operation sequence concatenation, SZ is also
the end state of OS_PATH(SY, PYZ', T).
OS_PATH(SY, PYZ, T)
= OS_PATH(SY, (PYZ'..., K), T) (by substitution)
= (OS_PATH(SX, PYZ', T)..., (by the OS_PATH definition)
OPERATION(SZ, K, T)...)
OS_PATH(SX, (PXY..., PYZ...), T)
= OS_PATH(SX, (PXY..., PYZ'..., K), T) (by substitution)
= OS_PATH(SX, ((PXY..., PYZ'...)..., K), T) (by associativity)
= (OS_PATH(SX, (PXY..., PYZ'...), T)..., (by the OS_PATH definition)
OPERATION(SZ, K, T)...)
= ((OS_PATH(SX, PXY, T)..., (by the I.H.)
OS_PATH(SY, PYZ', T)...)...,
OPERATION(SZ, K, T)...)
= (OS_PATH(SX, PXY, T)..., (by associativity)
(OS_PATH(SY, PYZ', T)...,
OPERATION(SZ, K, T)...)...)
= (OS_PATH(SX, PXY, T)..., (by the equation above)
OS_PATH(SY, PYZ, T)...)
Qed.
Lemma [entry-state]. (tree-independent)
For all spanning trees T, the state at the entry point under T is
STATE_EMPTY.
STATE_EMPTY is a well-formed state.
Pf.
Let a spanning tree T be given, and let P be the path to the entry point
in T. This is necessarily the empty path. By definition
OS(P, T) = OS_PATH(STATE_EMPTY, P, T)
Applying the definition of OS_PATH
OS(P, T) = [ STATE_EMPTY ]
So the end point of OS(P, T) is STATE_EMPTY, which is to say, the state
at the entry point under T is STATE_EMPTY.
STATE_EMPTY is well-formed because it contains no allocations, so no
allocations can repeat in it and the allocations having undeallocatable
status trivially form a prefix of the state.
Qed.
Lemma [operation-sequence-path-helper]. (tree-independent)
If SX is the state at X under T, and PXY is the path in T from X to Y,
then the end state of OS_PATH(SX, PXY, T) is SY, the state at Y under T.
Pf.
By the definition of state at a point, SX and SY are the end states of
OS(PX, T) and OS(PY, T), where PX and PY are the paths to X and Y in T.
Because T is a tree, it must be true that
PY = (PX..., PXY...)
Expanding the definition of OS:
OS(PX, T) = OS_PATH(STATE_EMPTY, PX, T)
OS(PY, T) = OS_PATH(STATE_EMPTY, PY, T)
So SX is the end state of OS_PATH(STATE_EMPTY, PX, T).
By [entry-state], EMPTY_STATE is the state at the entry under point under T.
Therefore
OS(PY, T) = OS_PATH(STATE_EMPTY, PY, T)
= (OS_PATH(STATE_EMPTY, PX, T)....
OS_PATH(SX, PXY, T)...)
(by [operation-sequence-concatenation-path])
So SY, the end state of OS(PY, T), must also be the end state of
OS_PATH(SX, PXY, T).
Qed.
Lemma [operation-sequence-concatenation]. (tree-independent)
If PXY is the path in T from X to Y, PYZ is the path in T from Y to Z,
SX is the state at X under T, and SY is the state at Y under T, then
OS_PATH(SX, (PXY..., PYZ...), T) =
(OS_PATH(SX, PXY, T)..., OS_PATH(SY, PYZ, T)...)
And in particular,
OS((PXY..., PYZ...), T) = (OS(PXY, T)..., OS_PATH(SY, PYZ, T)...)
Pf.
By [operation-sequence-path-helper], SY is the end state of
OS_PATH(SX, PXY, T). Then by [operation-sequence-concatenation-path],
OS_PATH(SX, (PXY..., PYZ...), T) =
(OS_PATH(SX, PXY, T)..., OS_PATH(SY, PYZ, T)...)
OS((PXY..., PYZ...), T)
= OS_PATH(STATE_EMPTY, (PXY..., PYZ...), T) (by the OS definition)
= (OS_PATH(STATE_EMPTY, PXY, T)...,
OS_PATH(SY, PYZ, T)...) (by the above)
= (OS(PXY, T)..., OS_PATH(SY, PYZ, T)...) (by the OS definition)
Qed.
Lemma [correctness-operation-sequence-helper]. (tree-independent)
For all spanning trees T1, if
- P is a simple path in T1 from X to Y,
- SX and SY are the states at X and Y under T1,
- SX is well-formed, and
- if SX contains some allocation A, then the path to X in T passes
through A,
then
- OS_PATH(SX, P, T1) is well-formed under T1.
- If SY contains some allocation A, but SX does not, then P passes
through A.
- If P is a life-preserving path, then for all spanning trees T2,
if SX is also the state at X under T2, then
OS_PATH(SX, P, T1) = OS_PATH(SX, P, T2).
Pf.
By induction on P.
If P is the empty path, then:
By the definition of OS_PATH
OS_PATH(SX, (), T1) = [ SX ]
[ SX ] is a well-formed operation sequence under T1:
The only state in [ SX ] is SX, and SX is well-formed by assumption.
There are no operations in [ SX ], so all such operations are vacuously
well-formed under T for the transition from their preceding state to
their following state.
If SY contains some allocation A, but SX does not, then P passes through A:
Let A be given. By assumption, the end state of [ SX ] contains A; but
the end state of [ SX ] is just SX, so this means SX contains A. This
contradicts the assumption that SX does not contain A. So this holds
(vacuously) for all allocations A.
If P is a life-preserving path...:
It is also true by the definition of OS_PATH that
OS_PATH(SX, (), T2) = [ SX ]
so OS_PATH(SX, (), T1) = [ SX ] = OS_PATH(SX, (), T2).
Otherwise, P = (P'..., K), and Y = K>. Let SK be the end state of
OS_PATH(SX, P', T1). Then by the definition of OS_PATH,
OS_PATH(SX, P, T1) = (OS_PATH(SX, P', T1)..., OPERATION(SK, K, T1)...)
Lemma [helper-induction-well-formed].
If OPERATION(SK, K, T1) = [ SK, O, SY ], and SY is well-formed,
and O is well-formed for the transition from SK to SY under T1,
then OS_PATH(SX, P, T1) is well-formed under T1.
Pf.
By the induction hypothesis, OS_PATH(SX, P', T1) is well-formed under T1,
and its end state is SK by construction. Then the concatenation in
OS_PATH(SX, P, T1) = (OS_PATH(SX, P', T1)..., OPERATION(SK, K, T1)...)
is proper, and this simplifies to
OS_PATH(SX, P, T1) = [ OS_PATH(SX, P', T1)..., O, SY ]
Then the states in OS_PATH(SX, P, T1) are the states in OS_PATH(SX, P', T1),
plus SY; the former are all well-formed because OS_PATH(SX, P', T1) is
well-formed, and the latter is well-formed by assumption.
Similarly, the operation triples in OS_PATH(SX, P, T1) are the triples
in OS_PATH(SX, P', T1), plus [ SX, O, SY ]; the former are all well-formed
under T1 because OS_PATH(SX, P', T1) is well-formed under T1, and
the latter is well-formed under T1 because O is well-formed for the
transition from SX to SY under T1 by assumption.
Qed.
Lemma [helper-induction-allocation-present].
If SK contains some allocation A, but SX does not, then P passes through A:
Pf.
By the induction hypothesis, P' passes through A, so P also passes through
A because P' is a subpath of P.
Qed.
Lemma [helper-induction-life-preserving].
If P is a life-preserving path, and T2 is a spanning tree, and SX is the
state at X under T2, and
OPERATION(SK, K, T1) = OPERATION(SK, K, T2)
then
OS_PATH(SX, P, T1) = OS_PATH(SX, P, T2).
Pf.
Let T2 be given, and suppose that SX is also the state at X under T2.
By the definition of OS_PATH
OS_PATH(SX, P, T2) = (OS_PATH(SX, P', T2)..., OPERATION(SK2, K, T2)...)
where SK2 is the end state of OS_PATH(SX, P', T2).
P' is a life-preserving path because it is a subpath of P and therefore
cannot pass through any components that P does not pass through. So by the
induction hypothesis, OS_PATH(SX, P', T1) = OS_PATH(SX, P', T2). In
particular, SK2 = SK.
Therefore we can substitute:
OS_PATH(SX, P, T2) = (OS_PATH(SX, P', T2)..., OPERATION(SK2, K, T2)...)
= (OS_PATH(SX, P', T1)..., OPERATION(SK2, K, T2)...)
= (OS_PATH(SX, P', T1)..., OPERATION(SK, K, T2)...)
= (OS_PATH(SX, P', T1)..., OPERATION(SK, K, T1)...)
= OS_PATH(SX, P, T1)
Qed.
By the induction hypothesis, OS_PATH(SX, P', T1) is well-formed under T1,
and SK is a state on OS_PATH(SX, P', T1), so SK is well-formed under T1.
Now we can expand the definition of OPERATION(SK, K, T1).
If K is an allocation A:
Then the definition in this case gives
OPERATION(SK, K, T1) = [ SK, PUSH@A, STATE_PUSH(SK, A) ]
Therefore SY = STATE_PUSH(SK, A).
STATE_PUSH(SK, A) has a precondition that SK must not contain A.
Let PX be the path to X in T, and let
PK' = (PX..., P'...)
PK = (PX..., P'..., K)
Suppose that SK contains A. Then PK' must pass through A:
Either SX contains A or not. If SX contains A, then by assumption,
PX passes through A, and so PK' passes through A because PX is a
subpath of PK'. If SX does not contain A, then by the induction
hypothesis, P' passes through A, and so PK' passes though A because
P' is a subpath of PK'.
PK is a concatenation of paths that are all in the same tree T, so it
must also be a simple path. But PK' is a subpath of PK, and because
PK' passes though K, it visits K> by definition; and (K) also visits
K>, and not at index 0; so the concatenation of K' and (K), i.e. PK,
must visit K> twice, which is a contradiction. Therefore SK does not
contain A.
SY is a well-formed state:
By the definition of STATE_PUSH
SY = (SK..., (A, allocated))
SY[I][0] = SY[J][0] => I = J:
If either I or J is LENGTH(SK), the allocation there in SY is A;
by the precondition we just proved, there is no index I' in SK such
that SY[I'][0] = A, so the other index must also be LENGTH(SK).
Otherwise both I and J are < LENGTH(SK), so SY[I][0] = SK[I][0] and
SY[J][0] = SK[J][0], so if SY[I][0] = SY[J][0] then SK[I][0] = SK[J][0]
and thus I = J because SK is well-formed.
SY[I][1] = undeallocatable /\ SY[J][1] != undeallocatable => I < J:
I must be < LENGTH(SK) because the status there is allocated.
If J = LENGTH(SK) then I < J. Otherwise SY[I][1] = SK[I][1]
and SY[J][1] = SK[J][1], so this holds because SK is well-formed.
PUSH@A changes SK to SY under T:
OPERATION_APPLY(SK, PUSH@A) = STATE_PUSH(SK, A)
= SY
PUSH@A is well-formed for the transition from SK to SY in T1:
PUSH@A satisfies its component restriction because A is an allocation.
All of the other requirements have just been proven.
So OS_PATH(SX, P, T1) is well-formed by [helper-induction-well-formed].
If SY contains some allocation A', but SX does not, then P passes through A':
Suppose that SY contains A' but SX does not. Then applying [push-cases]
to A', one of the following is true:
If #top: A' = A = K. P passes through K by assumption, so P passes
through A.
If #prefix: Since SY contains A, SK contains A, and P passes through
A by [helper-induction-allocation-present].
If P is a life-preserving path...:
Let T2 be given, and suppose that SX is the state at X under T2. Then
OPERATION(SK, K, T2) = [ SK, PUSH@A, STATE_PUSH(SK, A) ]
because K is an allocation and there are no earlier cases of OPERATION.
Therefore OPERATION(SK, K, T2) = OPERATION(SK, K, T1), so this
conclusion holds by [helper-induction-life-preserving].
If K is a deallocation D of an allocation A, and A has allocated
status and is at the top of the stack in SK:
Then the definition in this case gives
OPERATION(SK, K, T1) = [ SK, POP@D, STATE_POP(SK, A) ]
Therefore SY = STATE_POP(SK, A).
STATE_POP(SK, A) has a precondition that the last element of SK is
(A, allocated). This comes directly from the conditions on this case.
SY is a well-formed state:
SY = STATE_POP(SK, A) = PREFIX(SK, L) for some L whose exact value is
unimportant here.
SY[I][0] = SY[J][0] => I = J:
By the axioms of PREFIX, SY[I][0] = SK[I][0] and SY[J][0] = SK[J][0],
so this holds because SK is well-formed.
SY[I][1] = undeallocatable /\ SY[J][1] != undeallocatable => I < J:
By the axioms of PREFIX, SY[I][1] = SK[I][1] and SY[J][1] = SK[J][1],
so this holds because SK is well-formed.
POP@D changes SK to SY:
OPERATION_APPLY(SK, POP@D) = STATE_POP(SK, DEALLOCATION(D))
= STATE_POP(SK, A)
= SY
POP@D is well-formed for the transition from SK to SY in T1:
POP@D satisfies its component restriction because D is a deallocation.
All of the other requirements have just been proven.
So OS_PATH(SX, P, T1) is well-formed by [helper-induction-well-formed].
If SY contains some allocation A', but SX does not, then P passes through A':
Suppose that SY contains an allocation A' but SX does not. Then applying
[pop-preserves-prefix] to A', SK contains A'. Then P passes through A'
by [helper-induction-allocation-present].
If P is a life-preserving path...:
Let T2 be given, and suppose that SX is the state at X under T2. Then
OPERATION(SK, K, T2) = [ SK, POP@D, STATE_POP(SK, A) ]
because the first case of OPERATION does not apply because K is not an
allocation, and K is a deallocation D of A.
Therefore OPERATION(SK, K, T2) = OPERATION(SK, K, T1), so this
conclusion holds by [helper-induction-life-preserving].
If K is a deallocation D of an allocation A, and A has allocated
status and is not at the top of the stack in SK:
Then the definition in this case gives
OPERATION(SK, K, T1) = [ SK, PEND@D, STATE_PEND(SK, A) ]
Therefore SY = STATE_PEND(SK, A).
STATE_PEND(SK, A) has a precondition that SK has an element (A, allocatd)
that is not the last element. This comes directly from the conditions on
this case.
SY is a well-formed state:
SY = SET_STATUS(SK, AI, pending), where AI is the index of A in SK.
Because (A, allocated) must be in SK, there is such an index AI, and so
AI < LENGTH(SK), satisfying the precondition of SET_STATUS.
SY[I][0] = SY[J][0] => I = J:
SY[I][0] = SK[I][0] and SY[I][0] = SK[I][0] by the axioms of
SET_STATUS, so this holds because SK is well-formed.
SY[I][1] = undeallocatable /\ SY[J][1] != undeallocatable => I < J:
If I = AI, then SY[I][1] = pending, which is a contradiction.
So I != AI and SY[I][1] = SK[I][1] = undeallocatable.
If J = AI, then SY[J][1] = pending and SY[J][1] = allocated;
then since SK is well-formed, I < J.
If J != AI, then SY[J][1] = SK[J][1]; then since SK is well-formed,
I < J.
PEND@D changes SK to SY:
OPERATION_APPLY(SK, PEND@D) = STATE_PEND(SK, DEALLOCATION(D))
= STATE_PEND(SK, A)
= SY
PEND@D is well-formed for the transition from SK to SY in T1:
PEND@D satisfies its component restriction because D is a deallocation.
All of the other requirements have just been proven.
So OS_PATH(SX, P, T1) is well-formed by [helper-induction-well-formed].
If SY contains some allocation A', but SX does not, then P passes through A':
Suppose that SY contains an allocation A' but SX does not. Then applying
[pend-preserves-allocation-set] to A', SK contains A'. Then P passes
through A' by [helper-induction-allocation-present].
If P is a life-preserving path...:
Let T2 be given, and suppose that SX is the state at X under T2. Then
OPERATION(SK, K, T2) = [ SK, POP@D, STATE_PEND(SK, A) ]
because the first case of OPERATION does not apply because K is not an
allocation, the second case of OPERATION does not apply because A
is not at the end of SK, and K is a deallocation D of A.
Therefore OPERATION(SK, K, T2) = OPERATION(SK, K, T1), so this
conclusion holds by [helper-induction-life-preserving].
If K is a conservative entry edge M -> N into a dead-end region R:
Then the definition in this case gives
OPERATION(SK, K, T1) = [ SK, MERGE@(M -> N), STATE_MERGE(SK, PSS(R, T1)) ]
Therefore SY = STATE_MERGE(SK, PSS(R, T1)).
STATE_MERGE(SK, PSS(R, T1)) has a precondition that SK is in PSS(R, T1).
PSS(R, T1) is the set of all end states under T1 of all blocks that have
entry edges into R. M has an entry edge into R, namely M -> N. P' is the
path from X to M> in T1, and SX is the state at X under T1, so SK,
the end state of OS_PATH(SX, P', T1), is the state at M> under T1 by
[operation-sequence-path-helper]. Therefore SK is in PSS(R, T1).
SY is a well-formed state:
SY = CONSERVATIVE_MERGE(PSS(R, T1)). SK is a member of PSS(R, T1) by
the precondition.
SY[I][0] = SY[J][0] => I = J:
By [conservative-merge-index], SY[I][0] = SK[I][0] and
SY[J][0] = SK[J][0]. So this holds because SK is well-formed.
SY[I][1] = undeallocatable /\ SY[J][1] != undeallocatable => I < J:
By the axioms of CONSERVATIVE_MERGE, there is no J where
SY[J][1] != undeallocatable, so this is vacuously true.
MERGE@(M -> N) changes SK to SY:
OPERATION_APPLY(SK, MERGE@(M -> N)) = STATE_MERGE(SK, PSS(SCC(N), T1))
= STATE_MERGE(S', PSS(R, T1))
= SY
MERGE@(M -> N) is well-formed for the transition from SK to SY in T1:
PEND@(M -> N) satisfies its component restriction because M -> N is a
conservative entry edge. All of the other requirements have just been
proven.
So OS_PATH(SX, P, T1) is well-formed by [helper-induction-well-formed].
If SY contains some allocation A', but SX does not, then P passes through A':
Suppose that SY contains an allocation A', and let I be the index of A'
in SY. SK is in SY by the precondition proven above.
SY = CONSERVATIVE_MERGE(PSS(R, T1)), so by [conservative-merge-index],
SY[I][0] = A', so SK[I][0] = A'. Therefore SK contains A, so by
[helper-induction-allocation-present], P passes through A'.
If P is a life-preserving path...:
P passes through M -> N, an entry edge into a dead-end region, which
contradicts the assumption that P is a life-preserving path.
Otherwise:
The definition in this case gives
OPERATION(SK, K) = [ SK ]
So SY = SK and OS_PATH(SX, P, T1) = OS_PATH(SX, P', T1).
OS_PATH(SX, P, T1) is therefore well-formed by the induction hypothesis.
If SY contains some allocation A', but SX does not, then P passes through A':
Suppose that SY contains an allocation A', but SX does not. SY = SK,
so SK contains A'; therefore P passes through A' by
[helper-induction-allocation-present].
If P is a life-preserving path...:
Let T2 be given, and suppose that SX is the state at X under T2. Then
OPERATION(SK, K, T2) = [ SK ]
because the first three cases of OPERATION do not apply because K is
not an allocation or deallocation, and the fourth case of OPERATION
does not apply because, if K were any kind of entry edge into a dead-end
region (conservative or not), then since P passes through K, the
assumption that P is a life-preserving path would be contradicted.
Therefore OPERATION(SK, K, T2) = OPERATION(SK, K, T1), so this
conclusion holds by [helper-induction-life-preserving].
So the conclusions hold for P no matter what case of OPERATION applies. By
induction, they holds for all paths in T.
Qed.
Theorem [correctness-operation-sequence].
If P is an initial path in T, OS(P) is well-formed.
Pf.
The start point of P is the entry point, so by [entry-state], the state at
the start point of P is STATE_EMPTY, and STATE_EMPTY is well-formed.
Let OSP = OS_PATH(STATE_EMPTY, P). OSP is well-formed under T by
[correctness-operation-sequence-path], and by definition OS(P) = OSP.
Qed.
Theorem [correctness-state].
The state at any point is a well-formed state.
Pf.
Let an arbitrary point X be given, and let P be the path to X in T.
By definition, the state SX at X is the end state of OS(P).
By [correctness-operation-sequence], OS(P) is a well-formed operation
sequence, which means all states in it are well-formed, including
the end state SX. So the state at X is a well-formed state.
Qed.
Lemma [present-implies-visit-path].
If P is a path in T, and the state at the start point of P does not
contain an allocation A, and the state at the end point of P does,
then P passes through A.
Pf.
Let S be the state at the start point of P. By [correctness-state],
S is well-formed. Then by [correctness-operation-sequence-path],
P passes through A.
Qed.
Lemma [present-implies-visit].
If the state S at X contains an allocation A, the path P to X in T
passes through A.
Pf.
The start point of P is the entry point, so by [entry-state], the state at
the start point of P is STATE_EMPTY, and STATE_EMPTY is well-formed.
By definition, the state at X is the end point of OS(P).
Let OSP = OS_PATH(STATE_EMPTY, P); by definition, OS(P) = OSP, so
S must be the end state of OSP. Then P passes through A by
[correctness-operation-sequence-path],
Qed.
Lemma [operation-sequence-path].
If P is a path in T, then let S be the state at the start point of P.
OS_PATH(S, P) is a well-formed operation sequence, and the end state of
OS_PATH(S, P) is the state of the end point of P.
Pf.
Let X be the end point of P, and let P_EX be the path to X in T;
because T is a spanning tree, P must be a suffix of P_EX. Then by
[operation-sequence-concatenation],
OS(P_EX) = (OS(P_I)..., OS_PATH(S, P)...)
OS_PATH(S, P) is therefore a suffix of OS(P_EX). Since OS(P_EX) is
well-formed by [correctness-operation-sequence], OS_PATH(S, P) is as well.
Since the state at X is by definition the end state of OS(P_EX), and the
end states of OS(P_EX) and OS_PATH(S, P) are the same, the end state of
OS_PATH(S, P) is the same as the state at X.
Qed.
Lemma [operation-sequence-component].
For all point-graph components K in T, if S' is the state at <K and
S is the state at K>, then OPERATION(S', K) is a well-formed operation
sequence whose initial state is S' and whose final state is S; it
contains at most one operation.
Pf.
Let P be the path to K> in T. T is a spanning tree, so necessarily,
P = (P_K..., K)
By the definition of the state at a point, the state at K> is the end
state of OS(P).
By the definition of OS and OS_PATH
OS(P) = OS_PATH(STATE_EMPTY, P)
= OS_PATH(STATE_EMPTY, (P_K..., K))
= (OS_PATH(STATE_EMPTY, P_K)..., OPERATION(S'', K)...)
where S'' is the end state of OS_PATH(STATE_EMPTY, P_K), which by
definition is the state of the end point of P_K, i.e. <K. Therefore
S'' = S'. This must be the start state of OPERATION(S'', K) by the
properness of the concatenation.
By the path equality above, the end state of OPERATION(S', K) must be
the same as the end state of OS(P), i.e. to S.
By [correctness-operation-sequence], OS(P) is a well-formed operation
sequence, so its subpath OPERATION(S', K) is also a well-formed
operation sequence. By inspection of OPERATION, OPERATION(S', K) is
either empty or a single operation.
Qed.
Lemma [operation-sequence-instruction].
For all instructions I, if S' is the state at <I and S is the state
at I>, then OPERATION(S', I) is either the empty operation sequence
(in which case S' = S) or a single operation that changes S' to S.
Pf.
All instructions are in T, so this follows immediately from
[operation-sequence-component].
Qed.
Lemma [operation-cases-component].
For all components K in T, let S' be the state at <K and S be the
state at K>. Then one of the following is true:
#push PUSH@K is well-formed for the transition from S' to S;
#pop POP@K is well-formed for the transition from S' to S;
#pend PEND@K is well-formed for the transition from S' to S;
#merge MERGE@K is well-formed for the transition from S' to S; or
#other S = S', K is not an allocation, K is not a conservative entry edge,
and either (K is not a deallocation of an allocation A, or
A does not have allocated status in S').
Pf.
By [operation-sequence-component], OPERATION(S', K) is well-formed,
its start state is S', and its end state is S.
Applying the definition of OPERATION:
If K is an allocation:
By definition
OPERATION(S', K) = [ S', PUSH@K, STATE_PUSH(S', K) ]
Because this is a well-formed operation sequence, PUSH@K is well-formed
for the transition from S' to S.
So this satisfies the requirements of the #push case.
If K is a deallocation of an allocation A, and A has allocated status
and is at the top of the stack in S':
By definition
OPERATION(S', K) := [ S', POP@K, STATE_POP(S', A) ]
Because this is a well-formed operation sequence, POP@K is well-formed
for the transition from S' to S.
So this satisfies the requirements of the #pop case.
If K is a deallocation of an allocation A, and A has allocated status
and is no at the top of the stack in S':
By definition
OPERATION(S', K) := [ S', PEND@K, STATE_PEND(S', A) ]
Because this is a well-formed operation sequence, PEND@K is well-formed
for the transition from S' to S.
So this satisfies the requirements of the #pend case.
If K is a conservative entry edge into a dead-end region R:
By definition
OPERATION(S', K) := [ S', MERGE@K, STATE_MERGE(S', PSS(R)) ]
Because this is a well-formed operation sequence, MERGE@K is well-formed
for the transition from S' to S.
So this satisfies the requirements of the #merge case.
Otherwise:
By definition
OPERATION(S', C) := [ S' ]
S is the same as the end state, so S = S'.
Because OPERATION did not use the first case, K cannot be an allocation.
Because OPERATION did not use the fourth case, K is not a conservative
entry edge.
Let
PredA := K is not a deallocation of A
PredB := A is not the top of the stack in S'
PredC := S' does not give A allocated status
Because OPERATION did not use the second case,
PredA \/ PredB \/ PredC
Because OPERATION did not use the third case,
PredA \/ !PredB \/ PredC
Together, these simplify to PredA \/ PredC.
So this satisfies the requirements of the #other case.
Qed.
Lemma [operation-allocation].
If A is an allocation, then PUSH@A is well-formed for the transition
from the state at <A to the state at A>.
Pf.
By [operation-cases-component], one of five cases applies:
If #push: The lemma holds by the conditions of this case.
If #pop: A is not a deallocation, so this a contradiction.
If #pend: A is not a deallocation, so this a contradiction.
If #merge: A is not an edge, so this a contradiction.
If #other: A is an allocation, so this is a contradiction.
Qed.
Lemma [allocation-state].
If A is an allocation, then the state at A> gives A allocated status.
Pf.
Let SA' and SA be the state at <A and A>, respectively.
By [operation-allocation], PUSH@A is well-formed for the transition
from SA' to SA. By [push-allocated], SA gives A allocated status.
Qed.
Lemma [operation-cases-edge].
For all edges M -> N in T, let SM be the state at M> and SN be the
state at <N. Then one of the following is true:
#merge: MERGE@(M -> N) is well-formed for a transition from SM to SN; or
#other: M -> N is not a conservative entry edge, and SM = SN.
Pf.
By [operation-cases-component], one of five cases applies:
If #push: M -> N is not an allocation, so this is a contradiction.
If #pop: M -> N is not a deallocation, so this is a contradiction.
If #pend: M -> N is not a deallocation, so this is a contradiction.
If #merge: All of the conditions of this lemma's #merge case hold.
If #other: All of the conditions of this lemma's #other case hold.
Qed.
Lemma [operation-non-dead-entry-edge].
If M -> N is an edge in T that is not an entry edge into a dead-end
region, then the state SM at M> is the same as the state SN at <N.
Pf.
By [operation-cases-edge], one of two cases applies:
If #merge: Since MERGE@(M -> N) is well-formed for a transition from
SM to SN, M -> N is a conservative entry edge; but this contradicts
the assumption that it is not an entry edge into a dead-end region.
If #other: In this case, SM = SN.
Qed.
Lemma [operation-cases-dead-entry-edge].
If M -> N is a entry edge in T into a dead-end region R, and SM
is the state at M> and SN is the state at <N, then SM is in PSS(R),
and exactly one of the following is true:
#merge: MERGE@(M -> N) is well-formed for a transition from SM to SN,
M -> N is a conservative entry edge, PSS(R) is not singleton,
and SN = CONSERVATIVE_MERGE(PSS(R)); or
#other: M -> N is not a conservative entry edge, PSS(R) = { SN },
and SM = SN.
Pf.
By [operation-cases-edge], there are two cases:
If #merge:
Under this case, MERGE@(M -> N) is well-formed for the transition from
SM to SN. So M -> N is a conservative entry edge and
SN = CONSERVATIVE_MERGE(PSS(R)).
PSS(R) contains SM by the well-formedness conditions.
By the definition of a conservative entry edge, PSS(R) is not singleton.
So this lemma's #merge case holds. #other does not hold because
M -> N is a conservative entry edge.
If #other:
Under this case, M -> N is not a conservative entry edge and SM = SN.
Since M -> N satisfies all the other conditions of being a conservative
entry edge, PSS(R) must be singleton. M -> N is an entry edge into R,
so SM, as the state at M>, in in PSS(R) by definition. Since PSS(R) is
a singleton set containing SM, PSS(R) = { SN }.
So this lemma's #other case holds. #merge does not hold because
M -> N is not a conservative entry edge.
Qed.
Lemma [dead-end-entry-preserves-allocation-set-prefix].
If M -> N is a entry edge in T into a dead-end region R, and B is an
arbitrary block with an entry edge into R, then if A is
in the state at <N, then A is in the state at B>.
Pf.
Let SM be the state at M>, let SN be the state at <N, and let SB be the
state at B>.
By [operation-cases-edge], one of two cases must hold for M -> N:
If #merge:
Under this case, MERGE@(M -> N) is well-formed for the transition from
SM to SN. By [merge-preserves-allocation-set-prefix], SB contains A.
If #other:
Under this case, PSS(R) = { SN }. By definition, PSS(R) is the set of
all end states of blocks with entry edges into C; B is such a block,
so SB is in PSS(R), which means that SB = SN. Since SN contains A,
SB contains A.
Qed.
Lemma [absence-is-permanent].
If a path P in T passes through an allocation A, and there is a point X
following A on P where the state at X does not include A, then the state at
the end of P must also not include A.
Pf.
The only operation that can add an allocation that is not present in the
preceding state is PUSH, so if the state at the end of P contains A, there
must be a PUSH that adds A in the suffix of P that begins at X. This PUSH
in OS(P) must correspond to visiting A itself in P, but by assumption, P
already passes through A prior to reaching X. This would mean that P visits
A twice, but P is a path in the spanning tree T and must be simple.
Qed.
Lemma [allocation-index].
Let SA be the state at A>, for some allocation A. If the state S at some
point X contains A, then:
- LENGTH(S) >= LENGTH(SA),
- for all I < LENGTH(SA), S[I][0] = SA[I][0], and
- SA[LENGTH(SA) - 1][0] = S[LENGTH(SA) - 1][0] = A.
Pf.
By structural induction on the path P to X in T.
In the base case, P is the empty path at the entry point. The
state at X is the empty stack, which does not contain A, so the
implication holds vacuously.
In the inductive case,
P = (P'..., K)
for some component K.
By the inductive hypothesis, if S' contains A,
- LENGTH(S') >= LENGTH(SA)
- for all J < LENGTH(SA), S'[J][0] = SA[J][0], and, in particular,
- SA[LENGTH(SA) - 1][0] = S'[LENGTH(SA) - 1][0] = A
We must prove each of these for S instead of S'.
Let S' be the state at <K. By [operation-sequence-component],
OPERATION(S', K) is a well-formed operation sequence whose start state
is S' and whose end state is S, and it contains at most one operation.
If OPERATION(S', K) is the empty operation sequence, then S' = S,
so all of the implications hold by the inductive hypothesis.
Otherwise, there is an operation O that changes S' to S; we can
proceed by cases on O.
If O = PUSH@K:
K is an allocation, and S = STATE_PUSH(S', K) = (S'..., (K, allocated)).
LENGTH(S) = LENGTH(S') + 1, and therefore LENGTH(S) > LENGTH(S').
If S contains A, let AI be index of A in S, such that S[AI][0] = A.
Either AI < LENGTH(S') or AI = LENGTH(S').
If AI = LENGTH(S'):
S[AI][0] = (S'..., (K, allocated))[LENGTH(S')][0]
= (K, allocated)[0]
= K
But S[AI][0] = A by assumption, so K = A. But this means the end
point of P is A>, so S = SA. The first two implications then hold
trivially. For the third,
S[LENGTH(SA) - 1][0] = S[LENGTH(S) - 1][0]
= S[LENGTH(S')][0]
= (S'..., (K, allocated))[LENGTH(S')][0]
= (K, allocated)[0]
= K
= A
If AI < LENGTH(S'):
S[AI][0] = (S'..., (K, allocated))[AI][0]
= S'[AI][0]
Therefore S' contains A.
LENGTH(S') >= LENGTH(SA) by the I.H., and since LENGTH(S) > LENGTH(SA),
LENGTH(S) >= LENGTH(SA).
Let an index J < LENGTH(SA) be given.
S[J][0] = (S'..., (K, allocated))[J][0]
Since J < LENGTH(SA), and LENGTH(SA) <= LENGTH(S') by the I.H., this
can be simplified:
S[J][0] = S'[J][0]
= SA[J][0] (by the I.H.)
Finally,
S[LENGTH(SA) - 1] = (S'..., (K, allocated))[LENGTH(SA) - 1][0]
Since LENGTH(SA) - 1 < LENGTH(SA), and LENGTH(SA) <= LENGTH(S'),
S[LENGTH(SA) - 1][0] = S'[LENGTH(SA) - 1][0]
By the I.H. this is known to be A.
If O = POP@K:
K is an deallocation D of some allocation A, and
S = STATE_POP(S', A) = PREFIX(S', L)
for some index L whose exactly value isn't important here.
If S contains A, let AI be the index of A in S, such that S[AI][0] = A.
S[AI][0] = PREFIX(S', L)[AI][0]
= S'[AI][0]
So S'[AI][0] = A, and therefore S' contains A.
Then by the I.H., S'[LENGTH(SA) - 1][0] = A, and since S' is a well-formed
state, A cannot be repeated in it; this means AI = LENGTH(SA) - 1.
LENGTH(S) = L. L > AI, and AI = LENGTH(SA) - 1, so L > LENGTH(SA) - 1.
Therefore L >= LENGTH(SA), and so LENGTH(S) >= LENGTH(SA).
Let an index J < LENGTH(SA) be given.
S[J][0] = PREFIX(S', L)[J][0]
= S'[J][0]
= SA[J][0] (by the I.H.)
Finally,
S[LENGTH(SA) - 1][0] = PREFIX(S', L)[LENGTH(SA) - 1][0]
= S'[LENGTH(SA) - 1][0]
= A (by the I.H.)
If O = PEND@K:
K is an deallocation D of some allocation A, and
S = STATE_PEND(S', A) = SET_STATUS(S', I, pending)
for some index I whose value isn't important here.
If S contains A, let AI be the index of A in S, such that S[AI][0] = A.
Then by the axioms of SET_STATUS,
S[AI][0] = SET_STATUS(S', I, pending)[AI][0]
= S'[AI][0]
So S'[AI][0] = A, and therefore S' contains A.
LENGTH(S) = LENGTH(SET_STATUS(S', I, pending))
= LENGTH(S') (by the axioms of SET_STATUS)
>= LENGTH(SA) (by the I.H.)
Let an index J < LENGTH(SA) be given.
S[J][0] = SET_STATUS(S', I, pending)[J][0]
= S'[J][0] (by the axioms of SET_STATUS)
= SA[J][0] (by the I.H.)
Finally,
S[LENGTH(SA) - 1][0] = SET_STATUS(S', I, pending)[LENGTH(SA) - 1][0]
= S'[LENGTH(SA) - 1][0] (by the axioms)
= A (by the I.H.)
If O = MERGE@K:
K is an entry edge X -> Y into some dead-end region R, and
S = STATE_MERGE(S', PSS(R)) = CONSERVATIVE_MERGE(PSS(R))
If S contains A, let AI be the index of A in S, such that S[AI][0] = A.
S' is an element of PSS(R), so
S[AI][0] = CONSERVATIVE_MERGE(PSS(R))[AI][0]
= S'[AI][0] (by the axioms of CONSERVATIVE_MERGE)
So S'[AI][0] = A, and therefore S' contains A.
Then by the I.H., S'[LENGTH(SA) - 1][0] = A, and since S' is a well-formed
state, A cannot be repeated in it; this means AI = LENGTH(SA) - 1.
LENGTH(S) = L. L > AI, and AI = LENGTH(SA) - 1, so L > LENGTH(SA) - 1.
Therefore L >= LENGTH(SA), and so LENGTH(S) >= LENGTH(SA).
Let an index J < LENGTH(SA) be given.
S[J][0] = PREFIX(S', L)[J][0]
= S'[J][0]
= SA[J][0] (by the I.H.)
Finally,
S[LENGTH(SA) - 1][0] = PREFIX(S', L)[LENGTH(SA) - 1][0]
= S'[LENGTH(SA) - 1][0]
= A (by the I.H.)
So by induction, the lemma holds for all points X.
Qed.
Lemma [deallocatability-preserving-path].
If a path P is the path in T from X to Y, and P is
deallocatability-preserving, then the prefix of undeallocatable allocations
in the state SX of the X is the same as the prefix of deallocatable
allocations in the state SY of Y.
Pf.
By induction on P.
In the base case, X = Y and so SX = SY, so the prefix of deallocatable
allocations is the same.
In the inductive case, P = (P'..., K). Let Y' be the end point of P',
and let SY' be the state at Y'. By [operation-sequence-component],
OPERATION(SY', K) is either the empty operation sequence or a single
operation O that changes SY' to SY. If it is the empty sequence, then
SY = SY' and so SY has the same prefix of undeallocatable instructions
as SX. Otherwise, we proceed by cases:
If O = PUSH@K:
Then by construction
SY = STATE_PUSH(SY', K) = (SY'..., (K, allocated))
So SY still haas the same prefix of undeallocatable instructions as SY'
and therefore as SX.
If O = POP@K:
Then by construction
SY = STATE_POP(SY', K) = PREFIX(SY', I)
for some I such that all allocations at index >= I have a status of
either pending or allocated. So SY still haas the same prefix of
undeallocatable instructions as SY' and therefore as SX.
If O = PEND@K:
Then by construction
SY = STATE_PEND(SY', K) = SET_STATUS(SY', I, pending)
for some I whose status is allocated in SY'. So SY still haas the same
prefix of undeallocatable instructions as SY' and therefore as SX.
If O = MERGE@(M -> N):
Then M -> N is a conservative entry edge, but this contradicts our
assumption that P does not pass through such an edge.
Therefore the lemma holds for all paths P in T.
Qed.
Lemma [deallocatability-preserving-path-invariance].
If a path P in T is deallocatability-preserving, then the state SX at
the start point of P gives an allocation A undeallocatable status if
and only if the state SY at the end point of P gives A undeallocatable
status.
Pf.
Let U be the prefix U of undeallocatable allocations in SX.
By [deallocatability-preserving-path], U is the same as the prefix of
undeallocatable allocations in SY.
If SX gives A undeallocatable status, then SX has an element
(A, undeallocatable), which must be in U by the well-formedness of SX,
so SY also has such an element, so SY gives A undeallocatable status.
If SY gives A undeallocatable status, then SY has an element
(A, undeallocatable), which must be in U by the well-formedness of SY,
so SX also has such an element, so SX gives A undeallocatable status.
Qed.
Lemma [scc-undeallocatable-invariance].
The state at a point X gives an allocation A undeallocatable status if
and only if the state at the entry point of the path to X in T for the SCC
containing X gives A undeallocatable status.
Pf.
Let P be the path to X in T, and let P_F be the SCC entry suffix of P for
the SCC C containing X. Then P_F is a path entirely within C, so it passes
through no entry edges at all, so it is deallocatability-preserving.
Then the lemma holds by [deallocatability-preserving-path-invariance].
Qed.
Lemma [undeallocatable-implies-dead-end].
If the state at a point X gives an allocation A undeallocatable status,
then there exists a conservative entry edge M -> N into a dead-end region R
such that
P = (P_I..., M -> N, P_F...)
where P is the path to X in T, and where P_F is a deallocability-preserving
path. Furthermore, X is in a dead-end region.
Pf.
Let U be the prefix of undeallocatable allocations in the state at X.
U contains A by assumption.
If P did not pass through a conservative entry edge, then by
[deallocatability-preserving-path], the prefix of undeallocatable
allocations in the state at the start point of P would have to also be U.
But P is an initial path, so its start point is the entry point, so the
state there is the empty stack. U is not empty because it contains A, so U
cannot be a prefix of the empty stack.
Therefore P does pass through a conservative entry edge; let M -> N be
the last such edge that P passes through, such that
P = (P_I..., M -> N, P_F...)
P_F therefore does not pass through a conservative entry edge, so it is a
deallocatability-preserving path.
M -> N is an entry edge into a dead-end region, so <N is a point in a
dead-end region. By [dead-end-path-ancestry], the end point of P_F, which
is X, must be in a dead-end region.
Qed.
Lemma [undeallocatable-dead-end-entry].
If the state at X gives some allocation A undeallocatable status,
let P be the path to X in T, and let C be the SCC containing X. Then
P = (P_I..., M -> N, P_F...)
where M -> N is an entry edge into C, C is a dead-end-region, and
the state at <N gives A undeallocatable status.
Pf.
By [undeallocatable-implies-dead-end], C is a dead-end region, and
P = (P_I'..., M' -> N', P_F'...)
where M' -> N' is a conservative entry edge and P_F' is a
deallocatability-preserving path.
P is a path to a point in C that passes through an edge, so C is not the
entry SCC. Therefore P must pass through an entry edge into C, although
this is not necessarily M' -> N'. So by [scc-entry-edge-partition], let
P = (P_I.., M -> N, P_F...)
where M -> N is an entry edge into C and P_F is the SCC entry suffix
of P for C.
P_F is a suffix of P_F' by [scc-entry-suffix-suffix], so P_F is also a
deallocatability-preserving path. Since the state at X (the end point of
P_F) gives A undeallocatable status, the state at <N (the start point of
P_F) must also give A undeallocatable status by
[deallocatability-preserving-path.]
Qed.
Lemma [life-preserving-paths-undeallocatable-coherence].
If P1 and P2 are life-preserving paths from X1 and X2 to the same point Y,
and the state at X1 gives A undeallocatable status, the state at X2
gives A undeallocatable status.
Pf.
P1 and P2 do not pass through any conservative entry edges because they are
life-preserving paths and so do not pass through any entry edges into
dead-end regions at all. So by [deallocatability-preserving-path], the
states at X1 and X2 have the same prefix of undeallocatable allocations as
the state at Y, and so they have the same prefix as each other as well. So
if X1 gives A undeallocatable status, then A is in that prefix, and X2 also
gives A undeallocatable status.
Qed.
Lemma [undeallocatable-is-permanent].
If an path P in T passes through an allocation A, and there is a point X
following A on P where the state at X gives A undeallocatable status, then
the state at the end of P must either not include A or give it
undeallocatable status.
Pf.
POP and PEND do not affect allocations that have undeallocatable status, and
MERGE can only either leave it in undeallocatable status or remove it from
the the state completely. So the only way that the state at the end of P
can include A with a status other than deallocatable is if there is a PUSH
that adds it; but as above, this must correspond to P passing through A for
a second time on a simple path, which is impossible.
Qed.
Lemma [pending-is-permanent].
If a path P in T passes through an allocation A, and there
is a point X following A on P where the state at X gives A
pending status, then the state at the end of P must either
not include A, give it pending status, or give it undeallocatable
status.
Pf.
If the state at the end of P includes A and does not give it
pending or undeallocatable status, it must give it allocated
status. The only operation that can do this when the allocation
is absent or has a non-allocated status in the preceding state
is PUSH, and as above, this would require P visiting A twice
and is impossible.
Qed.
Lemma [deallocation-is-permanent].
If a path P in T passes through a deallocation D of allocation A, then the
state at the end of P either does not include A or gives it a status other
than allocated.
Pf.
Let P and A be given which satisfy the conditions, and let D be a
deallocation of D that P passes through. A dominates D, so A must appear
earlier on P than D.
If A is present and has allocated status in the state at <D, then D will
correspond either to a POP or PEND operation. The state at D> will
therefore either omit A or give it pending status. Otherwise, the state at
<D must either omit A or give it either pending or undeallocatable status.
By [absence-is-permanent], [undeallocatable-is-permanent], and
[pending-is-permanent], the state at the end of the path must either not
include A or give it a status other than permanent.
Lemma [allocated-implies-allocation-path].
Let P be a path in T from point X to point Y. If the state SX
at X does not contain X, and the state SY at Y gives A allocated status,
then /a/ matches AH(A, P).
Pf.
By [operation-sequence-path], OS_PATH(SX, P) is a well-formed operation
sequence with SY as its final state.
Since SY contains A, and SY is the final state of OS_PATH(SX, P), there
must exist a least index K such that the state SK at index K in
OS_PATH(SX, P) contains A. This cannot be the start index of
OS_PATH(SX, P) because the state there does not contain A by assumption;
therefore, there is some operation O preceding position K in
OS_PATH(SX, P). The state SK' at index K - 1 in OS_PATH(SX, P) does not
contain A (or the assumption that K is the least such index), and because
OS_PATH(SX, P) is a well-formed path, O must change SK' to SK. The only
operation that can add an allocation A to a state is PUSH@A, so
O = PUSH@A. A is therefore on P.
A path in T is a simple path, so P cannot pass through A twice.
If P passed through a deallocation of A, it would have to be after it
passed through A because A dominates all of its deallocations. Then the
state at the end of P could not include A and give it allocated status
because[deallocation-is-permanent]. So P does not pass through a
deallocation of A.
Since P passes through A a single time and does not pass through a
deallocation of A, /a/ matches AH(A, P).
Qed.
Lemma [allocated-implies-allocation].
If the state S at some point X gives an allocation A allocated status,
then /a/ matches AH(A,P), where P is the path P to X in T.
Pf.
Let P be the given to X in T. The state at the entry point
does not contain A because it is the empty sequence, so by
[allocated-implies-allocation-path], /a/ matches AH(A,P).
Qed.
Lemma [missing-implies-invariance].
Let P be a path in T that does not pass through any edges into
dead-end regions, and suppose that // matches AH(A, P) for some allocation
A. Let SX and SY be the states at the start point and end point of P,
respectively.
If SX does not include A, SY does not include A.
If SX gives A allocated status, SY gives A allocated status.
Pf.
Let SX and SY be the states at the start and end points of P,
respectively. By [operation-sequence-path], OS_PATH(SX, P) is a well-
formed path whose end state is SY. Let OSP := OS_PATH(SX, P); by
induction on OSP.
If OSP is the empty path, then SX = SY, so both implications hold trivially.
Otherwise, OSP = (OSP'..., O). Let SY' be the end state of OSP';
then because OSP is well-formed, O changes SY' to SY.
If O = PUSH@I:
// matches AH(A, P), so I cannot be A; therefore SY' gives A status U if
and only if SY gives A status U by by [push-cases].
If O = POP@I:
I is the deallocation of some allocation A'. By [pop-cases], one of the
following four cases is true for A.
If #absent:
Neither SY nor SY' include A by the rules of this case.
Suppose SX does not include A; then SY does not include A, because it
doesn't regardless.
Suppose SX gives A allocated status; then SY' must give A allocated status
by the inductive hypothesis, but this is a contradiction.
If #top:
A' = A by the rules of this case, but // matches AH(A,P), so no
deallocation of A can be on P; this is a contradiction.
If #popped:
SY' gives A pending status by the rules of this case.
Suppose SX does not include A; then SY' does not include A by the
induction hypothesis, so this is a contradiction.
Suppose SX gives A allocated status; then SY' gives A allocated status
by the induction hypothesis, so this is a contradiction.
If #prefix:
SY' and SY both contain A and give it the same status by the rules of
this case.
Suppose SX does not include A; then SY' does not include A by the
induction hypothesis, so this is a contradiction.
Suppose SX gives A allocated status; then SY' gives A allocated status
by the induction hypothesis, so SY gives A allocated status.
If O = PEND@I:
I is the deallocation of some allocation A'. By [pend-cases], one of
three cases is true:
If #changed:
A' = A by the rules of this case, but // matches AH(A,P), so no
deallocation of A can be on P; this is a contradiction.
If #kept:
Under this case, SY' and SY both contain A and give it the same status.
Suppose SX does not include A; then SY' does not include A by the
induction hypothesis; but this is a contradiction because SY
does gives A some status in this case.
Suppose SX gives A allocated status; then SY' gives A allocated status
by the induction hypothesis; so SY gives A allocated status.
If #absent:
Under this case, neitiher SY' nor SY contains A.
Suppose SX does not include A; then SY' does not include A by the
induction hypothesis, which is consistent with this case; then SY
does not contain A by the conditions of this case.
Suppose SX gives A allocated status; then SY' gives A allocated status
by the induction hypothesis; but this is a contradiction because
SY' does not contain A in this case.
If O = MERGE@(M -> N):
M -> N must be an edge into a dead-end block, but no such edges exists
on P by assumption, so this a contradiction.
Then by induction, the two implications hold for any path P that satisfies
the conditions.
Qed.
Lemma [allocation-implies-allocated].
Let P be a path in T that does not pass through any edges into dead-end
regions such that /a/ matches AH(A, P) for some allocation. Then the
state at the start point of P does not contain A, and the state at
the end point of P gives A allocated status.
Pf.
By [history-reverse-partition]
P = (P_I..., A, P_F...)
where // matches AH(A, P_I) and AH(A, P_F).
Let XI be the start point of P, and let P_EI be the path to XI in T.
If the state at XI contained A, then by [present-implies-visit], P_EI
would pass through A; but then (P_EI..., P...) would pass through A
twice, but all paths in T are simple because it is a spanning tree.
So the state at XI does not contain A. Therefore the state SA' at <A
does not contain A by [missing-implies-invariance]. OPERATION(SA', A)
is (PUSH@A) by definition, and by [operation-sequence-instruction]
its start state is SA' and its end state is the state SA at A>.
Therefore
SA = STATUS_PUSH(SA', A) = (SA'..., (A, allocated))
and so SA gives A allocated status. Therefore the state at the end
point of P_F gives A allocated status by [missing-implies-invariance],
and by construction this is also the state at the end point of P.
Qed.
Lemma [pending-implies-deallocation].
If the state S at some point X gives an allocation A pending
status, then the path P to X in T passes through a deallocation
of A which corresponds to a PEND operation on the operation
sequence for that path. Furthermore, /ad/ matches AH(A,P).
Pf.
By definition, S is the final state in OS(P). The empty stack does not give
A pending status, so there is an earliest operation O in OS(P) such that
the following state of O gives A pending status. Only PEND@D can make such
a state change. D must be a deallocation of A, and by construction P passes
through D.
P must pass through A prior to D because of dominance. P cannot pass through
A multiple times because it is a simple path. P cannot pass through
multiple deallocations of A because that would violate joint
post-dominance. Therefore /ad/ matches AH(A,P).
Qed.
Lemma [pop-implies-deallocated].
Let O == POP@D be part of the operation sequence corresponding
to some path P in T. If O removes an allocation A from its
preceding state, then P passes through a deallocation of A.
Pf.
To be removed by a POP operation, either A must be the top of the stack or A
has pending status.
If A has pending status, let PD be the subpath of P to <D. By
[pending-implies-deallocation], P passes through a deallocation of A; since
PD is subpath of P, P also passes through this deallocation.
If A is the top of the stack, then D must itself be a deallocation of A, and
by construction P passes through D.
Qed.
Lemma [merge-restricting-predecessor].
If MERGE@(X -> Y) is well-formed for a transition from S' to S, then if S'
contains some allocation A, and S does not, there exists a block B with an
entry edge into the SCC containing Y such that the state at <B does
not contain A.
Pf.
Let R be the SCC containing Y. By the well-formedness rules
S = STATE_MERGE(S', PSS(R)) = CONSERVATIVE_MERGE(PSS(R))
Let SA be the state at A>. Let I be the index of A in S'. By
[allocation-index], I = LENGTH(SA) - 1, and LENGTH(A) >= I.
If I < LENGTH(S), then by S[I][0] = S'[1][0] by
[conservative-merge-index], which contradicts the assumption that S does
not contain A; therefore I >= LENGTH(S).
By the axioms of CONSERVATIVE_MERGE, LENGTH(S) is the maximum L such that
- for all S1 in PSS(R), L <= LENGTH(S1)
- for all S1 and S2 in PSS(R), and for all I' < L, S1[I'][0] = S2[I'][0]
So since I >= LENGTH(S), at least one of those must be false. Consider
each in turn:
If there exists an S1 in PSS(R) such that I >= LENGTH(S1):
By the definition of PSS(R), S1 must be the end state of some block B
with an entry edge into R.
Suppose that S1 contains A. By [allocation-index],
LENGTH(SA) <= LENGTH(S1). But I = LENGTH(SA) - 1, so I < LENGTH(S1).
This contradicts the assumption that I >= LENGTH(S1). So S1 does not
contain A.
Therefore B satisfies the conditions of the lemma.
If there exist S1 and S2 in PSS(R) such that, for some I' < LENGTH(S),
S1[I'][0] != S2[I'][0]:
Suppose that S1 and S2 both contain A. By [allocation-index], for all
I'' < LENGTH(SA), S1[I''][0] = SA[I''][0] and S2[I''][0] = SA[I''][0].
But I' < LENGTH(S), and LENGTH(S) <= I, and I = LENGTH(SA) - 1,
and LENGTH(SA) - 1 < LENGTH(SA); transitively, then, I' < LENGTH(SA).
That means that S1[I'][0] = SA[I'][0] = S2[I'][0], but that contradicts
the assumption in this case that S1[I'][0] != S2[I'][0].
Therefore at least one of S1 or S2 does not contain A. Whichever it is, it
is in PSS(R) because there exists a block B of which it is the end state;
let B be such a block. Then the end state of B does not contain A.
Qed.
Lemma [dead-end-entry-coherence].
If X and Y are points in the same dead-end region R, then the states at
the SCC entry points for R of the paths to X and Y in T are the same,
and this state is either
- the STATE_EMPTY, if R is the entry SCC;
- the singleton element of PSS(R), if PSS(R) is a singleton; or
- CONSERVATIVE_MERGE(PSS(R)).
Pf.
Let PX and PY be the SCC entry prefixes of the paths to X and Y in T,
and let SX and SY be the states at their end points.
If R is the entry SCC, then those end points are the same point, the
entry point. Then by [entry-state], SX = STATE_EMPTY = SY.
Otherwise, PX and PY are non-empty, so by [scc-entry-prefix-ancestry]
PX = (PX'..., MX -> NX)
PY = (PY'..., MY -> NY)
where MX -> NX and MY -> NY are entry edges into R. Let SX' and SY'
be the states at MX> and MY>.
Applying [operation-cases-dead-entry-edge] to MX -> NX, either
SX' = SX and PSS(R) = { SX' }, or SX = CONSERVATIVE_MERGE(PSS(R))
and PSS(R) is not singleton.
Applying [operation-cases-dead-entry-edge] to MY -> NY, either
SY' = SY and PSS(R) = { SY' }, or SY = CONSERVATIVE_MERGE(PSS(R))
and PSS(R) is not singleton.
If SX' = SX and PSS(R) = { SX' }:
Then PSS(R) is singleton, so we must also have SY' = SY and
PSS(R) = { SY' }. Therefore SX' = SY', and by subsitution, SX = SY.
If SX = CONSERVATIVE_MERGE(PSS(R)) and PSS(R) is not singleton:
Then we must also have SY = CONSERVATIVE_MERGE(PSS(R)), and by
substitution, SX = SY.
Qed.
Lemma [absent-implies-missing-or-deallocation].
If the state S at some point X does not include an allocation A, then either
there exists an initial path to X that does not pass through A or there
exists an initial path to X that passes through both A and a deallocation
of A.
Pf.
By structural induction on the SCC tree.
Let a point X be given such that the lemma holds for all points in proper
ancestor SCCs of the SCC C that contains X.
Let PX be the path to X in T. If PX does not pass through A, then PX
is an initial path to X that does not pass through A, satisfying the
requirements of the lemma. Otherwise, partition PX at A>:
PX = (PA..., PF...)
PA is an initial path to A> in T; because T is a tree, PA is unique,
which is to say, PA is *the* path to A> in T.
Let SA', SA, and SX be the states at <A, A>, and X, respectively.
By [allocation-state], SA gives A allocated status.
By definition, SX is the end state of OS(PX). By
[operation-sequence-concatenation]
OS(PX) = OS((PA..., PF...))
= (OS(PA)..., OS_PATH(SA, PF)...)
By [correctness-operation-sequence], OS(PX) is well-formed. Therefore
its subpath OS_PATH(SA, PF) is a well-formed operation sequence whose
start state is SA and end state is SX.
SA contains A, and by assumption, SX does not. Let I be the index of the
first state in OS_PATH(SA, PF) that does not give A allocated status. I
cannot be 0, because SA does give A allocated status. Let SM the state at
index I - 1 in OS_PATH(SA, PF), and let SN be the state at index I in
OS_PATH(SA, PF). Then we can parititon OS_PATH(SA, PF) at I - 1 and I:
OS_PATH(SA, PF) = (OSP1..., O, OSP2...)
Since [ SM, O, SN ] is a triple in OS_PATH(SA, PF), then by the definition
of a well-formed operation sequence, the operation O must be well-formed for
the transition from SM to SN. Remember that SM gives A allocated status
and SN does not.
Now we can consider the cases for O.
If O = PUSH@K:
By [push-preserves-preceding], since SM gives A allocated status, SN must
give A allocated status. This is a contradiction.
If O = POP@K:
Applying [pop-cases] to A, one of the following is true:
If #absent: In this case, SM does not contain A, a contradiction.
If #top: In this case, K is a deallocation of some allocation A',
and A = A'. Therefore PF passes through a deallocation of A,
and so PX is a path that passes through both A and a
deallocation of A.
If #popped: In this case, SM gives A pending status, a contradiction.
If #prefix: In this case, since SM gives A allocated status, SN also
gives A allocated status, a contradiction.
If O = PEND@K:
Applying [pend-cases] to A, one of the following is true:
If #changed: In this case, K is a deallocation of some allocation A',
and A = A'. Therefore PF passes through a deallocation of A,
and so PX is a path that passes through both A and a
deallocation of A.
If #kept: In this case, since SM gives A allocated status, SN gives A
allocated status, a contradiction.
If #absent: In this case, SM does not contain A, a contradiction.
If O = MERGE@K:
In this case, K is an conservative entry edge into some region R.
By [merge-restricting-predecessor], there exists a block B with an
entry edge into R such that the state at B> does not contain A.
By the definition of entry edge, the SCC containing B must be a
proper ancestor of R.
PF passes through K, so we can partition it around K:
PF = (PFI..., K, PFF...)
(PFI...) is a path from A> to B>, so the SCC containing A is an ancestor
of the SCC containing B, which is a proper ancstor of R; therefore the
SCC containing A is a proper ancestor of R.
PFF is a path from K> (a point in R) to X (a point in C), which means R is
an ancestor of C. Transitively, the SCC containing B is a proper ancestor
of C. By the induction hypothesis, one of two cases is true:
If there exists an initial path PB to B> that does not pass through A:
Then define
P' = (PB..., K, PFF...)
P' is an initial path to X. The PB subpath does not pass through A
by assumption. The (K) subpath does not pass through A because it is
just a single edge. The PFF subpath does not pass through A because
its start point is in R, so it cannot pass through any components in
proper ancestors of R. Therefore P' does not pass through A, so P'
satisfies the requirements of the lemma.
If there exists an initial pah PB to B> that passes through both A
and a deallocation of A:
Then define
P' = (PB..., K, PFF...)
P' is an initial path to X, and because its PB subpath passes through
A and a deallocation of A, P' must as well. So P' satisifes the
requirements of the lemma.
By induction, the lemma holds for all points.
Qed.
Theorem [deallocation-preconditions].
In the state S at the point before a deallocation D of an allocation A, A is
on the stack and does not have pending status.
Pf.
Let P be the path to <D in T. P must pass through A because allocations must
dominate their deallocations. P is a simple path because T is a spanning
tree, so P can pass through A at most once.
Suppose that S does not include A. Then by
[absent-implies-missing-or-deallocation], there is a path P' to <D which
either does not pass through A or passes through both A and a deallocation
of A. In the latter case,(P'..., D) is a path from the entry point that
includes A and two successive deallocations of A, which violates the rules
of joint post-dominance. In the former case,(P'..., D) is a path from the
entry point that includes a deallocation of A but not A, which violates the
rules of dominance. So S must include A.
Suppose that S gives A pending status. Then by
[pending-implies-deallocation], there is a deallocation of A on P. But
then (P..., D) is a path from the entry point that includes A and two
successive deallocations of A, which violates the rules of joint
post-dominance. So S cannot give A pending status.
Qed.
Lemma [stack-order].
If the state at X has allocation B above allocation A,
then the path to X in T passes through A before it
passes through B.
Pf.
By induction on the path P to X in T.
In the base case, P is empty and the lemma is trivially true.
In the inductive case, either
P = (P'..., M -> N) or
P = (P'..., I)
Let S' be the state at X', the end point of P', and let
S be the state at X.
Let two allocations A and B be given such that B is above A in S.
If A and B are both in S' and in the same order, then the induction
hypothesis tells us that P' passes through A before it passes
through B; this must also be true of P since P' is a subpath of P,
so the lemma holds.
If S = S', A and B must be in S' and in the same order, so the
lemma holds. Otherwise, the last component of P must correspond
to an operation O that changes the state from S' to S:
If O is PUSH@I: The stack in S is exactly the stack in S'
with I on top. A != I because there are no allocations above
I in S. If B != I, then A and B must in the part of S unchanged
by O, so they are both in S' and in the same order, and the lemma
holds. Otherwise, B = I. A must be in S', so by
[present-implies-visit], P' passes through A. P passes through B
because B = I, and it does it this after P', so the lemma holds.
If O is POP@I: The stack in S is a prefix of the stack in S',
so A and B must be in S' and in the same order, so the lemma holds.
If O is PEND@I: The stack in S is the same as the stack in S';
only the statuses are different. So A and B must be in S' and
in the same order, so the lemma holds.
If O is MERGE@(M -> N): The stack in S is a prefix of the stack
in S', so A and B must be in S' and in the same order, so the
lemma holds.
In all cases, the lemma holds. By induction it holds for all points.
Qed.
Definition [undeallocatable allocations set].
The undeallocatable allocations set UDA(S) of a state S is the set of
allocations having undeallocatable status in S.
End.
Lemma [no-merge-implies-no-deallocatable-change].
If P is a path in T, SI and SF are the states at the start and end points
of P, and OS_PATH(SI, P) does not contain any MERGE operations, then
UDA(SI) = UDA(SF).
Pf.
Let OSF = OS_PATH(SI, P). By [operation-sequence-path], OSF is a well-formed
operation sequence, and SF is the state at the end point of P. By induction
on OSF.
In the base case, OSF is the empty operation sequence, so SI = SF
and thus by substitution UDA(SI) = UDA(SF).
In the inductive case, OSF = (OSF'..., O). Let SF' be the end state of OSF'.
By the induction hypothesis, UDA(SI) = UDA(SF'). OSF is a well-formed
operation sequence, so O must change SF' to SF. Consider the possible cases
of O.
If O = PUSH@A:
By definition:
SF = STATE_PUSH(SF', A)
= (SF'..., (A, allocated))
Therefore
UDA(SF) = UDA((SF'..., (A, allocated)))
= UNION(UDA(SF'), UDA(((A, allocated))))
= UNION(UDA(SF'), {})
= UDA(SF')
= UDA(SI)
If O = POP@D:
D is a deallocation of A. By definition:
SF = STATE_POP(SF', A)
= PREFIX(SF', I)
This could in theory remove allocations that undeallocated status,
except that I is chosen such that all allocations in the suffix of SF'
start at I have pending or allocated status. Therefore
UDA(SF) = UDA(SF') = UDA(SI).
If O = PEND@D:
D is a deallocation of A. By definition:
SF = STATE_PEND(SF', A)
= SET_STATUS(SF', I, pending)
SET_STATUS changes only the status of A, and A has allocated status in SF'
and pending status in SF. Therefore UDA(SF) = UDA(SF') = UDA(SI).
If O=MERGE@(X -> Y):
By assumption, there are no MERGE operations on OSF.
Therefore UDA(SF) = UDA(SI) in all cases, and by induction, the lemma holds
for all paths in T.
Qed.
Lemma [stack-order-consistency].
For all points X and Y, if the states at X and Y contain two different
allocations A and B, they contain them in the same order.
Pf.
Suppose A is below B in the state at X, but B is below A
in the state at Y. Let PX be the path to X in T, and let
PY be the path to Y in T. Then by [stack-order],
PX = (PX1..., A, PX2..., B, PX3...)
PY = (PY1..., B, PY2..., A, PY3...)
But then (A, PX2..., B, PY2..., A) would be a cyclic path
in T, which is an acylic graph.
Qed.
Lemma [state-differences-excess].
If SX and SY are well-formed states, and LENGTH(SX) > LENGTH(SY),
then SY does not contain the allocation that is at LENGTH(SY) in SX.
Pf.
Let A be the allocation at LENGTH(SY) in SX, and let SA be the state
at A>. By [allocation-index], since SX contains A,
SX[LENGTH(SA) - 1] = A.
SX is a well-formed state and so cannot repeat allocations, so
LENGTH(SY) = LENGTH(SA) - 1, and thus LENGTH(SY) < LENGTH(SA).
Suppose SY contains A; then by [allocation-index], LENGTH(SY) >= LENGTH(SA).
But this is a contradiction, so SY cannot contain A.
Qed.
Lemma [state-differences-allocation-mismatch].
If SX and SY are well-formed states of the same length, and there exists
an index I such that SX[I][0] != SY[I][0], then SY does not contain
the allocation that is at I in SX.
Pf.
Let A be the allocation at I in SX, and let SA be the state at A>.
By [allocation-index], since SX contains A,
SX[LENGTH(SA) - 1][0] = A.
SX is a well-formed state and so cannot repeat allocations, so
I = LENGTH(SA) - 1.
Suppose SY contains A; then by [allocation-index],
SY[LENGTH(SA) - 1][0] = A
But by substitution, this means SY[I][0] = A, and so SY[I][0] = SX[I][0],
which contradicts the assumption of the lemma. Therefore SY cannot contain A.
Qed.
Lemma [state-differences].
Let SX be the state at some point X and SY be the state at some point Y.
Then either
1. For some allocation A, SX gives A status U but SY does not.
2. For some allocation A, SY gives A status U but SX does not.
3. SX = SY.
Pf.
By [correctness-state], SX and SY are well-formed states.
Exhaustively, either LENGTH(SX) = LENGTH(SY), LENGTH(SX) < LENGTH(SY),
or LENGTH(SX) > LENGTH(SY).
If LENGTH(SX) > LENGTH(SY):
Let A be the allocation at LENGTH(SY) in SX; SX gives A some status U.
By [state-difference-excess], SY does not contain A, so SY cannot give
A the status U.
If LENGTH(SX) < LENGTH(SY):
Let A be the allocation at LENGTH(SX) in SY; SY gives A some status U.
By [state-difference-excess], SX does not contain A, so SX cannot give
A the status U.
If LENGTH(SX) = LENGTH(SY):
If SX != SY:
There exists a first index I such that SX[I] != SY[I].
If SX[I][0] = SY[I][0]:
It must be the case that SX[I][1] != SY[I][1], which is to say,
that SX and SY give the allocation SX[I][0] different statuses,
so SX gives SX[I][0] the status SX[I][1] and SY does not.
If SX[I][0] != SY[I][0]:
By [state-differences-allocation-mismatch], SY does not contain
the state at SX[I][0], so SX gives it the status SX[I][1] and
SY does not.
If SX = SY:
Then the states do not differ.
Qed.
Lemma [state-differences-specific].
Let SX be the state at some point X and SY be the state at some point Y.
Then either
1. For some allocation A, SX gives A undeallocatable status but SY does not.
2. For some allocation A, SY gives A undeallocatable status but SX does not.
3. For some allocation A, SX gives A allocated status but SY either does not
contain A or gives it pending status.
4. For some allocation A, SY gives A allocated status but SX either does not
contain A or gives it pending status.
5. For some allocation A, SX gives A pending status but SY does not contain A.
6. For some allocation A, SY gives A pending status but SX does not contain A.
7. SX = SY.
Pf.
By [state-differences], one of three things must be true.
If there exists A where SX gives A status U but SY does not:
If U = undeallocatable: We have case (1).
If U = allocated:
If SY gives A undeallocatable status: We have case (2).
If SY does not give A undeallocatable status: We have case (3).
If U = pending:
If SY gives A undeallocatable status: We have case (2).
If SY gives A allocated status: We have case (4).
If SY gives A pending status: Contradiction.
If SY does not contain A: We have case (5).
If there exists A where SY gives A status U but SX does not:
If U = undeallocatable: We have case (2).
If U = allocated:
If SX gives A undeallocatable status: We have case (1).
If SX does not give A undeallocatable status: We have case (4).
If U = pending:
If SX gives A undeallocatable status: We have case (1).
If SX gives A allocated status: We have case (3).
If SX gives A pending status: Contradiction.
If SX does not contain A: We have case (6).
If SX = SY:
We have case (7).
Qed.
Lemma [presence-implies-presence].
If the state at X contains A, then the path P to X in T passes through A,
and the state at all points in the suffix of P beginning at A> contains A.
Pf.
P passes through A by [present-implies-visit]. Let PF be the suffix of P
beginning at A>:
P = (PA..., A, PF...)
Let a point Y on PF given. Y follows A on P.
Suppose that the state at Y did not include A. Then by [absence-is-permanent],
the state at the end point of P must not contain A. But the end point of P
is X, and the state at X contains A by assumption. Therefore this is a
contradiction, and the state at Y must include A.
Qed.
Lemma [dominance-implies-precedes].
For all points X, if the state SX at X contains allocations A and B, and A
dominates B, then AI < BI, where AI and BI are the indices of A and B in S.
Pf.
Let P be the path to X in T. By [present-implies-visit], P passes through B;
then let P be partitioned around B:
P = (PB..., B, PBX...)
P is an initial path, so PB is an initial path. Since A dominates B, and
PB is an initial path to <B, PB passes through A; then let PB be partitioned
around A:
PB = (PA..., A, PAB...)
Thus:
P = (PA..., A, PAB..., B, PBX...)
Let SA and SB be the states at A> and B>.
By [allocation-index] for SX and A, AI = LENGTH(SA) - 1.
By [allocation-index] for SX and B, BI = LENGTH(SB) - 1, and BI is also
the index of B in SB.
B> is on the suffix of P beginning at A>, so by [presence-implies-presence],
SB contains A. Then by [allocation-index] for SB and A,
LENGTH(SA) <= LENGTH(SB), and the index of A in SB is LENGTH(SA) - 1. This
means that LENGTH(SA) - 1 <= LENGTH(SB) - 1, and therefore AI <= BI. But
AI != BI because then SX would have to contain both A and B at that index.
Therefore AI < BI.
Qed.
Theorem [correctness-invariant-3].
For all points X, if the state S at X is not the empty stack,
the top of the stack at S does not have pending status.
Pf.
No well-formed operation sequence ever leaves the top of
the stack having pending status. We prove this by structural
induction on the sequence.
In the base case, the end state of the sequence is the empty
stack, which contains no allocations.
Let an OS := (OS'..., O) be given, and assume the induction
hypothesis is true for OS'; therefore the end state of OS'
does not give the top of the stack (if any) pending status.
Now consider O:
- If O is PUSH, the end state of OS gives the top allocation
allocated status.
- If O is POP, the end state of OS removes the top allocation
and then iteratively removes all allocations in the pending
status; the top allocation in the end state of OS can never
have pending status, or it would have been popped as well.
- If O is PEND, then the status of the top allocation in the
end state of OS is the same as it was in the end state of
OS', which by the induction hypothesis was not pending.
- If O is MERGE, then the end state of OS gives every
allocation remaining on the stack undeallocatable status,
so the top allocation (if any) cannot have pending status.
Therefore this is true for all well-formed operation
sequences, and since S is by definition the end-state of a
well-formed operation sequence, S does not give the top of
the allocation stack pending status.
Qed.
Theorem [correctness-invariant-4].
For all points X, if the state S at X contains an allocation A having
undeallocatable status, every allocation below A on the stack at S also has
undeallocatable status.
Pf.
By [correctness-state], S is a well-formed state, from which this follows
immediately.
Qed.
Lemma [simple-path-dominance-transition]. (tree-independent)
For all simple paths P and points X, Y, and Z, if X precedes
Y on P, Y precedes Z on P, and X does not dominate Y, then
X also does not dominate Z.
Pf.
Let P, X, Y, and Z be given that satisfy the conditions.
Let P_YZ be the subpath of P from Y to Z. Because P is a simple
path and X occurs earlier on it, X cannot occur on P_YZ.
As X does not dominate Y, there must be a path P_EY from the
entry point to Y that does not pass through X. Then the path
(P_EY..., P_YZ...) is a path from the entry point to Z that
does not pass through X, meaning X does not dominate Z.
Qed.
Lemma [nested-allocations-are-dominated].
If a path P in T passes through an allocation A, then
for all points X on P dominated by A, all of the allocations
on the stack in the state at X are dominated by A.
Pf.
Let A, P, and X be given. If an allocation B is on the stack
in the state at X, B must precede X in P. Meanwhile, if B
is above A on the stack at X, then PUSH@B must appear after
PUSH@A in OS(P), so P must pass through B after it passes
through A. A must therefore dominate B, or else A would not
dominate X by [simple-path-dominance-transition], which
contradicts our assumptions.
Qed.
Theorem [correctness-invariant-5].
For all points X, for every allocation A in the state S at X,
if A has pending status in S, then S also contains an
allocation B dominated by A which S gives allocated status.
Pf.
Let T and X be given, and let P be the path to X in T.
By induction on P. In the base case, P is the empty path,
so S is the empty stack. Since it contains no allocations,
it satisfies the theorem trivially.
Now suppose that P is either (PI..., I) or (PI..., X -> Y).
Let SI be the state at the end point of PI; the inductive
hypothesis tells us that the theorem holds for SI.
If S = SI, then the theorem also holds for S. Otherwise,
the final component of P must correspond to an operation O
which changes the state from SI to S. We proceed by cases:
O is PUSH@A: S is exactly SI, but with A pushed onto the stack
and having allocated status. So let an allocation A' in S
be given. If A' = A, then A' does not have pending status in
S. Otherwise, SI must also have A' with pending status,
and so there is a B in SI dominated by A that has allocated
status. B must still exist in S with the same status.
O is MERGE@(X -> Y): All the allocations in S must have
undeallocatable status, so the theorem holds trivially.
O is PEND@D: Let A be the allocation deallocated by D.
A dominates D, so P must pass through A. Therefore, by
[nested-allocations-are-dominated], all of the allocations
above A on the stack in the state at <D (i.e. SI) are
dominated by A. Furthermore, the allocation AT at the top of
the stack in SI must have allocated status:
- AT does not have pending status in SI by
[correctness-invariant-3].
- AT does not have undeallocatable status in SI by
[correctness-invariant-4], since A has allocated status
in SI and is beneath B on the stack.
AT != A because the allocation whose status is changed by
PEND cannot be the top of the stack. Therefore the status
of AT is not changed by O, so it still has allocated status
in S.
Now let an allocation A' in S be given.
If A' = A: AT satisfies the conditions for the theorem.
If A' != A: the status of A' is not changed by O, so if
A' has pending status in S, it must have pending status
in SI. Then by the inductive hypothesis, let B be an
allocation in SI dominated by A' that has allocated
status in SI. If B != A, then the status of B is not
changed in S, so B still satisfies the conditions of
the theorem. Otherwise, B = A, so we now know that A'
dominates A, and since dominance is transitive, A' must
dominate AT. Since AT has allocated status in S, it
satisfies the conditions of the theorem.
O is POP@D: Let A be the allocation deallocated by D.
O removes A from S, as well as some number of other
allocations that all have pending status in SI.
Let A' be an allocation in S that has pending status
in S. O does not give any allocations pending status,
so A' must also have pending status in SI. By the
inductive hypothesis, let B be an allocation in SI
dominated by A' which has allocated status in SI.
Because B has allocated status in SI, it cannot be
one of the additional allocations removed by O.
If B != A, then B still exists in S and has allocated
status, and so it satisfies the condition.
If B = A, then B must dominate <D since D is a
deallocation of A. Since A' dominates B and
dominance is transitive, A' dominates <D.
Therefore A' dominates every allocation above it on
the stack in SI by [nested-allocations-are-dominated].
Because A' has pending status in SI, and O did not
remove A from S, there must be an allocation AT above
A' in the stack in SI which not have pending status
and therefore blocked POP from removing A'. AT cannot
have undeallocatable status in SI by
[correctness-invariant-4] because A' is below it on the
the stack and has allocated status in SI. O does not
change the status of any allocations it leaves on the
stack, so AT must still be in S and have allocated status.
Since AT is dominated by A', it satisfies the conditions
of the theorem.
Therefore by induction the theorem holds for all points X.
Qed.
Lemma [live-undeallocatable].
If X is a point that is not in a dead-end region, then the operation
sequence for the path to X in T does not include any MERGE operations,
and there are no allocations that have undeallocatable status in the
state at X.
Pf.
A MERGE operation can only correspond to an entry edge into a dead-end
region, so if there is a MERGE operation on OS(P), there is an edge X -> Y
on P such that Y is in a dead-end region. But then the suffix of P
beginning at <Y, concatenated with a path from X to a function exit
(which must exist because X is not in a dead-end region), is a path from a
block supposedly in a dead-end region to a function exit, which is a
contradiction.
OS(P) = OS_PATH(STATE_EMPTY, P), and STATE_EMPTY is the state at the entry
point. Let SX be the state at X; then UDA(SX) = UDA(STATE_EMPTY) by
[no-merge-implies-no-deallocatable-change], and since UDA(STATE_EMPTY)
is the empty set, there are no allocations that undeallocatable status
in SX.
Qed.
Coherence
---------
In this section, we are building up to a proof that states are always
consistent within the depth-first search. That is, if the depth-first
search *doesn't* take an edge because it's already enqueued the destination
block, the current state is the same as what the destination block is
ultimately processed with.
Note that this is different from proving that the search order doesn't matter;
that is proven later, in the Tree Invariance section.
This coherence property does not generally hold for depth-first searches that
track that they've passed through certain instructions. The property holds
for this algorithm specifically because it *stops* tracking allocations when
it processes deallocations, and only because deallocations jointly
post-dominate their allocations. Furthermore, because deallocation is not a
coherent joint post-dominance relationship, the algorithm must adopt its
twist around entry edges into dead-end blocks. In fact, the main function of
the twist is to establish the coherence property that the non-coherent joint
post-dominance relationship of allocations and deallocations otherwise lacks.
Definition [coherent point, coherence-prone path].
A point X is deallocatability-assuring for a set of allocations AS if,
for all allocations A in AS, for all spanning trees T, the state at X
under T does not give A undeallocatable status.
A point X is a coherent point for a set of allocations AS if is
deallocatability-ensuring for AS and if, for all initial paths P to X,
there exists a continuation of P' (a path of which P is a prefix) such
that, for all allocations A in AS, /(ad)*/ matches AH(A, P').
A path P is a coherence-prone path for a set of allocations AS if the end
point of P is a coherent point for AS, P is a life-preserving path, and //
matches AH(A, P) for all allocations A in AS.
End.
Lemma [coherent-point-allocated-coherence-helper]. (tree-independent)
If P is an initial path to a coherent point Y for a set of allocations
AS, and /a/ matches AH(A, P) for some allocation A in AS, then there does not
exist any initial path P' to Y for which either // or a refinement of /.*d/
matches AH(A, P').
Pf.
P is an initial path to a coherent point for AS, so there exists a
continuation
PC := (P..., PF...)
such that /(ad)*/ matches AH(A, P).
By substitution and [history-concatenation] we have:
AH(A, P_) = AH(A, PC)
= AH(A, (P..., PF...))
= (AH(A, P)..., AH(A, PF)...)
By assumption, /a/ must match AH(A, P), so /d(ad)*/ must match AH(A, PF) by
[history-pattern-prefix-removal].
Let P' be an arbitrary initial path to Y. Then
PC' := (P'..., PF...)
is an initial path.
If // matches AH(A, P'), then by [history-pattern-concatenation],
/d(ad)*/ matches A(PC'). Since PC' is an initial path, this violates
the dominance axiom.
If a refinement of /.*d/ matches AH(A, P'), then /.*d/ matches AH(A, P'),
and so, by [history-pattern-concatenation], /.*dd(ad)*/ matches A(PC');
but this is a refinement of /.*dd.*/, and since PC' is an initial path,
this violates the joint post-dominance axioms.
Qed.
Lemma [coherence-prone-paths-allocated-coherence]. (tree-independent)
If P1 and P2 are coherence-prone paths from X1 and X2 to the same point Y
for some set of allocations AS, then for all spanning trees T1 and T2,
for all allocations A in AS, if the state at X1 under T1 gives A allocated
status, the state at X2 under T2 gives A allocated status.
Pf.
Let PX1 be the path to X1 in T1, and let PX2 be the path to X2 in T2.
Then define
PEX1 := (PX1..., P1...)
PEX2 := (PX2..., P2...)
PEX1 and PEX2 are initial paths to Y.
Let an allocation A in AS be given such that the state at X1 under T1 gives
A allocated status.
By [history-concatenation], the definition of AH, and the assumptions
about P1,
AH(A, PEX1) = AH(A, (PX1..., P1))
= (AH(A, PX1)..., AH(A, P1)...)
= (AH(A, PX1)..., ()...)
= AH(A, PX1)
And similarly for P2,
AH(A, PEX2) = AH(A, (PX2..., P2))
= (AH(A, PX2)..., AH(A, P2)...)
= (AH(A, PX2)..., ()...)
= AH(A, PX2)
By [allocated-implies-allocation], /a/ matches AH(A, PX1), and
therefore /a/ matches AH(A, PEX1).
Exhaustively, the state at X2 under T2 must either not include A or give it
allocated, pending, or undeallocatable status.
If the state at X2 under T2 does not include A:
By [absent-implies-missing-or-deallocated], there exists an initial
path PX2' to X2 for which either // or /ad/ matches AH(A, PX2').
Then let PEX2' := (PX2'..., P2...). We have:
AH(A, PEX2') = AH(A, (PX2'..., P2))
= (AH(A, PX2')..., AH(A, P2)...)
= (AH(A, PX2')..., ()...)
= AH(A, PX2')
So either // or /ad/ matches AH(A, PEX2').
Either case is impossible by [coherent-point-allocated-coherence-helper].
If the state at X2 under T2 gives A pending status:
By [pending-implies-deallocation], /ad/ matches AH(A, PX2),
and so /ad/ matches AH(A, PEX2). This is also impossible by
[coherent-point-allocated-coherence-helper].
If the state at X2 under T2 gives A undeallocatable status:
Then by [deallocatibility-preserving-path], the state at Y gives
A undeallocatable status, which contradicts the assumption that Y is a
deallocatability-assuring point for AS.
So the state at X2 under T2 must give A allocated status.
Qed.
Lemma [coherence-prone-paths-pending-coherence]. (tree-independent)
If P1 and P2 are coherence-prone paths from X1 and X2 to the same point Y
for some set of allocations AS, then for all spanning trees T1 and T2, for
all allocations A in AS, if the state at X1 under T1 gives A pending
status, the state at X2 under T2 gives A pending status.
Pf.
Let S1 be the state at X1 under T1, and let S2 be the state at X2 under T2.
Let an allocation A in AS be given such that S1 gives A pending status. Then
by [correctness-invariant-5], there is an allocation B dominated by A that
S1 gives allocated status. S2 must therefore also give B allocated status
by [coherence-prone-paths-allocated-coherence].
S1 contains both A and B; let IA and IB be the index of A and B in S1.
Let SB be the state at B>. Since S1 contains B, by [allocation-index],
SB[LENGTH(SB) - 1][0] = S1[LENGTH(SB) - 1][0] = B
Since S1[IB][0] = B by construction, and S1 is a well-formed state that
cannot repeat allocations, it must be true that IB = LENGTH(SB) - 1. Since
A dominates B, IA < IB by [dominance-implies-precedes]. But this means that
IA < LENGTH(SB), so by [allocation-index]
SB[IA][0] = S1[IA][0] = A
But S2 also contains B, so by [allocation-index]
SB[IA][0] = S2[IA][0] = A
Therefore S2 contains A.
Exhaustively, S2 must either give A allocated, pending, or undeallocatable
status.
If S2 gives A allocated status:
S1 must give A allocated status by
[coherence-prone-paths-allocated-coherence], but this contradicts the
assumption that S1 gives A pending status.
If S2 gives A undeallocatable status:
Then by [deallocatibility-preserving-path], the state at Y gives
A undeallocatable status, which contradicts the assumption that Y is a
deallocatability-assuring point for AS.
Therefore S2 must give A pending status.
Qed.
Lemma [coherence-prone-paths-coherence]. (tree-independent)
If P1 and P2 are coherence-prone paths from X1 and X2 to the same point Y
for some set of allocations AS, then for all spanning trees T1 and T2, for
all allocations A in AS, if the state at X1 under T1 gives A status U, the
state at X1 under T2 gives A status U.
Pf.
Exhaustively, the state at X1 must give A allocated, pending, or
undeallocatable status.
If the state at X1 gives A allocated status, then so does the state at X2,
by [coherence-prone-paths-allocated-coherence].
If the state at X1 gives A pending status, then so does the state at X2,
by [coherence-prone-paths-pending-coherence].
If the state at X1 gives A undeallocatable status, then by
[deallocatability-preserving-path], the state at Y gives A undeallocatable
status, which contradicts the assumption that Y is a deallocatability-
assuring point for AS.
Qed.
Lemma [live-point-is-coherent]. (tree-independent)
If X is a point in a live SCC, then X is a coherent point for all
allocation sets AS.
Pf.
Let AS be an arbitrary set of allocations.
X is a deallocatability-assuring point for AS:
Let an allocation A in AS be given, and let a spanning tree T be given.
Because X is a point in a live SCC, the state at X cannot give A
undeallocatable status by [live-undeallocatable].
Because X is a point in a live SCC, there exists a path P_F from X
to a function exit. Let an arbitrary initial path P_I to X be given.
Then (P_I..., P_F...) is a terminating initial path, so by the joint
post-dominance axioms, /(ad)*/ matches AH(A, P) for all allocations A
(whether they're in AS or not).
Qed.
Lemma [live-coherence-helper].
If P is a path to a point in a live SCC, and // matches AH(A, P) for
all allocations A in some set AS, then P is a coherence-prone path for AS.
Pf.
The end point of P is a point in a live SCC, so by
[live-point-is-coherent], it is a coherent point for AS.
By [live-ancestry], P is a life-preserving path.
Therefore (X -> Y) satisfies the conditions of a coherence-prone path
for AS.
Qed.
Lemma [live-scc-coherence].
If an SCC C is not a dead-end region, then the end states
of all blocks with entry edges into C must be the same.
Pf.
Let two entry edges X1 -> Y1 and X2 -> Y2 into C be given. Because these
are entry edges, X1> and X2> are in proper ancestor SCCs of C.
Let P1I and P2I be the paths to X1> in X2> in T, and let S1 and S2 be the
states at X1> and X2>. Let AS be the set of all allocations that either P1I
or P2I passes through.
All allocations in AS are in proper ancestor SCCs of C:
Let A in AS be given. Either A is on P1I or A is on P2I. In either case,
the suffix of that path start at A> is a path to a point in a proper
ancestor SCC of C, and so A> must be in a proper ancestor SCC of C.
Y1 and Y2 are blocks in the same strongly-connected component, so
let P_Y2Y1 be a path from <Y1 to <Y2. Then let
P1 := (X1 -> Y1)
P2 := (X2 -> Y2, P_Y2Y1...)
For all allocations A in AS, AH(A, P1) = () by the definition of AH.
Therefore P1 is a coherence-prone path for AS by [live-coherence-helper];
it goes from X1> to <Y1.
For all allocations A in AS,
AH(A, P2) = AH(A, (X2 -> Y2, P_Y2Y1...))
By [history-concatenation]:
= (AH(A, (X2 -> Y2))..., AH(A, P_Y2Y1)...)
Applying the definiton of AH to the first component gives:
AH(A, (X2 -> Y2)) = ()
Applying [no-scc-self-path-history] to the second component gives:
AH(A, P_Y2Y1) = ()
Substituting and simplifying:
AH(A, P2) = (()..., ()...)
= ()
So P2 is also a coherence-prone path for AS by [live-coherence-helper]; it
goes from X2> to <Y1.
By [state-differences], one of three things must be true.
If S1 gives some allocation A a status U and S2 does not:
Since S1 contains A, by [present-implies-visit], P1I must pass through A.
Therefore A is in AS. So by [coherence-prone-paths-coherence], S2 must
give A the status U; so this is a contradiction.
If S2 gives some allocation A a status U and S1 does not:
Since S2 contains A, by [present-implies-visit], P2I must pass through A.
Therefore A is in AS. So by [coherence-prone-paths-coherence], S1 must
give A the status U; so this is a contradiction.
If S1 = S2:
The states at X1> and X2> are the same.
Therefore the end states of all blocks with entry edges into C are the same.
Qed.
Lemma [live-block-coherence].
If block B is live, then the end states of all blocks with edges to B must
be the same.
Pf.
Let B be given, alone with two edges X1 -> B and X2 -> B, and let
S1 and S2 be the states at X1> and X2>.
Let AS be the set of all allocations. By the definition of AH,
AH(A, (X1 -> B)) = AH(A, (X2 -> B)) = () for any allocation A.
Then by [live-coherence-helper], (X1 -> B) and (X2 -> B) are
coherence-prone paths for AS.
By [state-differences], one of three things must be true.
If S1 gives some allocation A a status U and S2 does not:
By [coherence-prone-paths-coherence], S2 must give A the status U; so this
is a contradiction.
If S2 gives some allocation A a status U and S1 does not:
By [coherence-prone-paths-coherence], S1 must give A the status U; so this
is a contradiction.
If S1 = S2:
The states at X1> and X2> are the same.
Therefore the end states of all blocks with entry edges to B are the same.
Qed.
Lemma [scc-entry-point-coherence].
For every SCC C, there is a unique state ENTRY(C) (under T) such
that, for all initial paths P in T that visit C, the state at the entry
point of P for C is ENTRY(C).
In tree-independent proofs, this is spelled ENTRY(C, T).
Pf.
Let X and Y be two points such that the paths PX and PY in T to X and Y,
respectively, both visit C.
If C is the entry SCC, then the entry point of both PX and PY for C is the
entry point of the function. The state at the entry point of the
function is always the empty stack.
Otherwise, C is not the entry SCC, so by [scc-entry-path-partition],
PX and PY can be partitioned at their entry edge to C:
PX = (PX_I..., MX -> NX, PX_F...)
PY = (PY_I..., MY -> NY, PY_F...)
where MX -> NX and MY -> NY are entry edges into C, and where PX_F and PY_F
are the entry suffixes of PX and PY for C, respectively. The start
points of PX_F and PY_F are the entry points of PX and PY for C by
[scc-entry-point].
Let SNX be the state at <NX and SNY be the state at <NY. Note that
(PX_I..., MX -> NX) is the path to <NX in T, and (PY_I..., MY -> NY)
is the path to <NY in T.
If C is a dead-end region, then by [dead-end-entry-coherence], SNX = SNY.
If C is not a dead-end region, then let SMX and SMY be the states at MX> and
MY>, respectively. By [live-scc-coherence], SMX = SMY.
Since MX -> NX and MY -> NY are not edges into a dead-end region, neither
edge corresponds to a MERGE operation, so by the state derivation rules,
SMX = SNX and SMY = SNY. By the transitivity of equality, SNX = SNY.
In all cases, then, given two arbitrary paths in T that visit C, the
start points of the entry suffixes of those paths for C have the
same state under T. Therefore ENTRY(C) is well-defined.
Qed.
Lemma [acyclic-scc-entry-state].
If a block B is in an acyclic SCC C, the state at <B is ENTRY(C).
Pf.
Let P be the path to <B in T. Since C is acyclic, the entry point of P
for C is <B by [acyclic-scc-entry-point]. By [scc-entry-point-coherence],
the state at <B is ENTRY(C).
Qed.
Lemma [scc-entry-operation-sequence].
Let P be the entry suffix of the path in T to some point X for the SCC C
containing X. Then
- P visits only points in C,
- OS_PATH(ENTRY(C), P) is a well-formed operation sequence,
- the end state of OS_PATH(ENTRY(C), P) is the state at X, and
- OS_PATH(ENTRY(C), P) does not contain any MERGE operations.
Pf.
By [scc-entry-point-coherence] and [scc-entry-point], the state at the
start point of P is ENTRY(C). By [operation-sequence-path], OS_PATH
(ENTRY(C), P) is a well-formed operation sequence, and the end state of
OS_PATH(ENTRY(C), P) is the state at X.
P ends at a point in C, so by [scc-entry-suffix-ancestry], all of the points
on P are in C.
Because all of the points on P at in a single SCC, P passes through no
entry edges (whether to a dead-end region or not), and so no component
of P can correspond to a MERGE operation.
Qed.
Lemma [scc-entry-path-end-state].
If P is an entry path of C in T, then the state at the end point of P is
ENTRY(C).
Pf.
P visits a point in C, so by [scc-entry-point],
P = (P_I..., P_F...)
where P_I is the SCC entry prefix of P for C and P_F is the SCC entry
suffix of P for C. By [scc-entry-path-ancestry], P = P_I, so P_F must be
the empty path at the end point of P.
By [scc-entry-point-coherence], the state at the entry point of P for C
(which is the start point of P_F) is ENTRY(C).
Qed.
Lemma [scc-entry-state-allocations].
For every SCC C, ENTRY(C) contains only allocations from proper ancestor
SCCs of C.
Pf.
Choose P to be an entry path of C in T, by [scc-entry-path-existence].
Let X be the end point of X. By [scc-entry-path-end-state], the state
at X is ENTRY(C).
Let A be an allocation in ENTRY(C); then P passes through A by
[present-implies-visit]. P is therefore non-empty, so by
[entry-path-ancestry], the points at all indexes of P except the final
index are in proper ancestor SCCs of C. Since P passes through A, the point
<A must be in a non-final index of P, and therefore it is a propery
ancestor SCC of C, and therefore A is in a proper ancestor SCC of C.
Qed.
Lemma [scc-external-allocation].
If the state at some point X in an SCC C contains an allocation A from
an SCC other than C, then ENTRY(C) contains A.
Pf.
Let P be the path to X in T. By [present-implies-visit], P passes through A,
which is not in C, so C cannot be the entry SCC. Then by
[scc-entry-path-partition]
P = (P_I, M -> N, P_F)
where <N is the entry point of P for C and P_F is the entry suffix
of P for C.
Since P is an initial path in T that visits C, the state at <N
is ENTRY(C) by [scc-entry-point-coherence]. If ENTRY(C) did not
contain A, then by [present-implies-visit-path], P_F would have to
pass through A. But P_F starts and ends at points in C, so it can
only pass through components of C, and A is not in C by assumption.
Therefore ENTRY(C) must contain A.
Qed.
Lemma [scc-undeallocatable-invariance].
If S is the state at a point X in SCC C, UDA(S) = UDA(ENTRY(C)).
Pf.
Let P be the path to X in T, and let P_F be the SCC entry suffix of P for C.
By [scc-entry-point-coherence] and [scc-entry-point], the state at the
start point of P_F is ENTRY(C).
By [scc-entry-operation-sequence], OS_PATH(ENTRY(C), P_F) is a well-formed
operation sequence that does not contain a MERGE operation and has S
as its end state. By [no-merge-implies-no-deallocatable-change],
UDA(ENTRY(C)) = UDA(S).
Qed.
Lemma [cyclic-scc-input-invariance].
Let SX be the state at a point X in a cyclic SCC C. Then there exists
a sequence SUF such that
SX = (ENTRY(C)..., SUF...)
where for all allocations A in SUF, A is in C and has a status of
either allocated or pending.
Pf.
Let P be the path to X in T, and let PX be the SCC entry suffix of P for C.
Let XC be the start point of PX. By [scc-entry-point-coherence], the
state at XC is ENTRY(C).
By [scc-entry-operation-sequence]
OSPX = OS_PATH(ENTRY(C), PX)
is a well-formed operation sequence, and the end state of OSPX is SX.
Furthermore, PX visits only points in C, and therefore only passes through
components of C, and so the operations on OSPX must correspond to
components of C.
We prove the lemma for SX by inducting on the operation sequence OSPX
that derives it from ENTRY(C).
In the base case, OSPX is the empty operation sequence at the state
ENTRY(C). Then let
SUF := ()
By definition, SX = ENTRY(C) = (ENTRY(C)..., SUF...). SUF is the empty
sequence and so the lemma holds vacuously.
In the inductive case,
OSPX = (OSPX'..., O)
where O corresponds to some component K, which must be a component of C
because all operations in OSPX correspond to components of C.
Let SX' be the preceding state of O in OSPX. Because OSPX is a well-formed
operation sequence, O must change SX' to SX.
By the inductive hypothesis, there exists a sequence SUF' such that
SX' = (ENTRY(C)..., SUF'...)
and where all the allocations in SUF' are in C and have a status of either
allocated or pending.
Note that K cannot be a deallocation of an allocation A in ENTRY(C), because
by [scc-entry-state-allocations], A would have to be from a proper ancestor
SCC of C, and by [no-cyclic-scc-deallocation], C cannot contain a
deallocation of an allocation that is not in C.
We proceed by cases on O.
If O is PUSH@K:
K is an allocation A. By definition and substitution,
SX = STATE_PUSH(SX', A)
= (SX'..., (A, allocated))
= (ENTRY(C)..., SUF'..., (A, allocated))
Then let
SUF := (SUF'..., (A, allocated))
Let be an allocation A' in SUF be given. Either A' = A or A' is in SUF'.
If A' = A, then A is a component of C and has allocated status.
If A' is in SUF', then A' is a component of C and has allocated or pending
status by the induction hypothesis.
If O is PEND@K:
K is a deallocation of some allocation A. By definition and substitution,
SX = STATE_PEND(SX', A)
= SET_STATUS(SX', A, pending)
= SET_STATUS((ENTRY(C)..., SUF'...), A, pending)
K is a component of C, so, as previously argued, it cannot be a
deallocation of an allocation in ENTRY(C). Therefore we can simplify
the SX equation further:
SX = (ENTRY(C)..., SET_STATUS(SUF', A, pending)...)
Then let
SUF := SET_STATUS(SUF', A, pending)
Let an allocation A' in SUF be given; then for some index I,
SUF[I][0] = A'. By the axioms of SET_STATUS, SUF'[I][0] = A', so by
the induction hypothesis, A' is a component of C. If A' = A, then
SUF gives A' pending status; otherwise, SUF gives A' the same status
that SUF' does, and by the induction hypothesis, SUF' gives A' either
pending or allocated status.
If O is POP@K:
K is a deallocation of some allocation A. By definition and substitution,
SX = STATE_POP(SX', A)
= PREFIX(SX', I)
= PREFIX((ENTRY(C)..., SUF'...), I)
where I is defined to be the smallest index such that the status in SX' at
index I' is pending for all indexes I' such that
I <= I' < LENGTH(SX') - 1
Therefore, if the status in SX' at index I' is not pending, either I > I'
or I' = LENGTH(SX') - 1.
By [correctness-invariant-3], the state at CX (which is ENTRY(C)) must
either be the empty stack or a stack whose top does not have pending
status. This means that either LENGTH(ENTRY(C)) = 0 or the status in
ENTRY(C) at index LENGTH(ENTRY(C)) - 1 is not pending.
If LENGTH(ENTRY(C)) = 0:
then I >= LENGTH(ENTRY(C)) trivially.
If the status in ENTRY(C) at index LENGTH(ENTRY(C)) - 1 is not pending:
By the earlier reasoning about STATE_POP, either
I > LENGTH(ENTRY(C)) - 1 or
LENGTH(ENTRY(C)) - 1 = LENGTH(SX') - 1.
If I > LENGTH(ENTRY(C)) - 1:
then I >= LENGTH(ENTRY(C)).
If LENGTH(ENTRY(C)) - 1 = LENGTH(SX') - 1:
Then we have
LENGTH(ENTRY(C)) = LENGTH(SX')
= LENGTH((ENTRY(C)..., SUF'...))
which means that SUF' must be the empty sequence. But K must be a
deallocation of the allocation at the top of the stack in SX', which
is to say, at the end of SX'; so if SUF' is the empty sequence, K
must be a dallocation of the last allocation in ENTRY(C), and as
previously argued this is impossible.
Therefore in all possible cases, I >= LENGTH(ENTRY(C)), so we can further
simplify the SX equation:
SX = (ENTRY(C)..., PREFIX(SUF', I - LENGTH(ENTRY(C)))...)
Then let
SUF := PREFIX(SUF', I - LENGTH(ENTRY(C)))
Let an allocation A' in SUF be given; let I be the index of A' in SUF.
By the axioms of prefix, SUF[I][0] = SUF'[I][0]; therefore A' is in SUF'
and must be in C by the induction hypothesis. Furthermore,
SUF[I][0] = SUF'[I][0], and by the induction hypothesis, SUF'[I][0] must
be either allocated or pending, so SUF[I][0] must be as well.
If O is MERGE@K:
This is impossible, because K is a component of C, which means it cannot
be an entry edge of any kind.
Therefore in all possible cases of O, there exists a sequence SUF such that
SX = (ENTRY(C)..., SUF...)
and where SUF only contains allocations in C and gives them allocated or
pending status. This proves the inductive case.
Then by induction the lemma holds regardless of the value of OSPX.
Qed.
Lemma [cyclic-scc-external-allocation].
If S is the state at some point in a cyclic SCC C, and A is an allocation
in an SCC other than C, then S gives A some status if and only if
ENTRY(C) gives A the same status.
Pf.
By [cyclic-scc-input-invariance], there exists a sequence SUF such that
SX = (ENTRY(C)..., SUF...)
where SUF contains only allocations in C. Since A is not in C, SUF
does not contain A, so only the elements of ENTRY(C) matter for whether
SX contains A and, if so, what status it gives A.
Qed.
Lemma [cyclic-scc-external-allocation-coherence].
If X and Y are points in a cyclic SCC C, and A is an allocation in an
SCC other than C, and the state at X gives A some status U, then
the state at Y gives A the same status.
Pf.
By [cyclic-scc-external-allocation], since the state at X gives A the
status U, ENTRY(C) gives A the status U.
By [cyclic-scc-external-allocation], since ENTRY(C) gives A the status U,
the state at Y gives A the status U.
Qed.
Lemma [cyclic-point-undeallocatable].
If X is a point in a cyclic SCC C, and A is an allocation in a descendent
SCC of C, the state SX at X cannot give A undeallocatable status.
Pf.
By [cyclic-scc-input-invariance], there exists a sequence SUF such that
SX = (ENTRY(C)..., SUF...)
where SUF contains only allocations in C and gives them only allocated
or pending status. By [scc-entry-state-allocations], ENTRY(C) contains
only allocations from proper ancestors of C and therefore cannot contain
A. Thus, if A is in SX, it must be in SUF, and therefore A can only have
pending or allocated status.
Qed.
Lemma [cyclic-point-is-coherent]. (tree-independent)
If X is a point in a cyclic SCC C, then X is a coherent point for the set
AS of all allocations in descendent SCCs of C.
Pf.
X is a deallocatability-assuring point for AS:
Let an allocation A in AS be given, and let a spanning tree T be given.
Because X is a point in a cyclic SCC, the state at X cannot give A
undeallocatable status by [cyclic-point-undeallocatable].
Let an arbitrary initial path P to X be given. By [scc-entry-point],
P = (P_I..., P_X...)
where P_X is the SCC entry suffix of P for C. The start point of P_X
is an SCC entry point for C.
Since C is cyclic, there exists a path P_XI from X back to the start
point of P_X. Define
P_L := (P_X..., P_XI...)
P_L is a cycle entirely within C, and it starts from an entry point of C.
Define
P_C := (P_I..., P_X..., P_XI...)
P_C is a continuation of P. By the associativity of concatenation,
P_C = (P_I..., (P_X..., P_XI...)...)
= (P_I..., P_L...)
Now also define
P_C' := (P_I..., P_L..., P_L...)
This is well-formed because P_L begins and ends at the start point of P_X.
P_C and P_C' are both initial paths.
Let an arbitrary allocation A be given from AS; by assumption, it is in
a descendent SCC of C. By [history-concatenation]
AH(A, P_C) = AH(A, (P_I..., P_L...))
= (AH(A, P_I)..., AH(A, P_L)...)
AH(A, P_C') = AH(A, (P_I..., P_L..., P_L...))
= (AH(A, P_I)..., AH(A, P_L)..., AH(A, P_L)...)
P_I is the entry prefix of P for C, so by [scc-entry-prefix-ancestry] it
cannot pass through any component of a descendent SCC of C, and therefore
it does not pass through A. It is an initial path, so it also cannot pass
through a deallocation of A, because then by the dominance axiom it would
also have to pass through A. So AH(A, P_I) = (). Therefore we have:
AH(A, P_C) = AH(A, P_L)
AH(A, P_C') = (AH(A, P_L)..., AH(A, P_L)...)
Because P_C is an initial path, /(ad)*a?/ must match AH(A, P_C) and
therefore AH(A, P_L). It is not directly required to end in a deallocation
by the joint post-dominance axioms because it is not terminal. But
AH(A, P_L) cannot end with an allocation: if it did, it would be non-empty,
and so AH(A, P_L) would also have to start with an A, and that means
AH(A, P_C') would pass through A twice without passing through an
intervening deallocation of A. In pattern terms, if /(ad)*a/ matches
AH(A, P_L), then /(ad)*a(ad)*a/ matches AH(A, P_C'); this is
equivalent to /(ad)*aa(da)*/, which is a refinement of /.*aa.*/, which
violates the joint post-dominance axioms.
So /(ad)*/ matches AH(A, P_L), and therefore also AH(A, P_C). Therefore
P_C is a continuation of P which satisfies the requirements.
Qed.
Lemma [scc-strong-entry-point-coherence].
If X -> Y is an entry edge into an SCC C, then the state at <Y is ENTRY(C).
(Note that [scc-entry-point-coherence] requires X -> Y to be in T.)
Pf.
Let PY be the path to <Y in T. If PY is an entry path of C, then
by [scc-entry-path-end-state], the state at <Y is ENTRY(C). Otherwise
PY = (PYI..., Z -> Y)
where Z -> Y is an internal edge of C by [scc-non-entry-path]. Because
C contains an internal edge, it is cyclic.
Let SY be the state at <Y. By [cyclic-scc-input-invariance], there exists
a state suffix SUF where
SY = (ENTRY(C)..., SUF...)
and where all allocations in SUF are in C and have either allocated or
pending status.
Suppose there is an allocation A in SUF. Then there exists an allocation
A' in SUF that has allocated status:
A either has allocated or pending status. If A has allocated status, then
let A' = A. Otherwise, A has pending status, so there is an allocation
A'' in SY which is dominated by A that has allocated status by
[correctness-invariant-5]; by [dominance-implies-precedes], the index of A''
in SX is greater than the index of A in SX, and therefore A'' is in SUF;
then let A' = A''.
By [allocated-implies-allocation], /a/ matches AH(A', PY).
<Y is in a cyclic SCC, and A' is an allocation in C, so there exists a
continuation of PY such that /(ad)*/ matches AH(A', PY) by
[cyclic-point-is-coherent]:
PY' = (PY..., PYF...)
By [history-prefix-removal], /d(ad)*/ matches AH(A', PYF).
Let PX be the path to X> in T, and define
PXY = (PX..., Z -> Y)
Then PXY is an initial path to <Y. Z -> Y is an entry edge into C, so
Z> is a point in a proper ancestor SCC of C, so by [history-proper-ancestor],
// matches AH(A', PX), and therefore (because Z -> Y is just an edge)
// matches AH(A', PXY).
Define
PXYF := (PXY..., PYF...)
PXYF is an initial path, and so /(ad)*a?/ matches AH(A', PXYF) by the
post-dominance axioms. But by [history-concatenation]
AH(A', PXYF) = (AH(A', PXY)..., AH(A', PYF)...)
= AH(A', PYF)
And since AH(A', PYF) starts with a deallocation of A', there is a
contradiction.
Therefore there cannot be any allocation A in SUF, so SUF is the empty
sequence, and therefore SY = ENTRY(C).
Qed.
Lemma [cyclic-edge-coherence].
If X -> Y is an internal edge of an SCC C, then the state at X>
is the same as the state at <Y.
Pf.
Because C has an internal edge, it must be a cyclic SCC. By
[cyclic-point-is-coherent], Y is a coherent point for the set SA of all
allocations in C.
The empty path at Y> is a coherence-prone path for SA because it ends
at a coherent point for SA, trivially does not pass through any entry
edges into dead-end regions, and trivially does not pass through any
allocations or deallocations for SA. It goes from Y> to Y>.
(X -> Y) is a coherence-prone path for SA because it ends at a coherent
point for SA, does not pass through any entry edges into dead-end regions
(because X -> Y is an internal edge), and trivially does not pass through
any allocations or deallocations for SA. It goes from <X to Y>.
Let SX be the state at X> and SY be the state at <Y. By [state-differences],
one of three things must be true:
If SX gives some allocation A a status U but SY does not:
If A is in AS, then by [coherence-prone-paths-coherence], SY must give
A status U, which is a contradiction.
If A is not in AS, then by [cyclic-scc-external-allocation-coherence],
SY must give A status U, which is a contradiction.
If SY gives some allocation A a status U but SX does not:
If A is in AS, then by [coherence-prone-paths-coherence], SX must give
A status U, which is a contradiction.
If A is not in AS, then by [cyclic-scc-external-allocation-coherence],
SX must give A status U, which is a contradiction.
If SX = SY:
Then the states are the same.
Qed.
Theorem [edge-coherence].
If there is an edge X -> Y that is not an entry edge into a dead-end
region, then the state at X> is the same as the state at <Y.
Pf.
If X -> Y is an internal edge of a SCC, then this is true by
[cyclic-edge-coherence].
Otherwise, X -> Y is an entry edge. Let P be the path to <Y in T. Y cannot
be the entry block because there is an edge to it, so P cannot be empty; let
P = (PX'..., X' -> Y)
The SCC containing Y cannot be a dead-end region by assumption, so Y must
be a live block.
Let SX be the state at X>, SX' be the state at X'>, and SY be the state at
<Y. By [live-block-coherence], SX = SX'. By [operation-cases-edge], there
are two cases.
If X' -> Y is a conservative entry edge:
Y is a live block, so this is a contradiction.
If X' -> Y is not a conservative entry edge:
SX' = SY by the conditions of this case. By transitivity, SX = SY.
Qed.
Dominance
---------
In this section, we are building up to a proof of invariant #2, that the
allocations on the current stack always dominate the point that the algorithm
is working on. Depth-first searches are not dominance-tree walks, and if
there are cycles in the CFG, a DFS can easily cross an edge to a block that
is not dominated by earlier blocks.
Fortunately, joint post-dominance means we cannot still have non-dominating
allocations with allocated status on the stack when crossing such edges, at
least when entering live regions: either there's a path where we don't
deallocate them before exiting the function, or there's some path from the
entry point that can reach that deallocation without ever passing through the
allocation.
Unfortunately, that doesn't save us directly when it comes to dead-end
regions. It would still be true that the algorithm wouldn't try to insert
deallocations for non-dominated allocations --- there's always a "guard"
allocation with allocated status on the stack which is dominated by any
pending allocations that we'd want to emit. But the twist we do with entry
edges into dead-end regions has the property of forcing coherence on those
edges in a way that re-establishes the stronger dominance property.
It's possible that this can be proven directly from coherence; if there
is, I didn't figure it out.
Definition [closed-under-dominance].
A set of allocations AS is closed under dominance if, for all allocations
A and B, if A is in AS, and A dominates B, then B is in AS.
End.
Lemma [coherent-point-allocated-dominance].
If the state at X gives an allocation A allocated status, and X is a
coherent point for a set of allocations including A, then A dominates X.
Pf.
Let P be the path to X in T. Since X is a coherent point for a set of
allocations that includes A, by definition there exists a continuation
of P:
P_C := (P..., P_F...)
such that /(ad)*/ matches AH(A, P_C). By [history-concatenation],
AH(A, P_C) = (AH(A, P)..., AH(A, P_F)...)
/a/ matches AH(A, P) by [allocated-implies-allocation], so by
[history-prefix-removal], /d(ad))*/ matches AH(A, P_F).
Let an arbitrary initial path P' to X be given, and define:
P_C' := (P'..., P_F...)
AH(A, P') must pass through A or else P_C', an initial path, will pass
through a deallocation of A without first passing through A, a violation
of the dominance axioms. Therefore A dominates X.
Qed.
Lemma [coherent-point-dominance].
If the state at X contains an allocation A, and X is a coherent point for a
set of allocations AS that includes A and is closed under dominance, then A
dominates X.
Pf.
By induction on the SCC tree.
Let X be a point in an SCC C.
Lemma [ih-apply].
If a point X' is in a proper ancestor SCC of C, and the state at X'
contains A, then A dominates X':
Pf.
Since X' is in a proper ancestor SCC of C, there is a path P_XX from
X' to X. Then, given an arbitrary initial path P' to X', the
path P := (P'..., P_XX...) is an initial path to X. Because X
is a coherent point for AS, there exists a continuation P_C of P
such that /(ad)*/ matches AH(A', P_C) for all allocations A' in AS.
P_C is also a continuation of P', so X' is a coherent point for AS.
Then by the inductive hypothesis, if the state at X' contains A,
A dominates X'.
Qed.
Exhaustively, the state at X either gives A allocated, pending, or
deallocated status.
If the state at X gives A allocated status:
A dominates X by [coherent-point-allocated-dominance].
If the state at X gives A pending status:
By [correctness-invariant-5], there is an allocation B dominated by A that
the state at X gives allocated status. Therefore B dominates X by
[coherent-point-allocated-dominance], and since A dominates B,
A dominates X by the transitivity of dominance.
If the state at X gives A undeallocatable status:
Let P be the path to path to X in T. By [undeallocatable-dead-end-entry],
P = (P_I..., M -> N, P_F...)
where M -> N is an entry edge into C, C is a dead-end region, and
the state SN at <N gives A undeallocatable status.
Now let an arbitrary initial path P' to X be given. By
[scc-entry-path-partition]
P' = (P_I', M' -> N', P_F')
where M' -> N' is an entry edge into C. M' is therefore a block with an
entry edge into C, so the state at M'> must contain A by
[dead-end-entry-preserves-allocation-set-prefix]. M' is a proper ancestor
of C, again because it has an entry edge into C, so by [ih-apply],
A dominates M'>, which is to say, all initial paths to M'> must pass
through A. P_I' is an initial path of M'>, so it must pass through A.
Therefore P' does as well.
Since the choice of initial path to X was arbitrary, this is true for
all such paths, and so A dominates X.
Therefore, regardless of exactly which status the state at X gives A,
A dominates X.
Qed.
Lemma [scc-descendent-allocations-set-is-closed-under-dominance].
The set AS of allocations in descendent SCCs of some SCC C is
closed under dominance.
Pf.
Let A and B be allocations, and suppose that A is in AS. By definition, A is
a descendent SCC of C. Let P_B be an initial path to B; since B is
dominated by A, A is on P_B. Let P_AB be a suffix from of P_B starting at
A>. Then P_AB is a path from A> to <B, so B must be in a descendent SCC of
the SCC containing A, which is itself a descendent SCC of C. Therefore B is
an allocation in a descendent SCC of C and must be in AS.
Qed.
Theorem [correctness-invariant-2].
If the state at X contains an allocation A, then A dominates X.
Pf.
By induction on the SCC tree.
Let X be a point in an SCC C. C is either a live SCC or a dead-end region.
If C is a live SCC:
Let AS be the set of all allocations. By [live-point-is-coherent], X is a
coherent point for AS. A is trivially in AS, and AS is trivially closed
under dominance. Then by [coherent-point-dominance], A dominates X.
If C is a dead-end region:
A is either in a descendent SCC of C or in a proper ancestor of C.
If A is in a descendent SCC of C:
A must actually be in C:
By [present-implies-visit], the path to X in T passes through A.
So the suffix of that path starting from A> is a path from a point
in the SCC containing A to a point in C, o the SCC containing A
is an ancestor of C; since it is also a descendent by assumption,
it must be C.
C is either a cyclic SCC or not.
If C is a cyclic SCC:
Let AS be the set of allocations in descendent SCCs of C.
By [cyclic-point-is-coherent], X is a coherent point for AS.
A is in AS by definition. AS is closed under dominance by
[scc-descendent-allocations-set-is-closed-under-dominance].
Therefore by [coherent-point-dominance], A dominates X.
If C is not a cyclic SCC:
An acyclic SCC can only contain one block, and both X and C
must be in that block. Then by [same-block-dominance], A
dominates X.
If A is in a proper ancestor SCC of C:
Since there exists a proper ancestor SCC of C, C is not the entry SCC.
Let P be the path to X in T. P can be partitioned at its entry edge
into C by [scc-entry-path-partition]:
P = (P_I..., M -> N, P_F...)
M -> N is an entry edge into R. It is in T because P is a path in T.
Since the state at X contains A, and A is a proper ancestor SCC of C,
ENTRY(C) contains A by [scc-external-allocation]. P is an initial
path in T, and <N is the entry point of P for C, so the state at <N is
ENTRY(C) by [scc-entry-point-coherence]. Therefore the state at <N
contains A.
Now let an arbitrary initial path P' to X be given. P' can also be
partitioned at its entry edge into C by [scc-entry-path-partition]:
P' = (P_I'..., M' -> N', P_F'...)
M' -> N' is an entry edge into C, so M' is a block with such an edge.
By applying [operation-cases-dead-entry-edge] to M -> N (the entry edge
in T taken from P above) and M' (the predecessor block from the
arbitrary path to X), we can conclude that the state at M'> contains A.
M'> is a point in a proper ancestor SCC of C, so by the induction
hypothesis, A dominates M'>. By the definition of dominance, any initial
path to M'> must pass through A. P_I' is such a path, and so it passes
through A; but then so must P'. Since P' was an arbitrary initial path
to X, all initial paths to X must pass through A. Then by definition,
A dominates X.
Qed.
Theorem [correctness-invariant-1].
For all points X, if the state at X contains allocations A and B, then A
dominates B if and only if A is below B on the stack.
Pf.
If SX[I][0] dominates SX[J][0], I < J:
By [dominance-implies-precedes].
If I < J, then SX[I][0] dominates SX[J][0]:
By [stack-order], the path PX to X in T passes through A before it passes
through B, so
PX = (PA..., A, PAB..., B, PBX...)
By [presence-implies-presence], since the state at X contains A, the
state at <B also contains A. So by [correctness-invariant-2], A
dominates B.
Qed.
Tree Invariance
---------------
In this section, we are building up to a proof that the search order doesn't
actually matter: the stack will be the same at a particular point in the
function, no matter how we we reached that point. What this looks like in
formal terms is that we prove that the states are the same regardless of
the choice of tree.
This largely builds on the earlier concepts of coherence, as the central
lemmas actually work independently of spanning trees. It's quite possible
that more of the coherence proofs could be directly generalized to work with
arbitrary spanning trees, the way that [coherence-prone-paths-coherence] has
been, making most of the following proof redundant. Many parts of this proof
still reflect the order in which results were proven. Coherence-prone paths
weren't constrained to be life-preserving until coherence had to be proven in
cyclic dead-end regions, and coherent points weren't constrained to be
deallocatability-assuring until invariance had to be proven across trees.
(Undeallocatable status can't change along a coherence-prone path within
a single tree because the entry states of a SCC are always the same.)
Lemma [life-preserving-path-invariance]. (tree-independent)
For any two spanning trees T1 and T2, if P is a life-preserving path from X
to Y, and P is in both T1 and T2, and the state at X is the same under T1
and T2, then the state at Y is the same under T1 and T2.
Pf.
Let SX be the state at X in both T1 and T2, and let SY1 and SY2 be the
states at Y in T1 and T2 respectively. By [operation-sequence-path],
SY1 is the end state of the path OS_PATH(S, P, T1), and SY2 is the end
state of the path OS_PATH(S, P, T2). But
OS_PATH(S, P, T1 = OS_PATH(S, P, T2)
by [correctness-operation-sequence-helper], and therefore SY1 = SY2.
Qed.
Lemma [block-invariance]. (tree-independent)
Given two spanning trees T1 and T2 and a point X, if the state at the start
point of the block containing X is the same under T1 and T2, then the
state at X is the same under T1 and T2.
Pf.
Let B be the block containing X, and let SB be the state at <B in both T1
and T2. Let P1 and P2 be the paths from <B to X in T1 and T2; because
these are simple paths between points in the same block, P1 = P2. Then
the states at X under T1 and T2 are the same by
[life-preserving-path-invariance].
Qed.
Lemma [predecessor-invariance-implies-pss-invariance]. (tree-independent)
If, for all points X in proper ancestor SCCs of some SCC C, the state
at X under T1 is the same as the state at X under T2, then
PSS(C, T1) = PSS(C, T2).
Pf.
Let a state S in PSS(C, T1) be given; by the definition of PSS, there is
a block B such that the state at B> under T1 is S. By the induction
hypothesis, the state at B> under T2 is also S, so S is in PSS(C, T2).
Let a state S in PSS(C, T2) be given; by the definition of PSS, there is
a block B such that the state at B> under T1 is S. By the induction
hypothesis, the state at B> under T1 is also S, so S is in PSS(C, T1).
Therefore PSS(C, T1) = PSS(C, T2).
Qed.
Lemma [predecessor-invariance-implies-entry-state-invariance]. (tree-independent)
If, for all points X in proper ancestor SCCs of some SCC C, the state
at X under T1 is the same as the state at X under T2, then
ENTRY(C, T1) = ENTRY(C, T2)
Pf.
If C is the entry SCC, then ENTRY(C, T1) = STATE_EMPTY = ENTRY(C, T1).
Otherwise, let X be a point in C, and let P be the path to X in T1. By
[scc-entry-path-partition]
P = (PI..., M -> N, PF...)
where M -> N is an entry edge into C.
Let SN1 and SN2 be the states at <N under T1 and T2. By
[scc-strong-entry-point-coherence], SN1 = ENTRY(C, T1) and
SN2 = ENTRY(C, T2).
If C is not a dead-end region:
Let SM1 and SM2 be the states at M> under T1 and T2. SM1 = SN1 and
SM2 = SN2 by [edge-coherence]. M> is a point in a proper ancestor SCC of
C, so by the invariance assumption, SM1 = SM2. Therefore
ENTRY(C, T1) = SN1 = SM1 = SM2 = SN2 = ENTRY(C, T2).
If C is a dead-end region:
Let PSS1 = PSS(C, T1), and let PSS2 = PSS(C, T2). PSS1 = PSS2 by
By [predecessor-invariance-implies-pss-invariance].
Applying [dead-end-entry-coherence] to SN1, one of three cases must apply.
If C is the entry SCC:
This contradicts the "otherwise" above.
If PSS1 is singleton:
Then SN1 is the single element of PSS1. But PSS1 = PSS2, so PSS2 is also
singleton. Applying [dead-end-entry-coherence] to SN2, and discarding
contradicted cases, SN2 must be the single element of PSS2. Since
PSS1 = PSS2, the single element of PSS1 is the same as the single
element of PSS2, so SN1 = SN2, so ENTRY(C, T1) = ENTRY(C, T2).
If PSS1 is not singleton:
Then SN1 = CONSERVATIVE_MERGE(PSS1). But PSS1 = PSS2, so PSS2 is also
not singleton. Applying [dead-end-entry-coherence] to SN2 and discarding
contradicted cases, SN2 = CONSERVATIVE_MERGE(PSS2). Since PSS1 = PSS2,
CONSERVATIVE_MERGE(PSS1) = CONSERVATIVE_MERGE(PSS2), so SN1 = SN2, so
ENTRY(C, T1) = ENTRY(C, T2).
Qed.
Lemma [cyclic-point-invariance-helper]. (tree-independent)
Given two spanning trees T1 and T2, if Y is a point in a cyclic SCC C, and
ENTRY(C, T1) = ENTRY(C, T2), then for all allocations A, if the state at Y
under T1 gives A some status U, then the state at Y under T2 gives A the
status U.
Pf.
Let P1 and P2 be the entry suffixes for C of the paths to Y in T1 and T2.
Let X1 and X2 be the start points of P1 and P2.
Let SX be the state at X1 under T1, and let SX2 be the state at X2 under T2.
By [scc-entry-point-coherence], SX = ENTRY(C, T1) and SX2 = ENTRY(C, T2).
By the assumption of the lemma, ENTRY(C, T1) = ENTRY(C, T2), so transitively
SX = SX2.
Let SY1 and SY2 be the states at Y under T1 and T2. By
[cyclic-scc-input-invariance],
SY1 = (SX..., SUF1...)
SY2 = (SX2..., SUF2...) = (SX..., SUF2...)
where SUF1 and SUF2 contain only allocations from C and give them either
allocated or pending status. By [scc-entry-state-allocations], SX contains
only allocations from proper ancestors of C.
Now let an allocation A be given such that SY1 gives A the status U. Let
AI be the index of A in SY1, such that SY1[AI][0] = A and SY1[AI][1] = U.
If A is not in C:
AI must be < LENGTH(SX):
If AI >= LENGTH(SX), then SY1[AI][0] = SUF1[AI - LENGTH(SX)][0] = A;
but SUF1 only contains allocations from C, and A is not in C.
Therefore SY1[AI][0] = SX[AI][0] = A, and SY1[AI][1] = SX[AI][0] = U.
But then SY2[AI][0] = SX[AI][0] = A, and SY2[AI][1] = SX[AI][0] = U,
so SY2 gives A the status U.
If A is in C:
Let AS be the set of allocations in descendent SCCs of C. By
[cyclic-point-is-coherent], Y is a coherent point for AS. A is in
AS because it is in C, which is a descendent SCC of itself.
The empty path at Y is a coherence-prone path for AS: its end point
is a coherent point for AS, and it is trivially a life-preserving path
for which // matches AH(A, P) for all allocations A in AS.
Then by [coherence-prone-paths-coherence], SY2 gives A the status U.
Qed.
Lemma [cyclic-point-invariance]. (tree-independent)
Given two spanning trees T1 and T2, if Y is a point in a cyclic SCC C, and
ENTRY(C, T1) = ENTRY(C, T2), then the state at Y under T1 is the same
as the state at Y under T2.
Pf.
Let SY1 and SY2 be the states at Y under T1 and T2. By [state-differences],
SY1 and SY2 must differ in one of two ways, or else they are the same.
If SY1 gives an allocation A the status U, and SY2 does not:
By [cyclic-point-invariance-helper], SY2 must give A the status U,
so this is a contradiction.
If SY2 gives an allocation A the status U, and SY1 does not:
By [cyclic-point-invariance-helper], SY1 must give A the status U,
so this is a contradiction.
Therefore SY1 = SY2.
Qed.
Theorem [tree-invariance]. (tree-independent)
Given two spanning trees T1 and T2, for all points X, the state at X under
T1 is the same as the state at X under T2.
Pf.
By induction on the SCC tree.
Let a point X be given in an SCC C such that, for all points X' in proper
ancestor SCCs of C, the state at X' under T1 is the same as the state at X'
under T2. Let SX1 and SX2 be the states at X under T1 and T2.
By [predecessor-invariance-implies-entry-state-invariance],
ENTRY(C, T1) = ENTRY(C, T2).
If C is not a cyclic SCC, then let B be the block containing X.
By [acyclic-scc-entry-point], the state at <B under T1 is ENTRY(C, T1),
and the state at <B under T2 is ENTRY(C, T2), which are equal. Therefore,
SX1 = SX2 by [block-invariance].
Otherwise, C is a cyclic SCC. SX1 = SX2 by [cyclic-point-invariance].
Qed.
Mutations
---------
This section proves the correctness of the actual removals and insertions
performed by the algorithm. Or, well, it will.
Theorem.
The algorithm preserves the joint post-dominance of
allocations and deallocations.
Pf.
FIXME