diff --git a/README.md b/README.md index 8e21fea..dc4e905 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ The spec states *what* the language does; the **why** lives in a parallel set of | [`stories/generics.md`](stories/generics.md) | [`spec/generics.md`](spec/generics.md) — the parameter model, the `<>`/`()` split, size-in-the-type, and the deferred features | | [`stories/dependencies.md`](stories/dependencies.md) | [`spec/dependencies.md`](spec/dependencies.md) — URL identity, the manifest/resolution split, prebuilt distribution, symbol-rewriting, the browsable global cache, the package-graph acyclicity rule, opt-in remapping, and why `core` became a bundled implementation package | | [`stories/memory.md`](stories/memory.md) | [`spec/memory.md`](spec/memory.md) — the no-GC-no-lifetimes goal, the move problem and the anchor, lazy backpointer creation, the indexed heap table, the rooted-guest rules and the host/guest terminology split, the collapse to one value/reference axis with a borrowed subject, the shift to segmented chunked bump arenas, the split into fixed-size and dynamic regions with anchors moved to a runtime-global recyclable pool, taking the bare symbol away as a guest source, the three passing modes that split out of it, giving both back when the ban cost more than the question it closed, and the dynamic region taking the boxes, each asking for exactly its own size, once recursion became hosting, and what a copy is for — the deep value copy that lets a value type recurse, and the reference-field ban that survived it | -| [`stories/lifetimes.md`](stories/lifetimes.md) | [`spec/lifetimes.md`](spec/lifetimes.md) — lexical scope in place of a borrow checker, what may be moved, the declaration-block rule that kills flow analysis, downgrade instead of use-after-move, parameter-rooted returned guests, why each strict rule is the minimal guard against one specific memory corruption, narrowing a returned guest's root to a guest parameter once borrows arrived, and the root rule going back to "any parameter" once they left | +| [`stories/lifetimes.md`](stories/lifetimes.md) | [`spec/lifetimes.md`](spec/lifetimes.md) — lexical scope in place of a borrow checker, what may be moved, the declaration-block rule that kills flow analysis, downgrade instead of use-after-move, parameter-rooted returned guests, why each strict rule is the minimal guard against one specific memory corruption, narrowing a returned guest's root to a guest parameter once borrows arrived, the root rule going back to "any parameter" once they left, the scope check that fired once at wiring and could be outrun by a later move, and the two lifetimes hiding behind that fix — a block and a hosting tree — which collapsed the raise enumeration into one owner comparison made at every store, paid for with a signature that records where a parameter comes to rest — and the stricter `init`-as-empty-template design that would have needed no signatures at all, kept on record with the two costs that turned it down | | [`stories/effects.md`](stories/effects.md) | [`spec/effects.md`](spec/effects.md) — inferring effects instead of annotating them, subject-scoped `mut`, capabilities in place of ambient I/O, the four-level ladder and the Total-Pure/Pure split, what deliberately is not an effect, and mutation through a borrowed subject | | [`stories/concurrency.md`](stories/concurrency.md) | [`spec/concurrency.md`](spec/concurrency.md) — the parallelism/concurrency split and the refusal of `async` coloring, why `spawn` marks only a call, water-tower lifetimes, signature-based safety without locks, and value-typed mutation closing the aliased-write gap | | [`stories/error-handling.md`](stories/error-handling.md) | [`spec/error-handling.md`](spec/error-handling.md) — the two-doors model and why failure is control flow rather than a `Result` value, `resolve` as expression-substitution rather than assignment, typed abort paths and the deliberately-absent propagate operator, keeping abortability orthogonal to effects, and explicit path values through `Unit` | diff --git a/spec/adt.md b/spec/adt.md index 0904f29..a0be2f0 100644 --- a/spec/adt.md +++ b/spec/adt.md @@ -174,7 +174,7 @@ program Expr = Expr.op(Operation(Expr.intLit("3"), Expr.intLit("2"), Operator.ad - **Boxing is required on a cycle and permitted off one.** The graph this rule reads is one of **owning** edges — members that store an instance of their type, inline or boxed. An `&` member is not one of them: a guest is fixed-size storage whatever it points at, so a cycle closed through `&` needs no boxing and never triggers this rule. A member **MUST** be boxed when its declared type can lead back to the enclosing type along owning edges — when its edge lies on a cycle in that graph — because no finite inline layout exists for it. Every such edge is boxed, so nothing depends on declaration order or on choosing where to cut the cycle: above, `Expr.op`, `Operation.left`, and `Operation.right` are all boxed, and every type on the cycle has a finite, statically known size. Off a cycle, an implementation **MAY** box or inline as it judges best. Inline is the ordinary choice, but a sum whose widest case dwarfs its common ones is the case an implementation may want to place out of line, and nothing here forecloses that. The choice is made per type, not per instance, so every value of a type is still the same size and uniform stride holds (see [`generics.md`](generics.md) §7); and a program cannot observe which was chosen, because placement is not a language-visible property (see [`memory.md`](memory.md) §3.5) and no operator exposes a type's footprint. - **Nothing is written for it.** The programmer writes `left Expr`; the compiler boxes it because inline placement is impossible, exactly as it places list elements out of line. There is no `Box` type and no marker, because placement was never a language-visible property (see [`memory.md`](memory.md) §3.5). - **Both kinds may recurse.** A recursive value type is legal, because a box is placement rather than a reference-type field. The body syntax is symmetric across all four kinds, and so is recursion: `#` decides identity, aliasing, and copy-versus-move, not whether a type may contain itself. What a copy of a recursive value costs, and why it shares no node with its original, is the deep-copy rule (see [`memory.md`](memory.md) §2.3). -- **`&` is for aliasing, not for recursion.** A guest expresses non-hosting access — a parent back-pointer, a symbol table naming nodes, a genuine graph edge — and a cycle of guests is a shape hosting could not express in the first place. An `&` member follows the ordinary guest rules — it must be assigned from a guest source ([`memory.md`](memory.md) §2.8) whose host is in the same or a higher scope ([`lifetimes.md`](lifetimes.md) §1.1); an owning member, boxed or not, is subject to neither. +- **`&` is for aliasing, not for recursion.** A guest expresses non-hosting access — a parent back-pointer, a symbol table naming nodes, a genuine graph edge — and a cycle of guests is a shape hosting could not express in the first place. An `&` member follows the ordinary guest rules — it must be assigned from a guest source ([`memory.md`](memory.md) §2.8) naming a host whose owner outlives the owner of the member's root symbol, and every later store of the containing value asks that again ([`lifetimes.md`](lifetimes.md) §1.1); an owning member, boxed or not, is subject to neither. The owning edges this section describes are also the edges that walk finds an `&` along ([`lifetimes.md`](lifetimes.md) §1.10). Boxing carries costs, all of them the price of the child actually being owned. Reaching a boxed child costs one indirection, which recursion cannot avoid. Rehosting a reference-type node relocates every boxed descendant into the destination scope's dynamic region, so moving a tree costs time proportional to the tree rather than to its root (see [`memory.md`](memory.md) §3.5) — the same price a `List` already pays for its backing store. A recursive **value** type pays that cost more often, because it is copied rather than moved — but only where a copy actually happens. Binding an existing **place** into another slot walks and reallocates the whole structure. Building a fresh one does not: a non-place result is constructed directly in its eventual destination, recursively, so `Countdown.more(Countdown.more(Countdown.done(Unit())))` builds each node once where it will live rather than copying each completed prefix into the next (see [`memory.md`](memory.md) §2.3). Passing one to a parameter does not either, since a value-type parameter is a read-only borrow (§2.9). The O(structure) case is the one that reads like a copy: `b Countdown = a`, or a field, container, or return store whose source is an **existing** value. A store whose source is a fresh non-place result constructs in place instead and costs nothing extra, returns included. Where a tree is large and shared handling is wanted, that is the signal to reach for the reference form. diff --git a/spec/functions.md b/spec/functions.md index 4deab01..576825d 100644 --- a/spec/functions.md +++ b/spec/functions.md @@ -63,7 +63,7 @@ A write to `this` lands on the caller's object; how `this` reaches the caller di - For a **value-type** subject, `this` is a **mutable borrow** of the caller's slot — the actual value, not a copy. The borrow makes the value mutable in place while preserving its value semantics. Because the borrow is scoped and non-escaping, `this` may be read and written but cannot be stored as an `&` or returned as one, since a value type is not `&`-rootable. - For a **reference-type** subject, `this` is an implicit **guest**. The subject parameter is never a swallow position — a method does not consume the object it is called on — so bare `this T` here is a guest rather than the swallow it would be on an ordinary parameter, and **`&` is never written on `this`**: there is no second mode for the marker to select. The caller stays a full host either way. -A guest subject may be read, mutated, stored in an `&` field, or returned as `&T` ([`lifetimes.md`](lifetimes.md) §1.7), so a method that needs to keep its subject past the call needs no special declaration to do it. +A guest subject may be read, mutated, returned as `&T` ([`lifetimes.md`](lifetimes.md) §1.7), or used as the destination of an `&` store ([`lifetimes.md`](lifetimes.md) §1.1), so a method that needs to keep its subject past the call needs no special declaration to do it. ```zane Unit setScale(this Node, scale Float) mut { // reference subject: the implicit guest @@ -113,7 +113,7 @@ Explicit parameters other than `this` are read-only: they cannot be assigned or ### 2.8 Swallow and guest method parameters A reference-type method parameter selects one of two passing modes ([`memory.md`](memory.md) §2.9): -- A parameter declared as `&T` is a **guest**: the caller supplies a guest source under [`memory.md`](memory.md) §2.8, which a bare symbol satisfies, and the callee may read it, mutate it, store it into an `&` field, or return it. +- A parameter declared as `&T` is a **guest**: the caller supplies a guest source under [`memory.md`](memory.md) §2.8, which a bare symbol satisfies, and the callee may read it, mutate it, return it, or store it into an `&` field or element. Where it comes to rest is recorded in the signature ([`lifetimes.md`](lifetimes.md) §1.11), and each call decides whether that store is legal. - A parameter declared as a plain reference type `T` **swallows** its argument — it takes the value by hosting access, which the value's call-site scope keeps ([`lifetimes.md`](lifetimes.md) §1.5). A swallowed parameter may not be bound into `&` storage, because it is hosted at the call site while an `&` field may outlive the call. A value-type parameter is always a read-only borrow. To pass a reference object without giving up hosting, use `&T`. @@ -124,17 +124,17 @@ type Car = #struct { _value Int; } -// `&` parameter: may be stored into an `&` field -Unit setEngine(this Car, engine &Engine) mut { - this.engine = engine // legal - return Unit() -} - // `&` parameter used only to read Int calculate(this Car, engine &Engine) { return this._value + engine.speed // legal: reading through the guest } +// `&` parameter stored into an `&` field: recorded in the signature, checked per call +Unit setEngine(this Car, engine &Engine) mut { + this.engine = engine // legal here; each call compares the two argument paths + return Unit() +} + // plain reference-type parameter swallows; the swallowed value is hosted at the call site Unit setEngineWrong(this Car, engine Engine) mut { this.engine = engine // ILLEGAL: cannot store a swallowed host into an `&` field @@ -149,11 +149,18 @@ engine Engine() garage Garage() car:calculate(engine) // legal: a bare symbol is a guest source -car!setEngine(engine) // legal: same, and the guest is stored in an `&` field -car!setEngine(garage.spare) // legal: a field access is a guest source +car!setEngine(engine) // legal: one block owns car and engine +car!setEngine(garage.spare) // legal: a field access is a guest source, and + // garage is owned by the same block car!setEngine(Engine()) // ILLEGAL: a temporary is not a place expression +{ + spare Engine() + car!setEngine(spare) // ILLEGAL: this block does not outlive car's +} ``` +The last two fail for unrelated reasons. A temporary is refused at the source end, by [`memory.md`](memory.md) §2.8; `spare` is a perfectly good guest source and is refused at the destination end, by the store rule ([`lifetimes.md`](lifetimes.md) §1.1) applied to the paths this call supplied. + ### 2.9 Subscripts are place projections Subscripts are package-scope declarations with the subject first: diff --git a/spec/glossary.md b/spec/glossary.md index bbf4828..093696c 100644 --- a/spec/glossary.md +++ b/spec/glossary.md @@ -225,7 +225,7 @@ This file gives short, reusable names to concepts that appear across multiple sp - **Canonical home:** [`memory.md`](memory.md) §2.1 ### 3.33 guest -- **Meaning:** The source-facing `&T`: access to a hosted reference-type object without storing that object or controlling its lifetime. A guest may be repointed, copied when assigned or passed, stored in an `&` field, or returned as `&T`, but it cannot outlive its host. It may be minted from any place but a `[]` expression (§3.36), and it names the object hosted there at that moment, travelling with that object if it is later moved. Internally, a guest is represented by a tether (§3.24) that resolves through an anchor cell (§3.23). +- **Meaning:** The source-facing `&T`: access to a hosted reference-type object without storing that object or controlling its lifetime. A guest may be repointed, copied when assigned or passed, stored in an `&` field or element, or returned as `&T`, but it cannot outlive its host. Every store of a guest, and every later store of a value that carries one (§3.42), compares owners (§3.43): what the guest names must have an owner that outlives the owner of the place holding it ([`lifetimes.md`](lifetimes.md) §1.1). It may be minted from any place but a `[]` expression (§3.36), and it names the object hosted there at that moment, travelling with that object if it is later moved. Internally, a guest is represented by a tether (§3.24) that resolves through an anchor cell (§3.23). - **Why this name:** A guest may use what a host provides without owning it, and the guest's stay cannot outlast the host. The pair names the source relationship without exposing its runtime mechanism. - **Canonical home:** [`memory.md`](memory.md) §2.4 @@ -245,7 +245,7 @@ This file gives short, reusable names to concepts that appear across multiple sp - **Canonical home:** [`memory.md`](memory.md) §2.8 ### 3.37 passing mode -- **Meaning:** Which of two ways a reference-type argument reaches a callee, fixed entirely by the parameter's surface form: `T` **swallows** it (hosting access; the caller downgrades to a guest), `&T` takes a **guest** (readable, mutable, storable, and returnable; requires a guest source, which a bare symbol satisfies). The subject parameter (§3.38) has no such choice — it is always a guest — so `&` is never written on `this`. Two overloads may not differ only by the mode at one position. +- **Meaning:** Which of two ways a reference-type argument reaches a callee, fixed entirely by the parameter's surface form: `T` **swallows** it (hosting access; the caller downgrades to a guest), `&T` takes a **guest** (readable, mutable, returnable, and storable, with a stored guest's resting place recorded in the signature and checked at each call; requires a guest source, which a bare symbol satisfies). The subject parameter (§3.38) has no such choice — it is always a guest — so `&` is never written on `this`. Two overloads may not differ only by the mode at one position. - **Why this name:** "Mode" names a choice about *how* the same argument travels rather than *what* it is — the type is unchanged in both, and only the caller's obligations and resulting state differ. - **Canonical home:** [`memory.md`](memory.md) §2.9 @@ -269,6 +269,16 @@ This file gives short, reusable names to concepts that appear across multiple sp - **Why this name:** It names the *source* end of a move, which is where the restriction lives: the rule is about what an expression is entitled to give up, not about where the value lands. - **Canonical home:** [`lifetimes.md`](lifetimes.md) §1.2 +### 3.42 carried guest +- **Meaning:** An `&` reachable from a value's type by following **owning** edges — the same graph the boxed-member rule reads (§3.39). The walk finds an `&` member and stops there: the `&` itself is a carried guest, and a type's own `&` field is the shortest case, found after no edges at all. What the walk does not do is continue *through* the `&` into whatever it names, because that object is hosted elsewhere and travels separately. The walk reads the declared type — for a `#variant`, every case, since which case is live is the flow-sensitive fact the declaration-block rule refuses to track — and so decides only whether a value *may* carry a guest; what one names is read from the value's construction. Every store of a value re-compares the owners (§3.43) of the hosts its carried guests name, which is why a check made where the value was first written does not have to survive the value moving. A guest naming a host **inside** the value is satisfied at every destination, because that host travels with it. A value carrying no guest skips this re-check only; its own host is still compared at every store and move, as for any value. +- **Why this name:** The value *carries* the guest the way luggage carries its contents — the guest travels with it and is not part of what the value is used for, which is exactly why the store that relocates the value is the one that has to look inside. +- **Canonical home:** [`lifetimes.md`](lifetimes.md) §1.10 + +### 3.43 owner +- **Meaning:** The lifetime a place belongs to, and the only thing the store rule compares. A **symbol** is owned by the block that declares it; a **field or element** reached from its root by **owning** steps is owned by that root symbol's owner, never its own; a **parameter** (`this` included) and a constructor's `init{ }` have no owner in the body at all — each stands for a path in the caller's frame, so a store reaching one is settled at the call site. A path that steps *through* an `&` has left the tree its root names: what lies beyond belongs to a tree the path never mentions, so it has **no** owner, may be read freely, and may never be a store destination — `main.peer.io` on an `&` field `peer` is not an owned place. A block outlives every block nested within it, and a block is one lifetime rather than a sequence: everything it owns dies when it drains, with nothing to observe between. Hosts inside a stored value travel with it and take the destination's owner. +- **Why this name:** It names what a place's lifetime *is owed to* rather than where the place is written, which is the distinction the rule turns on — a field's own position tells you nothing, its root's owner tells you everything. +- **Canonical home:** [`lifetimes.md`](lifetimes.md) §1.1 + --- ## 4. Packages, Operators, and Versioning diff --git a/spec/lifetimes.md b/spec/lifetimes.md index 45079c1..435859f 100644 --- a/spec/lifetimes.md +++ b/spec/lifetimes.md @@ -1,6 +1,6 @@ # Zane Lifetimes -This document specifies Zane's lexical lifetime rules: `&` assignment scope checks, rehosting, and deterministic destruction. It builds on the host and guest storage forms defined in [`memory.md`](memory.md). +This document specifies Zane's lexical lifetime rules: the owner comparison every store makes, rehosting, and deterministic destruction. It builds on the host and guest storage forms defined in [`memory.md`](memory.md). > **See also:** [`memory.md`](memory.md) §2 for hosting and storage, §4 for anchors and tethers. [`concurrency.md`](concurrency.md) §4 for water-tower lifetimes. [`effects.md`](effects.md) §2 for `mut`. @@ -8,30 +8,55 @@ This document specifies Zane's lexical lifetime rules: `&` assignment scope chec ## 1. Scope Rules and Moves -### 1.1 `&` assignment uses host scope -An `&` assignment is legal only when the source is a guest source ([`memory.md`](memory.md) §2.8) **and** the target's host is declared in the same or a higher lexical scope than the `&` itself. +### 1.1 A store may not raise a value above what it names +Every place has an **owner**, and an owner is a lifetime: + +- a **symbol** — a local binding — is owned by the block that declares it +- a **field or element** reached from its root by **owning** steps is owned by that root symbol's owner, never its own. Every element of a container shares the container's owner, so which element it is does not enter the comparison. +- a **parameter**, `this` included, and a constructor's `init{ }` have no owner in the body. Each stands for a path in the caller's frame, so a store through one is settled at the call site (§1.11). + +A path that steps *through* an `&` leaves the tree its root names. What lies beyond belongs to a different tree whose root the path does not mention, so no owner can be computed for it and it is not a place this rule can govern. Such a path may be **read** freely; it may not be the destination of a store: ```zane +main.peer.io = someIO // ILLEGAL: `peer` is an `&`, so `main` does not name + // the tree this would write into +``` + +A **store** is legal only when every host the stored value names — directly, or through an `&` it **carries** (§1.10) — has an owner that outlives the destination's owner. An assignment, a move, a return, and an argument are all stores. There is one comparison in this section, and those are the places it is made. + +Two clauses complete it. A **block** outlives every block nested within it, and which block owns a symbol is fixed at that symbol's declaration, so nothing later can falsify it. And the hosts **inside** a stored value travel with it, taking the destination's owner — which is why a value may always be stored somewhere its own guests already point into. + +A block is one lifetime, not a sequence of them. Everything it owns dies when it drains (§2.1), with no user code interleaved and no order among them to observe, so two things one block owns can never see each other's death. That is why the comparison is between owners rather than between declaration positions. + +```zane +node Node() +r &Node = node // legal: one block owns both + outerTree Tree() -r &Node = outerTree.root { + r2 &Node = outerTree.root // legal: the outer block outlives this one innerTree Tree() - r = innerTree.root // ILLEGAL: the host's scope is nested relative to the guest + r = innerTree.root // ILLEGAL: this block does not outlive r's } ``` -The two conditions are independent. Nearly every place expression is a guest source ([`memory.md`](memory.md) §2.8), so in practice this rule is the scope comparison, and it is the comparison that does the work: +The source must also be a guest source ([`memory.md`](memory.md) §2.8). That condition is independent of the comparison, and nearly every place expression satisfies it, so in practice this rule is the owner comparison. + +A field is **not** confined to its own tree. It inherits its root symbol's owner, so an object and what its `&` field names may be siblings in one block: ```zane -node Node() -r &Node = node // legal: same scope +io IO() +terminal Terminal(io) // legal: one block owns terminal and io ``` -The compiler compares declaration scopes. It does not perform borrow inference or lifetime annotation solving. +That costs nothing while both sit there, and the moment `terminal` is stored anywhere the comparison runs again — now against the new destination, and against the guest `terminal` carries (§1.10). A store through a path that has **no** owner in this frame is the deferred case: `init{ }` fills an object whose destination the constructor cannot see, so the obligation is published in the signature and discharged by each caller (§1.11). + +The comparison the compiler makes is between two declaration blocks, after resolving each place to the block that owns it. It does not perform borrow inference or lifetime annotation solving. > **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#inheriting-a-debt-safety-without-a-borrow-checker) — "Inheriting a debt: safety without a borrow checker". > **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#where-a-guest-may-be-rooted) — "Where a guest may be rooted". > **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#the-root-rule-that-got-shorter) — "The root rule that got shorter". +> **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#two-lifetimes-and-only-one-of-them-had-a-name) — "Two lifetimes, and only one of them had a name". ### 1.2 Move-sources are host symbols, hosting verb results, or `#variant` case forms A move-source must denote a **hosting value the expression is entitled to consume**. Three forms qualify: @@ -99,7 +124,7 @@ The restriction applies only to symbol move-sources. A hosting verb result or `# > **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#the-declaration-block-rule-and-the-flow-analysis-it-refuses) — "The declaration-block rule, and the flow analysis it refuses". ### 1.4 Destination scope must contain or match source scope -A value may move into a new host only when the destination host is declared in the same or a higher lexical scope than the source host. +A move is a store, so §1.1 governs it. Read against the moved value's own host, the comparison says: a value may move into a new host only when the destination host is declared in the same or a higher lexical scope than the source host. ```zane node Node() @@ -109,13 +134,17 @@ node Node() } ``` -A hosting verb result (§1.2) has no source host; its source scope is the expression that produces it. That scope is always nested within or equal to the destination host's scope, so this restriction is trivially satisfied and never blocks moving a verb result into any host. +A hosting verb result (§1.2) has no source host; its source scope is the expression that produces it. That scope is always nested within or equal to the destination host's scope, so this reading is trivially satisfied and never blocks moving a verb result into any host. What such a value **carries** is a separate question, and §1.1 asks it against the host the value is bound into. A parameter's value is exempt. Because a parameter belongs to the call-site scope and is not part of the body (§1.5), lending it into a local or a nested call does not sink hosting into that lower scope: the value returns to the call site when the local exits, unless the callee moves it into another parameter's hosting storage or into the return (§1.8). +This reading concerns the moved value's own host. When the value **carries guests**, §1.1 compares their owners too, and §1.10 says which guests those are. + ### 1.5 Parameters belong to the call site A reference-type parameter is **not part of the callee's body scope**. It behaves as a symbol in the **call-site scope**, one level above the body. Passing a hosting reference-type value to a plain `T` parameter lends it in with hosting access, but the value's lifetime stays with the call site. +Its **owner** (§1.1) is therefore no block of the body. A parameter stands for the argument path the caller wrote, which is why a store that reaches a parameter is settled by the call site rather than by the body (§1.11). + This is stated for the swallowing mode because that is the only mode where hosting crosses the call boundary at all. An `&T` guest parameter never takes hosting ([`memory.md`](memory.md) §2.9), so nothing about the argument's lifetime changes when one is used; the call-site scope keeps hosting throughout. This is what makes the passing rule safe. Because the parameter is not part of the body scope, the body draining never destroys the value. The body may read it, move it into a local, or pass it to a nested call; when a local that received it exits, the value is not dropped — the compiler moves it back up to the call site, and the chain repeats outward until the scope that first hosted the value drains. A value passed by hosting access therefore always outlives the call, which is what lets the caller's symbol downgrade to a live guest (§1.8) rather than a dangling one. @@ -151,15 +180,15 @@ This also applies across calls. Passing a hosting value to a plain `T` parameter A hosting verb result (§1.2) has no symbol to downgrade. The temporary is consumed by the move and cannot be named again, so the double-move question never arises for it. ### 1.7 Returned `&` values must be rooted in a parameter -A function may return an `&T` only when the returned guest is rooted in one of the function's **parameters** — the parameter used bare, or a field access whose base chain reaches it. `this` counts as a parameter for this rule. +A return is a store into the call-site scope, so §1.1 governs it, and this is what the comparison comes to for a returned guest: a function may return an `&T` only when the returned guest is rooted in one of the function's **parameters** — the parameter used bare, or a field access whose base chain reaches it. `this` counts as a parameter for this rule. ```zane &Weapon getWeapon(this Player) => this.weapon ``` -Both parameter modes are roots, and for the same reason: a parameter belongs to the **call-site scope** (§1.5), never to the body. A guest rooted in one therefore names something hosted in the scope the return value lands in, so §1.1 compares the two directly at the call site and rejects the cases that would dangle. A swallowing `T` parameter qualifies on exactly these terms — the value it took outlives the call (§1.5) — even though passing to it downgrades the caller (§1.8). +Both parameter modes are roots, and for the same reason: a parameter belongs to the **call-site scope** (§1.5), never to the body, so it has no owner the body could compare against. The obligation travels out with the signature and the call site discharges it against the argument path (§1.11), which is where the two owners are finally both in view. A swallowing `T` parameter qualifies on exactly these terms — the value it took outlives the call (§1.5) — even though passing to it downgrades the caller (§1.8). -A **local** is the case this rule excludes, and it is excluded by lifetime rather than by what may mint a guest: +A **local** is the case this rule excludes, and it is excluded by lifetime rather than by what may mint a guest. A body block does not outlive the call-site scope, so §1.1 rejects the store outright: ```zane &Node bad() { @@ -168,6 +197,8 @@ A **local** is the case this rule excludes, and it is excluded by lifetime rathe } ``` +This rule governs a return that **is** an `&T`. A return that *carries* one — a hosting value with an `&` reachable inside it — is the same store, and §1.1 compares the carried guest's owner on the same reasoning. + > **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#returning-a-ref-without-a-lifetime-to-name-it) — "Returning a ref without a lifetime to name it". > **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#where-a-guest-may-be-rooted) — "Where a guest may be rooted". > **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#the-root-rule-that-got-shorter) — "The root rule that got shorter". @@ -186,7 +217,7 @@ The value outlives the call (§1.5), so the downgraded guest always resolves to A verb treats a reference-type host argument in one of three ways, each fixed by its signature: -- it takes a **guest** — declares the parameter `&T`; the caller stays a full host, and the callee may read it, mutate it, or keep it past the call by storing or returning it. +- it takes a **guest** — declares the parameter `&T`; the caller stays a full host, and the callee may read it, mutate it, return it, or store it. Where a stored guest comes to rest is part of the signature (§1.11), and the caller's argument paths settle whether that store is legal (§1.1). - it **relays** the host — declares a swallowing `T` and returns a hosting handle; the caller downgrades to a guest but may bind the return to host the object again (§1.9). - it **consumes** the host — declares a swallowing `T` and returns no host; the caller downgrades to a guest, and the value stays wherever the verb placed it. @@ -215,7 +246,7 @@ Unit main() { } ``` -A verb that only reads its reference argument may still declare it plain `T`: reading does not change the fact that the signature asked for hosting access, so the caller downgrades all the same. Declaring the parameter `&T` is what keeps the caller as host. Because the signature alone decides the caller's state, there is no interprocedural consumption inference: whether a passed host downgrades never depends on the callee's body or on the build. Using hosting access only to read a value is legal. Leaving a parameter entirely unused is a separate, general matter — a release build rejects an unused parameter whether it hosts a value or not. +A verb that only reads its reference argument may still declare it plain `T`: reading does not change the fact that the signature asked for hosting access, so the caller downgrades all the same. Declaring the parameter `&T` is what keeps the caller as host. Because the signature alone decides the caller's state, there is no interprocedural consumption inference: whether a passed host downgrades never depends on the callee's body or on the build. The resting-place summary of §1.11 does not reopen this. It records **where** a parameter's value comes to rest, which the caller needs in order to compare owners; it never changes **whether** passing one downgrades the caller, which the declared mode fixes on its own. Using hosting access only to read a value is legal. Leaving a parameter entirely unused is a separate, general matter — a release build rejects an unused parameter whether it hosts a value or not. > **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#the-signature-is-the-whole-contract-retiring-inferred-consumption) — "The signature is the whole contract: retiring inferred consumption". > **Story:** [`stories/memory.md`](../stories/memory.md#three-ways-to-hand-over-an-object) — "Three ways to hand over an object". @@ -234,6 +265,144 @@ Because a floated result is kept rather than dropped, no guest dangles and no ho > **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#the-signature-is-the-whole-contract-retiring-inferred-consumption) — "The signature is the whole contract: retiring inferred consumption". +### 1.10 A value carries the guests reachable along owning edges +A value **carries a guest** when an `&` is reachable from its type by following **owning** edges (see [`adt.md`](adt.md) §4). The walk finds an `&` member and stops at it: a type's own `&` field is the shortest case, reached after no edges at all, and an `&` nested inside a hosting field or container element is reached by following those edges to it. The walk does not continue *through* an `&` into what it names, because that object is hosted elsewhere and moves separately. + +The hosts a value's carried guests name are what §1.1 compares alongside the value's own host. A guest naming a host **inside** the value is satisfied at every destination, because that host travels with it. A guest naming anything else keeps the owner it has, and every store of the value asks again whether that owner outlives the new destination: + +```zane +outerHolder Holder(Engine(Int(1))) +parked Car(outerHolder.engine) // Car holds an `&Engine` +{ + innerHolder Holder(Engine(Int(2))) + arriving Car(innerHolder.engine) + parked = arriving // ILLEGAL: the guest names a host owned by this +} // block, and parked is owned above it +``` + +The walk reads the **declared type**, not the value's current contents. For a `#variant` that means every case, because which case is live is the flow-sensitive fact §1.3 exists to refuse. That decides only whether a value *may* carry a guest. What a carried guest **names** is read from the value's construction, which §1.3 keeps in the same block as any move of it — so a case form that supplies no `&` names no host, nothing is compared, and the store passes. No valid program is rejected for holding a case the walk had to consider. + +A value with **no source host** — a hosting verb result or a `#variant` case form (§1.2) — is asked the same question, against the host it is bound into. It re-parents nothing, so §1.4 waves it through; what it carries still has to reach the destination: + +```zane +type Expr = #variant { + intLit String; + ref &Node; // an `&` payload, so this case form takes a guest source +} + +result Expr = Expr.intLit("0") +{ + innerTree Tree() + result = Expr.ref(innerTree.root) // ILLEGAL: the case form carries a guest to +} // this block, and result is owned above it +``` + +`innerTree.root` is a field access, which is a guest source ([`memory.md`](memory.md) §2.8) and so is what an `&` payload asks for. It would **not** do for a hosting payload, which takes a move-source ([`adt.md`](adt.md) §3.2) — the two payload kinds ask for different things, and only the `&` kind produces a carried guest here. + +A guest naming inside the value is what a constructor's `init{ }` normally settles: + +```zane +Main(io std$IO) => init{io, terminal = Terminal(io)} +``` + +`io` moves into the Main's own field, and the guest inside `terminal` follows it there ([`memory.md`](memory.md) §2.8.1). A `Main` may therefore be stored anywhere, while a `Car` holding a guest to storage it does not own may only go where that storage outlives it. + +Everything reachable under one root symbol belongs to one hosting tree ([`memory.md`](memory.md) §2.1), which is why a guest that names inside its own value needs no further comparison: it travels with what it points at and goes when the tree goes. What none of this reaches is a host destroyed while its tree lives on — a separate matter, governed by §2.1 and by [`memory.md`](memory.md) §2.8.1. + +> **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#the-check-that-fired-once-and-the-move-that-outran-it) — "The check that fired once, and the move that outran it". + +### 1.11 A signature records where its parameters come to rest +A parameter has no owner in the body (§1.5), so a store that reaches one cannot be settled there. What the body settles instead is **where the value comes to rest**: when a verb stores a parameter into a place reachable from another parameter or from the result, the parameter and the path it lands in are part of that verb's signature. Each call substitutes its own argument paths for the parameters and applies §1.1. + +```zane +type Terminal = #struct { + io &IO; // an `&` field +} + +type Main = #struct { + terminal Terminal; // a hosting field + peer &Terminal; // an `&` field + io IO; // a hosting field +} + +Unit setIO(this Terminal, io &IO) mut { + this.io = io // recorded: io comes to rest at this.io + return Unit() +} +``` + +Both paths below resolve to `main`'s owner, because a field takes its root symbol's owner (§1.1) and both are reached from `main`: + +```zane +main Main() +main.terminal!setIO(main.io) // → main.terminal.io = main.io + // one block owns both: legal +{ + ioInner IO() + main.terminal!setIO(ioInner) // → main.terminal.io = ioInner +} // ILLEGAL: this block does not outlive main's +``` + +A constructor is the same case. Its `init{ }` fills an object whose destination the body cannot see, so what the body can state is which parameters land in it: + +```zane +Terminal(io &IO) => init{io} // recorded: io comes to rest at the result's io +``` + +```zane +main Main() +{ + ioInner IO() + t Terminal(ioInner) // → t.io = ioInner; one block owns both: legal + main.terminal = t // ILLEGAL: t carries a guest owned by this block, +} // and main is owned above it +``` + +A swallowed `T` parameter is recorded the same way, and that is what settles an argument carrying a guest. Neither frame sees the problem alone — the argument reaches a parameter in the call-site scope, and inside the callee both parameters share it: + +```zane +cars List = [] +{ + innerHolder Holder(Engine(Int(2))) + arriving Car(innerHolder.engine) + cars!append(arriving) // append records: car comes to rest in this's elements +} // → ILLEGAL: arriving carries a guest owned by this + // block, and cars is owned above it +``` + +The summary is **transitive**, in the way the effect summaries of [`effects.md`](effects.md) §5.2 are: a verb that hands a parameter to another verb inherits the resting places that call records for it. Without that, a guest could be laundered by passing it one frame further than the check looked. + +```zane +Unit relay(this Terminal, io &IO) mut { + this!setIO(io) // recorded: io comes to rest at this.io, via setIO + return Unit() +} +``` + +A recorded path begins at a **root** — a parameter, or the result — and continues with the same **owning** steps §1.1 owns a place by: field selections, and "an element of" for a container. No index is recorded, because every element of a container shares its owner. The root itself may be an `&T` parameter, which is an ordinary root like any other; what a path may not do is step *through* an `&` further along, for the reason §1.1 gives — beyond that point the path has left the tree its root names. + +```zane +Unit setNested(target &Terminal, io &IO) mut { + target.io = io // recorded: io comes to rest at target.io + return Unit() +} + +Unit wire(this Main, io &IO) mut { + this.terminal.io = io // recorded: `terminal` is a hosting field of `this` + this.peer.io = io // ILLEGAL: `peer` is an `&` mid-path (§1.1) + return Unit() +} +``` + +A call **substitutes** the path the caller supplied — an argument path, or the path the result is bound into — for the root, keeps the recorded steps that follow it, and applies §1.1 to the place that results. The steps are preserved rather than collapsed, so `setNested` called as `outer!setNested(main.terminal, main.io)` compares `main.terminal.io` against `main.io`, and two implementations that agree on the summary agree on the verdict. + +The summary is derived from the body and published with the signature, so a call can be checked without the body in hand. A verb whose parameters come to rest nowhere records nothing, which is the common case; its calls need no substitution. + +For an `&` field the callee must still declare the corresponding parameter `&T` ([`memory.md`](memory.md) §2.9, [`types.md`](types.md) §3.9). A swallowed value is hosted at the call site, so binding one into `&` storage would leave the field naming storage the caller may move out from under it, and no argument path the caller could supply would fix that. + +> **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#two-lifetimes-and-only-one-of-them-had-a-name) — "Two lifetimes, and only one of them had a name". +> **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#the-empty-template-the-design-that-would-have-needed-no-signatures) — "The empty template: the design that would have needed no signatures". + --- ## 2. Lifetime and Destruction @@ -250,7 +419,7 @@ If a scope launches concurrent work, objects hosted by that scope remain alive u Guests do not participate in hosting and cannot prolong object lifetime. They only track a live object whose host is already guaranteed to outlive them. ### 2.4 Null guests are not a user-facing state -Because scope rules (§1.1) prevent guests from outliving their hosts, the runtime does not expose a normal “null guest” programming model to the user. +An `&` is never optional and is never tested for emptiness; the runtime exposes no “null guest” programming model to the user. One rule keeps a stored guest pointing at something live as values move: §1.1 compares owners at every store, over the value's own host and over the guests it carries (§1.10), deferring to the call site wherever a parameter stands in for a path it cannot see (§1.11). What that covers is **relocation** — a value travelling away from what its guests name. A host destroyed while its tree lives on is the separate question §2.1 and [`memory.md`](memory.md) §2.8.1 answer. --- @@ -271,11 +440,15 @@ Because scope rules (§1.1) prevent guests from outliving their hosts, the runti | Concept | Rule | |---|---| +| Store | Legal only when every host the stored value names — its own, and every host reached through a guest it carries — has an owner that outlives the destination's owner; an assignment, a move, a return, and an argument are all stores | +| Owner | A symbol is owned by its declaring block; a field or element reached by owning steps by its root symbol's owner; a parameter and a constructor's `init{ }` have none in the body and stand for a path in the caller's frame. A path stepping *through* an `&` has left its root's tree, has no owner, and may be read but never stored into. A block outlives every block nested in it; hosts inside a stored value travel with it and take the destination's owner | | `&` return | Returned `&T` must be rooted in a parameter of either mode, `this` included, because a parameter belongs to the call-site scope; a local is not a root | -| Guest assignment | Only from a guest source ([`memory.md`](memory.md) §2.8) whose host is in the same or a higher lexical scope than the guest; a bare symbol is a guest source, a `[]` expression is not | +| Guest assignment | Only from a guest source ([`memory.md`](memory.md) §2.8); a bare symbol is a guest source, a `[]` expression is not | | Move-source | A direct host symbol (local or parameter), a hosting verb result, or a `#variant` case form; not an `&`, a value-type borrow, a field, a container element, or any other access path | | Move declaration-block restriction | A direct host symbol may only be moved in the exact lexical block where it was declared; parameters may be moved at the body top level | -| Move destination scope | Destination host must be in the same or a higher lexical scope than the source host | +| Move destination scope | Destination host must be in the same or a higher lexical scope than the source host — the store rule read against the moved value's own host | +| Carried guest | A value carries every `&` reachable from its **declared** type along owning edges — for a `#variant`, across every case — stopping at each `&` rather than continuing through it; the type decides whether to look, the value's construction decides what is named. One naming a host inside the value satisfies any destination, one naming anything else keeps its owner and is compared at every store. Carrying none skips this comparison only, never the value's own host | +| Resting place | Where a verb stores a parameter is part of its signature: a path rooted at another parameter or at the result, continuing by owning steps only, never stepping through an `&`. Derived from the body, transitive through the calls the body makes, and published with the signature. A call substitutes the supplied path for the root, keeps the recorded steps, and applies the store rule to the result. It records where a parameter lands, never whether passing one downgrades the caller | | Post-move downgrade | After a move, the source symbol downgrades to an `&` and remains readable but is no longer a move-source | | Parameter scope | A reference parameter belongs to the call-site scope, not the body, so a value passed by hosting access outlives the call | | Hosting argument | A verb takes a **guest** (`&T`, caller keeps it), **relays** the host (`T` and returns a hosting handle, caller may bind it to host again), or **consumes** it (`T`, no host returned, caller keeps a guest); passing to a plain `T` downgrades the caller to a guest whatever the body does | diff --git a/spec/memory.md b/spec/memory.md index b01c844..4c5a106 100644 --- a/spec/memory.md +++ b/spec/memory.md @@ -107,7 +107,7 @@ Declaring an `&` symbol is legal; §2.8 governs what may initialize it. ### 2.5 Guests are repointable -An `&` symbol or `&` field may be assigned a different target later, as long as the new target is a guest source (§2.8) and the scope rule in [`lifetimes.md`](lifetimes.md) §1.1 is satisfied. +An `&` symbol or `&` field may be assigned a different target later, as long as the new target is a guest source (§2.8) and the store rule in [`lifetimes.md`](lifetimes.md) §1.1 is satisfied. For an `&` **field or element**, the owner that rule compares is the field's root symbol's, not the field's own. ### 2.6 Guests are independent @@ -209,10 +209,10 @@ A **reference type** parameter has two passing modes, one per surface form. The | Mode | Written | Caller supplies | The callee may | |---|---|---|---| | Swallow | `T` | a move-source ([`lifetimes.md`](lifetimes.md) §1.2) | take hosting access; the caller's symbol downgrades to a guest | -| Guest | `&T` | a guest source (§2.8) | read and mutate it, store it in `&` storage, or return it as `&T` | +| Guest | `&T` | a guest source (§2.8) | read it, mutate it, return it as `&T`, or store it; where a stored guest comes to rest is part of the signature ([`lifetimes.md`](lifetimes.md) §1.11) | - A parameter declared as a plain reference type `T` **swallows** its argument — it takes the value by hosting access. The value belongs to the call-site scope, not the callee body ([`lifetimes.md`](lifetimes.md) §1.5), so it outlives the call. Passing a hosting value to such a parameter downgrades the caller's symbol to a guest ([`lifetimes.md`](lifetimes.md) §1.8), whatever the callee does with it — whether the verb relays the host back through its return or consumes it outright. -- A parameter declared as `&T` is a **guest**: the caller supplies a source that may mint a new guest under §2.8 (so `T` is a reference type, §2.4), and inside the callee body it acts as a place expression that may be stored into `&` storage or returned as `&T` under [`lifetimes.md`](lifetimes.md) §1.7. A bare symbol is a guest source, so an ordinary local feeds an `&T` parameter directly. +- A parameter declared as `&T` is a **guest**: the caller supplies a source that may mint a new guest under §2.8 (so `T` is a reference type, §2.4), and inside the callee body it acts as a place expression that may be read, mutated, or returned as `&T` under [`lifetimes.md`](lifetimes.md) §1.7. A bare symbol is a guest source, so an ordinary local feeds an `&T` parameter directly. Binding it into an `&` **field** is decided elsewhere: the callee cannot see where the caller's argument is hosted relative to the object it would be stored in, so it does not try. It records that the parameter comes to rest in that field ([`lifetimes.md`](lifetimes.md) §1.11), and each call compares the owners of the two argument paths it actually wrote. `&T` is the mode for a call that must not take hosting. A verb that reads or mutates a caller's object without consuming it declares that object `&T`, and the caller passes the symbol as it stands: @@ -234,12 +234,6 @@ type Car = #struct { _value Int; } -// `&` parameter is a guest; it may be stored into an `&` field -Unit setEngine(this Car, engine &Engine) mut { - this.engine = engine - return Unit() -} - // plain reference-type parameter: taken by hosting access, then moved into a hosting field of this Unit setSpare(this Car, engine Engine) mut { this.spare = engine @@ -250,13 +244,40 @@ Unit setSpare(this Car, engine Engine) mut { Int inspect(this Car, engine &Engine) { return this._value + engine.speed } + +// `&` parameter stored into an `&` field: the signature records where it lands +Unit setEngine(this Car, engine &Engine) mut { + this.engine = engine + return Unit() +} + +// taking the host instead: the object owns the engine and points at its own field +Unit installEngine(this Car, engine Engine) mut { + this.spare = engine + this.engine = this.spare + return Unit() +} +``` + +`setEngine` stores a guest it was handed. The callee sees two parameters and cannot tell whether the caller's `engine` is hosted above or below the object `this` names, so it does not decide: its signature records that `engine` comes to rest at `this.engine` ([`lifetimes.md`](lifetimes.md) §1.11), and each call substitutes the argument paths it was given and compares owners ([`lifetimes.md`](lifetimes.md) §1.1). + +```zane +car Car(...) +engine Engine() +car!setEngine(engine) // → car.engine = engine; one block owns both: legal +{ + spare Engine() + car!setEngine(spare) // ILLEGAL: this block does not outlive car's +} ``` -**The subject parameter is never a swallow position.** A method does not consume the object it is called on, so `this` — the first parameter, and only it ([`functions.md`](functions.md) §2.1) — is not one of the two modes above. For a reference-type subject it is an implicit **guest**; `&` is **never** written on `this`, because there is no second mode for it to distinguish. A guest subject may be read, mutated, stored in `&` storage, or returned as `&T` ([`lifetimes.md`](lifetimes.md) §1.7), which is everything a method can want from its subject. +`installEngine` is the different shape, not the workaround: it is what to write when the object should **own** the engine rather than name one the caller keeps hosting. + +**The subject parameter is never a swallow position.** A method does not consume the object it is called on, so `this` — the first parameter, and only it ([`functions.md`](functions.md) §2.1) — is not one of the two modes above. For a reference-type subject it is an implicit **guest**; `&` is **never** written on `this`, because there is no second mode for it to distinguish. A guest subject may be read, mutated, returned as `&T` ([`lifetimes.md`](lifetimes.md) §1.7), or used as the destination of an `&` store ([`lifetimes.md`](lifetimes.md) §1.1), which is everything a method can want from its subject. So bare `T` does not mean the same thing in both positions — on an ordinary parameter it swallows, on `this` it guests — because `this` was never a swallow position to begin with. The two kinds diverge here, and visibly: a reference-type `this` is a guest, a value-type `this` is a borrow (mutable under `mut`). They are written identically because in both cases the subject is simply the object the method was called on, and neither kind has a choice to express. -Binding a swallowed parameter into `&` storage is illegal. This is not a guest-source restriction — a bare symbol is a guest source (§2.8) — but a scope one: a swallowed value is hosted at the call site, while an `&` field lives with the object that holds it, which may outlive the call. That object's host is not the call site and is not compared by [`lifetimes.md`](lifetimes.md) §1.1, so the restriction is stated directly rather than derived: +Binding a **swallowed** parameter into `&` storage is illegal, and the parameter mode is what makes that visible at the signature. This is not a guest-source restriction — a bare symbol is a guest source (§2.8) — but a lifetime one, and it is the one case no argument path can rescue. A swallowed value is hosted at the call site, and the caller has already given up its host by passing it ([`lifetimes.md`](lifetimes.md) §1.8), so there is no path the caller could name for the store rule to compare against: ```zane Unit setEngineSwallowed(this Car, engine Engine) mut { diff --git a/spec/types.md b/spec/types.md index cb07add..d7afb83 100644 --- a/spec/types.md +++ b/spec/types.md @@ -316,7 +316,7 @@ car Car(engine) // legal: a bare symbol is a guest source car Car(Engine()) // ILLEGAL: a temporary cannot initialize an `&` field ``` -What still constrains such a field is scope, not source: the object it points at must be hosted in the same or a higher lexical scope than the `&` itself ([`lifetimes.md`](lifetimes.md) §1.1). Recursion is not one of these cases at all: a recursive member is an ordinary owning field the compiler boxes, so it needs no `&` and no guest source (see [`adt.md`](adt.md) §4). +What still constrains such a field is lifetime, not source: the object it points at must have an owner that outlives the owner of the place holding the `&` ([`lifetimes.md`](lifetimes.md) §1.1). A field takes its root symbol's owner, so that comparison does not stop at construction — every later store of the containing value asks it again, over the guests that value carries ([`lifetimes.md`](lifetimes.md) §1.10). A constructor cannot make the comparison itself, because `init{ }` has no owner until the caller says where the object goes; it records which parameters land in `&` fields and each call settles it ([`lifetimes.md`](lifetimes.md) §1.11). Recursion is not one of these cases at all: a recursive member is an ordinary owning field the compiler boxes, so it needs no `&` and no guest source (see [`adt.md`](adt.md) §4). > **Story:** [`stories/memory.md`](../stories/memory.md#the-ban-that-cost-more-than-the-question-it-closed) — "The ban that cost more than the question it closed". diff --git a/stories/lifetimes.md b/stories/lifetimes.md index 344872f..0b5324b 100644 --- a/stories/lifetimes.md +++ b/stories/lifetimes.md @@ -195,3 +195,116 @@ What is interesting is that the rule which survives is not the old one restored A local is still not a root, and the difference is that it is now excluded for the true reason rather than a proxy for it. `return value` on a body-scope local fails because that scope drains at the return, full stop — not because of anything about where a guest may be minted. When we were routing the rejection through guest sources we were using a source restriction to enforce a lifetime property, and it happened to catch this case; it is better stated as the lifetime property it always was. The one line of the previous chapter that needs correcting rather than superseding is its account of §1.1. It reported that the scope check had become "the second line of defence", with the guest-source question in front of it catching the common case. That is no longer the shape: nearly every place is a guest source, so §1.1 is the front line again and does essentially all of the work. Its canonical illegal example goes back to being the one about scope, which is what the rule is actually about. + +## The check that fired once, and the move that outran it + +With the returned-guest root settled, one case was still open — and it is not one the previous chapters had occasion to look at, because it is not a question about where a guest may be rooted or minted. It is a question about what happens to a guest afterwards. + +The comparison [§1.1](https://github.com/zane-lang/spec/blob/b486fd5f8c4d2ecdb14b8ef105394dc43aaf3bc6/spec/lifetimes.md#11--assignment-uses-host-scope) makes is exact for an `&` **symbol**, whose scope is fixed the moment it is declared and cannot change afterwards. An `&` **field** is a different shape: its scope is its container's, and a container moves. So the comparison made when the field is written can be falsified later by a statement that never touches the field at all: + +```zane +parked Car(outerHolder.engine) +{ + innerHolder Holder(Engine(Int(2))) + arriving Car(innerHolder.engine) // §1.1 satisfied: both in this block + parked = arriving // a move, not an `&` assignment +} // innerHolder dies; parked.engine names its storage +``` + +Every line passes. §1.1 fired at the construction and was right at the time. The move is checked by [§1.4](https://github.com/zane-lang/spec/blob/b486fd5f8c4d2ecdb14b8ef105394dc43aaf3bc6/spec/lifetimes.md#14-destination-scope-must-contain-or-match-source-scope), which compares the *value's* scopes and says nothing about what the value contains. Between them the two rules cover both directions a value can travel and neither looks at the guest riding inside it. + +There is an asymmetry underneath that we had not noticed we were relying on. §1.4 exists to stop hosting from **sinking** into a nested scope. For a guest held inside the moved object the danger runs the other way — **raising** the container above what its guests name — and we had only ever constrained one of the two. + +Our first instinct was to pin the value: an object holding an `&` may not be rehosted into a higher scope, full stop. That is decidable from the type alone, using the same transitive walk the value-downstream rule already performs, and it needs no new machinery. It is also wrong in the case that matters most. A parent pointer — `parent &Node` on a node stored in `root.children` — is the commonest reason anyone wants an `&` field at all, and pinning would forbid appending the child to the very tree its guest points at. A rule whose first casualty is the canonical use of the feature is not a conservative rule, it is a broken one. + +That failure told us what the missing property was. What makes a parent pointer safe is not its scope but its *structure*: the guest names something in the same hosting tree as the object holding it, so the two move together and cannot come apart. [§1.2](https://github.com/zane-lang/spec/blob/b486fd5f8c4d2ecdb14b8ef105394dc43aaf3bc6/spec/lifetimes.md#12-move-sources-are-host-symbols-hosting-verb-results-or-variant-case-forms) already guarantees that, by refusing to let a subtree be prised out of its tree. So tree membership survives every operation that *relocates* a value, where scope survives none of them. It says nothing about a host that simply dies, which is the limit we come back to at the end of this chapter. + +We spent a while trying to make that the whole rule — an `&` may only name something inside its own tree — and it fails from the other side. A short-lived wrapper over something handed in, the sort of thing anyone writes without thinking, has no shared tree with what it points at: + +```zane +Unit report(io std$IO) { + terminal Terminal(io) // io belongs to the call site and outlives the body + terminal!print("hi") + return Unit() +} +``` + +Perfectly safe, and a blanket tree rule forbids it. The resolution was to stop treating the two as rivals. **Tree membership is not an alternative to the scope comparison; it is the permission to move.** A guest that names something inside the value it rides in satisfies any destination, because it travels. A guest that does not is fine wherever it currently sits, and must be re-examined if the value is raised. Both readings live in the carried-guest rule, and neither had to displace the other. + +The second realisation is the one that changed the shape of the fix, and it came from asking *when* the check should run. We had been trying to make the wiring-time check strong enough to survive everything that could happen later, which is why every version of it was either unsound or absurdly strict. It does not have to survive anything, because a raise is a statement the compiler can see. Check at the rise and the question of whether an earlier check has gone stale never arises: there is no earlier check to go stale. + +That would have been the end of it, except for a program with no rise in it anywhere: + +```zane +Unit setEngine(this Car, engine &Engine) mut { this.engine = engine; return Unit() } + +Unit main(car &Car) { + engine Engine() // a local of main + car!setEngine(engine) // car names something above main + return Unit() +} // engine dies; car.engine is left naming its storage +``` + +Nothing moves. The argument reaches a parameter in the call-site scope, and inside the body `this` and `engine` are both call-site scope, so every scope comparison in sight compares equals. And this is the shape [`memory.md` §2.9](https://github.com/zane-lang/spec/blob/b486fd5f8c4d2ecdb14b8ef105394dc43aaf3bc6/spec/memory.md#29-function-parameters-swallow-and-guest) had been holding up as the canonical thing an `&` parameter is *for*. What the callee cannot see is whether the caller's `engine` is hosted above or below the object `this` names — and it never can, because that is a fact about two of the caller's symbols, one frame away. + +We tried hardest to fix this by narrowing where a guest may be **minted**. If only a guest could be a guest source — an `&T` parameter, `this`, a guest local rooted in one — then every guest would name something at the call-site scope or above, the highest any frame can rehost anything, and the whole class of problem would be structurally impossible. It is an elegant rule and we followed it a long way before noticing it has no base case. A parameter is fed by the caller's guest source, a guest local by another guest source; the recursion bottoms out only at a package constant, or at `this` — whose subject expression nothing checks, and cannot check, because `terminal!print("hi")` on a local you own has to work. The subject position is not a leak in that rule. It is the only place the language mints a guest at all, and a rule that closes it is a rule that deletes guests. + +So the restriction had to land on the **store** rather than the source. Our first answer took that literally and syntactically: both paths of an `&` store must begin with the same root symbol. It is a comparison of two identifiers, it needs no scopes and no induction, and it holds however many frames a guest has travelled through, because it looks at the two paths in front of it rather than at their history. Everything reachable under one name is one tree, so a guest written through a name travels with what it points at and goes when the tree goes. + +We shipped that, alongside an enumeration of the four ways a value can be raised, and lived with it long enough to notice we did not like it. The next chapter is about what was wrong with it, which turned out not to be any of the things it got wrong. + +## Two lifetimes, and only one of them had a name + +The same-root rule was correct as far as we ever managed to push it, and the raise enumeration next to it caught every program we could write. What it was not, was memorable. Four raise forms, a walk over owning edges, a carve-out for `init{ }` because a half-built object has no root to share, and a separate scope comparison for `&` symbols that the field rule did not use. Each piece was justified on its own and none of them followed from any of the others. A language whose reference rules take a page to enumerate has not found its rule yet; it has found a list of the places its rule was supposed to be. + +The tell was the carve-out. `init{ }` was exempt from the root comparison because the object it fills does not exist yet — which is to say, the rule could not be stated for the one place where every `&` field in the language is first written. Everything the exemption let through then had to be caught downstream, and *that* is what the four raise forms were for. The enumeration was not a companion to the root rule. It was the patch over the hole the root rule could not cover. + +What we had actually been doing, without naming it, was recognising exactly one kind of lifetime. A hosting tree is a lifetime: everything under one root dies when the root does. That is what "same root symbol" tests, in a syntactic disguise. But a **block** is a lifetime on precisely the same terms — everything it owns dies when it drains, there is no user code between one death and the next, and nothing in the language can observe an order among them. We had been comparing blocks in [§1.1](https://github.com/zane-lang/spec/blob/6b668be1df1708cc23704254cc5aab57e1a365d2/spec/lifetimes.md#11-a-store-may-not-raise-a-value-above-what-it-names) since the beginning and comparing trees in the field rule, and we had never noticed that the two comparisons were the same comparison over two instances of one idea. + +Give it a name — an **owner** — and say where each place gets one. A symbol is owned by its declaring block. A field or element is owned by its root symbol's owner, never its own. A parameter and an `init{ }` have no owner in the body at all; each stands for a path in the caller's frame. Then the whole of it is one sentence: a store is legal when every host the stored value names, directly or through a guest it carries, has an owner that outlives the destination's owner. + +The four raise forms evaporate, and not by being absorbed into a longer sentence. They were four *syntactic occasions* on which a value changes lifetime, and a value changes lifetime by being **stored** — an assignment, a move, a return, an argument. There was never anything to enumerate. We had enumerated because we were looking for the places a check could go stale, which is a question you only have to ask if you believe some checks run once and stay believed. + +Two things we had assumed were consequences of the root rule turned out to be consequences of nothing, and both were losses. + +The first is that a field is **not** confined to its own tree. Under owners it inherits its root symbol's owner, so an object and what its `&` field names may be siblings in one block, and `terminal Terminal(io)` on two locals is exactly as legal as it always looked. The root rule forbade that shape and then re-permitted it through the `init{ }` carve-out, which is a strange thing for a rule to do and should have been read as a symptom. + +The second is `setEngine`. The previous chapter recorded its death and the `installEngine` that replaced it, and argued that taking hosting instead was the more honest signature. That argument was rationalisation. A verb that stores a guest it was handed is an ordinary thing to want — a setter, a registry, anything wired after construction — and the reason we could not check it was never that it is unsafe. It is that the callee is looking at two parameters and cannot see where either argument is hosted, which is a fact about the *caller's* frame that no rule inside the body will ever reach. + +So stop trying to decide it in the body. What the body can state is where the value **comes to rest**: `engine` lands at `this.engine`. That goes into the signature, and each call substitutes the paths it actually wrote and compares owners like any other store. `car!setEngine(engine)` on two locals of one block passes; the same call with an argument from a nested block does not. The one-hop-two-hop laundering that killed the `this`-rooted softening we considered is closed by the summary being transitive: a verb that passes its guest parameter on to `setEngine` records the resting place it inherits, the way [`effects.md` §5.2](https://github.com/zane-lang/spec/blob/6b668be1df1708cc23704254cc5aab57e1a365d2/spec/effects.md#52-call-graph-propagation) already propagates effects it does not itself perform. + +That summary is the price, and it is a real one. A verb's `&`-storing behaviour becomes public API — derived from the body, published with the signature, binding on every caller. It also inherits the shape of the carried-guest walk, and for the same reason: a resting place is a path of owning steps that stops at an `&`, because what a guest names is hosted somewhere the caller's argument path does not reach. Rooting a path *at* an `&T` parameter is fine — the caller names that one. Stepping through an `&` further along is not, and pushing on where the boundary sits turned up something we had stated too loosely one section earlier. "A field is owned by its root symbol's owner" is only true of a path made of owning steps; go through an `&` and the place you land on belongs to a tree the root does not name, and has no owner at all. It may be read. It may not be written. That is not a rule about signatures, it is the owner definition finally saying which paths it covers. The root rule bought its way out of exactly this by making the storing verb inexpressible, so there was nothing to summarise; we are now paying the bill it declined. What we are careful not to have reopened is [§1.8](https://github.com/zane-lang/spec/blob/6b668be1df1708cc23704254cc5aab57e1a365d2/spec/lifetimes.md#18-passing-a-host-to-a-t-parameter-downgrades-it-to-a-guest): the summary records *where* a parameter lands, never *whether* passing one downgrades the caller. That second question is still answered by the declared mode alone, and inferring it from the body is the mistake an earlier chapter of this story is named after. + +The orphan in [`memory.md` §2.9](https://github.com/zane-lang/spec/blob/6b668be1df1708cc23704254cc5aab57e1a365d2/spec/memory.md#29-function-parameters-swallow-and-guest) survives all of this, and now for a reason it can state. Binding a **swallowed** parameter into `&` storage is still illegal, and it is the single case no argument path rescues: the caller gave up its host in the act of passing, so at the call site there is no path left on the source side for the comparison to name. Every other refusal in this area is a comparison that failed. That one is a comparison that cannot be set up. + +What none of this touches is the other way a guest can be left naming nothing. Everything above is about a value **moving** away from what it points at. A host can also simply **die** while the guest is still there — an element removed from a container, a `#variant` slot changing case — and neither of those is a store, so no rule here reaches them. [`memory.md` §2.8.1](https://github.com/zane-lang/spec/blob/b486fd5f8c4d2ecdb14b8ef105394dc43aaf3bc6/spec/memory.md#281-a-guest-follows-the-object-an-overwritten-slot-carries-its-guests-forward) enumerates two fates for a hosted object, moved and overwritten, and says they "never compete, because an object cannot both leave and die in the same step." That is true, and the pair is not exhaustive: an overwrite leaves a successor occupant for the guest to carry forward to, and a removal leaves nothing. We are leaving that open deliberately rather than answering it here, because it is a different question with a different shape, and answering two at once is how the first version of this chapter got written three times. + +## The empty template: the design that would have needed no signatures + +The chapter above ends by paying a bill. It is worth writing down the design that never incurs it, because we did not reject that design for being wrong, and because this corner of the language is significant enough that a future revisit should start from what we already know rather than rediscover it. + +Push the owner idea one step harder and `init{ }` stops being a constructor at all. It becomes an **empty template** of the object under construction — a thing you fill in field by field, whose root is `init` itself: + +```zane +Main() { + init.io = IO() + init.terminal.io = init.io // legal: both rooted at `init` + return init +} +``` + +Now the root comparison holds everywhere with no exception, *including* inside a constructor, because `init` is a root like any other. Which makes `Terminal(io &IO) => init{io}` illegal — `init` and `io` do not share one. An `&` may only ever be created pointing inside the tree that will hold it, structurally, at the moment it is written. No deferral, no call-site substitution, no summary published in a signature. And no `init{ }` carve-out either, because the thing the carve-out existed to excuse cannot happen. + +That is a stricter language than the one we have and a strictly cheaper one to check. Every complaint this story makes about the raise enumeration it answers better than the owner rule does: nothing crosses a call boundary, so there is nothing to enumerate *and* nothing to publish. If the whole design had to be defended on the count of "how much does a compiler have to know about a function it cannot see", this version wins outright, because the answer is nothing. + +What it costs is **wiring**, in two places, and both are severe enough that we did not take it. + +A package can no longer ship a constructor that takes a guest. A `type Logger = #struct { out &Writer }` cannot come with a `Logger(out &Writer)`, because that constructor is precisely the illegal form. Whoever embeds a `Logger` has to reach into its `out` field from their own `init` to wire it — so every borrowing type leaks its internals to its user, and the encapsulation a library exists to provide is gone for the entire class of types that hold a reference. That is not a restriction on what a guest may point at. It is a restriction on who is allowed to write the pointing, and it lands on exactly the boundary a package is supposed to be. + +And a standalone `&`-holding local stops being expressible. `terminal Terminal(io)` on two siblings of one block — the most ordinary use of a guest there is, and the program this entire line of work started from — has no shared root to satisfy the rule and no aggregate to be embedded in. Under this design a type with an `&` field is usable only *inside* a larger tree whose constructor wires it. An object holding a reference is no longer a thing in its own right; it is a fragment of a tree, and saying so out loud is the honest description of what the design commits to. + +We are recording this rather than merely declining it because the trade is not settled forever. The owner rule buys library wiring and standalone locals with a per-signature summary that is **load-bearing for soundness**: omit a resting place and a dangling guest gets through. + +The effect summaries it is modelled on are not the reassuring analogy they look like, and the difference is worth being precise about. Effects have a safe direction that is also a **usable** one — [`effects.md` §5.4](https://github.com/zane-lang/spec/blob/b486fd5f8c4d2ecdb14b8ef105394dc43aaf3bc6/spec/effects.md#54-unknown-callees-are-conservatively-classified) classifies a callee the compiler cannot see at the strongest level, which costs precision and nothing else, and the program still compiles. Over-recording a resting place is safe in the same direction, but the conservative default for a callee whose body is unavailable would have to be *every parameter may come to rest anywhere reachable*, which refuses very nearly every call. There is no degraded mode to fall back to. The summary has to be **present and correct**, not merely bounded — a heavier obligation than the effects analogy suggests on first reading, and the reason this is a debt rather than a convenience. + +The empty template buys a checker with no interprocedural component at all, and pays in expressiveness that is visible in every program rather than in an obligation that is invisible until it breaks. If the summary turns out to be the wrong debt — too costly to compute, too brittle across separate compilation, too surprising once a library author discovers that where they store a parameter is public API — this is the road back. The two costs above are what a successor design would have to buy off, and it should buy them off knowingly rather than meet them again by accident.