Fully Path-Dependent Types: Stable Paths, Singletons, and Modules
Prerequisites. Direct starred prerequisites: Chapter 45. No later core chapter depends on this route.
Let 𝑟 denote a root module with a field 𝑝𝑎𝑙𝑒𝑡𝑡𝑒, and let that palette contain an abstract type member 𝐶𝑜𝑙𝑜𝑟. A method whose argument belongs to the root’s palette needs the type 𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝐶𝑜𝑙𝑜𝑟. The variable-path rules of chapter 45 form only 𝑥.𝐴; they reject the selection because 𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒 is not a variable. Inserting a let binding gives the palette a temporary name but changes interfaces under substitution and cannot express an exported type that mentions the original nested module. Paths must enter types directly. Arbitrary terms cannot: an application in a type may reduce, capture control, or cease to denote an object. The extension is therefore determined by stable field paths and by the equations that identify aliases of those paths.
A stable path is generated by 𝑝,𝑞::=𝑥∣𝑝.𝑎. Variables and immutable field selections are stable because evaluation can only look up their stored definitions. Application, object construction, and let expressions are not paths. The path-formation judgment Γ⊢𝑝𝗉𝖺𝗍𝗁 is derived only when every selected prefix has a field type:
𝑥:𝑇∈Γ
Γ⊢𝑥𝗉𝖺𝗍𝗁
P-Var
Γ⊢𝑝:{𝑎:𝑇}
Γ⊢𝑝.𝑎𝗉𝖺𝗍𝗁
P-Fld
Type selection is extended from 𝑥.𝐴 to 𝑝.𝐴 only under Γ⊢𝑝𝗉𝖺𝗍𝗁.
Define 𝖯𝖺𝗅𝖾𝗍𝗍𝖾:=𝜇(𝑠:{𝐶𝑜𝑙𝑜𝑟:⊥..⊤}∧{𝑧𝑒𝑟𝑜:𝑠.𝐶𝑜𝑙𝑜𝑟}),𝖱𝗈𝗈𝗍:=𝜇(𝑟:{𝑝𝑎𝑙𝑒𝑡𝑡𝑒:𝖯𝖺𝗅𝖾𝗍𝗍𝖾}∧{𝑑𝑒𝑓𝑎𝑢𝑙𝑡:𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝐶𝑜𝑙𝑜𝑟}). For 𝑟:𝖱𝗈𝗈𝗍, recursion elimination and field elimination give Γ⊢𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒:𝖯𝖺𝗅𝖾𝗍𝗍𝖾,Γ⊢𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒𝗉𝖺𝗍𝗁. Path-dependent selection therefore forms 𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝐶𝑜𝑙𝑜𝑟, and the second field has type Γ⊢𝑟.𝑑𝑒𝑓𝑎𝑢𝑙𝑡:𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝐶𝑜𝑙𝑜𝑟. The variable-only calculus stops after the first judgment because its selection grammar has no case for a field path.
★☆☆ For 𝑟:𝖱𝗈𝗈𝗍, write the complete derivation of 𝑟.𝑑𝑒𝑓𝑎𝑢𝑙𝑡:𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝐶𝑜𝑙𝑜𝑟. Include recursion elimination, both path formation premises, field elimination for 𝑝𝑎𝑙𝑒𝑡𝑡𝑒, formation of 𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝐶𝑜𝑙𝑜𝑟, and field elimination for 𝑑𝑒𝑓𝑎𝑢𝑙𝑡.
The path 𝑟.𝑎𝑙𝑖𝑎𝑠 may store 𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒. A field type alone forgets that the two paths denote one object. pDOT records the equation as a type.
Definition typing needs one more piece of information than in chapter 45, and it is needed already in the first rule below. A definition is checked at the path that names the object it belongs to, so its judgment is written 𝑝;Γ⊢𝑑:𝑇, read: the definition 𝑑 belongs to the object whose stable identity is 𝑝. Definition 108.7 gives the rule that makes this necessary; here it only records where a stored path is installed.
For a typeable path 𝑞, the singleton path type𝑞.𝗍𝗒𝗉𝖾 contains paths that denote the same run-time object as 𝑞. Path definition and alias propagation are governed by
Γ⊢𝑞𝗉𝖺𝗍𝗁
𝑝;Γ⊢{𝑎=𝑞}:{𝑎:𝑞.𝗍𝗒𝗉𝖾}
Def-Path
Γ⊢𝑝:𝑞.𝗍𝗒𝗉𝖾Γ⊢𝑞:𝑇
Γ⊢𝑝:𝑇
Sngl-Trans
Γ⊢𝑝:𝑞.𝗍𝗒𝗉𝖾Γ⊢𝑞.𝑎𝗉𝖺𝗍𝗁
Γ⊢𝑝.𝑎:𝑞.𝑎.𝗍𝗒𝗉𝖾
Sngl-E
The prefix premise in Sngl-E requires the target field path 𝑞.𝑎 to be well formed before aliasing extends through 𝑎.
If 𝑟.𝑎𝑙𝑖𝑎𝑠:𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝗍𝗒𝗉𝖾, then Sngl-E derives 𝑟.𝑎𝑙𝑖𝑎𝑠.𝑧𝑒𝑟𝑜:(𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝑧𝑒𝑟𝑜).𝗍𝗒𝗉𝖾, because 𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝑧𝑒𝑟𝑜 is a typeable path. Rule Sngl-Trans then gives 𝑟.𝑎𝑙𝑖𝑎𝑠.𝑧𝑒𝑟𝑜:𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝐶𝑜𝑙𝑜𝑟 from 𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝑧𝑒𝑟𝑜:𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝐶𝑜𝑙𝑜𝑟. The derivation transports a type along a run-time alias; it does not assert judgmental equality of the two path expressions.
The judgment Γ⊢Replace(𝑇,𝑝,𝑞,𝑈) holds when 𝑈 is obtained from 𝑇 by replacing exactly one occurrence whose prefix is 𝑝 by the corresponding occurrence with prefix 𝑞, and the complete changed leaf is formed in Γ. The formation indices prevent a replacement from constructing an ill-formed target path. The proposition below extracts the subtyping consequence used by later derivations.
The auxiliary path judgment changes one prefix, retains its suffix, and checks every newly constructed target prefix:
Γ⊢𝑞𝗉𝖺𝗍𝗁
Γ⊢RPath(𝑝,𝑝,𝑞,𝑞)
RP-Here
Γ⊢RPath(𝑟,𝑝,𝑞,𝑟′)Γ⊢𝑟′.𝑎𝗉𝖺𝗍𝗁
Γ⊢RPath(𝑟.𝑎,𝑝,𝑞,𝑟′.𝑎)
RP-Fld
Thus an induction on RPath proves Γ⊢𝑟′𝗉𝖺𝗍𝗁. The two path-bearing type leaves require formation of the complete changed type, not merely of the root 𝑞:
Γ⊢RPath(𝑟,𝑝,𝑞,𝑟′)Γ⊢𝑟′.𝐴𝗍𝗒𝗉𝖾
Γ⊢Replace(𝑟.𝐴,𝑝,𝑞,𝑟′.𝐴)
R-Sel
Γ⊢RPath(𝑟,𝑝,𝑞,𝑟′)Γ⊢𝑟′.𝗍𝗒𝗉𝖾𝗍𝗒𝗉𝖾
Γ⊢Replace(𝑟.𝗍𝗒𝗉𝖾,𝑝,𝑞,𝑟′.𝗍𝗒𝗉𝖾)
R-Sngl
Every remaining rule descends through exactly one type component:
Γ⊢Replace(𝑆,𝑝,𝑞,𝑆′)
Γ⊢Replace({𝑎:𝑆},𝑝,𝑞,{𝑎:𝑆′})
R-Fld
Γ⊢Replace(𝑆,𝑝,𝑞,𝑆′)
Γ⊢Replace({𝐴:𝑆..𝑈},𝑝,𝑞,{𝐴:𝑆′..𝑈})
R-Mem-L
Γ⊢Replace(𝑈,𝑝,𝑞,𝑈′)
Γ⊢Replace({𝐴:𝑆..𝑈},𝑝,𝑞,{𝐴:𝑆..𝑈′})
R-Mem-U
Γ⊢Replace(𝑆,𝑝,𝑞,𝑆′)
Γ⊢Replace(𝑆∧𝑈,𝑝,𝑞,𝑆′∧𝑈)
R-And-L
Γ⊢Replace(𝑈,𝑝,𝑞,𝑈′)
Γ⊢Replace(𝑆∧𝑈,𝑝,𝑞,𝑆∧𝑈′)
R-And-R
Changing a function domain also changes the binder context. Its replacement rule therefore checks the codomain in that new context:
Γ⊢Replace(𝑆,𝑝,𝑞,𝑆′)Γ,𝑥:𝑆′⊢𝑈𝗍𝗒𝗉𝖾
Γ⊢Replace(∀(𝑥:𝑆)𝑈,𝑝,𝑞,∀(𝑥:𝑆′)𝑈)
R-All-Dom
Γ,𝑥:𝑆⊢Replace(𝑈,𝑝,𝑞,𝑈′)𝑥∉fv(𝑝)∪fv(𝑞)
Γ⊢Replace(∀(𝑥:𝑆)𝑈,𝑝,𝑞,∀(𝑥:𝑆)𝑈′)
R-All-Cod
For a recursive self type the target recursive type must itself pass its formation rule; this is the recursive analogue of the codomain premise above:
There are no rules for ⊤ or ⊥: a derivation must select exactly one path-bearing leaf. Before a binder rule is used, alpha-renaming supplies its freshness premise. Thus the family neither replaces both branches nor crosses a binder for the root of 𝑝 or 𝑞.
If Γ⊢𝑝:𝑞.𝗍𝗒𝗉𝖾 and the target path is typeable, the two replacement rules are
Γ⊢𝑝:𝑞.𝗍𝗒𝗉𝖾Γ⊢𝑞𝗉𝖺𝗍𝗁Γ⊢Replace(𝑇,𝑝,𝑞,𝑈)
Γ⊢𝑇<:𝑈
Repl-pq
Γ⊢𝑝:𝑞.𝗍𝗒𝗉𝖾Γ⊢𝑝𝗉𝖺𝗍𝗁Γ⊢Replace(𝑇,𝑞,𝑝,𝑈)
Γ⊢𝑇<:𝑈
Repl-qp
Several occurrences are replaced by repeated subtyping steps. The operation is not capture-avoiding term substitution: it changes one stable path prefix and does not cross a binder that binds either prefix variable.
Proof of Proposition 108.4 — Indexed replacement yields subtyping
Proof. First prove that every derivation Γ⊢RPath(𝑟,𝑝,𝑞,𝑟′) contains a derivation of Γ⊢𝑞𝗉𝖺𝗍𝗁. Induction on RPath has two cases. Rule RP-Here supplies the required premise. Rule RP-Fld preserves the induction hypothesis while adding formation of 𝑟′.𝑎.
Now induct on the displayed Replace derivation. The leaf rules R-Sel and R-Sngl contain an RPath premise, so the preceding induction gives Γ⊢𝑞𝗉𝖺𝗍𝗁. Every structural replacement rule has exactly one premise; applying the induction hypothesis to that premise retains the same target root 𝑞. Thus every case supplies Γ⊢𝑞𝗉𝖺𝗍𝗁. Apply Repl-𝑝𝑞 to that path judgment, the assumed alias, and the assumed replacement derivation. ◻
Let 𝑇={𝑙𝑒𝑓𝑡:𝑝.𝗍𝗒𝗉𝖾}∧{𝑟𝑖𝑔ℎ𝑡:𝑝.𝗍𝗒𝗉𝖾} and assume Γ⊢𝑝:𝑞.𝗍𝗒𝗉𝖾 and Γ⊢𝑞.𝗍𝗒𝗉𝖾𝗍𝗒𝗉𝖾. One use of Repl-𝑝𝑞 may derive 𝑇<:{𝑙𝑒𝑓𝑡:𝑝.𝗍𝗒𝗉𝖾}∧{𝑟𝑖𝑔ℎ𝑡:𝑞.𝗍𝗒𝗉𝖾}. A second use replaces the remaining occurrence. The rule deliberately does not choose both occurrences at once, so induction on replacement has a single changed branch.
Proof of Lemma 108.5 — Replacement preserves formation
Proof. Induct on the indexed replacement derivation. In R-Sel and R-Sngl, the second premise is exactly the required formation derivation for the changed leaf. In R-Fld, inversion of source formation gives Γ⊢𝑆𝗍𝗒𝗉𝖾; the induction hypothesis gives formation of 𝑆′, and field formation rebuilds {𝑎:𝑆′}. Rules R-Mem-L and R-Mem-U apply that field-case schema to the lower and upper member bound, respectively. Rules R-And-L and R-And-R apply it to the left and right intersection component. In each instance, inversion forms the unchanged component and the induction hypothesis forms the changed component.
In R-All-Cod, inversion gives Γ,𝑥:𝑆⊢𝑈𝗍𝗒𝗉𝖾. Apply the induction hypothesis in that extended context and rebuild the function type. In R-All-Dom, the induction hypothesis forms 𝑆′ and the rule’s second premise forms 𝑈 under 𝑥:𝑆′; these are exactly the two premises of target function formation. In R-Rec, the displayed target-recursive-type premise is already the target formation judgment; it is necessary because changing a self type also changes the context in which its body is checked. Alpha-renaming validates the freshness premise before either binder case. These are all indexed replacement rules, so the induction is complete. ◻
Proof of Proposition 108.6 — Root formation does not form a changed leaf
Proof. Take Γ=𝑞:⊤,𝑝:𝑞.𝗍𝗒𝗉𝖾∧{𝑎:{𝐴:⊥..⊤}}. Then 𝑝.𝑎.𝐴 is formed, Γ⊢𝑝:𝑞.𝗍𝗒𝗉𝖾, and both 𝑝 and 𝑞 are paths. A root-only version of RP-Here, RP-Fld, and R-Sel would replace 𝑝.𝑎.𝐴 by 𝑞.𝑎.𝐴. But 𝑞:⊤ exposes no field 𝑎, so Γ⊬𝑞.𝑎𝗉𝖺𝗍𝗁 and 𝑞.𝑎.𝐴 is not formed. The actual RP-Fld stops because its second premise is unavailable; even if that premise were bypassed, R-Sel would still require formation of the complete changed selection. Thus both checks are load bearing. ◻
★★☆ Assume 𝑝:𝑞.𝗍𝗒𝗉𝖾. Starting from {𝑎:𝑝.𝐴}∧{𝑏:𝑝.𝐴}, derive a subtype in which both occurrences are 𝑞.𝐴. Give the two intermediate types and the occurrence changed at each step. State the path-formation premise required for 𝑞.𝐴.
A nested object is reached by an outer path before its own self variable can be exported. Introducing an unrelated variable for the nested receiver loses that identity. pDOT instead checks the nested body after replacing its self with the enclosing field path.
The judgment 𝑝;Γ⊢𝑑:𝑇 reads: definition 𝑑 belongs to the object whose stable identity is 𝑝. Its Def-New rule is
𝑝.𝑎;Γ⊢𝑑[𝑝.𝑎/𝑦]:𝑇[𝑝.𝑎/𝑦]TightRecord(𝑇)
𝑝;Γ⊢{𝑎=𝜈(𝑦:𝑇)𝑑}:{𝑎:𝜇(𝑦:𝑇)}
Def-New
The rule has no separate premise Γ⊢𝑝.𝑎𝗉𝖺𝗍𝗁. Instead, the body premise is an intrinsically well-formed definition judgment: every path in 𝑑[𝑝.𝑎/𝑦] and 𝑇[𝑝.𝑎/𝑦] must pass the path-formation rules before that premise can be derived. The side predicate TightRecord(𝑇) requires distinct labels and equal bounds for every type member. The body judgment and tightness have different jobs: the former prevents dangling receiver paths, while the latter prevents bad bounds.
In a root object at path 𝑟, a nested palette definition is checked with self 𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒, not with an abstract 𝑠 unrelated to the outer object. Consequently the stored field 𝑧𝑒𝑟𝑜=𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝑧𝑒𝑟𝑜 receives the singleton type (𝑟.𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝑧𝑒𝑟𝑜).𝗍𝗒𝗉𝖾 by Def-Path. The type remains meaningful after the nested object is inserted because every prefix is rooted at 𝑟.
Tightness alone does not make an instance of Def-New derivable. If a substituted occurrence of the inner self is rooted at an untypeable path, the body premise has no derivation.
Proof of Proposition 108.8 — The body premise checks the installed receiver
Proof. Take Γ=𝑝:⊤ and 𝑇={𝑏:𝑦.𝗍𝗒𝗉𝖾}. The record 𝑇 is tight: it has one field and no unequal type-member bounds. Substitution gives 𝑇[𝑝.𝑎/𝑦]={𝑏:𝑝.𝑎.𝗍𝗒𝗉𝖾}. Rule P-Fld cannot derive Γ⊢𝑝.𝑎𝗉𝖺𝗍𝗁 because 𝑝 has no field declaration for 𝑎. Therefore 𝑝.𝑎.𝗍𝗒𝗉𝖾 is not formed, so no judgment 𝑝.𝑎;Γ⊢𝑑[𝑝.𝑎/𝑦]:𝑇[𝑝.𝑎/𝑦] can be derived for any 𝑑. Rule Def-New rejects the instance through its body premise, without an extra explicit premise. ◻
This proposition is a premise-formation boundary, not a closed type-safety counterexample. Adding an explicit path premise would be a sound local strengthening, but it would be a rule delta rather than the pDOT rule and is not needed by the safety proof below.
Family polymorphism in full
The three devices now available—path selection, singleton types, and path-indexed definition typing—were introduced one obstruction at a time. Family polymorphism uses all three at once, so it is the right test of whether the calculus does what the opening promised.
The requirement is this. A tree object carries an abstract node type; two tree objects have unrelated node types, so a node of one tree may not be added to the other; and an alias of a tree has the same node type as the tree it aliases. Encode the family as 𝖳𝗋𝖾𝖾:=𝜇(𝑡:{𝑁𝑜𝑑𝑒:⊥..⊤}∧{𝑟𝑜𝑜𝑡:𝑡.𝑁𝑜𝑑𝑒}∧{𝑎𝑑𝑑:∀(𝑛:𝑡.𝑁𝑜𝑑𝑒)⊤}). For 𝑡:𝖳𝗋𝖾𝖾, recursion elimination gives 𝑡.𝑎𝑑𝑑:∀(𝑛:𝑡.𝑁𝑜𝑑𝑒)⊤ and 𝑡.𝑟𝑜𝑜𝑡:𝑡.𝑁𝑜𝑑𝑒, so a client may add a tree’s own root to that tree: 𝑡:𝖳𝗋𝖾𝖾⊢𝑡.𝑎𝑑𝑑(𝑡.𝑟𝑜𝑜𝑡):⊤. Now take two trees and an alias: Γ:=apple:𝖳𝗋𝖾𝖾,orange:𝖳𝗋𝖾𝖾,alias:orange.𝗍𝗒𝗉𝖾. Three judgments separate the three cases, and each uses a different rule.
Rejection across families. The application apple.𝑎𝑑𝑑(orange.𝑟𝑜𝑜𝑡) requires Γ⊢orange.𝑟𝑜𝑜𝑡:apple.𝑁𝑜𝑑𝑒. Precise typing instead gives orange.𝑟𝑜𝑜𝑡:orange.𝑁𝑜𝑑𝑒. At the declaration {𝑁𝑜𝑑𝑒:⊥..⊤}, rules DOT-Sel-L and DOT-Sel-U give only ⊥<:orange.𝑁𝑜𝑑𝑒<:⊤. Neither bound relates orange.𝑁𝑜𝑑𝑒 to apple.𝑁𝑜𝑑𝑒, because the two selections have different prefixes and no rule identifies distinct paths. The application does not type.
Acceptance within a family. The application apple.𝑎𝑑𝑑(apple.𝑟𝑜𝑜𝑡) types by the displayed judgment, with DOT-Fld-E supplying the argument and DOT-All-E substituting it.
Acceptance through an alias. The application apple.𝑎𝑑𝑑(alias.𝑟𝑜𝑜𝑡) also fails, but orange.𝑎𝑑𝑑(alias.𝑟𝑜𝑜𝑡) succeeds, and this is where singletons do the work that neither DOT nor a field type can do. From alias:orange.𝗍𝗒𝗉𝖾, rule Sngl-E gives alias.𝑟𝑜𝑜𝑡:(orange.𝑟𝑜𝑜𝑡).𝗍𝗒𝗉𝖾, and Sngl-Trans then transports the type of orange.𝑟𝑜𝑜𝑡: Γ⊢alias.𝑟𝑜𝑜𝑡:orange.𝑁𝑜𝑑𝑒. That is exactly the argument type orange.𝑎𝑑𝑑 demands. A field declaration {𝑎𝑙𝑖𝑎𝑠:𝖳𝗋𝖾𝖾} would have given alias.𝑟𝑜𝑜𝑡:alias.𝑁𝑜𝑑𝑒 and lost the identity, so the alias case is the one that forces singleton path types into the calculus.
★★☆ Extend 𝖳𝗋𝖾𝖾 with a nested 𝑝𝑎𝑙𝑒𝑡𝑡𝑒 whose 𝐶𝑜𝑙𝑜𝑟 member is selected through two fields, as in section 108.1. Derive the type selected by apple.𝑝𝑎𝑙𝑒𝑡𝑡𝑒, and state its two path-formation premises. Identify also the 𝐶𝑜𝑙𝑜𝑟 types selected through the alias and orange palettes. Prove that they agree, naming each use of Sngl-E and the final replacement rule.
Field paths are normal forms, so ordinary progress may stop before reaching a lambda or object. The runtime environment 𝛾 stores values at root variables. Path lookup follows immutable fields:
𝛾(𝑥)=𝑣
Lookup𝛾(𝑥)=𝑣
Lookup-Var
Lookup𝛾(𝑝)=𝜈(𝑧:𝑇)(𝑑1∧{𝑎=𝑠}∧𝑑2)
Lookup𝛾(𝑝.𝑎)=𝑠[𝑝/𝑧]
Lookup-Val
Lookup𝛾(𝑝)=𝑞
Lookup𝛾(𝑝.𝑎)=𝑞.𝑎
Lookup-Path
The displayed Lookup𝛾 is path lookup. It is not term reduction: a term step changes a configuration 𝛾∣𝑡, whereas lookup follows a stored path without reducing the surrounding term.
For 𝛾(𝑥)=𝜈(𝑥:{𝑎:𝑦.𝑏.𝗍𝗒𝗉𝖾}){𝑎=𝑦.𝑏},𝛾(𝑦)=𝜈(𝑦′:𝑇𝑦){𝑏=𝜈(𝑦″:𝑇𝑏){𝑐=𝜆(𝑧:⊤).𝑧}}, lookup computes Lookup𝛾(𝑥.𝑎)=𝑦.𝑏,Lookup𝛾(𝑥.𝑎.𝑐)=𝜆(𝑧:⊤).𝑧. The middle result is a path, so a semantics that projected only one field would stop too early.
Proof of Lemma 108.9 — Typed function paths terminate in lambdas
Proof. Apply the inherited general-to-tight bridge in the inert context. Precise path typing decomposes the stored root type and follows each field selected by 𝑝; singleton propagation replaces aliases only after the target prefix is known to be typeable. Induct on the length of 𝑝 and, inside that induction, on its precise typing derivation. For a variable path 𝑥, environment matching gives 𝛾(𝑥)=𝑣 with Γ⊢𝑣:Γ(𝑥). Function canonical forms exclude an object value at the required dependent-function type, so 𝑣=𝜆(𝑥:𝑆′)𝑡 with 𝑆<:𝑆′ and Γ,𝑥:𝑆⊢𝑡:𝑇.
For 𝑞.𝑎, invert path formation to obtain a precise field declaration for 𝑎 at 𝑞. By the outer induction hypothesis, lookup of 𝑞 terminates. If it terminates in an object, Lookup-Val selects the unique field body; precise object typing gives its declared type after self substitution. If it terminates in a path 𝑟, Lookup-Path replaces the prefix by 𝑟; the replacement rules preserve the selected field type, and the strict lookup chain has one fewer unresolved stored alias. Applying the inner induction to the resulting precise derivation eventually reaches a value. Function canonical forms again identify that value as a lambda and give the two stated typing comparisons. These are the variable, stored-value, and stored-path cases displayed above, so the induction is complete.
Termination is the part that DOT did not have to prove, because there lookup was a single variable dereference. Here it is recursive, and an infinite loop would be a hidden violation of progress. The decisive argument excludes a cycle in the typing context: if the direct lookup of 𝑝 yields another path 𝑞, then the context assigns 𝑝 the singleton type 𝑞.𝗍𝗒𝗉𝖾; so a cycle of paths in the execution environment would force a cycle of paths in the typing context, all of whose members carry only singleton types, and none of which can then carry a dependent-function type. The well-formedness hypothesis is what rules that out, and it is why the lemma is stated for well-formed inert contexts and not merely inert ones. ◻
Removing the function type permits a cycle such as 𝑥.𝑎=𝑦.𝑏 and 𝑦.𝑏=𝑥.𝑎; lookup may then diverge. The lemma does not claim termination for every well-typed path.
If ⋅⊢𝑡:𝑇, then either term reduction diverges, or ⋅∣𝑡 reduces to 𝛾∣𝑠 where 𝑠 is a path or value and some inert, well-formed Γ satisfies 𝛾:Γ and Γ⊢𝑠:𝑇.
If term reduction and path lookup are combined into extended reduction, then a closed well-typed term either has an infinite extended reduction or reaches a value.
If 𝛾:Γ with Γ inert and well-formed, Γ⊢𝑡:𝑇, and 𝛾∣𝑡 steps to 𝛾′∣𝑡′, then some inert, well-formed Γ′ satisfies 𝛾′:Γ′ and Γ′⊢𝑡′:𝑇.
Proof. We prove preservation first, strengthening it by simultaneous preservation of environment matching, context inertness, and path well-formedness. Induct on the term step. A beta root uses capture-avoiding substitution. A field root uses precise object typing to locate the unique field and substitutes the stable self path into its body. Installing a let-bound value extends 𝛾 and Γ together; precise typing of the value makes the new entry inert, and freshness of the bound variable preserves well-formedness. A singleton replacement step applies proposition 108.4; its target-path formation premise preserves every selected type. Congruence cases use the induction hypothesis and rebuild the surrounding evaluation context. These are the beta, object, let, replacement, and congruence families of term reduction, which proves clause 3.
For progress, decompose a term under a matching inert context by its longest evaluation context. A function application whose head is a value contracts by beta. If its head is a path, lemma 108.9 either exposes a lambda through lookup or takes a strict lookup step. Object projection uses the corresponding precise object canonical form; a singleton path uses replacement only after the changed path has been formed. A let either reduces its bound expression or installs its value. Constructors are values, and no other outer term form exists. Thus a typed configuration either takes a term step or its term is a path or value.
Start with the empty execution environment and empty context; both matching and inertness hold. Iterate preservation along any finite term reduction. If term reduction is infinite, clause 1 holds by its first alternative. Otherwise progress gives a terminal path or value, and the last preservation instance supplies the inert, well-formed context asserted in clause 1.
For clause 2, extend progress by treating a terminal path as the head of lookup. A path of function type terminates by lemma 108.9; precise canonical forms give the same termination fact for object and field paths. Each lookup result is either a value or a strictly shorter unresolved alias chain. Cycles cannot have a non-singleton terminal type in a well-formed context, by the cycle argument in the preceding lemma. Hence extended reduction either continues forever or reaches a value. This proves all three clauses. ◻
★★☆ Using the displayed environment, list every lookup judgment from 𝑥.𝑎.𝑐 to the lambda. Then type the application (𝑥.𝑎.𝑐)𝑦 under a declaration 𝑦:⊤ and identify the exact point where lemma 108.9 supplies progress.
The theorem proves declarative type safety for stable immutable paths, singleton types, path replacement, precise self typing, and the calculus’s initialization restrictions. It does not prove decidable pDOT typing, subtyping, or inference. No algorithmic judgment, termination measure, or completeness theorem has been introduced here.
pDOT is not full Scala. Mutation can invalidate a stable path; unrestricted initialization can expose a path before its field exists; implicits and Scala’s other path features add elaboration questions; none belongs to theorem 108.10. The nested-module derivation proves only that this immutable core can name and preserve the selected family relationship.
Suggested first pass.
None of these problems is a prerequisite for a later chapter. Begin with exercise 108.5, then complete exercise 108.9.
★★☆ Construct a root object with fields 𝑝𝑎𝑙𝑒𝑡𝑡𝑒, 𝑎𝑙𝑖𝑎𝑠, and 𝑑𝑒𝑓𝑎𝑢𝑙𝑡 such that 𝑎𝑙𝑖𝑎𝑠:𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝗍𝗒𝗉𝖾 and 𝑑𝑒𝑓𝑎𝑢𝑙𝑡:𝑎𝑙𝑖𝑎𝑠.𝐶𝑜𝑙𝑜𝑟. Derive 𝑑𝑒𝑓𝑎𝑢𝑙𝑡:𝑝𝑎𝑙𝑒𝑡𝑡𝑒.𝐶𝑜𝑙𝑜𝑟 using singleton propagation.
★★☆ Reconstruct proposition 108.6. Derive formation of 𝑝.𝑎.𝐴, then show separately why RP-Fld cannot form 𝑞.𝑎 and why R-Sel cannot form 𝑞.𝑎.𝐴. State the exact false premise in each rule.
★★★ Reconstruct the preservation case for installing a nested object. State the outer path, the substituted inner self path, the distinct-label premise, and the path-well-formedness facts added to the runtime context.
★★★Practical project.pdot-stable-path-lookup Implement in Kappa a finite immutable object environment, stable paths, singleton aliases, and bounded path lookup. The invariant is that every selected prefix names a declared field; after replacement, the checker must validate the complete changed path rather than only its root. It must reject both an untypeable target root and x.a.Missing -> y.b.Missing, where y.b itself is declared. On the environment in section 108.5, the program must print that x.a.c reaches a lambda, confirm the alias replacement for two occurrences, reject the naive missing-field example, and report a cycle for mutually recursive aliases. The complete eight-line oracle also distinguishes undeclared lookup from fuel exhaustion. A mutation that deletes the changed-leaf formation test must typecheck and fail the test suite.
Sources. Rapoport and Lhoták state pDOT safety in Theorems 5.1–5.2 and progress, preservation, and function-path lookup in Lemmas 5.3–5.5 on printed pp. 145:18–21 [RL19]. Their examples on printed pp. 145:2 and 145:4 motivate the family and alias constraints. The proof also uses inert-context staging from Rapoport, Kabir, He, and Lhoták [RKHL17]. Each cited package is shorter than ten pages, and its needed argument is proved locally.