Effect Rows, Principal Type-and-Effect Inference, and Handlers
Chapter 22 recorded effects in finite sets. A set says which operations may occur, and it is the right abstraction for the semantic arguments made there. But it forgets how many copies of a label a type contains. That loss is harmless until a handler, an operator that interprets selected requests, removes one occurrence of a label while permitting another occurrence to escape.
Effect rows retain those occurrences as finite multisets with a possibly open tail. Thus ⟨𝗋𝖺𝗂𝗌𝖾,𝗋𝖺𝗂𝗌𝖾∣𝜇⟩⧸≡⟨𝗋𝖺𝗂𝗌𝖾∣𝜇⟩. Read ⟨ℓ∣𝜀⟩ as the label ℓ consed onto the row 𝜀; the vertical bar separates the visible prefix from its tail. The duplicate is not decoration. A handler can consume the first occurrence and leave the second in its result effect. The same convention also gives a small, unconstrained unification algorithm: there is no separate “lacks” predicate saying that a tail does not contain a given label.
The transaction whose tail stays open
Fix the operation declarations used by the transaction. 𝗀𝖾𝗍:𝟏⇝𝖨𝗇𝗍,𝗉𝗎𝗍:𝖨𝗇𝗍⇝𝟏,𝗋𝖺𝗂𝗌𝖾:𝖤𝗋𝗋𝗈𝗋⇝𝟎,𝗅𝗈𝗀:𝖤𝗋𝗋𝗈𝗋⇝𝟏. For a callback 𝑓, use the core computation; the saved value 𝑠 witnesses the initial read, while the callback’s result is committed by 𝗉𝗎𝗍: 𝑡𝑓=𝗉𝖾𝗋𝖿𝗈𝗋𝗆𝗀𝖾𝗍()𝗍𝗈𝑠.𝑓()𝗍𝗈𝑥.𝗉𝖾𝗋𝖿𝗈𝗋𝗆𝗉𝗎𝗍𝑥𝗍𝗈𝑢.𝗋𝖾𝗍𝗎𝗋𝗇𝑥. The callback has different principal signatures when it is let-polymorphic and when it is a first-class monomorphic argument. Put 𝐸𝜇=⟨𝗀𝖾𝗍,𝗉𝗎𝗍,𝗋𝖺𝗂𝗌𝖾∣𝜇⟩,𝐺𝜇=⟨𝗀𝖾𝗍,𝗉𝗎𝗍∣𝜇⟩.(31.1) Write 𝜏!𝜀 for the type of a computation that returns a value of type 𝜏 and performs the effects listed in the row 𝜀; in chapter 22 the same separator instead carried a finite set of operations. If Γ𝑓=𝑓:∀𝜈.𝟏⟨𝗋𝖺𝗂𝗌𝖾∣𝜈⟩←←←←←←←←←←←←←←→𝖨𝗇𝗍, then the occurrence of 𝑓 in 𝑡𝑓 instantiates 𝜈=⟨𝗀𝖾𝗍,𝗉𝗎𝗍∣𝜇⟩, and Γ𝑓⊢𝑡𝑓:𝖨𝗇𝗍!𝐸𝜇,gen𝑐(Γ𝑓,𝖨𝗇𝗍!𝐸𝜇)=∀𝜇.(𝖨𝗇𝗍!𝐸𝜇).(31.2) Here gen𝑐(Γ,𝜏!𝜀) quantifies every type and row variable free in 𝜏!𝜀 but not in Γ. If instead we form the value 𝜆𝑓.𝑡𝑓, its parameter is monomorphic, so its principal scheme is ∀𝜇.(𝟏𝐺𝜇⟶𝖨𝗇𝗍)𝐺𝜇⟶𝖨𝗇𝗍.(31.3) The lambda-bound 𝑓 has no declared 𝗋𝖺𝗂𝗌𝖾 effect; only the 𝗀𝖾𝗍 and 𝗉𝗎𝗍 actually performed by 𝑡𝑓 remain. The row 𝐸𝜇 is a legal instance of this more general scheme, obtained by taking the tail of 𝐺 to begin with 𝗋𝖺𝗂𝗌𝖾, but it is not principal for the wrapper. Giving a first-class argument its own quantified row scheme would require higher-rank row polymorphism, which this Hindley–Milner core does not have. In both signatures 𝜇 is open, so callers may add logging or allocation effects.
Now compare two exception handlers. A fallback handler applied at the principal instance 𝐸𝜇 removes 𝗋𝖺𝗂𝗌𝖾 and leaves ⟨𝗀𝖾𝗍,𝗉𝗎𝗍∣𝜇⟩. A rethrowing handler must instead leave one 𝗋𝖺𝗂𝗌𝖾 in its output. It therefore asks for two occurrences in its input, one to consume and one to re-perform. The principal row of 𝑡𝑓 admits that instance by taking 𝜇=⟨𝗋𝖺𝗂𝗌𝖾∣𝜈⟩. Its input row is then ⟨𝗀𝖾𝗍,𝗉𝗎𝗍,𝗋𝖺𝗂𝗌𝖾,𝗋𝖺𝗂𝗌𝖾∣𝜈⟩; the rethrowing handler removes one occurrence and leaves ⟨𝗀𝖾𝗍,𝗉𝗎𝗍,𝗋𝖺𝗂𝗌𝖾∣𝜈⟩ for an outer handler.
Let labels ℓ range over a fixed finite operation signature Σ, with Σ(ℓ)=𝑃ℓ⇝𝑅ℓ. Every declared 𝑃ℓ and 𝑅ℓ is a closed, well-kinded type; in particular, type or row substitution never changes an operation declaration. Type variables are written 𝛼, and row variables 𝜇. Kinds keep the two sorts apart. 𝜅::=∗∣𝖱,𝛼::∗,𝜇::𝖱. The kind ∗ classifies ordinary value types and 𝖱 classifies effect rows. A substitution is kind preserving when it maps ∗-variables to types and 𝖱-variables to rows. 𝜏::=𝛼∣𝑏∣𝜏𝜀→𝜏,𝜀::=𝜇∣⟨⟩∣⟨ℓ∣𝜀⟩,𝜎::=∀¯𝛼¯𝜇.𝜏,𝜒::=∀¯𝛼¯𝜇.(𝜏!𝜀). We abbreviate nested extensions by ⟨ℓ1,ℓ2∣𝜀⟩. A closed row has no row variable. It is duplicate-free when no label occurs twice. The base types 𝑏 include 𝟎,𝟏,𝖡𝗈𝗈𝗅,𝖨𝗇𝗍, 𝖤𝗋𝗋𝗈𝗋, and 𝖤𝗇𝗏. Contexts assign value schemes 𝜎 to variables. A computation scheme 𝜒 records both inferred outputs; it is used to state principality, not as a context entry. Constants range over a fixed signature C. Every declaration C(𝑐)=∀¯𝛼¯𝜇.𝜏 is closed and well kinded: all free variables of 𝜏 are bound by the displayed quantifiers. Define ftv(𝜏!𝜀)=ftv(𝜏)∪ftv(𝜀),gen𝑐(Γ,𝜏!𝜀)=∀(ftv(𝜏!𝜀)∖ftv(Γ)).(𝜏!𝜀),gen𝑣(Γ,𝜏)=∀(ftv(𝜏)∖ftv(Γ)).𝜏.
The row equivalence is the least congruence generated by adjacent exchange: ⟨ℓ1,ℓ2∣𝜀⟩≡⟨ℓ2,ℓ1∣𝜀⟩.(31.4) Here ≡ is finite-multiset equivalence of effect rows and its congruential extension to the displayed types and contexts; it is not the book’s judgmental-equality relation. There is deliberately no contraction equation. Equality of closed rows is therefore equality of finite multisets. A substitution is kind preserving; its application is homomorphic and row equality is always read modulo ≡. No side condition ℓ1≠ℓ2 is needed in the exchange equation: when the two labels agree, it is simply an identity. Extend ≡ congruentially to types: base types and variables are equivalent only to themselves, and 𝐴1𝜀1⟶𝐵1≡𝐴2𝜀2⟶𝐵2 exactly when 𝐴1≡𝐴2, 𝜀1≡𝜀2, and 𝐵1≡𝐵2. Context equivalence is pointwise on the bodies of schemes, after alpha-renaming bound variables. Thus 𝑇Γ≡𝑄𝑆Γ below is a pointwise statement, not literal syntax equality.
For a row 𝜀, write M(𝜀):Σ→ℕ for the multiplicity function of its visible prefix. A variable or empty tail contributes zero, and + denotes pointwise addition. This notation will support both cancellation and the unification guard.
Proof. Every row has a normal form ⟨𝐿∣𝑡⟩, where 𝐿 is a finite multiset of visible labels and the tail atom 𝑡 is either one row variable or ⟨⟩. Bubble-sorting adjacent labels in a fixed total order proves ⟨𝐿∣𝑡⟩≡⟨𝐿′∣𝑡′⟩⟺𝐿=𝐿′asmultisetsand𝑡=𝑡′. Exchange leaves the multiplicity function M and the tail atom unchanged, so equivalent rows have equal multiplicities and equal tails; bubble sort gives the converse. The first claim subtracts one from the multiplicity of ℓ on both sides. For the second, the right side has positive ℓ-multiplicity and the prefix 𝐿 contributes none, so the tail contributes at least one. Exchange moves that occurrence to its head. ◻
Unique-label rows require an absence constraint such as ℓ∉𝜇 when unifying ⟨ℓ∣𝜇⟩. Here duplicates are legal, so ordinary substitutions suffice.
The two row systems have the following exact division of labor.
The two mechanisms prevent the same cyclic exposure in different places: Chapter 4 records a lacks predicate in the type, whereas this chapter checks a common-tail guard during unification. See also the scoped-duplicate comparison in subsection 7.8.2; effect multiplicity is a deliberate equational choice, not a novel exposure algorithm. Swapping the equalities is unsound: contraction would erase the distinction between a handled occurrence and a re-performed one, while allowing duplicate record labels would make field selection ambiguous.
★☆☆ Give a multiplicity model separating ⟨ℓ,ℓ∣𝜇⟩ from ⟨ℓ∣𝜇⟩. Explain why a handler that re-performs ℓ makes the distinction observable at the type level.
Values and computations are separate: 𝑣::=𝑥∣𝑐∣𝜆𝑥.𝑒,𝑒::=𝗋𝖾𝗍𝗎𝗋𝗇𝑣∣𝑣𝑣∣𝑒𝗍𝗈𝑥.𝑒∣𝗉𝖾𝗋𝖿𝗈𝗋𝗆ℓ𝑣∣𝗁𝖺𝗇𝖽𝗅𝖾𝑒𝗐𝗂𝗍𝗁𝐻∣𝗅𝖾𝗍𝑥=𝑒𝗂𝗇𝑒,𝐻::={𝗋𝖾𝗍𝗎𝗋𝗇𝑥↦𝑒𝑟;ℓ𝑖(𝑝𝑖;𝑘𝑖)↦𝑒𝑖}𝑛𝑖=1. This separation of syntactic value forms from computations, together with elimination forms that take values, is what fine-grain call by value means here. Unlike CBPV in chapter 22, this core has no 𝗍𝗁𝗎𝗇𝗄/𝖿𝗈𝗋𝖼𝖾 type boundary. The handled labels ℓ1,…,ℓ𝑛 are distinct. The judgments are Γ⊢𝑣𝑣:𝜏 and Γ⊢𝑒:𝜏!𝜀. Instantiation at a variable is implicit. Constants 𝑐 are base constructors with fixed closed schemes; all function values in the core are lambdas. For 𝐻={𝗋𝖾𝗍𝗎𝗋𝗇𝑥↦𝑒𝑟;ℓ𝑖(𝑝𝑖;𝑘𝑖)↦𝑒𝑖}𝑛𝑖=1, write 𝗁𝖺𝗇𝖽𝗅𝖾𝖽(𝐻)={ℓ1,…,ℓ𝑛}.
𝑥:∀¯𝛼¯𝜇.𝜏∈Γ𝑇instantiatesonly¯𝛼,¯𝜇
Γ⊢𝑣𝑥:𝑇𝜏
V-Var
C(𝑐)=∀¯𝛼¯𝜇.𝜏𝑇instantiatesonly¯𝛼,¯𝜇
Γ⊢𝑣𝑐:𝑇𝜏
V-Const
Γ,𝑥:𝜏1⊢𝑒:𝜏2!𝜀
Γ⊢𝑣𝜆𝑥.𝑒:𝜏1𝜀→𝜏2
V-Lam
Γ⊢𝑣𝑣:𝜏
Γ⊢𝗋𝖾𝗍𝗎𝗋𝗇𝑣:𝜏!𝜀
C-Return
Γ⊢𝑣𝑣1:𝜏1𝜀→𝜏2Γ⊢𝑣𝑣2:𝜏1
Γ⊢𝑣1𝑣2:𝜏2!𝜀
C-App
Γ⊢𝑒1:𝜏1!𝜀Γ,𝑥:𝜏1⊢𝑒2:𝜏2!𝜀
Γ⊢𝑒1𝗍𝗈𝑥.𝑒2:𝜏2!𝜀
C-To
Σ(ℓ)=𝑃ℓ⇝𝑅ℓΓ⊢𝑣𝑣:𝑃ℓ
Γ⊢𝗉𝖾𝗋𝖿𝗈𝗋𝗆ℓ𝑣:𝑅ℓ!⟨ℓ∣𝜀⟩
C-Op
Only C-Return and C-Op may choose their row tails freely. There is no general effect-weakening rule; in particular, an application has exactly the latent row written on its function type. Because C-Return and C-Op may choose any tail, both arms of a sequence can be typed at one common effect. Let-generalization is restricted to pure computations:
Here ¯𝑎 contains variables of both kinds. Effectful sequencing uses 𝗍𝗈, whose bound variable is monomorphic. This is also the surface elaboration policy. A source 𝗅𝖾𝗍 is accepted as the core 𝗅𝖾𝗍 form only when inference can unify the bound computation’s row with ⟨⟩; an effectful source binding must instead elaborate to core 𝗍𝗈, and is monomorphic. Thus the core syntax records, rather than retrospectively guesses, the generalization decision.
The sole conversion rule changes only the row annotation:
Γ⊢𝑒:𝜏!𝜀𝜀≡𝜀′
Γ⊢𝑒:𝜏!𝜀′
C-RowConv
Hence all typing rules are stable under row equivalence. In particular, the effect in C-To need only be equivalent in its two premises; this is a consequence of C-RowConv, not a second conversion convention.
Rule C-Let is already visible on a closed term. If Γ⊢𝑣0:𝖨𝗇𝗍, write 𝐶0=𝖨𝗇𝗍!⟨⟩,𝑒0=𝗅𝖾𝗍𝑥=𝗋𝖾𝗍𝗎𝗋𝗇0𝗂𝗇𝗋𝖾𝗍𝗎𝗋𝗇𝑥. Then Γ⊢𝑣0:𝖨𝗇𝗍Γ⊢𝗋𝖾𝗍𝗎𝗋𝗇0:𝐶0C−Returnftv(𝐶0)∖ftv(Γ)=∅Γ,𝑥:𝖨𝗇𝗍⊢𝑣𝑥:𝖨𝗇𝗍Γ,𝑥:𝖨𝗇𝗍⊢𝗋𝖾𝗍𝗎𝗋𝗇𝑥:𝐶0C−ReturnΓ⊢𝑒0:𝐶0C−Let. The generalized-variable set is empty here; replacing (0) by a polymorphic pure value exercises the same rule with a nonempty set.
The continuation in a clause is deep: invoking it resumes under the same handler. If a clause performs its own ℓ𝑖, its output row 𝜀 contains ℓ𝑖. The premise for the handled computation then contains two copies.
Here is the calculation for exceptions. Fix a result type 𝐵, a fallback 𝑑:𝐵, and a row 𝜈. Define 𝐶𝐵𝑑={𝗋𝖾𝗍𝗎𝗋𝗇𝑥↦𝗋𝖾𝗍𝗎𝗋𝗇𝑥;𝗋𝖺𝗂𝗌𝖾(𝑝;𝑘)↦𝗋𝖾𝗍𝗎𝗋𝗇𝑑},𝑅𝐵={𝗋𝖾𝗍𝗎𝗋𝗇𝑥↦𝗋𝖾𝗍𝗎𝗋𝗇𝑥;𝗋𝖺𝗂𝗌𝖾(𝑝;𝑘)↦𝗉𝖾𝗋𝖿𝗈𝗋𝗆𝗋𝖺𝗂𝗌𝖾𝑝𝗍𝗈𝑧.𝑘𝑧}. The fallback handler has output row 𝜈. Its operation-clause premise is 𝑑:𝐵,𝑝:𝖤𝗋𝗋𝗈𝗋,𝑘:𝟎𝜈→𝐵⊢𝗋𝖾𝗍𝗎𝗋𝗇𝑑:𝐵!𝜈. Consequently, if 𝑒:𝐵!⟨𝗋𝖺𝗂𝗌𝖾∣𝜈⟩, then 𝗁𝖺𝗇𝖽𝗅𝖾𝑒𝗐𝗂𝗍𝗁𝐶𝐵𝑑:𝐵!𝜈.
The rethrower has output row 𝜀=⟨𝗋𝖺𝗂𝗌𝖾∣𝜈⟩. Its operation clause uses the empty response only after the re-performed request: Σ(𝗋𝖺𝗂𝗌𝖾)=𝖤𝗋𝗋𝗈𝗋⇝𝟎𝑝:𝖤𝗋𝗋𝗈𝗋,𝑘:𝟎𝜀→𝐵⊢𝑣𝑝:𝖤𝗋𝗋𝗈𝗋𝑝:𝖤𝗋𝗋𝗈𝗋,𝑘:𝟎𝜀→𝐵⊢𝗉𝖾𝗋𝖿𝗈𝗋𝗆𝗋𝖺𝗂𝗌𝖾𝑝:𝟎!𝜀C−Op𝑝:𝖤𝗋𝗋𝗈𝗋,𝑧:𝟎,𝑘:𝟎𝜀→𝐵⊢𝑘𝑧:𝐵!𝜀𝑝:𝖤𝗋𝗋𝗈𝗋,𝑘:𝟎𝜀→𝐵⊢𝗉𝖾𝗋𝖿𝗈𝗋𝗆𝗋𝖺𝗂𝗌𝖾𝑝𝗍𝗈𝑧.𝑘𝑧:𝐵!𝜀C−To. Thus 𝑅𝐵 has input row ⟨𝗋𝖺𝗂𝗌𝖾,𝗋𝖺𝗂𝗌𝖾∣𝜈⟩ and output row ⟨𝗋𝖺𝗂𝗌𝖾∣𝜈⟩. No value of 𝟎 has been invented: 𝑧 is bound by the sequencing form, and the continuation call lies after the re-performed request. The fallback trace below discards that continuation instead of manufacturing a response.
This typing predicts the reduction. For a handler-free request context 𝑅, put 𝑞=𝑅[𝗉𝖾𝗋𝖿𝗈𝗋𝗆𝗋𝖺𝗂𝗌𝖾𝑝], and assume 𝑞:𝐵!⟨𝗋𝖺𝗂𝗌𝖾,𝗋𝖺𝗂𝗌𝖾∣𝜈⟩. Rule C-Op may choose any tail; here its tail already contains one 𝗋𝖺𝗂𝗌𝖾, while the displayed request contributes the other. The inner handler consumes the displayed occurrence and its clause re-performs the one that remains visible. The inner handler rethrows outside itself: 𝗁𝖺𝗇𝖽𝗅𝖾𝑞𝗐𝗂𝗍𝗁𝑅𝐵⟶𝗉𝖾𝗋𝖿𝗈𝗋𝗆𝗋𝖺𝗂𝗌𝖾𝑝𝗍𝗈𝑧.(𝜆𝑦.𝗁𝖺𝗇𝖽𝗅𝖾𝑅[𝗋𝖾𝗍𝗎𝗋𝗇𝑦]𝗐𝗂𝗍𝗁𝑅𝐵)𝑧. An outer fallback handler captures this request. Its clause discards the rebuilt continuation, so 𝗁𝖺𝗇𝖽𝗅𝖾(𝗁𝖺𝗇𝖽𝗅𝖾𝑞𝗐𝗂𝗍𝗁𝑅𝐵)𝗐𝗂𝗍𝗁𝐶𝐵𝑑⟶2𝗋𝖾𝗍𝗎𝗋𝗇𝑑. At the type level the two handlers remove the two occurrences in succession: 𝐵!⟨𝗋𝖺𝗂𝗌𝖾,𝗋𝖺𝗂𝗌𝖾∣𝜈⟩𝑅𝐵⟶𝐵!⟨𝗋𝖺𝗂𝗌𝖾∣𝜈⟩𝐶𝐵𝑑⟶𝐵!𝜈.
A logging rethrower prefixes the clause of 𝑅𝐵 by 𝗉𝖾𝗋𝖿𝗈𝗋𝗆𝗅𝗈𝗀𝑝𝗍𝗈𝑢. With 𝜀𝐿=⟨𝗅𝗈𝗀,𝗋𝖺𝗂𝗌𝖾∣𝜈⟩, both operations and the continuation call have effect 𝜀𝐿, modulo exchange. Its input is therefore ⟨𝗋𝖺𝗂𝗌𝖾,𝗅𝗈𝗀,𝗋𝖺𝗂𝗌𝖾∣𝜈⟩, and its output is 𝜀𝐿. Applied to the transaction, the tail in 𝐸𝜇 is instantiated to provide the second 𝗋𝖺𝗂𝗌𝖾 and the 𝗅𝗈𝗀; 𝗀𝖾𝗍 and 𝗉𝗎𝗍 remain in the ambient tail throughout. More explicitly, put 𝐸𝐿𝜉=⟨𝗀𝖾𝗍,𝗉𝗎𝗍,𝗅𝗈𝗀,𝗋𝖺𝗂𝗌𝖾∣𝜉⟩. The logging rethrower takes 𝖨𝗇𝗍!⟨𝗋𝖺𝗂𝗌𝖾∣𝐸𝐿𝜉⟩ to 𝖨𝗇𝗍!𝐸𝐿𝜉, while the required instance of the transaction is obtained from 𝐸𝜇 by the row calculation 𝐸𝜇[⟨𝗅𝗈𝗀,𝗋𝖺𝗂𝗌𝖾∣𝜉⟩/𝜇](25.1)≡⟨𝗀𝖾𝗍,𝗉𝗎𝗍,𝗋𝖺𝗂𝗌𝖾,𝗅𝗈𝗀,𝗋𝖺𝗂𝗌𝖾∣𝜉⟩(25.4)≡⟨𝗋𝖺𝗂𝗌𝖾∣𝗀𝖾𝗍,𝗉𝗎𝗍,𝗅𝗈𝗀,𝗋𝖺𝗂𝗌𝖾∣𝜉⟩≡⟨𝗋𝖺𝗂𝗌𝖾∣𝐸𝐿𝜉⟩. The handler removes the first displayed occurrence and leaves exactly 𝐸𝐿𝜉. Equivalently, within 𝜀𝐿 take 𝜈=⟨𝗀𝖾𝗍,𝗉𝗎𝗍∣𝜉⟩.
★★☆ Instantiate the displays above with 𝐵=𝖨𝗇𝗍 and 𝜈=⟨𝗀𝖾𝗍,𝗉𝗎𝗍∣𝜉⟩. Write both complete instances of C-Handle, including the return-clause premises, and display the effect before and after each handler.
Evaluation contexts and handler-free request contexts are generated by 𝐸::=[]∣𝐸𝗍𝗈𝑥.𝑒∣𝗅𝖾𝗍𝑥=𝐸𝗂𝗇𝑒∣𝗁𝖺𝗇𝖽𝗅𝖾𝐸𝗐𝗂𝗍𝗁𝐻,𝑅::=[]∣𝑅𝗍𝗈𝑥.𝑒∣𝗅𝖾𝗍𝑥=𝑅𝗂𝗇𝑒. A 𝗅𝖾𝗍-shaped request context is included so the context grammar is closed under the term syntax. It is unreachable in a well-typed terminal request: C-Let requires its bound computation to have the empty row, so that position cannot expose an operation. A request context 𝑅 contains no handler frame. Consequently the handler displayed at a root is the nearest syntactic handler around the request: it either handles that label or forwards the request across exactly one handler layer. The same grammar also describes a terminal request context when no handler encloses it. Besides beta and sequencing, the roots are (𝜆𝑥.𝑒)𝑣⇝0𝑒[𝑣/𝑥],(𝗋𝖾𝗍𝗎𝗋𝗇𝑣)𝗍𝗈𝑥.𝑒⇝0𝑒[𝑣/𝑥],𝗅𝖾𝗍𝑥=𝗋𝖾𝗍𝗎𝗋𝗇𝑣𝗂𝗇𝑒⇝0𝑒[𝑣/𝑥],𝗁𝖺𝗇𝖽𝗅𝖾(𝗋𝖾𝗍𝗎𝗋𝗇𝑣)𝗐𝗂𝗍𝗁𝐻⇝0𝑒𝑟[𝑣/𝑥],𝗁𝖺𝗇𝖽𝗅𝖾𝑅[𝗉𝖾𝗋𝖿𝗈𝗋𝗆ℓ𝑖𝑣]𝗐𝗂𝗍𝗁𝐻⇝0𝑒𝑖[𝑣/𝑝𝑖,(𝜆𝑦.𝗁𝖺𝗇𝖽𝗅𝖾𝑅[𝗋𝖾𝗍𝗎𝗋𝗇𝑦]𝗐𝗂𝗍𝗁𝐻)/𝑘𝑖]. If ℓ is not handled by 𝐻, forwarding preserves the operation and rebuilds its continuation: 𝗁𝖺𝗇𝖽𝗅𝖾𝑅[𝗉𝖾𝗋𝖿𝗈𝗋𝗆ℓ𝑣]𝗐𝗂𝗍𝗁𝐻⇝0𝗉𝖾𝗋𝖿𝗈𝗋𝗆ℓ𝑣𝗍𝗈𝑦.𝗁𝖺𝗇𝖽𝗅𝖾𝑅[𝗋𝖾𝗍𝗎𝗋𝗇𝑦]𝗐𝗂𝗍𝗁𝐻. The last root in the first display is H-Op; the forwarding root is H-Forward. The reduction relation is the least relation containing these roots and closed under 𝐸. Their label tests are complementary. Because 𝑅 has no handler frame, a nested request first meets its innermost handler; context closure cannot also contract an outer handler root. Repeated H-Forward steps give the familiar derived “open context” behavior, but that multi-layer behavior is not a primitive reduction.
For every evaluation context 𝐸, request label ℓ, and value 𝑣, exactly one of the following decompositions applies:
𝐸[𝗉𝖾𝗋𝖿𝗈𝗋𝗆ℓ𝑣]=𝑅[𝗉𝖾𝗋𝖿𝗈𝗋𝗆ℓ𝑣] for a unique request context 𝑅;
there are unique 𝐸′, 𝑅, and 𝐻 such that 𝐸[𝗉𝖾𝗋𝖿𝗈𝗋𝗆ℓ𝑣]=𝐸′[𝗁𝖺𝗇𝖽𝗅𝖾𝑅[𝗉𝖾𝗋𝖿𝗈𝗋𝗆ℓ𝑣]𝗐𝗂𝗍𝗁𝐻], and the displayed occurrence of 𝐻 is the innermost handler frame on the active spine.
In the second case, membership of ℓ in 𝗁𝖺𝗇𝖽𝗅𝖾𝖽(𝐻) selects exactly one of H-Op and H-Forward.
Proof of Lemma 25.6 — Nearest-handler decomposition
Proof. Read the frames of 𝐸 from the hole outward. If there is no handler frame, all frames belong to the grammar of 𝑅, giving the first decomposition. Otherwise split the frame list immediately after its first handler. The inner prefix is the unique 𝑅, that first handler is the unique 𝐻, and the remaining suffix is the unique 𝐸′. The two cases are disjoint by the presence or absence of a handler frame, and the two label tests are complementary. ◻
★★☆ Let 𝑅 be handler free, let ℓ∉𝗁𝖺𝗇𝖽𝗅𝖾𝖽(𝐻0), and let 𝐻1 contain the clause ℓ(𝑝;𝑘)↦𝑒ℓ. Starting from 𝗁𝖺𝗇𝖽𝗅𝖾(𝗁𝖺𝗇𝖽𝗅𝖾𝑅[𝗉𝖾𝗋𝖿𝗈𝗋𝗆ℓ𝑣]𝗐𝗂𝗍𝗁𝐻0)𝗐𝗂𝗍𝗁𝐻1, write the H-Forward step through 𝐻0 and the next H-Op step at 𝐻1, including both rebuilt continuations. Give the decomposition from Lemma 25.6 before each step, and explain syntactically why the outer H-Op is not a root of the initial term.
Proof of Lemma 25.7 — Replacement for request contexts
Proof. Induct on 𝑅. A sequencing frame uses the induction hypothesis in the left premise of C-To; the right premise is unchanged. A let frame uses it in the first premise of C-Let. Sequencing and let are exactly the two constructors of a request context. ◻
Write 𝜎′⊒𝜎 when every kind-correct instance of 𝜎 is an instance of 𝜎′, read “𝜎′ is at least as general as 𝜎” exactly as in chapter 3.
If Γ⊢𝐽 is a value or computation judgment and 𝑆 is kind preserving, then 𝑆Γ⊢𝑆𝐽, after renaming scheme binders away from dom(𝑆). If 𝜎′⊒𝜎, replacing an assumption 𝑦:𝜎 by 𝑦:𝜎′ preserves a derivation.
Proof of Lemma 31.8 — Type-and-row action and scheme enlargement
Proof. Induct on the derivation. The variable case composes the selected instance with 𝑆; the constant case does the same within its closed declaration; C-Op is unchanged because every 𝑃ℓ,𝑅ℓ is closed; and C-RowConv uses the fact that kind-preserving substitution preserves ≡. In C-Let, the variables later bound by generalization are still free in the bound monotype. Apply one finite kind-preserving bijection to the entire bound-expression premise and to the body’s generalized declaration. This is licensed because those variables are fresh for Γ, and it makes them fresh for both the domain and range variables of 𝑆; it is not merely an alpha-renaming of the scheme prefix. The scheme recomputed from 𝑆Γ,𝑆𝐴1 is at least as general as the pointwise image of the old scheme. Put 𝑄=ftv(𝐴1)∖ftv(Γ) and choose the variables of 𝑄 fresh for the domain and range of 𝑆. Then 𝑆 fixes every member of 𝑄, none occurs in 𝑆Γ, and 𝑄⊆ftv(𝑆𝐴1)∖ftv(𝑆Γ). Thus every instance of 𝑆(∀𝑄.𝐴1) is an instance of gen(𝑆Γ,𝑆𝐴1). Scheme enlargement therefore derives the body premise from its old instance. The handler case applies the induction hypotheses under the renamed return, parameter, and resumption binders. All binders are renamed fresh before an induction hypothesis is used. Scheme enlargement itself changes only V-Var: an instance admitted by 𝜎 is still admitted by 𝜎′. ◻
Proof of Lemma 25.8 — Type, row, ordinary, and generalized substitution
Proof. Use a simultaneous induction on value and computation typing. At V-Var, the distinguished variable uses the premise for 𝑣, while every other variable is unchanged. In V-Const, the declared instance is unchanged. In V-Lam, rename the lambda binder away from 𝑥 and the free variables of 𝑣, then apply the computation induction hypothesis below it. C-Return, C-App, and C-To apply the appropriate hypotheses to their immediate subderivations. In C-Op, substitute in the parameter value; the closed declaration types remain literally 𝑃ℓ,𝑅ℓ. The C-RowConv case reapplies the same row equivalence.
For C-Let, rename its binder 𝑦 away from 𝑥,𝑣. The induction hypothesis types the substituted bound computation. Removing 𝑥:𝐴 from the surrounding context can only enlarge ftv(𝐴1)∖ftv(Γ). Hence the new scheme 𝜎′1 computed by C-Let is at least as general as the old 𝜎1: 𝜎′1⊒𝜎1. Scheme enlargement retags the body derivation with 𝑦:𝜎′1, and its induction hypothesis substitutes for 𝑥. In C-Handle, alpha-rename the return binder and every parameter and resumption binder 𝑝𝑖,𝑘𝑖. Apply the induction hypotheses to the body, the return clause under its value binder, and every operation clause under both its parameter value binder and its resumption value binder. Their arrow and row annotations are unchanged, so C-Handle reconstructs the conclusion. ◻
Proof of Lemma 31.10 — Generalized value substitution
Proof. Repeat the simultaneous induction of lemma 25.8. Its only new variable case is an occurrence of 𝑥 at an instantiation 𝑇𝐴. Freshness gives 𝑇Γ=Γ, so lemma 31.8 derives Γ⊢𝑣𝑣:𝑇𝐴. Lambda, operation, conversion, let-scheme, return, parameter, and resumption binders use exactly the cases just enumerated. Thus separate occurrences may instantiate ¯𝑎 differently without changing the substituted term. ◻
Proof. Invert value typing. The empty context excludes V-Var, and every V-Const instance has the outer constructor of its closed base declaration. Only V-Lam concludes an arrow type. ◻
Proof. The beta and sequencing roots use ordinary substitution from lemma 25.8. For the let root, inversion of its pure-return premise gives Γ⊢𝑣𝑣:𝐴, and the variables generalized by C-Let are fresh for Γ; use lemma 31.10. A handled return uses ordinary substitution in the return clause.
For a handled operation, invert the typing of the request context. Its hole has type 𝑅𝑖!𝛿. Rule C-Return assigns the same hole effect to 𝗋𝖾𝗍𝗎𝗋𝗇𝑦; lemma 25.7 reconstructs the handler premise, and C-Handle gives the captured continuation type 𝑅𝑖𝜀→𝐵. First substitute the request parameter 𝑣:𝑃𝑖 for the clause’s value binder 𝑝𝑖; then substitute the rebuilt continuation value for its resumption binder 𝑘𝑖. Lemma 25.8 gives both steps, including any nested lambdas, lets, operations, handlers, or row conversions in the clause.
For forwarding, inversion gives ⟨ℓ1,…,ℓ𝑛∣𝜀⟩≡⟨ℓ∣𝛿⟩,ℓ∉{ℓ1,…,ℓ𝑛}.Lemma 25.2 yields 𝜀≡⟨ℓ∣𝜀′⟩. Replacement types the rebuilt continuation at 𝜀, and C-Op followed by C-To types H-Forward at the same effect. Contextual cases apply the induction hypothesis to the active premise; C-RowConv reapplies its unchanged equivalence. ◻
An exposed terminal request is 𝑅[𝗉𝖾𝗋𝖿𝗈𝗋𝗆ℓ𝑣]. It contains no handler frame: a request immediately inside a handler either takes H-Op or takes H-Forward, while a more deeply nested request first reduces at its innermost handler. Thus an exposed terminal request is an observable request, not a runtime fault and not a reducible forwarding configuration.
If ⋅⊢𝑒:𝐴!𝜀, then exactly one of the following holds: 𝑒=𝗋𝖾𝗍𝗎𝗋𝗇𝑣, the computation takes a step, or it is an exposed terminal request whose label occurs in 𝜀. Hence a closed computation of effect ⟨⟩ cannot expose an operation.
Proof of Theorem 25.10 — Progress and absence of unhandled operations
Proof. Induct on typing. Lemma 31.11 settles application. For sequencing, apply the induction hypothesis to the first computation: a return takes the sequencing step, a reduction lifts, and an exposed operation remains exposed in the enlarged terminal request context. The let case is similar, except that its first computation has empty effect: an exposed-operation outcome is impossible, a return takes the let step, and a reduction lifts through the let frame. For a handler, a returned value uses its return rule; a reduction lifts; a request for a handled label uses H-Op; and every other request uses H-Forward. It is therefore a step rather than a terminal-request outcome. For C-RowConv, apply the induction hypothesis to its premise. A return or reduction is unchanged. In the exposed-request alternative, row equivalence preserves every label multiplicity, so occurrence of the exposed label in the premise row implies its occurrence in the converted row. Inversion of C-Op and repeated cancellation show that the exposed label occurs in the concluding row. The empty row has multiplicity zero for every label. Finally, the three outcomes are disjoint: returns and terminal requests have different outer forms, and a terminal request has no redex. For a step, the handler-free grammar of 𝑅 and the ordinary call-by-value forms give unique active decompositions. In the request case lemma 25.6 gives the unique nearest root, where the handled-label test selects exactly one of H-Op and H-Forward. ◻
★★☆ Fill in the forwarding case of preservation without writing “by weakening.” Name the row equivalence obtained by inversion, apply the second part of lemma 25.2, and derive the type of the continuation rebuilt in H-Forward.
The ordinary first-order unification clauses handle type variables, constants, and arrows. The row clause exposes a requested label at the head and returns a most-general substitution.
Write 𝗋𝖾𝗐𝗋𝗂𝗍𝖾(𝜀,ℓ)=(𝑆,𝜀′) when the operation succeeds. Its contract is 𝑆𝜀≡⟨ℓ∣𝑆𝜀′⟩.(31.5) This is the constrained insertion operation of chapter 4 with its lacks premise removed: both operations expose one requested label and return the residue, but duplicate effect labels make absence evidence unnecessary here. It is defined by the following equations. In Rw-Var, choose 𝜈 outside the row variables in the entire unification problem and every substitution constructed so far. 𝗋𝖾𝗐𝗋𝗂𝗍𝖾(⟨ℓ∣𝜀⟩,ℓ)=(𝗂𝖽,𝜀)𝑅𝑤−𝐻𝑒𝑎𝑑𝗋𝖾𝗐𝗋𝗂𝗍𝖾(⟨ℓ′∣𝜀⟩,ℓ)=(𝑆,⟨ℓ′∣𝜀′⟩)𝑅𝑤−𝑆𝑘𝑖𝑝ℓ′≠ℓ,𝗋𝖾𝗐𝗋𝗂𝗍𝖾(𝜀,ℓ)=(𝑆,𝜀′),𝗋𝖾𝗐𝗋𝗂𝗍𝖾(𝜇,ℓ)=([𝜇↦⟨ℓ∣𝜈⟩],𝜈)𝑅𝑤−𝑉𝑎𝑟.(31.6) Rewriting the empty row fails. In Rw-Skip, the returned substitution also acts on the residual row.
The unifier state is a list of pending equations and an accumulated substitution. Write 𝖴(𝐴,𝐵) for the result from initial state ([𝐴≐𝐵],𝗂𝖽). After a clause returns 𝑆1, every residual equation is replaced by its 𝑆1-image before the work list continues; the arrow and row-extension displays spell out those states.
The unifier tries cases in the fixed order used by definition 25.11: reflexivity, variable orientation, empty and constructor clauses, and only then row extension. Hence the following clause does not compete with U-Var. Let tail(𝜀) be the final row variable, when one exists. The row-extension clause of unification is 𝖴(⟨ℓ∣𝜀1⟩,𝜀2):(𝑆1,𝜀3):=𝗋𝖾𝗐𝗋𝗂𝗍𝖾(𝜀2,ℓ);iftail(𝜀1)∈dom(𝑆1)thenfail;𝑆2:=𝖴(𝑆1𝜀1,𝑆1𝜀3);return𝑆2∘𝑆1.(31.7) The membership test is false when the left row has no variable tail.
Unification is called only on two objects of the same kind and preserves that kind. Its type objects are exactly 𝛼, 𝑏, and 𝐴𝜀→𝐵; its row objects are exactly 𝜇, ⟨⟩, and ⟨ℓ∣𝜀⟩. In the following display, 𝑎 ranges over variables of either kind, 𝑋 over an object of that same kind, and 𝑏 over base types: 𝖴(𝑎,𝑎)=𝗂𝖽𝑈−𝑅𝑒𝑓𝑙𝖴(𝑎,𝑋)=[𝑎↦𝑋]𝑈−𝑉𝑎𝑟,𝑎∉ftv(𝑋),𝑎≠𝑋𝖴(𝑋,𝑎)=𝖴(𝑎,𝑋)𝑈−𝑆𝑦𝑚,𝑋isnotavariable𝖴(𝑏,𝑏)=𝗂𝖽𝑈−𝐵𝑎𝑠𝑒𝖴(⟨⟩,⟨⟩)=𝗂𝖽𝑈−𝐸𝑚𝑝𝑡𝑦.(31.8) Rule U-Var fails its occurs check when 𝑎∈ftv(𝑋). After reflexivity and the variable clauses, the complete same-kind clash table is 𝖴(𝑏,𝑏′)=𝖿𝖺𝗂𝗅𝑈−𝐶𝑙𝑎𝑠ℎ−𝐵𝑎𝑠𝑒,𝑏≠𝑏′𝖴(𝑏,𝐴𝜀→𝐵)=𝖿𝖺𝗂𝗅𝑈−𝐶𝑙𝑎𝑠ℎ−𝐵𝑎𝑠𝑒𝐴𝑟𝑟𝑜𝑤𝖴(𝐴𝜀→𝐵,𝑏)=𝖿𝖺𝗂𝗅𝑈−𝐶𝑙𝑎𝑠ℎ−𝐴𝑟𝑟𝑜𝑤𝐵𝑎𝑠𝑒𝖴(⟨⟩,⟨ℓ∣𝜀⟩)=𝖿𝖺𝗂𝗅𝑈−𝐶𝑙𝑎𝑠ℎ−𝐸𝑚𝑝𝑡𝑦𝐸𝑥𝑡𝑒𝑛𝑑𝖴(⟨ℓ∣𝜀⟩,⟨⟩)=𝖿𝖺𝗂𝗅𝑈−𝐶𝑙𝑎𝑠ℎ−𝐸𝑥𝑡𝑒𝑛𝑑𝐸𝑚𝑝𝑡𝑦.(31.8𝑎) A proposed type–row equation is rejected as ill kinded before 𝖴 is called; it is not another same-kind clash case.
For arrows, the complete clause is 𝑆1:=𝖴(𝐴1,𝐴2);𝑆2:=𝖴(𝑆1𝜀1,𝑆1𝜀2);𝑆3:=𝖴(𝑆2𝑆1𝐵1,𝑆2𝑆1𝐵2);𝖴(𝐴1𝜀1⟶𝐵1,𝐴2𝜀2⟶𝐵2):=𝑆3𝑆2𝑆1.𝑈−𝐴𝑟𝑟𝑜𝑤(31.9) Rows headed by a label use (25.7), called U-Extend; a variable or empty row on either side has already been covered by U-Refl, U-Var, U-Sym, U-Empty, or one of the five displayed U-Clash cases. These cases are disjoint after trying reflexivity and variable binding first, so the displays define the whole algorithm at both kinds.
The guard in (25.7) is load bearing. Without it, unifying ⟨ℓ1∣𝜇⟩with⟨ℓ2∣𝜇⟩,ℓ1≠ℓ2.(31.10) first exposes ℓ1 on the right by proposing 𝑆1=[𝜇↦⟨ℓ1∣𝜇1⟩],𝜀3=⟨ℓ2∣𝜇1⟩,⟨ℓ1∣𝜇1⟩≐⟨ℓ2∣𝜇1⟩. The recursive equation is (25.10) again with fresh tail 𝜇1. Each round consumes one tail variable and produces the next, so the substitution grows without bound. With the guard the original equation fails. It has no finite-row solution: cancelling the common multiset denoted by 𝜇 would assert {ℓ1}={ℓ2}.
If 𝗋𝖾𝗐𝗋𝗂𝗍𝖾(𝜀,ℓ)=(𝑆,𝜀′), then (25.5) holds. Moreover, if 𝑇𝜀≡⟨ℓ∣𝛿⟩, then rewriting succeeds and, for every finite protected set 𝑍 of variables, its fresh 𝜈 may be chosen outside 𝑍 and there is a substitution 𝑄 such that 𝑄agreeswith𝑇on𝑍,𝑇=𝑄∘𝑆on𝑍∪ftv(𝜀),𝛿≡𝑄𝜀′.(31.11) Thus exposure is most general.
Proof. For soundness, induct on (25.6). The head case is reflexivity. The skip case applies the induction hypothesis below the skipped label and then exchanges ℓ past ℓ′. In the variable case, substitution gives ⟨ℓ∣𝜈⟩ directly.
For factorization, induct down the visible prefix of 𝜀. If its head is ℓ, cancel it and take the identity factor. If 𝜀=⟨ℓ′∣𝜀1⟩ with ℓ′≠ℓ, the recursive rewrite returns 𝜀′1, and the second clause of lemma 25.2 gives 𝑇𝜀1≡⟨ℓ∣𝛿′⟩; its first clause then gives 𝛿≡⟨ℓ′∣𝛿′⟩. The induction hypothesis gives 𝛿′≡𝑄𝜀′1, and congruence restores the skipped ℓ′. If the tail is 𝜇, the assumed equivalence says that 𝑇𝜇 contains ℓ; exchange that occurrence to the head and write 𝑇𝜇≡⟨ℓ∣𝜌⟩. Choose 𝜈∉𝑍, define 𝑄𝜈=𝜌, and put 𝑄𝛼=𝑇𝛼 for every 𝛼≠𝜈. Then 𝑇=𝑄∘[𝜇↦⟨ℓ∣𝜈⟩] on 𝑍∪ftv(𝜀), and 𝑄 agrees with 𝑇 on 𝑍. The empty case contradicts the assumed positive ℓ-multiplicity. ◻
Proof. Rewriting either stops or calls itself on a proper row tail. For a row 𝜀, let |𝜀| be the number of visible label constructors; a variable or empty tail contributes zero. Consider the recursive call made by (25.7). If rewriting finds ℓ in the visible prefix of the right row, that call has removed the left head and the exposed right occurrence. The sum of the two visible sizes decreases by two.
Otherwise the right row is ⟨𝐿∣𝜇⟩, where 𝐿 has no ℓ, and Rw-Var binds 𝜇↦⟨ℓ∣𝜈⟩. The guard says that 𝜇 is not the tail of 𝜀1; since a row has only one tail, 𝜇 does not occur in 𝜀1. If |𝜀1|=𝑚 and |𝐿|=𝑛, the old extension equation has visible size 1+𝑚+𝑛, while its recursive tail equation has size 𝑚+𝑛. Thus every recursive row-extension call strictly decreases a natural number. Variable, empty, and equal-head cases either stop or enter that decreasing call; both empty–extension clash orientations stop immediately. Thus row unification terminates.
The recursive U-Arrow presentation and the work-list presentation agree: its three recursive calls push the domain, effect, and codomain equations in that order, composing each returned substitution into the remaining list. For that work-list representation, let 𝐶 be the finite list of equations remaining after applying the accumulated substitution. Let 𝑣(𝐶) count distinct unsolved variables of either kind, let 𝑐(𝐶) count all base and arrow constructors occurring in the equations, and let 𝑛(𝐶) count equations. Use the lexicographic measure 𝑀(𝐶)=(𝑣(𝐶),𝑐(𝐶),𝑛(𝐶))∈ℕ3.(31.12) Rows contain no base or arrow constructors. A binding of a row variable therefore cannot duplicate anything counted by 𝑐(𝐶); it changes only the row-specific measure handled by the atomic row procedure. A U-Var step permanently removes its variable and introduces no fresh outer-work-list variable, so the first component decreases even if applying the binding duplicates constructors. A U-Arrow step keeps the first component fixed and removes the two arrow heads. Its domain, effect, and codomain constructors already occurred below those heads, so the second component decreases. A successful U-Base step also decreases the second component. A U-Refl step removes an equation. Each base–base or base–arrow clash stops immediately. A row-unification call is the terminating atomic procedure proved above. Every fresh tail introduced by exposure replaces one bound old tail, so this call does not increase 𝑣; if 𝑣 stays fixed, removing the solved row equation without adding an outer equation decreases 𝑛. Thus every outer transition decreases 𝑀, and the complete unifier terminates. ◻
Proof of Theorem 25.14 — Most-general type-and-row unification
Proof. Soundness is by induction on a successful unifier derivation; the five clash cases have no successful conclusion. If a variable 𝛼 is bound to 𝐵, the occurs check has established 𝛼∉ftv(𝐵), and the returned substitution makes both sides literally 𝐵. Equal constants return the identity. For arrows, the domain hypothesis equates the domains; the effect hypothesis equates the two effects after that substitution; and the codomain hypothesis equates the codomains after both earlier substitutions. Congruence then equates the arrows. For (25.7), Lemma 25.12 gives 𝑆1𝜀2≡⟨ℓ∣𝑆1𝜀3⟩; the recursive hypothesis equates the tails after 𝑆2, so congruence equates the original rows after 𝑆2𝑆1.
For completeness, strengthen the induction statement to factorization. In a variable equation 𝛼≐𝐵, any unifier 𝑇 satisfies 𝑇𝛼=𝑇𝐵. Define 𝑄 to agree with 𝑇 away from 𝛼; then 𝑇=𝑄∘[𝛼↦𝐵] on the variables of the equation. If 𝛼∈ftv(𝐵) and 𝐵≠𝛼, substitution gives an equation in which 𝑇𝛼 contains itself beneath at least one constructor. Exchange preserves the number of row-extension constructors, and type congruence preserves all other constructor counts, so this equation would imply |𝑇𝛼|≥1+|𝑇𝛼|, impossible for a finite type. Thus no unifier exists. The constant case is reflexive. Distinct bases cannot be unified; a base and an arrow have different outer type constructors; and an empty row cannot equal an extension because their multiplicities are respectively zero everywhere and positive at the head label. These arguments cover both displayed orientations, so every same-kind U-Clash failure is complete. A type–row pair is not a well-kinded equation. For arrows, factor the given unifier through the domain MGU, apply the residual factor to the substituted effect equation, and then to the substituted codomain equation. The three induction hypotheses compose in exactly that order.
In the extension case, apply the factorization half of lemma 25.12 to the occurrence of ℓ on the right, taking 𝑍=ftv(𝜀1). The protected-set clause produces 𝑇=𝑄1𝑆1 on every variable of the left row and reduces the remaining equality to 𝑄1𝑆1𝜀1≡𝑄1𝑆1𝜀3. The induction hypothesis gives 𝑄1=𝑄2𝑆2, hence 𝑇=𝑄2𝑆2𝑆1.
For the guard, suppose it fires while unifying ⟨ℓ,𝐿∣𝜇⟩ with ⟨𝑀∣𝜇⟩. Rewriting reached the shared tail 𝜇, so no label in the finite prefix 𝑀 is ℓ. If a finite substitution 𝑇 unified the rows, equality of multiplicities would give {ℓ}+M(𝐿)+M(𝑇𝜇)=M(𝑀)+M(𝑇𝜇). Cancel the common multiset M(𝑇𝜇). The multiplicity of ℓ on the left is 1+M(𝐿)(ℓ)>0, while it is zero on the right, a contradiction. Thus the guard discards no unifier; (25.10) is its smallest instance. Remaining failures are constructor conflicts, empty-row exposure, or the occurs check, each incompatible with a finite unifier. ◻
★★☆ Trace (25.6)– (25.7) on (25.10), naming the fresh variable and the substitution returned by rewriting. Identify the exact guard test that fails. Then trace ⟨ℓ1∣𝜇⟩ against ⟨ℓ2,ℓ1∣𝜈⟩ and give its MGU.
★★☆ Compute an MGU of ⟨𝗋𝖺𝗂𝗌𝖾,𝗀𝖾𝗍∣𝜇⟩and⟨𝗀𝖾𝗍,𝗋𝖺𝗂𝗌𝖾,𝗋𝖺𝗂𝗌𝖾∣𝜈⟩. State the residual equation and show explicitly how every other unifier factors through yours.
Algorithm W gains a second variable sort and clauses for operation requests, handlers, and row equations. Variable instantiation, lambda, application, and let-generalization keep their Hindley–Milner form.
Write 𝖶𝑣(Γ,𝑣)=(𝑆,𝜏),𝖶𝑐(Γ,𝑒)=(𝑆,𝜏,𝜀). Fresh variables are drawn from one monotonically consumed supply. Thus each choice is distinct from every variable in the environment or term, in the domain or range of a substitution already constructed, and in every type or row returned by an earlier recursive call. A recursive call receives the unused tail of the supply; this state parameter is implicit in the displayed 𝖶 notation. Substitutions act on the remaining environment before that call. We maintain the invariant that the reported type and row are fixed by the reported substitution; every tuple below is written after applying the substitutions accumulated at that point.
For values:
A variable instantiates every quantified type and row variable in its scheme with a fresh variable and returns the identity substitution.
If C(𝑐)=∀¯𝛼¯𝜇.𝜏, a constant replaces every displayed quantifier by a fresh variable, obtaining 𝑇𝜏, and returns (𝗂𝖽,𝑇𝜏).
For 𝜆𝑥.𝑒, choose fresh 𝛼, compute 𝖶𝑐(Γ,𝑥:𝛼,𝑒)=(𝑆,𝜏,𝜀), and return (𝑆,𝑆𝛼𝜀→𝜏).
For computations:
For 𝗋𝖾𝗍𝗎𝗋𝗇𝑣, infer (𝑆,𝜏) for 𝑣, choose a fresh row variable 𝜇, and return (𝑆,𝜏,𝜇). A fresh open row is principal because C-Return admits every effect.
For 𝑣1𝑣2, compute (𝑆1,𝜏1)=𝖶𝑣(Γ,𝑣1),(𝑆2,𝜏2)=𝖶𝑣(𝑆1Γ,𝑣2). Choose fresh 𝛼,𝜇.
Let 𝑆3=𝖴(𝑆2𝜏1,𝜏2𝜇→𝛼). The answer is (𝑆3𝑆2𝑆1,𝑆3𝛼,𝑆3𝜇). The fresh application-row variable 𝜇 is immediately unified with the latent row of the inferred function type; it is therefore absent from later worked traces unless that unification leaves it unconstrained.
For 𝑒1𝗍𝗈𝑥.𝑒2, compute (𝑆1,𝜏1,𝜀1), then (𝑆2,𝜏2,𝜀2) under 𝑆1Γ,𝑥:𝑆1𝜏1. Let 𝑆3=𝖴(𝑆2𝜀1,𝜀2). Return (𝑆3𝑆2𝑆1,𝑆3𝜏2,𝑆3𝜀2).
For 𝗉𝖾𝗋𝖿𝗈𝗋𝗆ℓ𝑣, compute (𝑆1,𝜏)=𝖶𝑣(Γ,𝑣), let 𝑆2=𝖴(𝜏,𝑃ℓ), and choose fresh 𝜇. Return (𝑆2𝑆1,𝑅ℓ,⟨ℓ∣𝜇⟩). The declaration types 𝑃ℓ,𝑅ℓ are closed, so applying any current substitution to them would be redundant.
For 𝗅𝖾𝗍𝑥=𝑒1𝗂𝗇𝑒2, infer (𝑆1,𝜏1,𝜀1), unify 𝜀1 with ⟨⟩, and call the result 𝑆2. Put 𝜒1=gen𝑐(𝑆2𝑆1Γ,𝑆2𝜏1!⟨⟩)=∀¯𝑎.(𝑆2𝜏1!⟨⟩),𝜎1=∀¯𝑎.𝑆2𝜏1 by quantifying variables free in the type-and-effect pair but not the environment. The computation scheme 𝜒1 records the pure effect for the soundness and principality statement; the term environment stores only its value-type projection 𝜎1. Infer (𝑆3,𝜏2,𝜀2) for 𝑒2 under 𝑆2𝑆1Γ,𝑥:𝜎1, and return (𝑆3𝑆2𝑆1,𝜏2,𝜀2).
For 𝗁𝖺𝗇𝖽𝗅𝖾𝑒𝗐𝗂𝗍𝗁𝐻, use the following accumulator. For an accumulated substitution 𝑄, define the oriented solve step 𝑅=𝖴(𝑄𝑋,𝑄𝑌),solve(𝑄;𝑋≐𝑌)=𝑅∘𝑄.(31.13) where the inferred object is always on the left and the expected object on the right. Choose fresh 𝛽,𝜇, compute (𝑆0,𝐴0,𝜀0)=𝖶𝑐(Γ,𝑒), and put 𝑄0=𝑆0. For the return clause, compute (𝑆𝑟,𝐵𝑟,𝛿𝑟)=𝖶𝑐(𝑄0Γ,𝑥:𝑄0𝐴0,𝑒𝑟), then set 𝑄0𝑟=𝑆𝑟𝑄0,𝑄1𝑟=solve(𝑄0𝑟;𝐵𝑟≐𝛽),𝑄𝑟=solve(𝑄1𝑟;𝛿𝑟≐𝜇). Starting with 𝑄𝑐0=𝑄𝑟, process clauses in the fixed source order. The tempting resumption assumption 𝑘𝑖:𝑅𝑖𝑄𝑐𝑖−1𝜀0←←←←←←←←←←←→𝑄𝑐𝑖−1𝛽(𝑏𝑎𝑑) uses the handler input row. It is wrong: resuming reinstalls the handler, so a request already removed by the handler would be counted again. For a concrete counterexample, extend the operation signature by Σ(𝖺𝗌𝗄)=𝟏⇝𝖤𝗇𝗏. For a one-clause 𝖺𝗌𝗄 handler with input ⟨𝖺𝗌𝗄∣𝜇⟩ and output 𝜇, the bad type gives 𝑘𝑖() an extra 𝖺𝗌𝗄; the clause can no longer satisfy its required output 𝜇. The resumption therefore has type 𝑅𝑖𝑄𝑐𝑖−1𝜇←←←←←←←←←←→𝑄𝑐𝑖−1𝛽, using the accumulated output row. At clause 𝑖, compute (𝑆𝑖,𝐵𝑖,𝛿𝑖)=𝖶𝑐(𝑄𝑐𝑖−1Γ,𝑝𝑖:𝑃𝑖,𝑘𝑖:𝑅𝑖𝑄𝑐𝑖−1𝜇←←←←←←←←←←→𝑄𝑐𝑖−1𝛽;𝑒𝑖). Again 𝑃𝑖 and 𝑅𝑖 are closed declarations; only the inferred output type and row require the accumulated substitution. The semicolon separates the extended context from the clause term. Put 𝑄0𝑖=𝑆𝑖𝑄𝑐𝑖−1,𝑄1𝑖=solve(𝑄0𝑖;𝐵𝑖≐𝛽),𝑄𝑐𝑖=solve(𝑄1𝑖;𝛿𝑖≐𝜇). Finally set 𝑄′=solve(𝑄𝑐𝑛;𝜀0≐⟨ℓ1,…,ℓ𝑛∣𝜇⟩).(31.14) Return (𝑄′,𝑄′𝛽,𝑄′𝜇).
Every recursive call is on a proper subterm, and every equation goes to the terminating unifier. The fixed source order is part of this algorithm. We do not claim a separate clause-order-independence theorem; principality is proved for this stated order below.
Let Γ⊢𝑒:𝐴!𝜀, and put 𝜒=gen𝑐(Γ,𝐴!𝜀). Every instance of the pair 𝐴!𝜀 that leaves Γ fixed is an instance of 𝜒, and 𝜒 is most general with this property. If 𝜀=⟨⟩, deleting the fixed empty effect from 𝜒 gives gen𝑣(Γ,𝐴), the most-general value scheme admitted by C-Let.
Proof of Lemma 25.16 — Computation and pure-let generalization
Proof. Partition the free variables of 𝐴!𝜀 into those free in Γ and the remainder. A substitution that leaves Γ fixed cannot quantify or rename the first part; its action on the remainder is precisely an instantiation of the variables quantified by gen𝑐. The identity instantiation recovers the pair, and any other scheme whose instances are exactly the Γ-fixing instances must quantify only variables in the same remainder. Its instances therefore factor through 𝜒. When the row is empty, it contributes no free row variables, so the quantified list is exactly the one in gen𝑣(Γ,𝐴). Purity permits the returned value to be used repeatedly without duplicating an operation. ◻
Proof. Proceed by simultaneous induction on values and computations. A variable is typed by V-Var at the fresh instance selected by W; a constant is typed by V-Const at the corresponding fresh instance of its closed declared scheme. For a lambda, the computation induction hypothesis types the body under 𝑆Γ,𝑥:𝑆𝛼, so V-Lam gives precisely W’s arrow.
At every sequential inference stage, maintain this invariant: after the accumulator is 𝑄𝑗, lemma 31.8 transports every judgment inferred through stage 𝑗 to context 𝑄𝑗Γ and applies 𝑄𝑗 to its reported type and row. Each new MGU is composed only after this transport.
For a return, the value hypothesis derives 𝑆Γ⊢𝑣𝑣:𝐴, and C-Return permits W’s fresh 𝜇. In an application, lemma 31.8 transports both value hypotheses through the substitutions returned by the later calls, so they type the operator and operand after the accumulated substitution. Unifier soundness changes the operator type to 𝜏2𝜇→𝛼; C-App gives the reported result and effect. The operation case is the same calculation against the fixed parameter type 𝑃ℓ, followed by C-Op. For sequencing, the two computation hypotheses type the premises at 𝑆2𝜀1 and 𝜀2; unifier soundness proves 𝑆2𝜀1≡𝜀2, and C-To gives W’s substituted result.
For let, the accumulator invariant transports the bound judgment before unification establishes the empty effect required by C-Let. Then lemma 25.16 justifies the environment scheme, and the induction hypothesis types the body. For a handler, the invariant transports the body, return, and each operation-clause judgment in the fixed clause order. The sequential unifications establish exactly (25.14), a return clause of result 𝛽!𝜇, and operation clauses of that result under 𝑝𝑖:𝑃𝑖 and 𝑘𝑖:𝑅𝑖𝜇→𝛽. These are the premises of C-Handle. ◻
Consider a declarative value or computation derivation whose last rule is not C-Handle. Suppose that the principal-factorization conclusion holds for each immediate typing premise of that last rule. Then W succeeds on the conclusion. For a computation conclusion 𝑇Γ⊢𝑒:𝐴!𝜀, if 𝖶𝑐(Γ,𝑒)=(𝑆,𝐴0,𝜀0), there is a substitution 𝑄 such that 𝑇Γ≡𝑄𝑆Γ,𝐴≡𝑄𝐴0,𝜀≡𝑄𝜀0.(31.15) For a value conclusion 𝑇Γ⊢𝑣𝑣:𝐴, 𝖶𝑣(Γ,𝑣)=(𝑆,𝐴0) succeeds, and there is a substitution 𝑄 such that 𝑇Γ≡𝑄𝑆Γ,𝐴≡𝑄𝐴0.
Proof of Lemma 31.21 — Non-handler factorization step
Proof. Inspect the last rule, using the assumed factorization for each immediate typing premise.
Ordinary forms. The value and computation cases use the common factorization statement (25.15). A declarative variable instance factors through W’s fresh instance by mapping each fresh variable to the type chosen in the derivation. A declarative constant instance factors in the same way through W’s fresh instance of its closed declaration. In the lambda case, inversion gives a monomorphic parameter type and the corresponding body derivation. Map W’s fresh parameter variable to that type and apply the assumed computation factorization to the body. In the return case, apply the assumed value factorization and map W’s fresh 𝜇 to the derivation’s chosen effect row.
Application, operation, and sequencing. For application, inversion yields types 𝐴 and 𝐵 such that the operator has type 𝐴𝜀→𝐵 and the argument has type 𝐴. The two value hypotheses first factor their inferences; the residual declarative substitution then unifies W’s generated arrow equation. MGU factorization produces the next residual 𝑄. For an operation, the assumed value factorization handles the parameter inference; the residual declarative substitution then unifies W’s equation with 𝑃ℓ, after which the declarative tail is the image of W’s fresh 𝜇. For sequencing, the two computation hypotheses factor the subterms from left to right; inversion says their effects are equivalent, so the residual substitution solves W’s effect equation. The row MGU theorem factors it once more. These compositions give all three equations in (25.15).
Pure let. In the let case, inversion gives an empty effect. The assumed factorization followed by MGU factorization reaches W’s empty-row unifier. Lemma 25.16 factors the scheme used by the declarative body through W’s generalized scheme. Apply the assumed body factorization and compose.
Row conversion. If the last rule is C-RowConv, write its premise effect as 𝜀−, so 𝜀−≡𝜀. The assumed premise factorization premise factors the same syntax-directed W run as 𝖶𝑐(Γ,𝑒)=(𝑆,𝐴0,𝜀0), with residual 𝑄 such that 𝑇Γ≡𝑄𝑆Γ, 𝐴≡𝑄𝐴0, and 𝜀−≡𝑄𝜀0. Symmetry and transitivity give 𝜀≡𝑄𝜀0, so the same 𝑄 proves (25.15) for the converted conclusion; W needs no additional equation or solve step. ◻
Suppose the last rule is C-Handle, and suppose the principal-factorization conclusion holds for its body, return clause, and operation clauses. Then the handler run of 𝖶𝑐 succeeds and produces (𝑆,𝐴0,𝜀0) with a residual 𝑄 satisfying (25.15).
Proof of Lemma 31.22 — Handler-accumulator factorization step
Proof. For a handler, invert C-Handle and follow the accumulator literally. Before following its premises, extend the declarative substitution 𝑇 to each fresh W variable by the type or row selected by the inverted declarative derivation. The fresh variables lie outside the original domain, so the two parts of the extension are disjoint. After accumulator stage 𝑗, maintain a residual substitution Θ𝑗 with 𝑇=Θ𝑗𝑄𝑗ontheenvironmentvariablesandeveryfreshvariableintroducedthroughstage𝑗.(31.16) The assumed body factorization initializes this invariant at 𝑄0=𝑆0. For the return clause, its assumed factorization handles the declarative clause derivation through 𝑆𝑟𝑄0. Inversion says that the declarative return type and effect are the handler’s 𝛽 and 𝜇. The residual sends the fresh reported type variable to the inverted declarative result and therefore unifies the first oriented equation 𝑄0𝑟𝐵𝑟≐𝑄0𝑟𝛽. After that MGU is composed, the residual sends the fresh row variable to the inverted declarative effect and unifies the second equation 𝑄1𝑟𝛿𝑟≐𝑄1𝑟𝜇. The MGU factorization theorem factors it through 𝑄1𝑟 and then 𝑄𝑟, establishing (25.16) after the return stage.
Suppose (25.16) holds before clause 𝑖. Its assumed factorization handles the declarative clause derivation through 𝑆𝑖𝑄𝑐𝑖−1. The inverted clause premise gives the expected type 𝑇𝛽, effect 𝑇𝜇, and continuation type 𝑇𝑅𝑖𝑇𝜇⟶𝑇𝛽. Thus the residual solves the reported-type equation and then the reported-effect equation. Two MGU factorizations yield 𝑄1𝑖 and 𝑄𝑐𝑖, preserving the invariant. Induction over the fixed clause order reaches 𝑄𝑐𝑛. Finally, the inverted handler-input premise solves the row equation in (25.14); one last MGU factorization yields 𝑄′ and its residual. This is the required factor 𝑄 in (25.15). ◻
Suppose 𝑇Γ⊢𝑒:𝐴!𝜀. Then 𝖶𝑐(Γ,𝑒)=(𝑆,𝐴0,𝜀0) succeeds, and there is a substitution 𝑄 satisfying (25.15). If 𝑇Γ⊢𝑣𝑣:𝐴, then 𝖶𝑣(Γ,𝑣)=(𝑆,𝐴0) succeeds, and there is a substitution 𝑄 such that 𝑇Γ≡𝑄𝑆Γ,𝐴≡𝑄𝐴0. Thus, for an empty environment, gen𝑐(∅,𝐴0!𝜀0) is the principal computation scheme.
Proof of Theorem 25.18 — Completeness and principal factorization
Proof. Proceed by simultaneous induction on value and computation derivations. At every last rule other than C-Handle, apply lemma 31.21 to the induction hypotheses for its immediate typing premises. At C-Handle, apply lemma 31.22 to the body, return, and clause induction hypotheses. These cases exhaust the declarative rules and establish both factorization statements. For Γ=∅, lemma 25.16 turns the computation factorization into the displayed principal scheme. ◻
Here is the complete row part of W on the transaction. Name the fresh tail of the final return 𝜌𝑟, the tail of 𝗉𝗎𝗍 by 𝜌𝑝, the instantiated callback tail by 𝜌𝑓, and the tail of 𝗀𝖾𝗍 by 𝜌𝑔. Working from the final sequence outward, W generates the following equations and MGUs: equationnewbindings⟨𝗉𝗎𝗍∣𝜌𝑝⟩≐𝜌𝑟𝜌𝑟↦⟨𝗉𝗎𝗍∣𝜌𝑝⟩⟨𝗋𝖺𝗂𝗌𝖾∣𝜌𝑓⟩≐⟨𝗉𝗎𝗍∣𝜌𝑝⟩𝜌𝑝↦⟨𝗋𝖺𝗂𝗌𝖾∣𝜉⟩,𝜌𝑓↦⟨𝗉𝗎𝗍∣𝜉⟩⟨𝗀𝖾𝗍∣𝜌𝑔⟩≐⟨𝗉𝗎𝗍,𝗋𝖺𝗂𝗌𝖾∣𝜉⟩𝜉↦⟨𝗀𝖾𝗍∣𝜇⟩,𝜌𝑔↦⟨𝗉𝗎𝗍,𝗋𝖺𝗂𝗌𝖾∣𝜇⟩. For the second equation, exposure skips 𝗉𝗎𝗍, expands 𝜌𝑝 with 𝗋𝖺𝗂𝗌𝖾, and leaves residual ⟨𝗉𝗎𝗍∣𝜉⟩. For the third it skips 𝗉𝗎𝗍,𝗋𝖺𝗂𝗌𝖾, expands 𝜉 with 𝗀𝖾𝗍, and leaves residual ⟨𝗉𝗎𝗍,𝗋𝖺𝗂𝗌𝖾∣𝜇⟩. Thus the composite substitution is 𝜌𝑟↦⟨𝗉𝗎𝗍,𝗋𝖺𝗂𝗌𝖾,𝗀𝖾𝗍∣𝜇⟩,𝜌𝑝↦⟨𝗋𝖺𝗂𝗌𝖾,𝗀𝖾𝗍∣𝜇⟩,𝜌𝑓↦⟨𝗉𝗎𝗍,𝗀𝖾𝗍∣𝜇⟩,𝜉↦⟨𝗀𝖾𝗍∣𝜇⟩,𝜌𝑔↦⟨𝗉𝗎𝗍,𝗋𝖺𝗂𝗌𝖾∣𝜇⟩. The callback instance is therefore 𝜈=𝜌𝑓=⟨𝗀𝖾𝗍,𝗉𝗎𝗍∣𝜇⟩, modulo exchange, and every sequencing node has common effect 𝐸𝜇. Since 𝜇∉ftv(Γ𝑓), computation generalization gives gen𝑐(Γ𝑓,𝖨𝗇𝗍!𝐸𝜇)=∀𝜇.(𝖨𝗇𝗍!𝐸𝜇), as in (25.2). For the monomorphic value 𝜆𝑓.𝑡𝑓, W instead gives 𝑓:𝟏𝜀𝑓⟶𝖨𝗇𝗍 a fresh monotype. Its row equations are equationnewbindings⟨𝗉𝗎𝗍∣𝜌𝑝⟩≐𝜌𝑟𝜌𝑟↦⟨𝗉𝗎𝗍∣𝜌𝑝⟩𝜀𝑓≐⟨𝗉𝗎𝗍∣𝜌𝑝⟩𝜀𝑓↦⟨𝗉𝗎𝗍∣𝜌𝑝⟩⟨𝗀𝖾𝗍∣𝜌𝑔⟩≐⟨𝗉𝗎𝗍∣𝜌𝑝⟩𝜌𝑝↦⟨𝗀𝖾𝗍∣𝜇⟩,𝜌𝑔↦⟨𝗉𝗎𝗍∣𝜇⟩. Thus the common row is 𝐺𝜇, and V-Lam gives (25.3). This is the promised full trace; no handler-specific heuristic occurs in it.
We can now compare four handlers at one formal signature. Write Γ⊢𝐻:𝐴!𝜀in⟹𝐵!𝜀out as an abbreviation for the return- and operation-clause premises of C-Handle, with those input and output rows. Combining those premises with Γ⊢𝑒:𝐴!𝜀in in C-Handle gives Γ⊢𝗁𝖺𝗇𝖽𝗅𝖾𝑒𝗐𝗂𝗍𝗁𝐻:𝐵!𝜀out.
The fallback handler already calculated above satisfies 𝑑:𝐵⊢𝐶𝐵𝑑:𝐵!⟨𝗋𝖺𝗂𝗌𝖾∣𝜇⟩⟹𝐵!𝜇. For a reader environment 𝑟:𝖤𝗇𝗏, define 𝐻𝑟𝖺𝗌𝗄={𝗋𝖾𝗍𝗎𝗋𝗇𝑥↦𝗋𝖾𝗍𝗎𝗋𝗇𝑥;𝖺𝗌𝗄(𝑢;𝑘)↦𝑘𝑟}. The two premises are 𝑥:𝐴⊢𝗋𝖾𝗍𝗎𝗋𝗇𝑥:𝐴!𝜇,𝑢:𝟏,𝑘:𝖤𝗇𝗏𝜇→𝐴⊢𝑘𝑟:𝐴!𝜇, so 𝑟:𝖤𝗇𝗏⊢𝐻𝑟𝖺𝗌𝗄:𝐴!⟨𝖺𝗌𝗄∣𝜇⟩⟹𝐴!𝜇.
Here is the one-clause handler accumulator once in full. For ℎ𝑟=𝗁𝖺𝗇𝖽𝗅𝖾(𝗉𝖾𝗋𝖿𝗈𝗋𝗆𝖺𝗌𝗄())𝗐𝗂𝗍𝗁𝐻𝑟𝖺𝗌𝗄, the body reports 𝖤𝗇𝗏!⟨𝖺𝗌𝗄∣𝜇0⟩. Choose output variables 𝛽,𝜇. The return clause reports 𝖤𝗇𝗏!𝛿𝑟, so its two solve steps bind 𝛽↦𝖤𝗇𝗏 and 𝛿𝑟↦𝜇. The operation context is then constructed as 𝑢:𝟏,𝑘:𝖤𝗇𝗏𝜇→𝖤𝗇𝗏,𝑟:𝖤𝗇𝗏. Its body 𝑘𝑟 already reports 𝖤𝗇𝗏!𝜇, so the clause’s two solve steps are identities. The final input equation ⟨𝖺𝗌𝗄∣𝜇0⟩≐⟨𝖺𝗌𝗄∣𝜇⟩ binds 𝜇0↦𝜇. Thus W returns 𝖤𝗇𝗏!𝜇, with relative scheme ∀𝜇.(𝖤𝗇𝗏!𝜇). The continuation’s output row is chosen when its context entry is constructed; it is not discovered by a later equation.
For a second comparison, add the operation Σ(𝖼𝗁𝗈𝗈𝗌𝖾)=𝟏⇝𝖡𝗈𝗈𝗅. The next handler deliberately resumes twice: 𝐻𝗍𝗐𝗂𝖼𝖾={𝗋𝖾𝗍𝗎𝗋𝗇𝑥↦𝗋𝖾𝗍𝗎𝗋𝗇𝑥;𝖼𝗁𝗈𝗈𝗌𝖾(𝑢;𝑘)↦𝑘𝗍𝗋𝗎𝖾𝗍𝗈𝑥.𝑘𝖿𝖺𝗅𝗌𝖾}. Here 𝑥:𝟏 is intentionally unused. Both applications of 𝑘:𝖡𝗈𝗈𝗅𝜇→𝟏 have result 𝟏!𝜇; C-To therefore derives the clause, while the return clause is 𝑥:𝟏⊢𝗋𝖾𝗍𝗎𝗋𝗇𝑥:𝟏!𝜇. Hence ⊢𝐻𝗍𝗐𝗂𝖼𝖾:𝟏!⟨𝖼𝗁𝗈𝗈𝗌𝖾∣𝜇⟩⟹𝟏!𝜇. Although 𝑘 occurs twice, C-To derives the clause because it places no affine-use premise on the continuation variable.
Finally define a state-passing handler for unit-returning computations: 𝐻𝗌𝗍={𝗋𝖾𝗍𝗎𝗋𝗇𝑥↦𝗋𝖾𝗍𝗎𝗋𝗇(𝜆𝑠.𝗋𝖾𝗍𝗎𝗋𝗇𝑠);𝗀𝖾𝗍(𝑢;𝑘)↦𝗋𝖾𝗍𝗎𝗋𝗇(𝜆𝑠.𝑘𝑠𝗍𝗈𝑔.𝑔𝑠);𝗉𝗎𝗍(𝑠′;𝑘)↦𝗋𝖾𝗍𝗎𝗋𝗇(𝜆𝑠.𝑘()𝗍𝗈𝑔.𝑔𝑠′)}. Put 𝐷=𝖨𝗇𝗍𝜇→𝖨𝗇𝗍. The return clause has type 𝐷!𝜇. In the get clause, 𝑘:𝖨𝗇𝗍𝜇→𝐷; both 𝑘𝑠 and 𝑔𝑠 have effect 𝜇, so the lambda has type 𝐷. In the put clause, 𝑘:𝟏𝜇→𝐷; the same derivation ends in 𝑔𝑠′. Thus ⊢𝐻𝗌𝗍:𝟏!⟨𝗀𝖾𝗍,𝗉𝗎𝗍∣𝜇⟩⟹𝐷!𝜇. The exception, reader, choice, and state examples all use the same final row equation in W. Only their clause terms differ.
★★★ Insert 𝗉𝖾𝗋𝖿𝗈𝗋𝗆𝗅𝗈𝗀𝑒𝗍𝗈𝑤 after the put in 𝑡𝑓, with 𝑒:𝖤𝗋𝗋𝗈𝗋, and run W on the monomorphic wrapper. Name the fresh row variables and show that the callback and body share the principal row ⟨𝗀𝖾𝗍,𝗉𝗎𝗍,𝗅𝗈𝗀∣𝜇⟩. Then instantiate its tail with one 𝗋𝖺𝗂𝗌𝖾 and compare that instance with the separate let-polymorphic callback trace above.
★★☆ In the context 𝑟:𝖤𝗇𝗏, run W on the fully specified computation ℎ𝑟=𝗁𝖺𝗇𝖽𝗅𝖾(𝗉𝖾𝗋𝖿𝗈𝗋𝗆𝖺𝗌𝗄())𝗐𝗂𝗍𝗁𝐻𝑟𝖺𝗌𝗄. Name the fresh variables and the accumulator substitutions after the body, return clause, operation clause, and input-row equation; then give the principal computation scheme relative to 𝑟:𝖤𝗇𝗏. Point to the accumulator extension where the continuation is constructed with the output row 𝜇, and explain why using the handler’s input row there would be unsound.
Row polymorphism here is ordinary kinded quantification. A scheme may quantify 𝜇, and duplicate labels make extension and elimination unconstrained. There are no type classes, qualified types, subeffect inequalities, or predicates ℓ∉𝜇. Adding any of those changes both principal schemes and the solver theorem. The MGU theorem is not a theorem about unique-label rows with hidden lacks constraints.
The set-indexed calculus forgets multiplicity. For any closed row, write ⌊𝜀⌋ for its finite support: the set of labels occurring at least once. The target signature is ⌊Σ⌋(ℓ)=⌊𝑃ℓ⌋⇝⌊𝑅ℓ⌋. Translate by ⌊𝐴𝜀→𝐵⌋=𝑈∅(⌊𝐴⌋⇒⌊𝜀⌋𝐹⌊𝐵⌋),⌊𝜆𝑥.𝑒⌋=𝗍𝗁𝗎𝗇𝗄(𝜆𝑥.⌊𝑒⌋),⌊𝑣𝑤⌋=(𝖿𝗈𝗋𝖼𝖾⌊𝑣⌋)⌊𝑤⌋,⌊𝗋𝖾𝗍𝗎𝗋𝗇𝑣⌋=𝗋𝖾𝗍𝗎𝗋𝗇⌊𝑣⌋,⌊𝑒1𝗍𝗈𝑥.𝑒2⌋=⌊𝑒1⌋𝗍𝗈𝑥.⌊𝑒2⌋,⌊𝗉𝖾𝗋𝖿𝗈𝗋𝗆ℓ𝑣⌋=ℓ⌊𝑣⌋(𝑥.𝗋𝖾𝗍𝗎𝗋𝗇𝑥),⌊𝗁𝖺𝗇𝖽𝗅𝖾𝑒𝗐𝗂𝗍𝗁𝐻⌋=𝗁𝖺𝗇𝖽𝗅𝖾⌊𝑒⌋𝗐𝗂𝗍𝗁⌊𝐻⌋.(31.17) Variables and base constants map to themselves, and handlers map clause by clause: ⌊𝐻⌋={𝗋𝖾𝗍𝗎𝗋𝗇𝑥↦⌊𝑒𝑟⌋;ℓ𝑖(𝑝𝑖;𝑘𝑖)↦⌊𝑒𝑖⌋}𝑛𝑖=1. Contexts map pointwise: ⌊Γ,𝑥:𝐴⌋=⌊Γ⌋,𝑥:⌊𝐴⌋. Within a translated clause, a source call 𝑘𝑖𝑣 is (𝖿𝗈𝗋𝖼𝖾𝑘𝑖)⌊𝑣⌋, because the CBPV handler binds its continuation as a thunk. A computation type 𝐴!𝜀 maps to 𝐹⌊𝐴⌋!⌊𝜀⌋.
The target handler rule of chapter 22 checks each continuation at the chosen output set Eout, requires Ein∖H⊆Eout, and admits ordinary effect weakening. The translation proof establishes precisely these continuation, residual- inclusion, and weakening premises; it does not require equality of the two effect sets.
Call a derivation monomorphic when every variable assumption has a scheme with no quantified type or row variables. Require every constant instantiation to be fixed before the derivation begins. If such a derivation contains no C-Let root and uses only closed rows, then the judgment Γ⊢𝑒:𝐴!𝜀 yields ⌊Γ⌋⊢𝑐⌊𝑒⌋:𝐹⌊𝐴⌋!⌊𝜀⌋ in the set-indexed CBPV core. A source value judgment Γ⊢𝑣𝑣:𝐴 yields the value judgment ⌊Γ⌋⊢𝑣⌊𝑣⌋:⌊𝐴⌋.
Proof of Theorem 25.19 — Closed-row support translation
Proof. Induct on typing. A source return first uses the pure CBPV return rule and then effect weakening; an atomic operation uses an explicit pure return continuation and then weakening to the translated tail. Sequencing uses 𝐸∪𝐸=𝐸. Lambda, force/application, and the remaining value rules are the corresponding CBPV rules after type translation. The let-free hypothesis removes the only rule whose target would require polymorphic schemes, which the chapter 22 core does not contain. A C-RowConv step translates to the identical target annotation because row exchange does not change the underlying finite set.
It remains to check the displayed handler clause. Invert C-Handle and write H={ℓ1,…,ℓ𝑛},E=⌊𝜀⌋,Ein=H∪E. The body induction hypothesis gives ⌊Γ⌋⊢𝑐⌊𝑒⌋:𝐹⌊𝐴⌋!Ein, and the return-clause hypothesis gives ⌊𝑒𝑟⌋:𝐹⌊𝐵⌋!E under 𝑥:⌊𝐴⌋. For operation clause 𝑖, type translation maps the source resumption assumption 𝑘𝑖:𝑅𝑖𝜀→𝐵 exactly to 𝑝𝑖:⌊𝑃𝑖⌋,𝑘𝑖:𝑈∅(⌊𝑅𝑖⌋⇒E𝐹⌊𝐵⌋). The clause induction hypothesis therefore types ⌊𝑒𝑖⌋ at 𝐹⌊𝐵⌋!E under the target parameter and resumption binders; its calls use (𝖿𝗈𝗋𝖼𝖾𝑘𝑖)⌊𝑣⌋, as specified above. For arbitrary multiplicities, set algebra gives Ein∖H=(H∪E)∖H⊆E. This inclusion discharges the residual-effect premise of target C-Handle; the return and operation-clause induction hypotheses discharge its remaining premises. Hence the translated handler has type 𝐹⌊𝐵⌋!E. ◻
For a concrete covered term, fix 𝑒0:𝖤𝗋𝗋𝗈𝗋. The closed, duplicate-free judgment ⋅⊢𝗉𝖾𝗋𝖿𝗈𝗋𝗆𝗅𝗈𝗀𝑒0:𝟏!⟨𝗅𝗈𝗀⟩ translates to 𝗅𝗈𝗀𝑒0(𝑥.𝗋𝖾𝗍𝗎𝗋𝗇𝑥), typed in the set core at 𝐹𝟏!{𝗅𝗈𝗀}. A closed-row fallback handler gives the corresponding one-clause handler example.
The target calculus makes an operation’s continuation part of the operation node, whereas the row calculus first exposes an atomic request and lets an evaluation context supply its continuation. Its target also has administrative force/thunk steps and algebraic reassociation of sequencing. A simulation theorem would therefore need an explicit target closure modulo those equations and a proof that every source context maps to that closure. The theorem is a typing bridge rather than a simulation. Its information loss is visible on the rethrower: the target handler premise is an inclusion, so erasing both input and output to a set containing 𝗋𝖺𝗂𝗌𝖾 still typechecks. What erasure cannot express is which occurrence was discharged. On duplicate-free closed rows, support is injective up to row exchange; on rows with multiplicity, distinct source annotations collapse. Consequently support erasure is not faithful on general rows.
Generalized evidence passing
The inference calculus above and the following compilation calculus share scoped rows, but they have different endpoints. Freeze Xie and Leijen’s explicitly typed System 𝐹𝜀 source with 𝜎::=𝛼𝜅∣𝑐𝜅¯𝜎∣𝜎1→𝜀𝜎2∣∀𝛼𝜅.𝜎,𝜀::=⟨⟩∣⟨ℓ∣𝜀⟩∣𝛼𝖾𝖿𝖿. The prompt/evidence intermediate 𝐹𝑝𝑤 adds internal terms 𝗉𝗋𝗈𝗆𝗉𝗍𝑚ℎ𝑒 and 𝗒𝗂𝖾𝗅𝖽𝑚𝑣. Evidence and evidence vectors have the exact forms 𝑞::=(𝑚,ℎ,𝑤),𝑤::=⟨⟩⟩∣⟨ℓ:𝑞∣𝑤⟩⟩. Selection must find the most recently extended evidence: ⟨ℓ:𝑞∣𝑤⟩⟩.ℓ=𝑞,⟨ℓ′:𝑞∣𝑤⟩⟩.ℓ=𝑤.ℓ(ℓ≠ℓ′). The third component stores the evidence context in which ℎ was defined. The 𝐹𝑝𝑤 fragment frozen here retains that source representation but does not inspect the selected tail 𝑤′ in its displayed perform step. The source’s later tail-resumptive optimization gives the component operational work through an 𝗎𝗇𝖽𝖾𝗋 frame; that optimization and its rules are outside this card.
On a performed operation, evidence selection replaces dynamic search: 𝑤⊢𝗉𝖾𝗋𝖿𝗈𝗋𝗆op𝜀0¯𝜎𝑣⇝0𝗒𝗂𝖾𝗅𝖽𝑚(𝜆𝜀𝑘.𝑓¯𝜎𝑣𝑘) when 𝑤.ℓ=(𝑚,ℎ,𝑤′), the clause (op↦𝑓) occurs in ℎ, and the global signature assigns op to ℓ. A prompt extends the current evidence vector by ⟨ℓ:(𝑚,ℎ,𝑤)∣𝑤⟩⟩ while evaluating its body. Thus the selected evidence and the nearest dynamic prompt agree only under a reachability invariant.
This is Definition 1 of the source read as its least fixed point, rather than as a circular closure clause. It ensures that each prompt owns a unique marker generated by the handler rule and that every yielded marker came from type-correct evidence selection. Arbitrary user-written prompt or yield terms are outside the definition.
Proof of Theorem 31.26 — Internal-safe preservation
Source import. This is Theorem 2, §3.1.4, pp. 18–19 of [XL21]. Its induction uses the clause-typing and generated-marker invariants of definition 31.25. ◻
Source import. This is Theorem 3 at the same source location. Source Theorem 1, immediately before Definition 1 on p. 19, relates evidence-indexed reduction to the evidence extracted from the evaluation context and supplies the selected handler in the request case. ◻
Source import. This is Theorem 4, §3.1.4, p. 19 of [XL21]. Both markers arise from reachable handler-generated prompts; the theorem’s distinct-marker invariant therefore applies to the displayed nesting. ◻
The next intermediate 𝐹𝑝𝑏 propagates a 𝗒𝗂𝖾𝗅𝖽𝑚𝑓𝑘 term outward through evaluation contexts, so the monadic translation can realize it as the 𝖸𝗂𝖾𝗅𝖽 constructor of the target control monad. The target is a plain higher-kinded polymorphic lambda calculus with 𝖬𝗈𝗇𝜀𝐴:=𝖤𝗏𝗏𝜀→𝖢𝗍𝗅𝜀𝐴, where a translated computation accepts its evidence vector explicitly. Translation is type directed: Γ⊢𝑒:𝜎∣𝜀⇝𝑒′. Values become functions returning 𝖯𝗎𝗋𝖾; application sequences the two translated computations by monadic bind, ⟨⟨𝑒1𝑒2⟩⟩𝗀𝖾𝗉=⟨⟨𝑒1⟩⟩𝗀𝖾𝗉▹(𝜆𝑓.⟨⟨𝑒2⟩⟩𝗀𝖾𝗉▹𝑓); a performed operation uses the evidence selector for its label, for example ⟨⟨𝑣⟩⟩𝗀𝖾𝗉=𝜆𝑤:𝖤𝗏𝗏𝜀.𝖯𝗎𝗋𝖾𝜀⟨⟨𝜎⟩⟩𝗀𝖾𝗉⟨⟨𝑣⟩⟩𝗀𝖾𝗉,𝑣,⟨⟨𝗉𝖾𝗋𝖿𝗈𝗋𝗆op⟩⟩𝗀𝖾𝗉=𝗉𝖾𝗋𝖿𝗈𝗋𝗆ℓ(𝗌𝖾𝗅𝖾𝖼𝗍op). The complete target and representative translation rules are frozen in subappendix A.28.
Proof of Theorem 31.29 — Generalized-evidence operational endpoint
Source import. This is Theorem 7, §4.4, p. 25 of the version-4 report [XL21]. Its proof composes the source to multi-prompt simulation (Theorem 14), evidence-passing simulation (Theorem 15), bubbling simulation (Theorem 18), and monadic simulation (Theorem 19). The statement requires a closed integer source, empty source row, empty initial evidence vector, and the final 𝖯𝗎𝗋𝖾 result. It states neither arbitrary-open-program correctness nor correctness of generated C. ◻
The pinned MpEff release implements insertion-ordered generalized evidence vectors as a Haskell library. It does not contain the mechanized proofs and is not the Koka-to-C compiler. Current Koka uses canonical evidence and further optimizations. Results from either implementation are artifact evidence, not new hypotheses for theorem 25.18.
★★☆ Build two nested evidence entries for the same label ℓ, with distinct markers 𝑚𝑜 and 𝑚𝑖. Calculate selection of the newest entry and the resulting 𝗒𝗂𝖾𝗅𝖽 term. Then explain which definition 31.25 invariant rules out reusing 𝑚𝑜 for the inner prompt. State separately why the displayed 𝐹𝑝𝑤 perform step does not inspect the selected entry’s third component.
An open duplicate row ⟨ℓ∣𝜇⟩ states that ℓ is present. It cannot state that ℓ is absent from the unknown tail 𝜇. For example, a recovery combinator may require its handler argument to perform any effects except 𝗋𝖺𝗂𝗌𝖾. Neither 𝜇 nor ⟨𝗋𝖺𝗂𝗌𝖾∣𝜇⟩ expresses that negative premise.
The calculus 𝜆∁ solves a different problem. Fix a finite closed universe U of effects and take Boolean formulas 𝜑::=𝛽∣∅∣{𝐹}∣𝜑𝖼∣𝜑∪𝜑∣𝜑∩𝜑,𝜑∖𝜓:=𝜑∩𝜓𝖼. Formulas are identified by equality under every valuation, 𝜑≡𝖡𝜓. Function types carry latent formulas. There is no subeffecting rule.
The term 𝑒𝗐𝗂𝗍𝗁𝗈𝗎𝗍𝐹 pushes a stack frame forbidding 𝐹. To keep the source’s 𝑇-prefixed rules distinct from the adjacent System-𝖷𝗂 card, this book uses the prefix 𝖤𝗑. Its typing rule is Γ⊢∁𝑒:𝜏∣𝜑𝜑∩{𝐹}≡𝖡∅Γ⊢∁𝑒𝗐𝗂𝗍𝗁𝗈𝗎𝗍𝐹:𝜏∣𝜑Ex−Without. At runtime, 𝖽𝗈𝐹(𝑣) steps only when 𝐹∉𝖿𝗈𝗋𝖻(𝑘). The local machine interface is 𝑊𝐹:=[]𝗐𝗂𝗍𝗁𝗈𝗎𝗍𝐹,𝐿𝑥,𝑒:=𝗅𝖾𝗍𝑥=[]𝗂𝗇𝑒,𝑘::=∙∣𝑊𝐹::𝑘∣𝐿𝑥,𝑒::𝑘,𝖿𝗈𝗋𝖻(∙)=∅,𝖿𝗈𝗋𝖻(𝑊𝐹::𝑘)={𝐹}∪𝖿𝗈𝗋𝖻(𝑘),𝖿𝗈𝗋𝖻(𝐿𝑥,𝑒::𝑘)=𝖿𝗈𝗋𝖻(𝑘). A configuration is ⟨𝑒∣𝑘⟩. The machine judgment is ⊢𝑚⟨𝑒∣𝑘⟩𝗈𝗄; the stack judgment 𝜏⊢𝑘𝑘∖𝜑 records the formula forbidden by 𝑘. Here ∖ is the source’s judgment separator, not the Boolean difference operation in 𝜑∖𝜓. The exact machine and stack rules in subappendix A.28 maintain disjointness between performed and forbidden effects.
Source import. This is Theorem 3.14, §3.4, p. 16 of Lutze et al. [LMSB23]. Inversion of machine typing gives a performed formula 𝜑1, a forbidden formula 𝜑2, and 𝜑1∩𝜑2≡𝖡∅. Lemma 3.12 identifies 𝜑1 with {𝐹}; Lemma 3.13 identifies 𝜑2 with 𝖿𝗈𝗋𝖻(𝑘). ◻
Proof of Theorem 31.31 — Effect-exclusion machine safety
Source import. Progress and preservation are Theorems 3.10–3.11, §3.4, p. 15 of [LMSB23]. The progress proof uses theorem 31.30 in its T-Do case. Preservation is a separate case analysis on the machine step and does not depend on effect exclusion safety. ◻
This theorem belongs to the fixed-universe Boolean row theory. Its principal types are principal only modulo Boolean equivalence, and its inference uses Boolean unification. None of these facts follows from the duplicate-label MGU of theorem 25.14; conversely, the exclusion paper and its VM artifact prove no theorem about the inference calculus of this chapter.
★★☆ Give a type for a recovery combinator whose handler may perform effects 𝛽∖{𝗋𝖺𝗂𝗌𝖾}. Derive its Ex-Without premise, then show why neither an open row variable 𝜇 nor one positive extension of 𝜇 entails the required absence statement.
The row choice and exposure unifier follow Leijen’s scoped-label development [Lei05]. Koka uses duplicate labels and a closely related inference algorithm [Lei14]. It uses duplicate effect labels for the same reason and proves soundness and principality for its own syntax-directed system; the deep-handler calculus and set-erasure theorem above are proved locally.
★★☆ Apply set erasure to the pure and rethrowing catches. Show which two distinct rows ⟨𝗋𝖺𝗂𝗌𝖾,𝗋𝖺𝗂𝗌𝖾∣𝜈⟩ and ⟨𝗋𝖺𝗂𝗌𝖾∣𝜈⟩ collapse to one set. Show that the rethrower’s erased target handler nevertheless satisfies the target premise Ein∖H⊆Eout. Compare this with a fallback whose output contains no 𝗋𝖺𝗂𝗌𝖾, and explain exactly which multiplicity information is lost even though typing is preserved.
★★☆ Redesign row extension so labels are unique. State the lacks constraint needed to extend 𝜇 by ℓ. Rewrite the one-clause instance of C-Handle and W’s final input-row equation with that premise. List the soundness, completeness, constraint-entailment, and factorization properties needed before a principality theorem could be claimed.
★★☆ Prove the factorization half of lemma 25.12 when the visible prefix has two equal skipped labels. Display the exchanges, cancellation, and residual factor; identify the step that needs multiplicity.
★★★ Give the complete one-operation handler case of Theorem 25.18. Give the residual after the body, return clause, operation clause, and input-row equation, then pair every equation with its rule, MGU, and factor.
★★☆ Nest a handler that forwards 𝗋𝖺𝗂𝗌𝖾 inside one that handles it. Derive the types of both reduction roots, then use multiplicities and Theorem 25.10 to exclude an exposed request when the final effect is empty.
★★★ Replace duplicate labels by unique rows plus lacks constraints. State a well-kinded exposure judgment with its lacks premise, its factorization theorem, and the one-operation handler case of W. Compare these proof obligations with those of (25.11).
★★★Practical project.effect-row-inferencer Implement the finite row and handler fragment in Kappa. Represent open rows with duplicate-preserving prefixes; implement exchange comparison, single-occurrence cancellation, guarded exposure, and the one-layer handler transition. The permanent corpus must distinguish exchange from contraction, remove exactly one duplicate, compute one open-row MGU, reject the shared-tail cycle by the guard rather than fuel exhaustion, retain 𝗀𝖾𝗍, 𝗉𝗎𝗍, and 𝗋𝖺𝗂𝗌𝖾 in the transaction trace, forward once to the nearest handler, leave one 𝗋𝖺𝗂𝗌𝖾 for an outer fallback, and construct the reader resumption with the handler output tail. The oracle is the ordered list of these eight results. As a semantic mutation, recurse on the unmodified right row in prefix comparison; it must still type-check and audit cleanly but fail the oracle. Appendix E records the four acceptance commands, and appendix F gives the implementation stages.