Lectures onType Theory
Chapter 22
Chapter 22Core route

Refinement Types and Proof-Carrying Programs

Chapter 9 stopped at the relational obligation 0i<len(a): its finite semantic types could classify an array operation but could not relate one runtime index to one runtime length. This chapter takes that inequality as its entire assertion logic. A refinement type attaches a logical predicate to the values of an ordinary type; the subtyping judgment compares such types under a logical context.

The type int says that an array index is an integer. It does not say that the index belongs to the particular array being read. For example, the simply typed function (λa.let n=lena in let z=getan in z:a:arrint) gets stuck on every array: its index is exactly one past the last position. An intersection or a fixed subtype of integers cannot repair the program, because the missing upper bound is the run-time measure lena.

Refine an integer by conjunctions of difference constraints xyk. The bounds 0i<len(a) then become two paths in a finite weighted graph, and the checker replays those paths without trusting the search procedure.

One A-normal array calculus

Definition 10.1 — Source calculus, bind composition, and reduction

Every intermediate computation is named by a let, and every computation operand is already an atom—a variable or value. Consequently a closed nonfinal term has at most one active redex, at its outer let or conditional. Integer literals range over all of Z, array literals contain integers, and every function value carries its full arrow annotation. B::=intarr,v::=nn0,,nk1(λx.e:x:st)(fix f(x).e:x:st),a::=xv,c::=a+klenageta1a2a1a2,e::=alet x=c in eif δ then e1 else e2error. Here k is a literal constant, so a+k is the only arithmetic primitive. This restriction matters: the exact relation z=x+k is expressible by two difference constraints, whereas z=x+y is not. Each test δ is one difference constraint, so taking a branch adds a predicate in the same logic. The term error is a checked failure, not a value. The conditional scrutinee δ is a logical difference atom, not a Boolean-valued term. At runtime all its vertices have integer values, and the truth value of the resulting inequality selects the branch.

This is the standard difference-logic fragment: formulas built from conjunctions of inequalities xyk. Allowing arbitrary addition such as z=x+y would leave this fragment for Presburger arithmetic, which is still decidable but requires a substantially heavier procedure. Allowing products of variables leaves difference logic and would require a different arithmetic theory and certificate format; no decision claim for that extension is made here. The narrow fragment is intentional: it lets us state and prove the complete certificate checker rather than trust an opaque solver.

Definition 22.2 — Bind composition

Function application is a computation and must therefore occur on the right-hand side of a let. To reduce such an application without leaving the A-normal grammar, define bind composition e1xe2 by recursion on e1: axe2=e2[a/x],(let y=c in e)xe2=let y=c in (exe2),(if δ then e0 else e1)xe2=if δ then(e0xe2) else(e1xe2),errorxe2=error. Bound variables are renamed before the second clause when necessary. Bind composition substitutes an A-normal computation into its continuation while preserving the A-normal grammar.

Definition 22.3 — Reduction

Reduction is defined on closed terms. Closed atoms are values. For a literal array A with entries n0,,nm1, the rules are as follows.

q=n+k in Z
let x=n+k in ee[q/x]
E-Shift
let x=lenA in ee[m/x]
E-Len
0i<m
let x=getAi in ee[ni/x]
E-Get
let y=(λx.e1:x:st)v in e2e1[v/x]ye2
E-Beta
F=(fix f(x).e1:x:st)
let y=Fv in e2e1[F/f,v/x]ye2
E-Fix
δ is true
if δ then e1 else e2e1
E-IfT
δ is false
if δ then e1 else e2e2
E-IfF

Term substitution acts inside δ using the normalizing predicate substitution of definition 10.3. After closing substitution, every integer variable and array-length vertex has therefore become an integer literal, and a test normalizes to a comparison between integer constants. Exactly one of E-IfT and E-IfF applies. There is deliberately no rule for an out-of-bounds get. Such a term is stuck, and ruling it out will be the safety theorem rather than a convention inside evaluation.

Proposition 10.2 — The unguarded last index is stuck

Let A=7. The closed program let n=lenA in let z=getAn in z reduces once to let z=getA1 in z, which is neither a value nor error and has no successor.

Proof of Proposition 10.2 — The unguarded last index is stuck

Proof. len7=1, so E-Len yields let z=get71 in z. Its only possible rule is E-Get; its side condition is 01<1, which is false. No other rule has that head form. ◻

The stuck term is the calculus’s formal proxy for an unchecked bounds fault. Nothing here predicts whether a host implementation would trap, raise an exception, or perform an unsafe memory access; those machine behaviors are outside this source semantics. Array safety means that no reachable source term has this stuck get form.

Exercise 10.1

★★★ Assume xy, yfv(e0), and xfv(e2). Prove by induction on e0 that (e0xe1)ye2=e0x(e1ye2), after the capture-avoiding renamings demanded by the definition. Treat the conditional and error clauses explicitly. For the atom case, first prove by induction on e1 the auxiliary commutation equation e1[a/x]ye2=(e1ye2)[a/x] under the stated freshness hypotheses.

Difference refinements

Definition 10.3 — Difference predicates, refinement types, and substitution

Write La as an abbreviation for the measure len(a); it is not a new program variable. In a scope, the vertices available to predicates are r::=0xLa, where x and a range over raw term-variable names and 0 denotes the integer zero. Grammar alone does not assign a shape to those names: definition 10.4 admits x only for an integer-shaped declaration and La only for an array-shaped declaration. A difference atom and a predicate are δ::=r1r2k,p::=trueδp. Thus predicates are finite conjunctions only. We use the derived writings r1<r2 for r1r21, r1r2 for r1r20, and r1=r2 for the conjunction r1r20r2r10. Negating one atom stays in the language because integers are discrete: r1r2k:=r2r1k1. There is no disjunction, multiplication, sum of two vertices, array equality, or arbitrary quantifier-free linear arithmetic.

For a base-type binder ν, its observable vertex is μint(ν)=ν and μarr(ν)=Lν. Types are t::={ν:Bp}x:st. A refinement type {ν:Bp} restricts the values of base type B to those whose distinguished binder ν satisfies p. The binder x of an arrow may occur in the codomain only through x when s has integer shape, or through Lx when s has array shape. A function-typed x has no predicate vertex. We abbreviate {ν:Btrue} by B.

Literal substitution is normalized back into the grammar. Replacing an integer variable by n replaces its vertex by 0+n; replacing La by an array literal of length m replaces it by 0+m; constants are moved to the right of . For instance, (xLa1)[3/x,4,5/a]normalizes to002. Substitution for function variables changes no predicate. Under these clauses, t[a/x] is total and capture avoiding for every well-shaped atom a.

Definition 10.4 — Scopes, well-formed contexts, and types

A context is a list Γ::=Γ,x:tΓ,δ with distinct term variables. Its vertex scope V(Γ) contains x for an integer-shaped entry, Lx for an array-shaped entry, and no vertex for a function-shaped entry. If p is a conjunction, Γ,p abbreviates the context obtained by appending its atoms in order. The following rules define well-formedness; “p over W” means that every vertex of p belongs to W{0}. Γallp means that every valuation satisfying the refinements and guards in Γ also satisfies p. By contrast, Γat is a syntactic typing judgment.

 ctx
WF-Empty
Γ ctxΓt typexdom(Γ)
Γ,x:t ctx
WF-Var
Γ ctxδ over V(Γ)
Γ,δ ctx
WF-Guard
Γ ctxp over V(Γ){μB(ν)}
Γ{ν:Bp} type
WF-Base
Γs typeΓ,x:st type
Γx:st type
WF-Arrow

The shape |t| is obtained by erasing predicates: |{ν:Bp}|=B and |x:st|=|s||t|.

The type a:{ν:arr0Lν1}int is well formed: its domain says 1len(a). In contrast, a:arr{ν:intaν0} is not a type, since an array variable is not an integer vertex. Nor can a refinement say that two arrays are equal; only their lengths are observable.

Exercise 10.2

★☆☆ Determine which of the following are well formed in the empty context. For each rejection, say first whether the displayed predicate is grammatical and, if it is, give the failed scope check: (a)a:arr{ν:intνLa1},(b)i:int{ν:arrLνi0},(c)f:(intint){ν:intνf0},(d){ν:arrLν04}.

Valuations and entailment

