Prerequisites. Direct starred prerequisites: none. No later core chapter depends on this route.
A program may own a file handle exactly once and still execute 𝗋𝖾𝖺𝖽 after 𝖼𝗅𝗈𝗌𝖾. Ownership controls aliases; it does not say which operations are available in the handle’s current state. The missing datum is a state that changes when an operation succeeds.
The file protocol
Let the protocol states be 𝑄={𝗈𝗉𝖾𝗇,𝖾𝗈𝖿,𝖼𝗅𝗈𝗌𝖾𝖽}. A protocol transition is a labeled edge 𝑞𝑎→𝑞′ between states. The file automaton has 𝗈𝗉𝖾𝗇𝗋𝖾𝖺𝖽(𝗆𝗈𝗋𝖾)←←←←←←←←←←←←←←←←←←→𝗈𝗉𝖾𝗇,𝗈𝗉𝖾𝗇𝗋𝖾𝖺𝖽(𝖾𝗈𝖿)←←←←←←←←←←←←←←←←→𝖾𝗈𝖿,𝗈𝗉𝖾𝗇𝖼𝗅𝗈𝗌𝖾←←←←←←←←←→𝖼𝗅𝗈𝗌𝖾𝖽,𝖾𝗈𝖿𝖼𝗅𝗈𝗌𝖾←←←←←←←←←→𝖼𝗅𝗈𝗌𝖾𝖽. There is no outgoing read edge from 𝖾𝗈𝖿 or 𝖼𝗅𝗈𝗌𝖾𝖽, and there is no outgoing close edge from 𝖼𝗅𝗈𝗌𝖾𝖽. These absences, rather than run-time tests inserted by the type system, reject the two protocol errors.
For 𝑞∈𝑄, the affine type 𝖥𝗂𝗅𝖾[𝑞] contains one file handle whose run-time state is 𝑞. A protocol context Δ is a finite map from handle variables to such types. It has exchange but no contraction; weakening is permitted only by an explicit finalizer that consumes the handle.
The distinction between a state and a type index is exact. The run-time map 𝐻 stores 𝐻(ℓ)=𝑞 for a concrete handle identity ℓ. The static context stores 𝑓:𝖥𝗂𝗅𝖾[𝑞]. A value environment 𝜂 maps 𝑓 to ℓ. We write 𝐻,𝜂⊧𝗍𝗌Δ if and only if every 𝑓:𝖥𝗂𝗅𝖾[𝑞]∈Δ has a distinct 𝜂(𝑓)=ℓ and 𝐻(ℓ)=𝑞. Distinctness is the alias restriction. This is the sole-handle pattern of chapter 19 instantiated with a file: ownership supplies the non-aliasing fact, while the index 𝑞 adds the operation-availability invariant that ownership alone lacks.
Commands and flow-sensitive typing
Programs are in administrative form: 𝑃::=𝗋𝖾𝗍𝗎𝗋𝗇∣𝗈𝗉𝖾𝗇𝑓;𝑃∣𝖼𝗅𝗈𝗌𝖾𝑓;𝑃∣𝗋𝖾𝖺𝖽𝑓𝖺𝗌{𝗆𝗈𝗋𝖾(𝑓)⇒𝑃𝑚∣𝖾𝗈𝖿(𝑓)⇒𝑃𝑒}∣𝑋(𝑓), where 𝑋 ranges over declared protocol procedures. The flow judgment Δ⊢𝑃⊣Δ′ means that 𝑃 consumes the handles in Δ according to their states and, if it returns, leaves exactly Δ′. The principal rules are
Δ⊢𝗋𝖾𝗍𝗎𝗋𝗇⊣Δ
TS-Return
Δ,𝑓:𝖥𝗂𝗅𝖾[𝗈𝗉𝖾𝗇]⊢𝑃⊣Δ′𝑓∉dom(Δ)
Δ⊢𝗈𝗉𝖾𝗇𝑓;𝑃⊣Δ′
TS-Open
𝑞∈{𝗈𝗉𝖾𝗇,𝖾𝗈𝖿}Δ⊢𝑃⊣Δ′
Δ,𝑓:𝖥𝗂𝗅𝖾[𝑞]⊢𝖼𝗅𝗈𝗌𝖾𝑓;𝑃⊣Δ′
TS-Close
Δ,𝑓:𝖥𝗂𝗅𝖾[𝗈𝗉𝖾𝗇]⊢𝑃𝑚⊣Δ′Δ,𝑓:𝖥𝗂𝗅𝖾[𝖾𝗈𝖿]⊢𝑃𝑒⊣Δ′
Δ,𝑓:𝖥𝗂𝗅𝖾[𝗈𝗉𝖾𝗇]⊢𝗋𝖾𝖺𝖽𝑓𝖺𝗌{𝗆𝗈𝗋𝖾(𝑓)⇒𝑃𝑚∣𝖾𝗈𝖿(𝑓)⇒𝑃𝑒}⊣Δ′
TS-Read
The two read branches start with different state indices but must finish in the same context Δ′. This is the join condition. Taking the union of the two contexts would be unsound: a handle closed on one branch and open on the other would then appear available after the join.
Fix a procedure table Ω. Its entries have the form 𝑋:(𝑥:𝖥𝗂𝗅𝖾[𝑞])⊸Δ𝑋=𝑃𝑋, where 𝑥 may occur in the output context. To keep this first-order interface closed, dom(Δ𝑋)⊆{𝑥}; a handle opened inside 𝑃𝑋 must be closed before the procedure returns. The table is suppressed in the flow judgment. Its call rule is 𝑋:(𝑥:𝖥𝗂𝗅𝖾[𝑞])⊸Δ𝑋=𝑃𝑋∈Ω𝑓:𝖥𝗂𝗅𝖾[𝑞]⊢𝑋(𝑓)⊣Δ𝑋[𝑓/𝑥]TS−Call. A recursive table is admitted only after every declared body checks from its declared input to its declared output while calls use the assumed table. The rule therefore unfolds a previously checked body; it does not infer a recursive invariant.
The following program closes the file on every returning path: 𝖽𝗋𝖺𝗂𝗇(𝑓)=𝗋𝖾𝖺𝖽𝑓𝖺𝗌{𝗆𝗈𝗋𝖾(𝑓)⇒𝖽𝗋𝖺𝗂𝗇(𝑓)∣𝖾𝗈𝖿(𝑓)⇒𝖼𝗅𝗈𝗌𝖾𝑓;𝗋𝖾𝗍𝗎𝗋𝗇},𝗆𝖺𝗂𝗇=𝗈𝗉𝖾𝗇𝑓;𝖽𝗋𝖺𝗂𝗇(𝑓). Its checked signatures are 𝖽𝗋𝖺𝗂𝗇:𝖥𝗂𝗅𝖾[𝗈𝗉𝖾𝗇]⊸∅,∅⊢𝗆𝖺𝗂𝗇⊣∅. The recursive branch re-establishes the exact input state of 𝖽𝗋𝖺𝗂𝗇. The eof branch consumes the handle. Thus both branches join at the empty context.
A configuration is ⟨𝐻,𝜂,𝑃⟩. Opening chooses a handle ℓ∉dom(𝐻). Closing removes the handle from 𝐻 and its variable from 𝜂. A read step consults an external reply 𝑏∈{𝗆𝗈𝗋𝖾,𝖾𝗈𝖿}:
ℓ∉dom(𝐻)
⟨𝐻,𝜂,𝗈𝗉𝖾𝗇𝑓;𝑃⟩⟶𝗍𝗌⟨𝐻[ℓ↦𝗈𝗉𝖾𝗇],𝜂[𝑓↦ℓ],𝑃⟩
TSO-Open
𝜂(𝑓)=ℓ𝐻(ℓ)∈{𝗈𝗉𝖾𝗇,𝖾𝗈𝖿}
⟨𝐻,𝜂,𝖼𝗅𝗈𝗌𝖾𝑓;𝑃⟩⟶𝗍𝗌⟨𝐻∖ℓ,𝜂∖𝑓,𝑃⟩
TSO-Close
𝜂(𝑓)=ℓ𝐻(ℓ)=𝗈𝗉𝖾𝗇
⟨𝐻,𝜂,𝗋𝖾𝖺𝖽𝑓𝖺𝗌{𝑃𝑚∣𝑃𝑒}⟩⟶𝗍𝗌⟨𝐻,𝜂,𝑃𝑚⟩
TSO-Read-More
𝜂(𝑓)=ℓ𝐻(ℓ)=𝗈𝗉𝖾𝗇
⟨𝐻,𝜂,𝗋𝖾𝖺𝖽𝑓𝖺𝗌{𝑃𝑚∣𝑃𝑒}⟩⟶𝗍𝗌⟨𝐻[ℓ↦𝖾𝗈𝖿],𝜂,𝑃𝑒⟩
TSO-Read-Eof
The abbreviated branch syntax in the dynamics keeps the same binder 𝑓 in each continuation. Procedure calls use 𝑋:(𝑥:𝖥𝗂𝗅𝖾[𝑞])⊸Δ𝑋=𝑃𝑋∈Ω⟨𝐻,𝜂,𝑋(𝑓)⟩⟶𝗍𝗌⟨𝐻,𝜂,𝑃𝑋[𝑓/𝑥]⟩TSO−Call Before this rule is used, every procedure-local handle binder is alpha-renamed fresh for 𝐻, 𝜂, and the caller; the displayed substitution then renames the formal handle to 𝑓.
Let 𝐷𝑓 abbreviate the displayed body of 𝖽𝗋𝖺𝗂𝗇(𝑓). For an input consisting of one chunk followed by eof, the complete trace is ⟨∅,∅,𝗆𝖺𝗂𝗇⟩𝑇𝑆𝑂−𝑂𝑝𝑒𝑛⟶𝗍𝗌⟨ℓ↦𝗈𝗉𝖾𝗇,𝑓↦ℓ,𝖽𝗋𝖺𝗂𝗇(𝑓)⟩𝑇𝑆𝑂−𝐶𝑎𝑙𝑙⟶𝗍𝗌⟨ℓ↦𝗈𝗉𝖾𝗇,𝑓↦ℓ,𝐷𝑓⟩𝑇𝑆𝑂−𝑅𝑒𝑎𝑑−𝑀𝑜𝑟𝑒⟶𝗍𝗌⟨ℓ↦𝗈𝗉𝖾𝗇,𝑓↦ℓ,𝖽𝗋𝖺𝗂𝗇(𝑓)⟩𝑇𝑆𝑂−𝐶𝑎𝑙𝑙⟶𝗍𝗌⟨ℓ↦𝗈𝗉𝖾𝗇,𝑓↦ℓ,𝐷𝑓⟩𝑇𝑆𝑂−𝑅𝑒𝑎𝑑−𝐸𝑜𝑓⟶𝗍𝗌⟨ℓ↦𝖾𝗈𝖿,𝑓↦ℓ,𝖼𝗅𝗈𝗌𝖾𝑓;𝗋𝖾𝗍𝗎𝗋𝗇⟩𝑇𝑆𝑂−𝐶𝑙𝑜𝑠𝑒⟶𝗍𝗌⟨∅,∅,𝗋𝖾𝗍𝗎𝗋𝗇⟩. Every relation is one step. The final configuration has no live handle.
Proof. Induct on the dynamic step. For TSO-Open, the fresh identity ℓ extends both maps and TS-Open’s premise supplies the continuation derivation; take Δ1=Δ,𝑓:𝖥𝗂𝗅𝖾[𝗈𝗉𝖾𝗇]. For TSO-Close, agreement gives the state 𝑞 required by TS-Close; removing ℓ, 𝑓, and their unique correspondence leaves agreement with the rule’s premise context Δ.
For TSO-Read-More, take Δ1=Δ,𝑓:𝖥𝗂𝗅𝖾[𝗈𝗉𝖾𝗇] and use the first TS-Read premise. For TSO-Read-Eof, update the one agreeing heap entry to 𝖾𝗈𝖿, take Δ1=Δ,𝑓:𝖥𝗂𝗅𝖾[𝖾𝗈𝖿], and use the second premise. Procedure unfolding uses the checked declaration and renames its formal handle to 𝑓; the injectivity clause of agreement is unchanged. These are all dynamic rules. ◻
Proof. Invert the last typing rule. TS-Return gives the first alternative. For TS-Open, choose an identity outside the finite domain of 𝐻. For TS-Close, agreement provides the concrete identity and exact state, so TSO-Close applies. For TS-Read, agreement gives an open heap entry; the external reply selects one of the two read rules. A procedure call unfolds because every admitted signature has a checked body. ◻
Assume 𝐻0,𝜂0⊧𝗍𝗌Δ0 and Δ0⊢𝑃0⊣Δ𝑓. Along every finite reduction from ⟨𝐻0,𝜂0,𝑃0⟩, written ⟨𝐻0,𝜂0,𝑃0⟩⟶∗𝗍𝗌𝑐, no command reads or closes a handle whose run-time state lacks the corresponding protocol edge. If execution returns, the final heap and value environment agree with Δ𝑓.
Proof. Induct on the length of the reduction. The zero-step case is the initial agreement. For a successor length, apply lemma 52.2 to obtain the typed agreeing successor configuration, then apply the induction hypothesis. Every read or close step uses a dynamic premise matching an edge printed in section 52.1; progress shows that a well-typed command is never blocked by a missing edge. At 𝗋𝖾𝗍𝗎𝗋𝗇, the flow judgment is TS-Return, so its input context is Δ𝑓, and the maintained agreement gives the final claim. ◻
The theorem depends on affine handle identity. If agreement allowed 𝜂(𝑓)=𝜂(𝑔)=ℓ, one alias could close ℓ while the other retained type 𝖥𝗂𝗅𝖾[𝗈𝗉𝖾𝗇]; the next read would refute preservation.
Encodings and theorem boundaries
Sources.
Strom and Yemini’s archived PDF page 3 (printed page 159) defines typestates, operation preconditions, and operation-dependent poststates [SY86]. Modern modular systems combine protocol states with access permissions so that aliases cannot invalidate a state guarantee; the archived modular-typestate PDF pages 8 and 10 print the input stream automaton, permissions, and branching read postcondition [BA07]. Featherweight Typestate makes state change a primitive language operation: its archived PDF pages 4–5 print the file automaton and aliasing counterexample, and pages 21 and 33 state progress and preservation for the static and gradual calculi [GTWA14]. The finite kernel in this chapter is book-owned; its preceding proof does not claim to be a translation of any of those richer object calculi.
A sum-type encoding returns 𝖥𝗂𝗅𝖾[𝗈𝗉𝖾𝗇]⊕𝖥𝗂𝗅𝖾[𝖾𝗈𝖿] from read. It accounts for branching but needs the same affine elimination discipline to prevent duplication. A binary session type coordinates dual communicating endpoints; the file kernel has one local resource and therefore proves no duality, communication fidelity, or deadlock freedom. Affine typestate combines the two independent obligations visible here: a state index selects the available edge, and affinity prevents a stale alias.
Protocol safety is a safety property. The external source may return 𝗆𝗈𝗋𝖾 forever, so 𝖽𝗋𝖺𝗂𝗇 may diverge. Nothing in theorem 52.4 proves termination, eventual close, or temporal liveness.
★★☆ Delete injectivity from 𝐻,𝜂⊧𝗍𝗌Δ. Construct the two-alias close/read execution described above and identify the first false conclusion of lemma 52.2.
★★☆ Add a 𝗌𝗎𝗌𝗉𝖾𝗇𝖽𝖾𝖽 state and the transitions 𝗈𝗉𝖾𝗇𝗌𝗎𝗌𝗉𝖾𝗇𝖽←←←←←←←←←←←←←←→𝗌𝗎𝗌𝗉𝖾𝗇𝖽𝖾𝖽𝗌𝗎𝗌𝗉𝖾𝗇𝖽𝖾𝖽𝗋𝖾𝗌𝗎𝗆𝖾←←←←←←←←←←←←←→𝗈𝗉𝖾𝗇. Give both typing and dynamic rules and add their two cases to lemma 52.2.
★★☆ Encode the read result by a binary sum and write the eliminator that reproduces TS-Read’s common-output condition. State the additional affine premise. Then explain why the encoding introduces neither a peer endpoint nor a deadlock-freedom theorem.
★★★Practical project.typestate-trace-checker Implement the file automaton and trace checker in Kappa. Maintain agreement between each accepted command and the current state. Accept open–read-more–read-eof–close; reject read after eof, close twice, read after close, and a branch join whose final states differ. Mutate the read-eof case so that it leaves the state open: the mutant must typecheck and audit cleanly but fail the read-after-eof oracle. Use artifacts/ch52-typestate-protocol/corpus.kp; the accepted run must end with All 5 typestate corpus cases passed. The checker illustrates theorem 52.4; it does not prove the theorem.