Lectures onType Theory
Chapter 108
Chapter 108Optional

Fully Path-Dependent Types: Stable Paths, Singletons, and Modules

Prerequisites. Direct starred prerequisites: Chapter 45. No later core chapter depends on this route.

Let r denote a root module with a field palette, and let that palette contain an abstract type member Color. A method whose argument belongs to the root’s palette needs the type r.palette.Color. The variable-path rules of chapter 45 form only x.A; they reject the selection because r.palette 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.

Stable paths and a nested module

Definition 108.1 — Stable path

A stable path is generated by p,q::=xp.a. 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 Γppath is derived only when every selected prefix has a field type:

x:TΓ
Γxpath
P-Var
Γp:{a:T}
Γp.apath
P-Fld

Type selection is extended from x.A to p.A only under Γppath.

Define Palette:=μ(s:{Color:..}{zero:s.Color}),Root:=μ(r:{palette:Palette}{default:r.palette.Color}). For r:Root, recursion elimination and field elimination give Γr.palette:Palette,Γr.palettepath. Path-dependent selection therefore forms r.palette.Color, and the second field has type Γr.default:r.palette.Color. The variable-only calculus stops after the first judgment because its selection grammar has no case for a field path.

Exercise 108.1

★☆☆ For r:Root, write the complete derivation of r.default:r.palette.Color. Include recursion elimination, both path formation premises, field elimination for palette, formation of r.palette.Color, and field elimination for default.

Singleton paths and replacement

The path r.alias may store r.palette. 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 p;Γd:T, read: the definition d belongs to the object whose stable identity is p. Definition 108.7 gives the rule that makes this necessary; here it only records where a stored path is installed.

Definition 108.2 — Singleton path type

For a typeable path q, the singleton path type q.type contains paths that denote the same run-time object as q. Path definition and alias propagation are governed by

Γqpath
p;Γ{a=q}:{a:q.type}
Def-Path
Γp:q.typeΓq:T
Γp:T
Sngl-Trans
Γp:q.typeΓq.apath
Γp.a:q.a.type
Sngl-E

The prefix premise in Sngl-E requires the target field path q.a to be well formed before aliasing extends through a.

If r.alias:r.palette.type, then Sngl-E derives r.alias.zero:(r.palette.zero).type, because r.palette.zero is a typeable path. Rule Sngl-Trans then gives r.alias.zero:r.palette.Color from r.palette.zero:r.palette.Color. The derivation transports a type along a run-time alias; it does not assert judgmental equality of the two path expressions.

Definition 108.3 — One-occurrence path replacement

The judgment ΓReplace(T,p,q,U) holds when U is obtained from T by replacing exactly one occurrence whose prefix is p by the corresponding occurrence with prefix q, 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:

Γqpath
ΓRPath(p,p,q,q)
RP-Here
ΓRPath(r,p,q,r)Γr.apath
ΓRPath(r.a,p,q,r.a)
RP-Fld

Thus an induction on RPath proves Γrpath. The two path-bearing type leaves require formation of the complete changed type, not merely of the root q:

ΓRPath(r,p,q,r)Γr.Atype
ΓReplace(r.A,p,q,r.A)
R-Sel
ΓRPath(r,p,q,r)Γr.typetype
ΓReplace(r.type,p,q,r.type)
R-Sngl

Every remaining rule descends through exactly one type component:

ΓReplace(S,p,q,S)
ΓReplace({a:S},p,q,{a:S})
R-Fld
ΓReplace(S,p,q,S)
ΓReplace({A:S..U},p,q,{A:S..U})
R-Mem-L
ΓReplace(U,p,q,U)
ΓReplace({A:S..U},p,q,{A:S..U})
R-Mem-U
ΓReplace(S,p,q,S)
ΓReplace(SU,p,q,SU)
R-And-L
ΓReplace(U,p,q,U)
ΓReplace(SU,p,q,SU)
R-And-R

Changing a function domain also changes the binder context. Its replacement rule therefore checks the codomain in that new context:

ΓReplace(S,p,q,S)Γ,x:SUtype
ΓReplace((x:S)U,p,q,(x:S)U)
R-All-Dom
Γ,x:SReplace(U,p,q,U)xfv(p)fv(q)
ΓReplace((x:S)U,p,q,(x:S)U)
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:

Γ,x:μ(x:T)Replace(T,p,q,T)Γμ(x:T)typexfv(p)fv(q)
ΓReplace(μ(x:T),p,q,μ(x:T))
R-Rec

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 p or q.