A valuation ρ assigns an integer to every integer variable and an integer array to every array variable. It interprets 0 as 0 and La as the length of ρ(a). Satisfaction of an atom is integer comparison, and satisfaction of a conjunction is componentwise.

Definition 10.5 — Context embedding and entailment

The predicate contributed by a base entry is obtained by replacing the distinguished vertex by the entry’s vertex: =true,Γ,x:{ν:Bp}=Γp[x/ν],Γ,x:(y:st)=Γ,Γ,δ=Γδ. For arrays, p[x/ν] replaces Lν by Lx. We write ρΓ when ρΓ, and Γallp when every valuation of the variables of Γ that satisfies Γ also satisfies p. Array valuations are actual finite arrays, so 0La0 is valid for every array variable a. ρΓ is a fact about one valuation; Γallp quantifies over all such ρ.

Lemma 22.8 — Semantic substitution for predicate vertices

Let a be a well-sorted atom of the same base shape as x, and let p be a predicate over V(Γ,x:s,Δ){0}. For every valuation ρ of Γ,Δ[a/x], extend ρ by assigning to x the value of a under ρ. Then ρp[a/x]ρ[xρ(a)]p, where the left predicate is normalized as in definition 10.3. The equivalence also holds entry by entry for the context embedding of Δ.

Proof of Lemma 22.8 — Semantic substitution for predicate vertices

Proof. It is enough to check vertices. An unrelated vertex keeps the same integer value. If x has integer shape, substitution replaces x by the integer representative of a, so both sides evaluate it as ρ(a). If x has array shape, substitution replaces Lx by the length representative of a, so both sides evaluate it as len(ρ(a)). Moving the representative’s integer offset to the right of preserves the atom’s truth value. Conjunction and the ordered context embedding follow by induction on their finite lists. ◻

For example, in Γ0=a:{ν:arr0Lν1},n:{ν:intνLa0Laν0}, we have Γ0all0n1. A satisfying valuation has n=La and La1. The superficially similar conclusion nLa1 is false: a one-element array with n=1 is a countervaluation.

Certificates for implication

Entailment is semantic, so the word “valid” is not evidence. A finite certificate must exhibit either a weighted path proving the requested bound or a negative cycle proving inconsistency. A replay check verifies the named edges, their endpoints, and their integer weights.

Ramalingam, Song, Joskowicz, and Miller formulate systems xixjbij, their feasibility problem, and the corresponding negative-cycle view on pp. 261–263 [RSJM99]. Their paper studies an incremental algorithm. Definition 10.6, Definition 10.6 fix a nonincremental path-or-cycle certificate; completeness is theorem 10.9.

Definition 10.6 — Constraint graph and replay certificates

For a context Γ, form the weighted directed multigraph GΓ. Its vertices are 0 and the vertices in V(Γ). Each hypothesis r1r2k contributes the edge r2kr1: this includes every atom in every base-type entry of Γ and every explicit guard entry, not merely the latter. For every array vertex La, add the implicit length edge La00, which expresses 0La. Edges carry identifiers, so duplicate constraints remain distinguishable.

If h assigns integers to vertices with h(0)=0, then it satisfies the graph when h(v)h(u)+kfor every edge ukv. This is exactly satisfaction of the corresponding difference constraints. Because the length edges force h(La)0, every satisfying h is realized by a program valuation: assign h(x) to integer x and choose, independently for each a, any integer array of length h(La).

Definition 10.6 — Constraint graph and replay certificates

For a goal r1r2k, a path certificate is a possibly empty list of edge identifiers forming a directed path from r2 to r1 whose integer weight sum is at most k. Thus the empty list certifies r1r1k when 0k. A contradiction certificate is a nonempty cyclic list of edge identifiers in GΓ whose weight sum is negative.

The checker re-reads every named edge from GΓ, checks adjacency and the required endpoints, adds weights as mathematical integers, and checks the final inequality. For a conjunctive goal it accepts either one contradiction certificate for the context or one path certificate for each conjunct. Parsing, edge lookup, adjacency, and unbounded integer addition are therefore part of the trusted checker.

Lemma 10.7 — Path and negative-cycle soundness

If the checker accepts a path certificate for Γallr1r2k, then that entailment is valid. If it accepts a contradiction certificate for Γ, then Γ has no satisfying valuation.

Proof of Lemma 10.7 — Path and negative-cycle soundness

Proof. Let u0k1u1k2kmum be a checked path and let h satisfy the graph. Adding the m edge inequalities cancels the intermediate potentials and gives h(um)h(u0)k1++km. For a path from r2 to r1 with sum at most k, this is h(r1)h(r2)k, the goal.

For a checked cycle, um=u0, so the same addition gives 0k1++km. A negative checked sum contradicts this inequality. Hence no potential, and therefore no valuation, satisfies Γ. ◻

Lemma 10.8 — Potentials from absence of negative cycles

A finite integer-weighted graph containing the distinguished vertex 0 has a satisfying integer potential h with h(0)=0 iff it has no negative directed cycle.

Proof of Lemma 10.8 — Potentials from absence of negative cycles

Proof. For the forward implication, add the potential inequalities around any directed cycle. Intermediate terms cancel and give 0w, where w is the cycle’s total weight; hence no negative cycle exists. For the reverse implication, assume there is no negative cycle. Shortest paths from 0 alone need not reach every vertex. Add a fresh source q and a zero-weight edge from q to every old vertex. For each vertex v, let d(v) be the least weight of a path from q to v. This least integer exists: every path may have its directed cycles removed; the removed cycles have nonnegative total weight, and only finitely many simple paths remain. For every old edge ukv, appending that edge to a shortest path to u gives d(v)d(u)+k. Set h(v)=d(v)d(0). Subtracting the same integer preserves all edge inequalities and makes h(0)=0. Thus h is the required integer potential. ◻

Theorem 10.9 — Certificate checker soundness and completeness

For every well-formed context Γ and difference atom r1r2k over V(Γ){0}, Γallr1r2k iff the checker accepts either a negative-cycle certificate for GΓ or a path certificate from r2 to r1 of weight at most k. Consequently, the bundle checker is sound and complete for conjunctive goals.

Proof of Theorem 10.9 — Certificate checker soundness and completeness

Proof. Soundness is lemma 10.7. For completeness, first suppose GΓ has a negative cycle. Its edge list is an accepted contradiction certificate.

Now suppose it has no negative cycle. Add to it the edge r1k1r2, which represents the integer negation r2r1k1 of the goal. If the augmented graph had no negative cycle, lemma 10.8 would give an integer potential satisfying both Γ and the negated goal. The length edges make every array potential nonnegative, so choose arrays of those lengths; this realizes the potential as a countervaluation, contrary to the assumed entailment. Thus the augmented graph has a negative closed walk. Decompose that walk at repeated vertices into simple directed cycles. Their weights sum to the negative total, so at least one component cycle is negative. It cannot lie entirely in the old graph, which has no negative cycle; hence it contains the single new edge, exactly once. Removing that edge leaves an old path from r2 to r1, say of weight w, and negativity says wk1<0. Since weights are integers, wk. This path is the required certificate.

A conjunction is valid exactly when each conjunct is valid. Applying the atomic result to every conjunct gives a path bundle, unless the common context is inconsistent, in which case its single negative cycle certifies every conjunct. ◻

Corollary 10.10 — Certificate-producing decision

Validity of a well-formed difference sequent is decidable, and a positive decision can be accompanied by evidence accepted by the replay checker.

Proof of Corollary 10.10 — Certificate-producing decision

Proof. On the finite graph GΓ, first search for a simple negative cycle. If one exists, its edge list is a contradiction certificate. Otherwise, compute shortest paths from the goal source r2 and compare the distance to r1 with k. A path of weight at most k is a certificate of validity. If no such path exists, add the negated-goal edge r1k1r2. The augmented graph has no negative cycle: an old one was excluded, and a new one would remove to an r2-to-r1 path of weight at most k. Apply lemma 10.8 to the augmented graph. Its potential satisfies the old context and the negated goal, hence is a countervaluation. Both searches terminate because a simple path or cycle uses at most the finite number of vertices. ◻

Bellman–Ford finds a negative cycle or all relevant shortest paths in O(|V||E|) integer relaxations; predecessor pointers reconstruct the emitted edge list. The decision theorem depends only on termination and on replay of that evidence, not on this complexity bound.

