You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Corrected twice. (1) The original claim that a field access rooted in a bare symbol reproduces the five-liner's ambiguity is wrong — see "Correction" below. (2) Option 3 is now struck; it contradicts adt.md. Both are left on the page rather than deleted.
What the goal actually is
Not "avoid merging the two objects" — objects never merge. The destination's occupant is destroyed by the overwrite and the source's object survives at the destination location; that part is ordinary.
The thing to avoid is two live anchor identities resolving to the same payload. A payload carries a single four-byte backpointer, so it can be found from only one cell; a second cell naming it cannot be updated on the next relocation. Forwarding anchors are the workaround for exactly that.
The situation arises only when a move has anchored objects on both sides. An anchored source alone is fine (one cell, rewrite it). An anchored destination alone is fine. Any rule that removes either side is sufficient.
Canonical example
Every option below is described against this program. All four statements are legal under the current spec.
package Demo
type Engine = #struct { power Int; }
type Car = #struct { engine Engine; }
type Garage = #struct { car Car; }
Engine(power Int) => init{power}
Car(engine Engine) => init{engine}
Garage(car Car) => init{car}
Unit demo() {
garage Garage(Car(Engine(Int(1)))) // garage.car hosts parkedCar
// parkedCar.engine hosts parkedEngine
guestToParked &Engine = garage.car.engine // mints parkedAnchor, naming parkedEngine
{
arriving Car(Engine(Int(2))) // arriving hosts a Car
// its .engine hosts arrivingEngine
guestToArriving &Engine = arriving.engine // mints arrivingAnchor,
// naming arrivingEngine
garage.car = arriving // ← the move
}
return Unit()
}
Naming: parked* is everything already in the garage — the destination side. arriving* is everything in the local being moved in — the source side.
What the move does:
The parked Car is destroyed, and parkedEngine with it.
The arriving Car lands in garage.car, so arrivingEngine is now at garage.car.engine.
guestToParked follows the slot (§2.2), so it must now observe arrivingEngine.
guestToArriving follows the object, so it also observes arrivingEngine.
Both parkedAnchor and arrivingAnchor now resolve to one payload, which has one backpointer. That is the merge, and it is why forwarding exists: arrivingAnchor becomes a forwarding cell targeting parkedAnchor, which stays terminal and owns the backpointer.
option
effect on the example
where the error lands
current
merge; arrivingAnchor forwards to parkedAnchor
legal
1 declare
arriving must be transit to be moved, so guestToArriving is rejected
at the guest
2 infer
guestToArriving pins arriving, so garage.car = arriving is rejected
at the move
3 structure
both guests rejected
struck — see below
4 siblings
merge allowed; the two anchors are ring-linked, both terminal
legal
5 accept
merge allowed; unchanged from current
legal
Options 1 and 2 differ mainly in where you find out: option 1 forces the choice at the declaration, option 2 lets both be written and rejects the second.
Correction: there is no ambiguity gap
The original version of this issue claimed these lines reproduce the problem memory.md §2.8.1 solves:
g &Item = box.item
other = box
They do not. lifetimes.md §1.8 makes a moved-from symbol downgrade to a guest to the same object rather than becoming empty, so box.item still denotes the same field afterwards and both readings coincide.
A field cannot be moved out of (§1.2) and travels with its object when rehosted, so a field access denotes the same storage regardless of what happens to the names above it. A bare symbol is the only place where a name and its object can part company, which is what makes §2.8.1 specific rather than an instance of something broader.
1. Declare it
Split local hosts at the declaration site into a stable storage point (may be a guest root, may not be moved) and a package in transit (may be moved or swallowed, may not be pointed at).
Open question — polarity. Marking the transit kind puts the mark on the common case and gives bare T a third position-dependent meaning. Marking the stable kind keeps bare T meaning "ordinary, in transit" for locals and parameters alike. Sigil is an open cost; may want a keyword.
Objection: building and shipping are not separable activities, and this forces a declaration-time choice between them.
2. Infer it
Minting a guest from a chain rooted at a symbol stops that symbol being a move-source.
Statically checkable without whole-program analysis: §1.3 confines moves of a symbol to its declaration block, and minting from sym.… requires sym in scope. Conservative reading (pinned if minted anywhere in the block) keeps it flow-insensitive.
Cost: borrow-checker-shaped — "once pointed at, no longer movable".
Cost: visible in APIs. Handing out an & to a field means the owner can never be moved.
Recursive structures remain buildable; minting from tree.root merely pins tree.
3. Structure it — struck: contradicts adt.md
The proposal was to require a guest to live strictly below its host's scope, which is the tightening of §1.1 from "the same or a higher lexical scope" to "strictly higher".
It would deliver the guarantee, and — contrary to an earlier draft of this issue — it would cover field-held guests too, since §1.1 already reasons about the scope of the & itself rather than only about local symbols.
It fails for a different reason. Banning an & field from naming a host in its own scope makes recursive types unbuildable: a recursive #variant boxes its recursive case through &, and the child's & field and the host it names are inside the same structure and therefore the same scope. Parent back-pointers and sibling wiring go the same way. This is a contradiction with adt.md, not a cost to weigh.
4. Sibling-linked anchors (runtime only)
Let both anchors stay terminal and link the cells to each other, so a relocation walks the ring and rewrites every member. O(identities merged) per move, always-direct reads, no source-language change.
Against: path compression already makes forwarding reads O(1) in practice, so this pays real move cost for a largely theoretical read cost; and retirement needs unlinking rather than a pop.
5. Accept merging, and make a moved-from symbol spent — maintainer's preferred direction
Keep the merge machinery; simplify the source language instead.
Revert §2.8.1. A bare symbol is a guest source again.
Change §1.8. A moved-from symbol is spent — no longer readable — rather than downgrading to a guest.
Remove 'T. With bare symbols pointable again, the borrow mode's motivating hole is gone.
Why this resolves the original five-liner. The story's argument was that guest &Player = main admits two readings. Making the moved-from symbol spent removes one of them: there is no "watch that variable" once the variable is finished. What remains is a single teachable rule — a guest follows the object; when the host symbol is moved from it is spent, and the guest carries on — with no competing referent.
What it simplifies.
Reference-type parameters return to two modes, T and &T.
The subject regains a single mode: bare this T is an implicit guest, and this &T becomes redundant.
§1.8 loses a concept (downgrade) and memory.md §2.4 loses a storage state (a host-capable slot readable in guest state).
The retired-forms guard in CLAUDE.md needs '[A-Z] restored and the borrow-exclusion clause in §2.8 removed.
Detail that must be stated explicitly: spent ≠ unassignable. The slot still exists and may be overwritten to host again (§2.2). The relay example at §1.8 survives on that basis, but its prose must change from "downgrades to a guest" to "is spent", and any example that reads a moved-from symbol needs rewriting.
What it costs.
Merging stays reachable, so forwarding anchors, retirement stacks and the target-kind discriminator are permanent. Options 1 and 2 become moot.
Signatures lose the non-escaping promise. With only &T, a caller cannot tell whether a callee retains a reference. Still safe under §1.1, but less legible. Worth considering keeping 'T for this reason alone, independent of the hole it was created to fill.
Rationale for preferring it: the merge machinery is implemented once by the compiler author; the §2.8.1 restriction is paid by every program that builds a structure and hands out a reference to it.
Disposition
With option 3 struck, the real choice is 1 or 2 — prevent the merge, delete the machinery, pay in language restriction — versus 5, accept the merge, keep the machinery, simpler language. Option 4 is a runtime-only variation of 5.
Nothing here is a defect in #151, which is merged. Option 5 reverts most of its language changes while keeping the anchor and arena work inherited from #147, and keeping the receiver → subject rename, which is independent.
Reverting a merged rule needs its own chapter in stories/memory.md saying plainly that the ban was the wrong instrument and why. "The slot that could not be pointed at" stays as written; the new chapter names its claim and supersedes it. naming-terms.md §6 also puts the rejected candidates above on the record.
What the goal actually is
Not "avoid merging the two objects" — objects never merge. The destination's occupant is destroyed by the overwrite and the source's object survives at the destination location; that part is ordinary.
The thing to avoid is two live anchor identities resolving to the same payload. A payload carries a single four-byte backpointer, so it can be found from only one cell; a second cell naming it cannot be updated on the next relocation. Forwarding anchors are the workaround for exactly that.
The situation arises only when a move has anchored objects on both sides. An anchored source alone is fine (one cell, rewrite it). An anchored destination alone is fine. Any rule that removes either side is sufficient.
Canonical example
Every option below is described against this program. All four statements are legal under the current spec.
Naming:
parked*is everything already in the garage — the destination side.arriving*is everything in the local being moved in — the source side.What the move does:
parkedEnginewith it.garage.car, soarrivingEngineis now atgarage.car.engine.guestToParkedfollows the slot (§2.2), so it must now observearrivingEngine.guestToArrivingfollows the object, so it also observesarrivingEngine.Both
parkedAnchorandarrivingAnchornow resolve to one payload, which has one backpointer. That is the merge, and it is why forwarding exists:arrivingAnchorbecomes a forwarding cell targetingparkedAnchor, which stays terminal and owns the backpointer.arrivingAnchorforwards toparkedAnchorarrivingmust be transit to be moved, soguestToArrivingis rejectedguestToArrivingpinsarriving, sogarage.car = arrivingis rejected3 structureboth guests rejectedOptions 1 and 2 differ mainly in where you find out: option 1 forces the choice at the declaration, option 2 lets both be written and rejects the second.
Correction: there is no ambiguity gap
The original version of this issue claimed these lines reproduce the problem
memory.md§2.8.1 solves:They do not.
lifetimes.md§1.8 makes a moved-from symbol downgrade to a guest to the same object rather than becoming empty, sobox.itemstill denotes the same field afterwards and both readings coincide.A field cannot be moved out of (§1.2) and travels with its object when rehosted, so a field access denotes the same storage regardless of what happens to the names above it. A bare symbol is the only place where a name and its object can part company, which is what makes §2.8.1 specific rather than an instance of something broader.
1. Declare it
Split local hosts at the declaration site into a stable storage point (may be a guest root, may not be moved) and a package in transit (may be moved or swallowed, may not be pointed at).
Ta third position-dependent meaning. Marking the stable kind keeps bareTmeaning "ordinary, in transit" for locals and parameters alike. Sigil is an open cost; may want a keyword.2. Infer it
Minting a guest from a chain rooted at a symbol stops that symbol being a move-source.
Statically checkable without whole-program analysis: §1.3 confines moves of a symbol to its declaration block, and minting from
sym.…requiressymin scope. Conservative reading (pinned if minted anywhere in the block) keeps it flow-insensitive.&to a field means the owner can never be moved.tree.rootmerely pinstree.3. Structure it— struck: contradictsadt.mdThe proposal was to require a guest to live strictly below its host's scope, which is the tightening of §1.1 from "the same or a higher lexical scope" to "strictly higher".
It would deliver the guarantee, and — contrary to an earlier draft of this issue — it would cover field-held guests too, since §1.1 already reasons about the scope of the
&itself rather than only about local symbols.It fails for a different reason. Banning an
&field from naming a host in its own scope makes recursive types unbuildable: a recursive#variantboxes its recursive case through&, and the child's&field and the host it names are inside the same structure and therefore the same scope. Parent back-pointers and sibling wiring go the same way. This is a contradiction withadt.md, not a cost to weigh.4. Sibling-linked anchors (runtime only)
Let both anchors stay terminal and link the cells to each other, so a relocation walks the ring and rewrites every member. O(identities merged) per move, always-direct reads, no source-language change.
Against: path compression already makes forwarding reads O(1) in practice, so this pays real move cost for a largely theoretical read cost; and retirement needs unlinking rather than a pop.
5. Accept merging, and make a moved-from symbol spent — maintainer's preferred direction
Keep the merge machinery; simplify the source language instead.
'T. With bare symbols pointable again, the borrow mode's motivating hole is gone.Why this resolves the original five-liner. The story's argument was that
guest &Player = mainadmits two readings. Making the moved-from symbol spent removes one of them: there is no "watch that variable" once the variable is finished. What remains is a single teachable rule — a guest follows the object; when the host symbol is moved from it is spent, and the guest carries on — with no competing referent.What it simplifies.
Tand&T.this Tis an implicit guest, andthis &Tbecomes redundant.adt.md§4.1 goes; a recursive structure may be rooted in a bare local again, soExpr.flip(leaf)is legal and the one admitted cost of Bare symbols are no longer guest sources; add the'Tborrow mode; rename receiver to subject #151 disappears.memory.md§2.4 loses a storage state (a host-capable slot readable in guest state).CLAUDE.mdneeds'[A-Z]restored and the borrow-exclusion clause in §2.8 removed.Detail that must be stated explicitly: spent ≠ unassignable. The slot still exists and may be overwritten to host again (§2.2). The relay example at §1.8 survives on that basis, but its prose must change from "downgrades to a guest" to "is spent", and any example that reads a moved-from symbol needs rewriting.
What it costs.
mutsubject is a mutable borrow for values and a guest for references again, the asymmetry Bare symbols are no longer guest sources; add the'Tborrow mode; rename receiver to subject #151 removed and the story calls "the pleasing part".&T, a caller cannot tell whether a callee retains a reference. Still safe under §1.1, but less legible. Worth considering keeping'Tfor this reason alone, independent of the hole it was created to fill.Rationale for preferring it: the merge machinery is implemented once by the compiler author; the §2.8.1 restriction is paid by every program that builds a structure and hands out a reference to it.
Disposition
With option 3 struck, the real choice is 1 or 2 — prevent the merge, delete the machinery, pay in language restriction — versus 5, accept the merge, keep the machinery, simpler language. Option 4 is a runtime-only variation of 5.
Nothing here is a defect in #151, which is merged. Option 5 reverts most of its language changes while keeping the anchor and arena work inherited from #147, and keeping the
receiver→subjectrename, which is independent.Reverting a merged rule needs its own chapter in
stories/memory.mdsaying plainly that the ban was the wrong instrument and why. "The slot that could not be pointed at" stays as written; the new chapter names its claim and supersedes it.naming-terms.md§6 also puts the rejected candidates above on the record.