If Γp:q.type and the target path is typeable, the two replacement rules are

Γp:q.typeΓqpathΓReplace(T,p,q,U)
ΓT<:U
Repl-pq
Γp:q.typeΓppathΓReplace(T,q,p,U)
ΓT<:U
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.

Proposition 108.4 — Indexed replacement yields subtyping

If Γp:q.type and ΓReplace(T,p,q,U), then the exact pDOT rules derive ΓT<:U.

Proof of Proposition 108.4 — Indexed replacement yields subtyping

Proof. First prove that every derivation ΓRPath(r,p,q,r) contains a derivation of Γqpath. Induction on RPath has two cases. Rule RP-Here supplies the required premise. Rule RP-Fld preserves the induction hypothesis while adding formation of r.a.

Now induct on the displayed Replace derivation. The leaf rules R-Sel and R-Sngl contain an RPath premise, so the preceding induction gives Γqpath. Every structural replacement rule has exactly one premise; applying the induction hypothesis to that premise retains the same target root q. Thus every case supplies Γqpath. Apply Repl-pq to that path judgment, the assumed alias, and the assumed replacement derivation. ◻

Let T={left:p.type}{right:p.type} and assume Γp:q.type and Γq.typetype. One use of Repl-pq may derive T<:{left:p.type}{right:q.type}. 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.

Lemma 108.5 — Replacement preserves formation

Let Γp:q.type, Γqpath, and ΓReplace(T,p,q,U). If ΓTtype, then ΓUtype.

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 ΓStype; the induction hypothesis gives formation of S, and field formation rebuilds {a:S}. 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 Γ,x:SUtype. Apply the induction hypothesis in that extended context and rebuild the function type. In R-All-Dom, the induction hypothesis forms S and the rule’s second premise forms U under x:S; 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. ◻

Proposition 108.6 — Root formation does not form a changed leaf

Replacing a typeable root by another typeable root does not suffice to form the retained suffix of a selected type.

Proof of Proposition 108.6 — Root formation does not form a changed leaf

Proof. Take Γ=q:,p:q.type{a:{A:..}}. Then p.a.A is formed, Γp:q.type, and both p and q are paths. A root-only version of RP-Here, RP-Fld, and R-Sel would replace p.a.A by q.a.A. But q: exposes no field a, so Γq.apath and q.a.A 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. ◻

Exercise 108.2

★★☆ Assume p:q.type. Starting from {a:p.A}{b:p.A}, derive a subtype in which both occurrences are q.A. Give the two intermediate types and the occurrence changed at each step. State the path-formation premise required for q.A.

Precise self and safe initialization

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.

Definition 108.7 — Path-indexed definition typing

The judgment p;Γd:T reads: definition d belongs to the object whose stable identity is p. Its Def-New rule is

p.a;Γd[p.a/y]:T[p.a/y]TightRecord(T)
p;Γ{a=ν(y:T)d}:{a:μ(y:T)}
Def-New

The rule has no separate premise Γp.apath. Instead, the body premise is an intrinsically well-formed definition judgment: every path in d[p.a/y] and T[p.a/y] must pass the path-formation rules before that premise can be derived. The side predicate TightRecord(T) 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 r, a nested palette definition is checked with self r.palette, not with an abstract s unrelated to the outer object. Consequently the stored field zero=r.palette.zero receives the singleton type (r.palette.zero).type by Def-Path. The type remains meaningful after the nested object is inserted because every prefix is rooted at r.

Proposition 108.8 — The body premise checks the installed receiver

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 Γ=p: and T={b:y.type}. The record T is tight: it has one field and no unequal type-member bounds. Substitution gives T[p.a/y]={b:p.a.type}. Rule P-Fld cannot derive Γp.apath because p has no field declaration for a. Therefore p.a.type is not formed, so no judgment p.a;Γd[p.a/y]:T[p.a/y] can be derived for any d. 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 Tree:=μ(t:{Node:..}{root:t.Node}{add:(n:t.Node)}). For t:Tree, recursion elimination gives t.add:(n:t.Node) and t.root:t.Node, so a client may add a tree’s own root to that tree: t:Tree  t.add(t.root):. Now take two trees and an alias: Γ:=apple:Tree,  orange:Tree,  alias:orange.type. Three judgments separate the three cases, and each uses a different rule.