Exercise 10.3

★☆☆ For hypotheses xy2, yz4, and zx1, write the three graph edges, give a contradiction certificate, and reproduce the integer sum that the checker tests. Then change the last constant to 2 and give a satisfying potential with h(0)=0.

Subtyping and its structural laws

Definition 10.11 — Declarative subtyping

Base refinement subtyping is implication. For arrows, a function of the subtype must handle every argument promised by the supertype, so the domain premise is t1<:s1; codomains are compared under x:t1. The printed glyph is shared with the structural subtyping relation of chapter 8, but this judgment is refinement subtyping under a logical context. No rule or law of the earlier relation is imported here.

zdom(Γ)Γ,z:{ν:Bp}allq[z/ν]
Γ{ν:Bp}<:{ν:Bq}
S-Base
Γt1<:s1Γ,x:t1s2<:t2
Γ(x:s1s2)<:(x:t1t2)
S-Arrow

All conclusion types must be well formed in their respective contexts. A function at the left arrow can be applied to every s1, whereas a context using the right arrow checks its argument at t1; hence S-Arrow requires t1<:s1. Because these are the only two subtyping rules, every derivation preserves outer shape: S-Base relates base refinements and S-Arrow relates arrows. Thus “same shape” means the outer constructor equality obtained by this rule inversion.

Lemma 22.16 — Shape preservation for refinement subtyping

If Γs<:t, then |s|=|t|.

Proof of Lemma 22.16 — Shape preservation for refinement subtyping

Proof. Induct on the subtyping derivation. Rule S-Base uses one common base type B. Rule S-Arrow has arrow types in its conclusion, and its two induction hypotheses identify the corresponding domain and codomain shapes. ◻

For example, put N0={ν:int0ν0},N1={ν:int0ν1},Rx={ν:intνx1xν1}. Then z:N1all0z0N1<:N0SBasex:N1,z:Rxall0z0x:N1Rx<:N0SBase(x:N0Rx)<:(x:N1N0)SArrow. The first certificate is the single edge z0 of weight 1, accepted against the weaker bound 0. In the second premise the path zx0 has weight 1+(1)=20. This displays both contravariance and the use of the target-domain hypothesis in the codomain.

Lemma 10.12 — Context implication and narrowing

Suppose Γs<:s, and suppose the suffix Δ is well formed after either Γ,x:s or Γ,x:s. Every valuation satisfying Γ,x:s,Δ also satisfies Γ,x:s,Δ. Hence:

  1. if Γ,x:s,Δallp, then Γ,x:s,Δallp;

  2. any type-formation or subtyping derivation under Γ,x:s,Δ may be narrowed to Γ,x:s,Δ.

Proof of Lemma 10.12 — Context implication and narrowing

Proof. If ρ satisfies Γ,x:s,Δ, the premise Γs<:s implies that ρ(x) satisfies the base refinement in s, and the unchanged entries of Δ remain true in order. For arrow types neither entry contributes a predicate. Therefore every entailment valid under x:s is valid under x:s.

For clause 2, induct on the formation or subtyping derivation. In a base formation premise, the vertex scope is unchanged by lemma 22.16. In an S-Base premise, clause 1 transports the entailment. In the representative S-Arrow case, narrow both domain and codomain premises; alpha-rename the arrow binder away from x and append it to Δ before applying the induction hypothesis to the codomain. ◻

Proposition 10.13 — Subtyping laws

For well-formed types, subtyping preserves shape: if Γs<:t, then |s|=|t|. It is reflexive: Γt<:t. It is transitive: if Γr<:s and Γs<:t, then Γr<:t.

Proof of Proposition 10.13 — Subtyping laws

Proof. Clause 1 is lemma 22.16.

For reflexivity, induct on t. At a base, every valuation satisfying p[z/ν] satisfies it, so S-Base applies. At an arrow, the domain induction hypothesis gives the contravariant premise, and the codomain hypothesis gives the covariant premise in the extended context.

For transitivity, induct on the common erased shape. At a base, suppose the predicates are p,q,r. A valuation satisfying p satisfies q by the first subtyping premise and then r by the second; S-Base gives the result. At arrows, write the three domains A0,A1,A2 and codomains C0,C1,C2. The two derivations give A1<:A0,A2<:A1,C0<:C1 under x:A1,C1<:C2 under x:A2. The domain induction gives A2<:A0. Narrow the first codomain derivation from x:A1 to x:A2 by lemma 10.12; the codomain induction then gives C0<:C2 under x:A2. Rule S-Arrow assembles the desired arrow subtyping. ◻

Exercise 10.4

★★☆ Derive a:arr{ν:intνLa10ν0}<:{ν:intνLa0}. Give the path certificate for its only nontrivial goal. Then explain why the reverse subtyping fails by giving an array and an integer countervaluation.

Declarative typing and verification conditions

A tempting checking rule would permit subsumption at every syntax node: ΓesΓs<:tΓetbadSub. A top-down checker would have to guess s; choosing s=t reproduces its original goal, so the search is non-structural. Exact synthesis removes that choice: an atom or computation determines s, and subtyping compares that s with the expected type.

Definition 10.14 — Exact base types and declarative typing

Atoms synthesize singleton refinements. For an integer atom a represented by (r,k), the two constraints are νrk and rνk. Write repI(a)=(r,k), where (x,0) represents an integer variable x and (0,n) represents the literal n. For an array atom a, write repL(a)=(r,k) for its length representative, where (La,0) represents an array variable and (0,m) represents an array literal of length m. Define EqI(r,k)={ν:intνrkrνk},Shift(a,j)=EqI(r,k+j)when a has integer representative (r,k),Length(a)=EqI(r,k)when a has length representative (r,k),Arraym={ν:arrLν0m0Lνm}. The exact integer type of n is EqI(0,n).

If i has integer representative (ri,ki) and a has length representative (ra,ka), define the bounds predicate Bnd(a,i):=(0riki)  (rirakaki1). The first conjunct is 0i and the second is i<len(a), after moving offsets to the right.

Typing is declarative but bidirectional. Atoms and computations synthesize; expressions check.

x:tΓ
Γxt
D-Var
ΓnEqI(0,n)
D-Int
A has length m
ΓAArraym
D-Array
Γx:st typeΓ,x:set
Γ(λx.e:x:st)x:st
D-Lam
Γx:st typef,xdom(Γ)fxΓ,f:(x:st),x:set
Γ(fix f(x).e:x:st)x:st
D-Fix
Γas|s|=int
Γa+kcShift(a,k)
D-Shift
Γas|s|=arr
ΓlenacLength(a)
D-Length
Γas|s|=arrΓiu|u|=intΓallBnd(a,i)
Γgetaicint
D-Get
Γfx:stΓas
Γfact[a/x]
D-App
ΓasΓs<:t
Γat
D-Sub
ΓccsΓ,x:setxdom(Γ)fv(t)
Γlet x=c in et
D-Let
Γ,δ ctxΓ,δe1tΓ,δe2t
Γif δ then e1 else e2t
D-If
Γt type
Γerrort
D-Error

Rule D-Sub is the only subsumption rule, and it applies only when a synthesized atom meets an expected type. The side condition of D-Let prevents a local name from escaping in the result type. Dependency is nevertheless useful inside the body, where the exact type synthesized for the computation becomes a hypothesis.

Lemma 22.20 — Regularity of synthesis

If Γ ctx, then every derivation Γas or Γccs has a conclusion type satisfying Γs type. Consequently the context Γ,x:s in D-Let is well formed when x is fresh.

Proof of Lemma 22.20 — Regularity of synthesis

Proof. Induct mutually on atom and computation synthesis. Variable lookup in a well-formed context gives formation of its declared type. The literal and array types are formed because their representatives use only vertices in V(Γ){0}. Lambda and fixpoint conclusions use their stated formation premises. Arrow formation derives Γ,x:s ctx for D-Lam; for D-Fix, the premises f,xdom(Γ) and fx derive Γ,f:(x:st),x:s ctx.