Rejection across families. The application apple.add(orange.root) requires Γorange.root:apple.Node. Precise typing instead gives orange.root:orange.Node. At the declaration {Node:..}, rules DOT-Sel-L and DOT-Sel-U give only <:orange.Node<:. Neither bound relates orange.Node to apple.Node, 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.add(apple.root) 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.add(alias.root) also fails, but orange.add(alias.root) succeeds, and this is where singletons do the work that neither DOT nor a field type can do. From alias:orange.type, rule Sngl-E gives alias.root:(orange.root).type, and Sngl-Trans then transports the type of orange.root: Γalias.root:orange.Node. That is exactly the argument type orange.add demands. A field declaration {alias:Tree} would have given alias.root:alias.Node and lost the identity, so the alias case is the one that forces singleton path types into the calculus.

Exercise 108.3

★★☆ Extend Tree with a nested palette whose Color member is selected through two fields, as in section 108.1. Derive the type selected by apple.palette, and state its two path-formation premises. Identify also the Color types selected through the alias and orange palettes. Prove that they agree, naming each use of Sngl-E and the final replacement rule.

Lookup and the stronger safety statement

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:

γ(x)=v
Lookupγ(x)=v
Lookup-Var
Lookupγ(p)=ν(z:T)(d1{a=s}d2)
Lookupγ(p.a)=s[p/z]
Lookup-Val
Lookupγ(p)=q
Lookupγ(p.a)=q.a
Lookup-Path

The displayed Lookupγ is path lookup. It is not term reduction: a term step changes a configuration γt, whereas lookup follows a stored path without reducing the surrounding term.

For γ(x)=ν(x:{a:y.b.type}){a=y.b},γ(y)=ν(y:Ty){b=ν(y:Tb){c=λ(z:).z}}, lookup computes Lookupγ(x.a)=y.b,Lookupγ(x.a.c)=λ(z:).z. The middle result is a path, so a semantics that projected only one field would stop too early.

Lemma 108.9 — Typed function paths terminate in lambdas

Let γ match an inert, well-formed context Γ. If Γp:(x:S)T, then lookup from p terminates in a value λ(x:S)t with ΓS<:S and Γ,x:St:T.

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 p; singleton propagation replaces aliases only after the target prefix is known to be typeable. Induct on the length of p and, inside that induction, on its precise typing derivation. For a variable path x, environment matching gives γ(x)=v with Γv:Γ(x). Function canonical forms exclude an object value at the required dependent-function type, so v=λ(x:S)t with S<:S and Γ,x:St:T.

For q.a, invert path formation to obtain a precise field declaration for a at q. By the outer induction hypothesis, lookup of q 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 r, Lookup-Path replaces the prefix by r; 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 p yields another path q, then the context assigns p the singleton type q.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 x.a=y.b and y.b=x.a; lookup may then diverge. The lemma does not claim termination for every well-typed path.

Theorem 108.10 — pDOT type safety

For the DOT rules of definition 107.2 extended by definition 108.1, definition 108.2, definition 108.3, definition 108.7 and the lookup relation displayed in section 108.5:

  1. If t:T, then either term reduction diverges, or t reduces to γs where s is a path or value and some inert, well-formed Γ satisfies γ:Γ and Γs:T.

  2. 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.

  3. If γ:Γ with Γ inert and well-formed, Γt:T, and γt steps to γt, then some inert, well-formed Γ satisfies γ:Γ and Γt:T.

Proof of Theorem 108.10 — pDOT type safety

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. ◻

Exercise 108.4

★★☆ Using the displayed environment, list every lookup judgment from x.a.c to the lambda. Then type the application (x.a.c)y under a declaration y: and identify the exact point where lemma 108.9 supplies progress.

The algorithmic and language boundary

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.

Exercise 108.5

★☆☆ Decide which expressions are stable paths: x, x.a.b, (λy.y)x, ν(x:T)d, and let y=x.a in y.b. For each rejection, name the absent grammar clause.

Exercise 108.6

★★☆ Construct a root object with fields palette, alias, and default such that alias:palette.type and default:alias.Color. Derive default:palette.Color using singleton propagation.

Exercise 108.7

★★☆ Reconstruct proposition 108.6. Derive formation of p.a.A, then show separately why RP-Fld cannot form q.a and why R-Sel cannot form q.a.A. State the exact false premise in each rule.

Exercise 108.8

★★★ 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.

Exercise 108.9

★★★ 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.

Search the book

Type to search the local edition.