Shift and length merely change integer offsets in a well-scoped exact type, and get returns int. In the application case, inversion of formation for x:st gives formation of t under Γ,x:s. The argument check gives a well-sorted atom of shape |s|. Induct on the formation derivation of t. At a base refinement, the representative of a replaces only x or Lx and belongs to V(Γ){0} by the recursive synthesis hypothesis; hence the normalized predicate is still in scope. At an arrow, alpha-rename the binder away from a and apply the induction hypothesis to its domain and codomain. Thus t[a/x] is formed under Γ. ◻

Lemma 22.21 — Substitution commutes with representatives and guards

Let ζ=[a/x] be a well-sorted atom substitution, and normalize every result as in definition 10.3. If rep(a)=(ra,ka) at the relevant sort, define ζ(r,k)={(ra,k+ka),r=x or r=Lx at that sort,(r,k),otherwise. Then:

  1. computing repI or repL after ζ gives rep(bζ)=ζ(rep(b));

  2. Shift, Length, and Bnd commute with ζ (for example, Bnd(b,i)ζ=Bnd(bζ,iζ));

  3. for every atomic guard δ, δζ=δζ.

All equalities are syntactic equalities of normalized predicates or types.

Proof of Lemma 22.21 — Substitution commutes with representatives and guards

Proof. For clause 1, inspect the two atom forms. A literal has representative (0,n) and is unchanged. A variable other than x is unchanged. The distinguished variable is replaced by a, so its pair becomes exactly repI(a), or repL(a) at array shape. Moving the resulting constant offset to the right is precisely the stipulated normalization.

Clause 2 follows by substituting those pairs into the defining equations of Shift, Length, and Bnd. For clause 3, write δ=r1r2k. Substitution changes only r1 and r2, after which both orders of calculation produce r2ζr1ζk1 before the same normalization. No semantic entailment is used in these commutation facts. ◻

Definition 10.15 — VC generation

KΓ(e,t) either fails structurally or returns the finite sequents whose validity is equivalent to Γet. Each emitted verification condition (VC) is a sequent (Γ;p), discharged by checking Γallp. Define the partial finite translation SubVC(Γ;s,t) recursively. With z fresh and arrow binders alpha-aligned, its successful clauses are SubVC(Γ;{ν:Bp},{ν:Bq})={(Γ,z:{ν:Bp};q[z/ν])},SubVC(Γ;(x:s1s2),(x:t1t2))=SubVC(Γ;t1,s1)SubVC(Γ,x:t1;s2,t2). The translation fails on a shape mismatch, an ill-formed input type, or a failed recursive call. The first arrow call compares t1 with s1, which is the contravariant premise of S-Arrow.

The executable checker uses judgments ΓatC,ΓcctC,ΓetC. The function AΓ(a) returns an atom type and a VC list; CompΓ(c) returns a computation type and a VC list; and KΓ(e,t) returns the VCs for checking e against t. The get clause emits the two conjuncts of Bnd(a,i), and SubVC emits one implication at each base refinement. Write for union of finite VC lists. A clause fails when its shape or well-formedness test fails. AΓ(x)=(Γ(x),),AΓ(n)=(EqI(0,n),),AΓ(n0,,nm1)=(Arraym,),AΓ(λx.e:x:st)=(x:st,C)if KΓ,x:s(e,t)=C,AΓ(fix f(x).e:x:st)=(x:st,C)if KΓ,f:(x:st),x:s(e,t)=C. Put BΓ(a,i)={(Γ;δ)δ is a conjunct of Bnd(a,i)}. The computation clauses are CompΓ(a+k)=(Shift(a,k),C)if AΓ(a)=(s,C), |s|=int,CompΓ(lena)=(Length(a),C)if AΓ(a)=(s,C), |s|=arr,CompΓ(getai)=(int,CaCiBΓ(a,i))if AΓ(a)=(s,Ca), |s|=arr,AΓ(i)=(u,Ci), |u|=int,CompΓ(fa)=(t[a/x],CfCa)if AΓ(f)=(x:st,Cf),KΓ(a,s)=Ca. Abbreviate the conditional source expression by eδ. The checking clauses are KΓ(a,t)=CSubVC(Γ;s,t)if AΓ(a)=(s,C),KΓ(let x=c in e,t)=CcCeif CompΓ(c)=(s,Cc),xdom(Γ)fv(t),KΓ,x:s(e,t)=Ce,KΓ(eδ,t)=C1C2if KΓ,δ(e1,t)=C1,KΓ,δ(e2,t)=C2,KΓ(error,t)=, where eδ=if δ then e1 else e2. Every clause first checks its context and input types for well-formedness. Arithmetic occurs only in the sequents emitted by SubVC and the get clause.

Lemma 10.16 — Subtyping VCs are exact

If s,t are well formed, then SubVC(Γ;s,t) succeeds with valid sequentsΓs<:t.

Proof of Lemma 10.16 — Subtyping VCs are exact

Proof. Induct simultaneously on the shapes of s,t. At equal bases, the translation is the premise of S-Base, so the claims are identical. At arrows, the translation produces exactly the domain VCs and, under the target domain, exactly the codomain VCs. Apply the two induction hypotheses and S-Arrow. Unequal shapes admit neither a translation nor, by proposition 10.13, a subtyping derivation. ◻

Lemma 22.24 — Generated-output exactness

Whenever generation returns an output, AΓ(a)=(s,C)(Γasevery VC in C is valid),CompΓ(c)=(s,C)(Γccsevery VC in C is valid),KΓ(e,t)=C(Γetevery VC in C is valid).

Proof of Lemma 22.24 — Generated-output exactness

Proof. Use simultaneous structural induction on atoms, computations, and checked expressions. The induction hypothesis equates each recursive declarative premise with validity of the VC list returned by its recursive call.

Variables, literals, and arrays emit no VCs, so their equations coincide with D-Var, D-Int, and D-Array. The lambda equation succeeds exactly when its annotation is well formed and the body VCs are valid; by the induction hypothesis this is the premise of D-Lam. The fixpoint equation adds the recursive function binder to that lambda calculation and gives the body premise of D-Fix under both binders.

For get, the generated list is CaCiBΓ(a,i). Its first two parts validate the operand typings; its last two sequents are exactly the bounds premise of D-Get. Shift and length use the same atom shape tests as their declarative rules. If AΓ(f)=(x:st,Cf) and KΓ(a,s)=Ca, the induction hypotheses give the two premises of D-App, and the computation result is t[a/x].

For checking, the atomic equation combines the atom equivalence with lemma 10.16; this is precisely D-Sub in each direction. For a let, the computation induction hypothesis gives its synthesized type s, and the expression hypothesis gives the continuation judgment under x:s; the escape condition is exactly that of D-Let. The two conditional lists give the branch premises under δ and δ, and error requires only formation of its expected type. ◻

Lemma 22.25 — Declarative generation completeness

Each declarative judgment has a generated output of the same result type whose VCs are all valid. More precisely: ΓasC. AΓ(a)=(s,C)Valid(C),ΓccsC. CompΓ(c)=(s,C)Valid(C),ΓetC. KΓ(e,t)=CValid(C), where Valid(C) means that every VC in C is valid.

Proof of Lemma 22.25 — Declarative generation completeness

Proof. Induct simultaneously on the declarative derivations. Rules D-Var, D-Int, and D-Array select their matching generation clauses, whose shape tests succeed by the conclusion type of the rule. In D-Lam and D-Fix, the formation premise makes the annotation test succeed; the induction hypothesis generates the body list under exactly the binders in the declarative premise. Rules D-Shift, D-Length, and D-Get determine the operand shapes by inversion, and the last rule’s bounds premise validates BΓ(a,i). In D-App, inversion of the synthesized operator type selects the application clause, and the two induction hypotheses generate its operator and argument lists.

For checking, D-Sub generates the atom list followed by SubVCΓ(s,t); the induction hypothesis and lemma 10.16 validate both parts. Rule D-Let generates the computation list and then the body list under its synthesized type; its freshness and escape tests are the rule’s side conditions. Rules D-If and D-Error select their unique syntax clauses, with the branch and formation premises proving that generation succeeds. These cases cover every declarative rule and establish validity of every returned list. ◻

Theorem 10.17 — VC-producing checker correctness

The three generated-output equivalences of lemma 22.24 hold, and every declarative judgment has the valid generated output stated in lemma 22.25. Consequently, generation failure means declarative failure. Successful generation—which may initially emit invalid VCs—reduces typing exactly to validation and replay of the emitted certificates.

Proof of Theorem 10.17 — VC-producing checker correctness

Proof. Generated-output exactness is lemma 22.24; the existence of an output for every declarative derivation is lemma 22.25. Finally, theorem 10.9 equates validity of each finite VC list with accepted replay evidence. ◻

The corrected last-element trace

Define the nonempty-array type NEArr:={ν:arr0Lν1} and the annotated program last:=(λa.let n=lena inlet i=n+(1) inlet z=getai in z:a:NEArrint). The checker synthesizes n=La,i=n1, as pairs of constraints. At the read its context graph contains, among other duplicate or weaker edges, La10,La0n,n0La,n1i,i1n. The two bounds VCs and their certificates are goalpathweight0i0inLa01+01=0,iLa1Lani01=1. This is the decisive correction to the unguarded program: the read uses i=n1, and the nonempty precondition adds the graph edge used by the path that proves 0i. An empty literal has LA=0; checking it against NEArr asks for 01, so no certificate exists.

Testing the empty case explicitly yields a total function on arbitrary arrays: lastOrZero:=(λa.let n=lena inif n00 then 0 elselet i=n+(1) inlet z=getai in z:a:arrint). The exact type of 0 is a subtype of int, so the then branch checks at int. In the else branch the complement is 0n1; length synthesis gives n=La, and shift synthesis gives i=n1. The lower-bound certificate is i1n10,1+(1)=0, and the upper-bound certificate is La0n1i,0+(1)=1. For the D-Get premise, put Γa=a:arr, sn=Length(a), δ=(n00), and si=Shift(n,1). Write Γi=Γa,n:sn,δ and Γe=Γi,i:si. For the nested terms, put ez=let z=getai in z,ei=let i=n+(1) in ez,eif=if δ then 0 else ei,en=let n=lena in eif. In the else context, the two paths prove the final premise of a:arrΓeΓeaarrDVar|arr|=arri:siΓeΓeisiDVar|si|=intΓeallBnd(a,i)ΓegetaicintDGet. Call this D-Get derivation Dz. Let Dn denote the D-Length derivation of Γalenacsn, and let Di denote the D-Shift derivation of Γin+(1)csi. Let D0,Dz be the D-Sub checks of 0 and z against int. These derivations combine as follows: a:arrint typeDnΓa,n:sn,δ ctxD0:Γa,n:sn,δ0intDiDzDz:Γe,z:intzintΓeezintDLetΓieiintDLetΓa,n:sneifintDIfΓaenintDLetlastOrZeroa:arrintDLam. The two path sums prove ΓeallBnd(a,i). Length and shift synthesis emit no VCs; the two bounds sequents are therefore the only VCs in this derivation.

Exercise 10.5

★☆☆ Replace the input type of last by arr. Show that the second bounds VC still has a path certificate but the first does not. Give a potential for the resulting context with La=n=0 and i=1 that refutes the first VC.

Safety from the generated obligations

Preservation uses two local facts: a checked base atom satisfies its refinement, and bind composition preserves the type of its continuation.

Lemma 10.18 — Atom reflection and static substitution

Assume every context and type named in the two clauses is well formed.

  1. If Γa{ν:Bp}, then a has base shape B and Γallp[a/ν] after literal normalization.

  2. If Γas, then formation and subtyping derivations under Γ,x:s,Δ survive capture-avoiding substitution in Γ,Δ[a/x].

Proof of Lemma 10.18 — Atom reflection and static substitution

Proof. For clause 1, invert D-Sub to obtain an exact synthesized type s0 and Γs0<:{ν:Bp}. Let ρΓ, let va be the value of the closed atom obtained from a under ρ, and extend ρ by a fresh z with value va. For a variable atom, satisfaction of its declaration shows that this extension satisfies z:s0. For an integer or array literal, the two equality constraints in EqI or Arraym hold by calculation. A function form cannot synthesize the required base shape. The premise Γs0<:{ν:Bp} gives p[va/ν] by S-Base. Since ρ was arbitrary, this is the required entailment.

For clause 2, induct simultaneously on formation and subtyping. At base shape, let ρ satisfy Γ,Δ[a/x], evaluate a in ρ, and extend ρ by assigning that value to x. Clause 1 states that this value satisfies the base refinement in s, so the x:s entry is true. By lemma 22.8, the extension satisfies Γ,x:s,Δ entry by entry. At arrow shape, well-formed predicates cannot mention x, so their substitution and the context embedding are unchanged and no semantic value for a function variable is needed. Consequently every S-Base entailment survives substitution. Base formation uses the same scope-substitution calculation. Arrow formation and S-Arrow apply the induction hypotheses beneath a fresh binder. ◻

Lemma 22.28 — Application transport

Suppose a is an atom, Γ(y:AC)<:(y:AC)andΓaA. Then ΓaAandΓC[a/y]<:C[a/y].

Proof of Lemma 22.28 — Application transport

Proof. Arrow inversion gives ΓA<:A and Γ,y:AC<:C. Invert the check of a: it synthesizes some A0 with A0<:A. Transitivity followed by D-Sub checks a at A. Applying lemma 10.18(2) to the codomain comparison gives the second conclusion. ◻

Lemma 10.19 — Typing under a more precise context

Suppose Γs<:s, and suppose the same suffix Δ is well formed after either x:s or x:s. Then:

  1. a checking derivation under Γ,x:s,Δ remains a checking derivation under Γ,x:s,Δ;

  2. if an atom or computation synthesizes u under Γ,x:s,Δ, then under Γ,x:s,Δ it synthesizes some u with u<:u in the more precise context.

Formation and subtyping premises are transported by lemma 10.12; exact synthesis is not claimed.

Proof of Lemma 10.19 — Typing under a more precise context

Proof. Induct mutually on checking, atom synthesis, and computation synthesis. For D-Var, the distinguished variable changes its synthesized type from s to s, and the required comparison is the hypothesis; every other variable keeps its declared type. Literals keep their exact singleton type. Lambda and fixpoint annotations do not change: narrow their formation premises, apply the checking induction hypothesis to their bodies after freshening the binders, and return the same arrow.

Shift, length, and get apply the atom induction hypotheses. Shape preservation for subtyping retains their side conditions, while narrowing transports the bounds entailment in the get case; their synthesized result types depend on atom syntax, so they are unchanged. For application, suppose the new function type is y:AC and Γ,x:s,Δ(y:AC)<:(y:AC). The old argument checks at A by the checking induction hypothesis. Lemma 22.28 checks it at A and compares the two instantiated codomains. Rule D-App therefore derives result type C[a/y].

For D-Sub, the atom induction hypothesis gives u<:u; narrowing gives the old u<:t premise in the new context, and transitivity gives the check at t. In D-Let, computation precision gives u<:u. Apply the checking induction hypothesis to the tail under y:u, then apply this lemma recursively to change that declaration to y:u before applying the let. The two conditional branches use the induction hypotheses after the same guard has been appended; error uses narrowed formation. ◻

Lemma 10.20 — Weakening, atom reflection, and substitution

Assume every context and type named in the three clauses is well formed.

  1. If a judgment of formation, subtyping, synthesis, or checking holds under Γ, it continues to hold after inserting fresh, well-formed entries whose variables are not captured.

  2. Suppose Γas. Formation, subtyping, and checking judgments under Γ,x:s,Δ remain derivable after capture-avoiding substitution in Γ,Δ[a/x]. If an atom or computation synthesized u before substitution, its substituted syntax synthesizes some u with Γ,Δ[a/x]u<:u[a/x].

  3. If Γ,x:st type and xfv(t), then Γt type.

Proof of Lemma 10.20 — Weakening, atom reflection, and substitution

Proof. For weakening, use mutual rule induction. Extending a context preserves every vertex scope, and a valuation satisfying the extension also satisfies its prefix. Hence formation and entailment premises remain valid. Alpha-rename a binder before extending beneath it.

For substitution, strengthen the mutual induction so that synthesis of a substituted term returns a subtype of the substituted result type. Consider an entailment premise and let ρ satisfy Γ,Δ[a/x]. If s is a base refinement, evaluate a under ρ and extend ρ with xρ(a). By lemma 10.18(1), this value satisfies s. The context clause of lemma 22.8 then gives ρ[xρ(a)]Γ,x:s,Δ. The original entailment and normalization of literal substitution give the required entailment under Γ,Δ[a/x]. If s is an arrow, scope excludes x from every refinement, so the entailment is unchanged. For the variable x, inversion of Γas gives Γas0 and s0<:s; every other variable synthesizes its substituted declaration.

Literal, array, lambda, and fixpoint synthesis returns the substituted result type. For shift, length, and get, lemma 22.21(1–2) gives the substituted exact type and bounds predicate. In the application case, suppose the function induction hypothesis gives y:AC<:y:AC. The substituted argument checks at A. Lemma 22.28 checks it at A, so D-App synthesizes C[a0/y], and supplies the required comparison C[a0/y]<:C[a0/y]. If a substituted let computation synthesizes u<:u[a/x], lemma 10.19 changes the tail declaration from y:u[a/x] to y:u, after which D-Let applies. Atomic checking uses transitivity with its subtyping premise. Binders are alpha-renamed away from x. Clause 3 of lemma 22.21 gives, in D-If, δ[a/x]=δ[a/x].

For clause 3, induct on the formation derivation. Base refinements cannot mention x when it contributes no vertex, and otherwise the hypothesis xfv(t) removes every possible occurrence. Arrow formation applies the induction hypothesis to the domain and, after alpha-renaming its binder, to the codomain. If xfv(t), deleting x:s preserves formation of t; this is precisely the escape condition required by D-Let. ◻

Lemma 10.21 — Canonical forms through subsumption

If vt, then:

  1. if |t|=int, v is an integer literal;

  2. if |t|=arr, v is an array literal;

  3. if |t| is an arrow, v is an annotated lambda or annotated fixpoint.

Proof of Lemma 10.21 — Canonical forms through subsumption

Proof. The last checking rule is D-Sub, so v synthesizes some s with s<:t. By proposition 10.13, |s|=|t|. Inspecting the five synthesis rules gives exactly the constructor stated for each shape: integer literals are the only closed atoms synthesized at integer shape, array literals the only ones at array shape, and the two annotated function forms the only ones at arrow shape. Rule D-Var cannot conclude in the empty context. ◻

Lemma 10.22 — Typing bind composition

If Γe1s and Γ,x:se2t, with xfv(t), then Γe1xe2t.

Proof of Lemma 10.22 — Typing bind composition

Proof. Induct on the syntax-directed checking derivation of e1. If e1 is an atom, bind composition is e2[e1/x], typed by lemma 10.20(2). If it is a let, bind composition retains the same computation and composes into the tail. If that computation synthesizes u, weaken the continuation from Γ,x:s to Γ,y:u,x:s after freshening y, apply the induction hypothesis under Γ,y:u, and apply D-Let. If it is a conditional, weaken the continuation once under Γ,δ and once under Γ,δ, apply the branch induction hypotheses, and apply D-If. If it is error, composition is error, typed by D-Error. ◻

Arbitrary strengthening is false: deleting a guard may destroy the very path used by a bounds proof. Preservation needs only the following exact case, where the deleted guard already follows from the remaining context. For example, in Γ=a:arr,i:int, append the guard δiLa1. Its graph edge La1i is itself the path certificate for the upper bound of getai. Delete δ, and that path disappears; the valuation La=i=0 refutes the same bound. The valid-guard lemma applies only when another path in GΓ already proves δ.

Lemma 10.23 — Valid-guard discharge

Suppose Γallδ. Let Δ be any suffix well formed after Γ,δ; deleting the guard leaves the same vertex scope. Every one of the following judgments transports from Γ,δ,Δ to Γ,Δ with the same subject and type: context and type formation, subtyping, atom synthesis, computation synthesis, and expression checking. In particular, Γ,δetΓet.

Proof of Lemma 10.23 — Valid-guard discharge

Proof. Use simultaneous induction on the five derivations, generalized over Δ. A guard contributes no vertex, so formation scopes do not change. For an S-Base premise, let ρΓ,Δ. Then ρΓ, hence ρ satisfies δ, and therefore ρΓ,δ,Δ; the original entailment applies. All syntactic premises retain the same lookup, shape, freshness, and escape conditions. Under an arrow, lambda, fixpoint, or let binder, extend the generalized suffix by its fresh declaration. For D-If, extend it by the selected branch guard. The induction hypotheses then give the original conclusion under Γ,Δ. ◻

Theorem 10.24 — Preservation

If et and ee, then et.

Proof of Theorem 10.24 — Preservation

Proof. Invert the checking rule and consider the reduction used.

For E-Shift, the let-bound computation has type Shift(n,k) and q=n+k. Directly checking the two defining constraints shows qShift(n,k). Substitute q for the let variable in the tail by lemma 10.20(2). The E-Len case is the same calculation with m=len(A) and Length(A).

For E-Get, the computation type is int. The selected array element ni checks at int because its exact singleton subtype has the valid conclusion true. Substitute it into the tail.

For E-Beta, inversion of D-App and D-Lam gives x:se1t0, the closed argument vs, and a continuation typed under y:t0[v/x]. Substitution gives e1[v/x]t0[v/x]; then lemma 10.22 types the reduct. For E-Fix, write F=fix f(x).e1:x:st0. Inversion of D-Fix gives f:(x:st0),x:se1t0. The same rule synthesizes Fx:st0; reflexive subtyping and D-Sub therefore check F at that type. Apply clause 2 of lemma 10.20 first at f, then at x. This gives x:se1[F/f]t0,e1[F/f,v/x]t0[v/x]. The inverted continuation premise is y:t0[v/x]e2t. The bind-typing lemma then types the reduct at t.

For E-IfT, the closed true atom δ is valid in the empty context, so lemma 10.23 types the selected branch. For E-IfF, the integer complement δ is true and the same argument selects the other branch. Valid-guard discharge thus removes δ, respectively δ, from the selected branch typing. ◻

Theorem 10.25 — Progress up to checked error

If et, then exactly one of the following classes applies: e is a value; e=error; or some e satisfies ee.

Proof of Theorem 10.25 — Progress up to checked error

Proof. Proceed by the final checking rule. An atom is a value, and error is checked error. A closed conditional test is an integer inequality, so exactly one of E-IfT and E-IfF applies.

For a let-bound shift, lemma 10.21(1) writes the operand as an integer literal, and E-Shift applies. For length, lemma 10.21(2) writes it as an array literal, and E-Len applies. For get, write the operands as A=n0,,nm1 and i. The typing premise allBnd(A,i) is 0i<m, which is precisely the side condition of E-Get. A closed function atom is an annotated lambda or fixpoint, so application takes E-Beta or E-Fix. Values, checked error, and these redex heads are syntactically disjoint. ◻

Corollary 10.26 — Array safety and refinement soundness

Let et and ee. Then e is a value, is error, or can step; in particular it is not stuck at an out-of-bounds read. Moreover, if t={ν:Bp} and e is a value, then e has base shape B and satisfies p[e/ν].

Proof of Corollary 10.26 — Array safety and refinement soundness

Proof. Repeated preservation types e at t, and progress gives the first claim. An out-of-bounds get is neither a value nor error and has no successor, so it cannot occur. For the second claim, apply atom reflection lemma 10.18(1) in the empty context. This also explains the qualification “up to checked error”: D-Error permits a deliberate contract failure, but no unclassified stuck state. ◻

Refinement typing classifies every reachable state as a value, error, or a reducible term. A separate reachability proof is required to show that the distinguished checked error is never reached.

Exercise 10.6

★★☆ Write the two substitutions in the E-Fix preservation case with all types displayed. Verify first that the annotated fixpoint checks at its own arrow type, then that substituting it for f leaves the argument type s unchanged, and finally that substituting v for x changes the result to t[v/x].

Finite-qualifier Liquid inference

A Liquid template replaces selected base predicates by unknown conjunctions. Each unknown ranges over conjunctions drawn from a fixed finite qualifier set.

Definition 10.27 — Finite qualifier templates and enumeration

Let K be a finite set of predicate unknowns. For each κK, let Qκ be a finite set of well-scoped difference atoms. An assignment η chooses a subset of Qκ and interprets κ as the conjunction of that subset. The empty subset means true. The inference procedure enumerates the finite product κKP(Qκ), instantiates the annotated program, runs the VC-producing checker, and accepts the first assignment for which all VCs have checked certificates.

A fixed lexicographic order on the unknowns and on each qualifier set defines a deterministic returned assignment. The decreasing-fixed-point algorithm of Liquid Types starts every unknown at the conjunction of all its qualifiers, removes qualifiers responsible for failed VCs, and stops at a fixed point [RKJ08]. It performs at most κ|Qκ| strict weakenings and, on success, returns the strongest assignment in the qualifier lattice. Direct enumeration performs at most 2κ|Qκ| complete checker runs.

Consider the following recursive program F, in a context a:arr. The notation κ(ν,a) writes the unknown κ together with the two vertices in its declared scope; the parentheses are scope annotations, not object-language predicate application: F:=(fix f(i). if i00 then 0 elselet j=i+(1) inlet z=getaj inlet r=fj in r:i:{ν:intκ(ν,a)}int). It is called by the enclosing body let n=lena in let r=Fn in r. Take Qκ={0ν0, νLa0}. The assignment containing both qualifiers says 0iLa. The entry call establishes it because n=La and array lengths are nonnegative. In the else branch, the complement of i0 is 0i1, hence i1. With j=i1, the checker obtains 0j<La and 0jLa, so both the read and recursive call are accepted. In fact the first qualifier is redundant for this program: the else guard itself proves j0. The smaller assignment κ(ν,a):=νLa0 is therefore also accepted. Dropping that upper qualifier instead loses the proof of j<La. Finite search is allowed to find such a weaker invariant. Completeness therefore asserts the existence of an accepted assignment, not selection of a strongest one.

Fix a template instance I=(Γ,e,t,K,(Qκ)κK). After applying an assignment η, a closing substitution θ is well typed for Γη when each θ(x) is a closed value checking at its substituted declaration after substitution for earlier entries, and each substituted guard is true.

Theorem 10.28 — Finite-qualifier inference

If K and every Qκ are finite, enumeration examines every assignment in κKP(Qκ) and terminates. Every returned η satisfies Γηeηtη. For every well-typed closing substitution θ for Γη, the closed term (eη)θ is array safe; in particular this holds directly when Γη=. If any assignment in the product validates every generated VC, enumeration returns an assignment that does so.

Proof of Theorem 10.28 — Finite-qualifier inference

Proof. The product has κK2|Qκ| elements. VC generation terminates on each finite instantiated syntax tree, and corollary 10.10 decides every emitted difference sequent. Hence enumeration terminates. If it returns η, certificate acceptance implies that every emitted sequent is valid by theorem 10.9, so theorem 10.17 gives Γηeηtη. Apply lemma 10.20(2) successively at the variable entries. After the preceding entries have been substituted, each guard is a closed true difference atom, hence is entailed by the empty context. Apply lemma 10.23 to each such guard. This gives (eη)θ(tη)θ, and corollary 10.26 gives safety. Enumeration reaches every product element, so it reaches and accepts any successful assignment. ◻

Rondon, Kawaguchi, and Jhala formulate logical qualifiers, dependent templates, and predicate-abstraction solving in Sections 2 and 4, pp. 159–166, and state inference soundness and failure completeness for a fixed qualifier set in their Theorem 2 on proceedings p. 166 [RKJ08]. Their system uses a richer background logic and an HM-shape phase. In this finite difference-logic instance, completeness ranges exactly over κKP(Qκ); predicates absent from those qualifier sets are outside the theorem.

Exercise 10.7

★★☆ Let Qκ contain only 0ν0 for the recursive program F. Exhibit the failed upper-bound VC using La=1, i=2, and j=1. Then use the two qualifiers 0ν0 and νLa1. Show that the entry and recursive-call constraints hold but the same valuation still makes the read use index La; hence the nearby upper qualifier is insufficient. The countervaluation need not be reachable from the entry call: a VC is a semantic implication over every valuation satisfying the proposed invariant.

One dynamic contract, and its removal

Definition 10.29 — First-order guard elaboration

When a bounds sequent is not proved, it may be guarded dynamically rather than asserted as kernel truth. For p=δ1δm, define within the existing language guard(true,e)=e,guard(δp,e)=if δ then guard(p,e) else error. guard(p,e) checks a finite conjunction before executing the first-order array operation; it introduces no function wrapper or blame labels.

Call a closing substitution θ for Γ respecting when it maps variables to closed values of the declared shapes and its induced valuation satisfies Γ.

Theorem 10.30 — Bounds-contract insertion and certified removal

Suppose Γ,z:intet, Γt type, and zfv(t). Suppose also Γasa with |sa|=arr and Γisi with |si|=int. Let E=let z=getai in e,p=Bnd(a,i). Then:

  1. Γguard(p,E)t, without assuming Γallp;

  2. if a checked certificate proves Γallp, then ΓEt, and for every respecting closing substitution θ, guard(p,E)θEθ.

Thus a proved contract may be removed without changing any execution from a state satisfying the static context.

Proof of Theorem 10.30 — Bounds-contract insertion and certified removal

Proof. Atom synthesis weakens from Γ to Γ,p. Weakening the given body derivation gives Γ,p,z:intet, and the assumed Γt type, weakening forms t under Γ,p, while zfv(t) is exactly the escape side condition of D-Let. Under that context, entailment reflexivity proves both conjuncts of Bnd(a,i), so D-Get and D-Let show that E checks at t. Induct on the conjunct list p, allowing an arbitrary prefix context. The empty list gives E. For δp, the induction hypothesis types the then branch under Γ,δ, and D-Error types the else branch under Γ,δ; D-If gives clause 1.

For clause 2, theorem 10.9 derives the semantic premise Γallp from the accepted certificate, so D-Get and D-Let type E. If θ respects Γ, every conjunct of pθ is true. Applying E-IfT once per conjunct removes the nested guards and reaches Eθ. Thus guard(p,E)θEθ. ◻

Flanagan’s hybrid checker inserts a cast when subtyping is unknown (Section 3, Figure 6, proceedings p. 250), and proves results for that richer calculus in Section 5, pp. 252–254, and Appendix A [Fla06]. Findler and Felleisen motivate delayed checking and blame in Sections 2.2–2.4, pp. 50–52, then give the calculus, monitor semantics, compilation, and correctness results in Sections 3–6, pp. 53–57 [FF02]. The operation guard(p,E) of definition 10.29 is the first-order specialization: it checks a finite conjunction before the array operation and returns error when a conjunct fails.

Exercise 10.8

★☆☆ Instrument the unguarded program of proposition 10.2 with guard. Reduce it on 7 through both tests and show that it reaches error rather than the stuck get. Identify the test that fails.

Source-level proof-carrying code

Definition 10.31 — Source proof-carrying-code protocol

A producer sends source term e, proposed type t, and edge lists Π: (e,t,Π). The consumer reparses e,t, recomputes the VCs, and replays Π against that recomputed list before evaluation:

  1. parse e and t and check well-formedness;

  2. recompute C by running the deterministic VC generator on et; producer-supplied VCs are ignored;

  3. match Π against C and replay every certificate with the checker of definition 10.6;

  4. enable evaluation only after all checks accept.

The trusted base is the parser and well-formedness checker, VC generator, certificate replay checker with mathematical integer arithmetic, and the reduction implementation. The producer, inference search, shortest-path search, and certificate generator are untrusted.

Theorem 10.32 — Source PCC acceptance

If the consumer accepts (e,t,Π), then et, and every state reachable from e is a value, error, or can take a step. If t is a base refinement and evaluation returns a value, that value satisfies the refinement. Conversely, whenever VC generation succeeds and all generated VCs are semantically valid, the consumer accepts some certificate bundle.

Proof of Theorem 10.32 — Source PCC acceptance

Proof. Acceptance says that replay accepted a certificate for each recomputed VC. By theorem 10.9, every recomputed VC is valid; then theorem 10.17 derives et. Preservation and progress classify every reachable state, and lemma 10.18(1) proves that a returned base value satisfies its refinement. Conversely, theorem 10.9 assigns every valid generated sequent a path certificate or, for an inconsistent context, a negative-cycle certificate. The replay equations accept those finite edge lists. ◻

Necula separates an untrusted producer from a consumer that defines a safety policy and validates evidence (Sections 1–3, pp. 106–111) [Nec97]. Its Theorem 3.1 uses an assembly-language VC generator. Here the consumer recomputes the source-language VCs, and theorem 10.17, corollary 10.26 give the stated acceptance result for that generator.

The nondependent boundary

The arrow x:st is dependent in a deliberately restricted sense: the codomain refinement may mention the integer value x or the length Lx. The calculus as a whole is still a refinement of a nondependent simply typed language. Its erasure target has simple types τ::=intarrττ and the same A-normal term constructors and reduction rules as section 10.1, with lambda and fixpoint annotations drawn from τ. Dynamic conditionals, including those introduced by guard, remain executable syntax. Only logical guard entries in a typing context are erased.

Use the shape erasure |t| defined in definition 10.4. Erase a context by mapping declarations to their simple shapes and dropping logical guard entries. Term erasure is homomorphic, except that a lambda or fixpoint annotation is replaced by its shape. In particular, |if δ then e1 else e2|=if δ then |e1| else |e2|,|error|=error.

Definition 10.33 — Simply typed erasure target

Write Ξ0aτ, Ξ0ccτ, and Ξ0eτ. Simple contexts contain only declarations x:τ. Besides the ordinary base and arrow formation rules, the target typing rules are exactly these:

x:τΞ
Ξ0xτ
ST-Var
Ξ0nint
ST-Int
Ξ0Aarr
ST-Array
Ξ,x:τ0eσ
Ξ0(λx.e:τσ)τσ
ST-Lam
Ξ,f:(τσ),x:τ0eσ
Ξ0(fix f(x).e:τσ)τσ
ST-Fix
Ξ0aint
Ξ0a+kcint
ST-Shift
Ξ0aarr
Ξ0lenacint
ST-Length
Ξ0aarrΞ0iint
Ξ0getaicint
ST-Get
Ξ0fτσΞ0aτ
Ξ0facσ
ST-App
Ξ0aτ
Ξ0aτ
ST-Atom
Ξ0ccτΞ,x:τ0eσ
Ξ0let x=c in eσ
ST-Let
δ over V(Ξ)Ξ0e1τΞ0e2τ
Ξ0if δ then e1 else e2τ
ST-If
Ξ0τ type
Ξ0errorτ
ST-Error

Here V(Ξ) contains x at integer shape and Lx at array shape. There is no target subsumption judgment: the erasure of a source subtype derivation is equality of simple shapes.

A refinement changes which existing base values inhabit a type; it does not compute a type from an arbitrary program.

In particular, this syntax has no family application Fe, no vectors indexed by a term, no equality type, and no conversion rule comparing indices by program reduction. Predicates can mention only 0, integer variables, and array-length measures, in the one form r1r2k. Array contents and function values are invisible to the logic. Calling this a fully dependent type theory would therefore confuse a restricted logical dependency inside refinements with arbitrary terms occurring in types.

For example, the family-shaped expression Vecint(n+1) is not a type in this grammar. The property needed for an array of exactly that length is nevertheless expressible, in context n:int, as {ν:arrLνn1nLν1}. This comparison separates a missing type-family former from a relation that the deliberately small refinement logic can already state.

There is also genuine result dependency inside the admitted boundary. Put LenOf(a):={ν:intνLa0Laν0}. Length synthesis gives a:arrlenacLenOf(a). Therefore the one-bind function (λa.let n=lena in n:a:arrLenOf(a)) synthesizes the stated arrow type: the continuation variable already has the exact expected refinement. The codomain depends on the input’s observed length, but no type is computed by applying a family to a. This is the positive half of the boundary rather than merely a list of missing formers.

Lemma 10.34 — Measure indistinguishability

Let ρ and ρ agree on every integer variable and on the length of every array variable in the scope of p. Then ρp iff ρp. In particular, no refinement in this chapter distinguishes two arrays of the same length by their elements.

Proof of Lemma 10.34 — Measure indistinguishability

Proof. The two valuations assign the same integer to 0, to every integer vertex x, and to every measure vertex La. Therefore they give the same two integers to the left side of every atom r1r2k, so they agree on the truth of that atom. Induction over the finite conjunction proves the first claim. For the second, change the elements of one array while retaining its length and apply the first claim to every refinement in which its variable occurs. ◻

This is a static limitation, not a claim that execution ignores elements: get returns an element, but its result has the unrefined type int. Enriching that result with a predicate about the stored value would require a new observable measure and a new solver theory, followed by a new certificate and safety proof.

Refinement checking is conservative even for this small operational language. Let loop:=(fix f(x).let y=fx in y:x:intint) and consider let y=loop0 in let z=get71 in z. Every finite execution prefix remains in the first recursive call, so the out-of-bounds read is never reached and the program never gets stuck. Nevertheless the checker rejects its continuation: the false bound 1<1 is still a premise of D-Get. Thus safety does not imply typability; the rules deliberately avoid termination-sensitive dead-code reasoning.

Proposition 10.35 — Erasure boundary

If a formation, subtyping, synthesis, or checking judgment of this chapter holds, type erasure is shape preserving and term erasure yields the corresponding well-formed or well-typed judgment of the simply typed target. If Γs<:t, then |s|=|t|. The converse fails: the closed unguarded last-index program of proposition 10.2 is simply typed but is rejected by refinement typing.

Proof of Proposition 10.35 — Erasure boundary

Proof. Induct over formation, subtyping, and the three typing judgments. Base formation erases to its base sort, arrow formation to simple arrow formation, and both subtyping rules relate equal erased shapes by proposition 10.13. Each atom and computation rule erases to the simple rule for the same constructor. The semantic bounds premise of D-Get has no simple counterpart. Let, conditional, lambda, fixpoint, and error use their homomorphic target rules; a logical guard in the source context is irrelevant to simple typing, while a source conditional is retained.

For the converse, let A=7. Simple typing assigns A type arr, lenA type int, and getAn type int. In the refinement checker, however, length synthesis fixes n=1 and the exact array length is also 1. The upper get VC is therefore 1<1, which is false. Inversion of D-Let and D-Get shows that this failed premise is unavoidable. ◻

The stopping point is exact. Difference refinements already demonstrate program-specific implication, proof replay, inference relative to a finite logical vocabulary, and certified removal of a dynamic check. Arbitrary term indices would additionally require a term-indexed type grammar, a conversion judgment, and metatheory for that conversion; none is assumed by the safety or PCC result proved here.

Sources.

Refinement motivation is due to Freeman and Pfenning; the difference-constraint syntax and negative-cycle criterion are from Ramalingam et al.; the finite qualifier-template method is from Rondon et al. [FP91, RSJM99, RKJ08]. Flanagan and Findler–Felleisen define the cited dynamic-check boundaries; Necula defines the producer/consumer PCC architecture [Fla06, FF02, Nec97]. Freeman and Pfenning lift finite, programmer-declared datatype-refinement lattices through function types; the calculus here instead uses arithmetic refinements. See Sections 1, 3–4, and 6 of their paper (proceedings pp. 268–275) [FP91].

Suggested first pass.

Begin with exercise 10.9 to audit the trust boundary; then use exercise 10.10 to test the exact expressive boundary of the arithmetic fragment.

Exercise 10.9

★☆☆ Suppose a malicious producer sends last with the get VC omitted but includes valid certificates for every VC it reports. Explain exactly which protocol step rejects the package. Then explain why accepting the producer’s VC list without recomputation would invalidate theorem 10.32.

Exercise 10.10

★★☆ Prove that no refinement {ν:intp} in the empty context contains exactly the even integers. Hint: after normalization, and allowing every integer constant kZ, an atom using only 0 and ν is an upper bound νk, a lower bound kν, or a constant truth value. A finite conjunction therefore describes all of Z, an empty set, a finite interval of integers, or a one-sided interval, none of which is the set of even integers.

Exercise 22.11

★★★ Practical project.refinement-certificate-replay Build a producer and independent consumer that generate and replay every verification condition for last and lastOrZero. Recompute the VC list before checking producer certificates. Maintain the invariant that each path begins at the requested source, follows identified graph edges, ends at the requested target, and has weight no greater than the claimed bound. The run must print eight PASS lines and end with All 8 refinement-certificate corpus cases passed.; the audit must be empty. Test malicious producers that weaken a bound, skip a VC, and send a malformed certificate; the consumer must reject each package.

Search the book

Type to search the local edition.