From c36ef08c07f7bd2d0aa18ace977aa36210bea314 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 16:02:59 +0000 Subject: [PATCH 1/5] docs: a bare symbol is a guest source again, and the borrow mode goes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ban in `memory.md` §2.8.1 closed a question — what a guest means after the symbol it was minted from is moved from — by deleting the source. The companion it needed, the `'T` borrow mode, cost a sigil, a third passing mode, and an asymmetry between the value and reference subject. Recursion stopped depending on the ban when #155 boxed recursive members through a handle, and what remained was not worth the price. So a bare symbol is a guest source again, and `'T` is removed. §2.8.1 now answers the question the ban avoided: a guest names the object hosted at its source, travels with that object when the object is moved, and carries forward to the replacement when the object is destroyed in place by an overwrite. A move and an overwrite are different statements, so the two cases never compete. A reference-type parameter has two modes, `T` and `&T`. A reference-type subject is an implicit guest and takes no marker, matching the value subject's bare form. Binding a swallowed parameter into `&` storage stays illegal on scope grounds rather than for want of a guest source, and `lifetimes.md` §1.7 loosens: any parameter may root a returned `&`, because every parameter belongs to the call-site scope. Restores `'[A-Z]` to the retired-forms guard and retires the bare-symbol guard, which now matches only correct Zane. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017AYiJXjzCPEDfYRW1ZtxH7 --- CLAUDE.md | 48 ++++++------------ spec/adt.md | 2 +- spec/concurrency.md | 2 +- spec/effects.md | 2 +- spec/foundations.md | 4 +- spec/functions.md | 41 +++++++--------- spec/glossary.md | 22 ++++----- spec/lexical.md | 3 +- spec/lifetimes.md | 43 ++++++++-------- spec/memory.md | 117 +++++++++++++++++++++++--------------------- spec/syntax.md | 36 +++----------- spec/types.md | 8 +-- 12 files changed, 145 insertions(+), 183 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5186e28..418630f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,46 +50,28 @@ home `spec/generics.md`, casing rules `spec/lexical.md`). Several pre-redesign forms are now illegal and must never reappear. Grep for them — none should hit: ```sh -grep -RIn -E "Array\[|\[size\]|Array[0-9]+|Matrix10|\[rows\]|\[cols\]|inferred type generic|type-parameter symbol|root form" spec/ +grep -RIn -E "Array\[|\[size\]|Array[0-9]+|Matrix10|\[rows\]|\[cols\]|inferred type generic|type-parameter symbol|root form|'[A-Z]" spec/ ``` -`'[A-Z]` used to be on that list — it is **not** any more. A leading `'` is now -the **borrow** type marker (`'Node`), canonical home `spec/memory.md` §2.9, -surface form `spec/syntax.md` §2.3. Do not re-add it to the retired-forms grep. +`'[A-Z]` is back on that list. A leading `'` was the **borrow** type marker +(`'Node`) for one release of the design; the borrow mode for reference types has +since been removed, so `'` is again a character Zane's lexis does not use. A +reference-type parameter has exactly two modes, `T` and `&T` (`spec/memory.md` +§2.9), and `&` is the only marker a type may carry (`spec/syntax.md` §2.3). The only legitimate stray `<...>` is `Result` in `spec/error-handling.md` — Rust's type named as a comparison, not Zane's. -A second guard covers the memory model. A **bare symbol is not a guest source** -(`spec/memory.md` §2.8.1), so a spec example that mints an `&` from one is a -bug. Eyeball every hit of: +There used to be a second guard here, matching `&X = bareSymbol` to catch spec +examples that mint an `&` from a bare symbol. It is **gone**, and must not be +restored: a bare symbol is a guest source again (`spec/memory.md` §2.8), so +every line that guard was written to find is now correct Zane. What still +governs an `&` assignment is the scope comparison in `spec/lifetimes.md` §1.1, +and no grep can check that — it needs the declaration scopes of both sides. -```sh -grep -RIn -E "&[A-Z][A-Za-z0-9]*[[:space:]]*=[[:space:]]*_?[a-z][A-Za-z0-9]*[[:space:]]*(//.*)?[[:space:]]*$" spec/ -``` - -The pattern matches a **bare-symbol** right-hand side. Only one legal source is -excluded syntactically: a field access (`= car.engine`) never matches, because -`.` is outside the character class. The other legal source **does** match — an -`&T` parameter is written bare, so `r &Node = source` inside a callee is a hit -even though it is correct. Read every hit and keep it if any of these hold: - -- the right-hand side is an `&T` parameter of the enclosing verb (check the - signature, not the line); -- it is a deliberate `// ILLEGAL:` example; -- it is a grammar metavariable, as in `syntax.md`. - -Anything else is a real one to fix. - -Two details are load-bearing. The trailing `(//.*)?[[:space:]]*$` is what makes -the guard see the `// ILLEGAL: ...` examples; without it the end anchor skipped -every commented line, which is most of them. The `_?` catches a private -lowercase name (`_engine`) — Zane allows `_` only as a leading character, never -inside a name (`lexical.md` §4.1–4.2), so nothing more is needed there. - -Run both with `-R` on the directory, not a `spec/*.md` glob plus a bare -directory argument: `grep` prints `bench/: Is a directory` and silently skips -it otherwise. +Run the retired-forms grep with `-R` on the directory, not a `spec/*.md` glob +plus a bare directory argument: `grep` prints `bench/: Is a directory` and +silently skips it otherwise. Stories are exempt from both greps: `stories/` records the language as it was at each turn and is never rewritten to match the present spec. diff --git a/spec/adt.md b/spec/adt.md index b63f6ea..0904f29 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, including the guest-source restriction (see [`memory.md`](memory.md) §2.8); an owning member, boxed or not, does not. +- **`&` 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. 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/concurrency.md b/spec/concurrency.md index 378278f..c6c875a 100644 --- a/spec/concurrency.md +++ b/spec/concurrency.md @@ -121,7 +121,7 @@ A spawned call may **mutate** state only through a value-typed subject. A spawne A direct consequence is that reference types are never mutated by spawned work, so every concurrent **read** of the reference-typed object graph is safe by construction. ### 4.3 Single writer per storage location -For any one storage location, at most one live spawned call may hold a **mutable borrow** — the `!` subject of a spawned `mut` call. By §4.2 that subject is always value-typed, so the borrows this rule counts are value borrows; a reference-type `'T` borrow ([`memory.md`](memory.md) §2.9) never reaches a spawned `mut` subject. Two spawned calls that mutably borrow the same location are a compile-time error. Because value types carry no `&`, a location's identity is unambiguous — there is no hidden alias to obscure that two subjects denote the same slot — so this disjointness is checked at the spawn site by inspecting the subjects, not by tracing the program. The hosting scope may not access a location while a live spawn holds its mutable borrow; the borrow is released when that spawn completes (§4.1). +For any one storage location, at most one live spawned call may hold a **mutable borrow** — the `!` subject of a spawned `mut` call. By §4.2 that subject is always value-typed, so every borrow this rule counts is a value borrow — the only kind there is ([`memory.md`](memory.md) §2.9). Two spawned calls that mutably borrow the same location are a compile-time error. Because value types carry no `&`, a location's identity is unambiguous — there is no hidden alias to obscure that two subjects denote the same slot — so this disjointness is checked at the spawn site by inspecting the subjects, not by tracing the program. The hosting scope may not access a location while a live spawn holds its mutable borrow; the borrow is released when that spawn completes (§4.1). ### 4.4 Reads take a coherent snapshot A spawned call may read a value that another live spawn is mutating; the read observes a **coherent snapshot** of the value rather than blocking. Reading a shared value into a fresh binding — `snap VarType = shared` — is what takes the snapshot, and the copy is tear-free even when the writer is mid-update. This replaces lock-based serialization for in-memory value state, so a real-time reader never waits on a writer. Serialization still applies to external, capability-backed resources (§4.5). diff --git a/spec/effects.md b/spec/effects.md index 9414015..e92e03f 100644 --- a/spec/effects.md +++ b/spec/effects.md @@ -31,7 +31,7 @@ A side effect is any observable interaction beyond returning a value, including: A capability is an object whose methods model access to external state, such as a filesystem, logger, socket, clock, or random source. ### 2.3 `mut` -`mut` is the only effect modifier in the language. It appears on methods and grants write access to state reachable through `this`; the write lands on the caller's object or on state reachable from it. `this` is a **borrow** of the caller's slot for both kinds: a value-type `this` borrows the value, and a reference-type `this` written bare is a borrow of the object, `'` never being written on `this` (see [`functions.md`](functions.md) §2.4). +`mut` is the only effect modifier in the language. It appears on methods and grants write access to state reachable through `this`; the write lands on the caller's object or on state reachable from it. `this` is written bare for both kinds and carries no marker: a value-type `this` is a **borrow** of the caller's slot, and a reference-type `this` is an implicit **guest** to the object (see [`functions.md`](functions.md) §2.4). Neither takes hosting, so a `mut` call leaves the caller exactly as it found it. ### 2.4 Parameters are not mutable by default Parameters other than `this` are read-only. Mutation of another object must be expressed by calling a `mut` method on that object as the subject. A number parameter that resolves to a number value in body positions (see [`generics.md`](generics.md) §3.5) is a value-like binding and is read-only by default; mutating it requires a `mut` declaration. diff --git a/spec/foundations.md b/spec/foundations.md index 3baab75..980a515 100644 --- a/spec/foundations.md +++ b/spec/foundations.md @@ -91,13 +91,13 @@ A value type is copied on assignment, has no identity, and — the load-bearing What the axis does **not** decide is **recursion**. Either kind may contain itself, through a member the compiler boxes. That rule and the reasoning behind it belong to [`adt.md`](adt.md) §4. -Both kinds are mutated in place through a `mut` method, and the subject reaches the caller the same way in each: `this` is a *borrow* of the caller's slot, so a value is mutable without gaining identity and a reference object is mutable without minting a guest to it. Borrowing serves both worlds; what the reference world adds on top is `&`, for the cases where a callee must keep the object past the call. +Both kinds are mutated in place through a `mut` method, and the subject is written the same way in each — bare `this`, no marker — though what it is differs by kind: a value subject is a *borrow* of the caller's slot, so the value is mutable without gaining identity, while a reference subject is a *guest*, so the object is mutable without the method taking hosting. Neither consumes the caller's host. - **`#` is the only kind modifier**, applied uniformly to any type. See [`types.md`](types.md) §2 and [`adt.md`](adt.md) §2–§3. - **A value type is transitively value** (no reference-type or `&` field, anywhere downstream). This closed value world is specified by [`memory.md`](memory.md) §2.10. - **A value copy is deep.** Copying a value copies every payload it owns out of line into fresh storage, which is what lets a value type recurse without ever aliasing. See [`memory.md`](memory.md) §2.3. - **`&` rides on `#`.** A non-hosting `&` exists only for reference types; a value is shared by copy or by a scoped borrow, never by a stored `&`. See [`memory.md`](memory.md) §2.4. -- **A guest comes from a field, not a symbol.** A new `&` is minted only from a qualifying field access — base a place, not reached through a `'T` borrow — or from an `&T` parameter; a bare symbol is a place but never a guest source, so a local's own hosting slot has nothing pointing at it. Such a symbol may still be swallowed by a plain `T` parameter; the borrow mode is the only non-swallowing way to pass one. See [`memory.md`](memory.md) §2.8.1 and §2.9. +- **A guest follows its object.** A new `&` is minted from almost any place — a bare symbol, a field access, an `&T` parameter — and it names the object hosted there at that moment. That object may later move, and the guest travels with it. See [`memory.md`](memory.md) §2.8 and §2.8.1. - **Concurrency reads this axis.** A spawned call may mutate only a value-typed subject, because a value's transitive alias-freedom is exactly what lets the compiler rule out a data race from the signature alone. See [`concurrency.md`](concurrency.md) §4. > **Story:** [`stories/foundations.md`](../stories/foundations.md#identity-is-opt-in-one-axis-for-value-and-reference) — "Identity is opt-in: one axis for value and reference". diff --git a/spec/functions.md b/spec/functions.md index 84fc502..a252159 100644 --- a/spec/functions.md +++ b/spec/functions.md @@ -60,19 +60,19 @@ A method marked `mut` may write to any state reachable through `this`, whether t A write to `this` lands on the caller's object; how `this` reaches the caller differs by kind (see [`memory.md`](memory.md) §2.9): - 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 a **mutable borrow** too. 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 the borrow rather than the swallow it would be on an ordinary parameter, and **`'` is never written on `this`**. This is where a bare reference-type `this`'s implicit `&` went: the subject expression is usually a bare symbol, which is not a guest source ([`memory.md`](memory.md) §2.8.1), so the implicit mode became the borrow. Either way the caller stays a full host. +- 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 method that needs to keep the subject past the call — store it in an `&` field, or return it as `&T` ([`lifetimes.md`](lifetimes.md) §1.7) — declares `this &T` instead. That is a guest subject, so the call site must supply a guest source. +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. ```zane -Unit setScale(this Node, scale Float) mut { // reference subject: the implicit borrow +Unit setScale(this Node, scale Float) mut { // reference subject: the implicit guest this.scale = scale return Unit() } ``` ```zane -&Weapon mainWeapon(this &Player) => this.weapon // guest subject: may be returned as `&` +&Weapon mainWeapon(this Player) => this.weapon // the subject may be returned as `&` ``` ```zane @@ -107,16 +107,15 @@ subject!Pkg$method(arg) → Pkg$method(subject, arg) ``` ### 2.7 Parameters are read-only -Explicit parameters other than `this` are read-only: they cannot be assigned or marked `mut`. Mutation of another object must be expressed as a `mut` method call on that object as the subject. How each parameter is passed — the three reference modes, or a value borrow — is covered in [`memory.md`](memory.md) §2.9. +Explicit parameters other than `this` are read-only: they cannot be assigned or marked `mut`. Mutation of another object must be expressed as a `mut` method call on that object as the subject. How each parameter is passed — the two reference modes, or a value borrow — is covered in [`memory.md`](memory.md) §2.9. -### 2.8 Swallow, guest, and borrow method parameters -A reference-type method parameter selects one of three passing modes ([`memory.md`](memory.md) §2.9): +### 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, and the callee may store it into an `&` field or return it. -- A parameter declared as `'T` is a **borrow**: the caller may supply any place expression, a bare symbol included, and the callee gets read and `mut` access for the call and nothing more. +- 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 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). -Neither a swallowed nor a borrowed parameter may be bound into `&` storage: a swallowed value is hosted at the call site while an `&` field may outlive the call, and a borrow does not survive the call at all. A value-type parameter is always a read-only borrow. To pass a reference object for reading only, use `'T`. +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`. ```zane type Car = #struct { @@ -130,12 +129,12 @@ Unit setEngine(this Car, engine &Engine) mut { return Unit() } -// borrow parameter, read only -Int calculate(this Car, engine 'Engine) { - return this._value + engine.speed // legal: reading through the borrow +// `&` parameter used only to read +Int calculate(this Car, engine &Engine) { + return this._value + engine.speed // legal: reading through the guest } -// plain reference-type parameter swallows; a swallowed host is not a guest source +// 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 return Unit() @@ -148,8 +147,8 @@ Call syntax is uniform regardless of the parameter mode; only what the caller ma engine Engine() garage Garage() -car:calculate(engine) // legal: a bare symbol may be borrowed -car!setEngine(engine) // ILLEGAL: a bare symbol is not a guest source +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()) // ILLEGAL: a temporary is not a place expression ``` @@ -228,12 +227,11 @@ The return checker does not synthesize a constructor call for `Unit` or any othe ### 4.1 Overload identity is parameter types only Two declarations in the same package conflict when they have the same ordered parameter types. Parameter names, `this`, `mut`, and return type do not distinguish overloads. -Two overloads **MUST NOT** differ only by the **passing mode** at the same parameter position — that is, only by whether that position is `T`, `&T`, or `'T`, the subject included. Such declarations are illegal and the compiler **MUST** reject them with a compile-time error, for example: "illegal overload set: differs only by the passing mode on a parameter; rename one declaration or choose a single signature." +Two overloads **MUST NOT** differ only by the **passing mode** at the same parameter position — that is, only by whether that position is `T` or `&T`. Such declarations are illegal and the compiler **MUST** reject them with a compile-time error, for example: "illegal overload set: differs only by the passing mode on a parameter; rename one declaration or choose a single signature." ```zane Unit consume(this Car, engine Engine) Unit consume(this Car, engine &Engine) // ERROR: differs only by the passing mode -Unit consume(this Car, engine 'Engine) // ERROR: same ``` The mode changes what the caller must supply and what state the call leaves the caller in — not the shape of the call. Overloading on it would make `consume(e)` mean two different things about `e`'s ownership with nothing at the call site to tell them apart. @@ -417,12 +415,11 @@ Read-only methods and functions are effect-free with respect to their subject un | Read-only method | Called with `:`; may read but not write `this` | | Function | Identifier-named package-scope verb without `this`; no private-field privilege | | Block-bodied return | Every returning path uses `return expr`; `Unit` receives no fallthrough or bare-return exception | -| `&` method parameter | Caller must supply a guest source (never a bare symbol); callee may store it into `&` fields or return it | -| `'T` method parameter | Caller may supply any place expression, bare symbols included; read and `mut` access for the call only; **MUST NOT** be stored, returned, or moved | +| `&` method parameter | Caller must supply a guest source, which a bare symbol satisfies; callee may read, mutate, store it into `&` fields, or return it | | Plain `T` method parameter | Swallows; caller may supply a temporary and downgrades to a guest; callee **MUST NOT** bind it into `&` storage | -| Reference-type `this` | Never a swallow position: bare `this T` is the borrow subject — `'` is never written on `this` — and `this &T` is a guest subject, required to store or return the subject | +| Reference-type `this` | Never a swallow position: it is an implicit guest, and `&` is never written on `this` | | Subscript | Package-scope place projection written `(this T)[...] => placeExpr`; no explicit return type | -| Overload identity | Parameter types only; not names, return type, or `mut`; overloads differing only by the passing mode (`T` / `&T` / `'T`) at one position are illegal | +| Overload identity | Parameter types only; not names, return type, or `mut`; overloads differing only by the passing mode (`T` / `&T`) at one position are illegal | | Overload resolution phases | Direct match, then generic match, then implicit match; ambiguity within any one phase is an error | | Callable reference | Illegal; methods, functions, and operators are call-only and have no value form | | Lambda | Self-typed function value: explicit parameter types, return type, abort type, and `mut`; no capture | diff --git a/spec/glossary.md b/spec/glossary.md index 6a40587..bbf4828 100644 --- a/spec/glossary.md +++ b/spec/glossary.md @@ -65,7 +65,7 @@ This file gives short, reusable names to concepts that appear across multiple sp ## 3. Types, Storage, and Binding ### 3.1 place expression -- **Meaning:** A place expression denotes an existing, stable storage location. Being a place is necessary but not sufficient to mint an `&`: only an `&T` parameter and a field access of a place whose base chain does not pass through a `'T` borrow are guest sources, while bare symbols, `[]` expressions, and anything reached through a borrow are places that are excluded (§3.36). +- **Meaning:** A place expression denotes an existing, stable storage location. Almost every place may mint an `&` — a bare symbol, a field access of a place, an `&T` parameter — and only a `[]` expression is a place excluded from doing so (§3.36). - **Why this name:** The term names the expressions that refer to a storage "place" rather than to a temporary value. - **Canonical home:** [`memory.md`](memory.md) §2.8 @@ -195,7 +195,7 @@ This file gives short, reusable names to concepts that appear across multiple sp - **Canonical home:** [`functions.md`](functions.md) §8 ### 3.27 borrow -- **Meaning:** Non-hosting, non-escaping access to a caller's storage for the duration of a call. Every value type is passed this way — a value parameter is a read-only borrow, a value-type `mut` subject is a mutable borrow, and a value is copied only when bound into a fresh slot. A reference type may also be borrowed, written `'T`, which is the only non-swallowing way to pass a bare symbol (§3.36); a bare reference-type `this` is that borrow — `'` is never written on `this`. +- **Meaning:** Non-hosting, non-escaping access to a caller's value storage for the duration of a call. It is how every value type is passed, and the only way one is passed: a value parameter is a read-only borrow, a value-type `mut` subject is a mutable borrow, and a value is copied only when bound into a fresh slot. A reference type is never borrowed — it is swallowed or guested (§3.37). - **Why this name:** The callee is lent the caller's storage for the call and gives it back at return — it does not host it and cannot keep it. Unlike a guest, the borrow itself has no anchor or tether and cannot be stored, returned, or used as a move source — a restriction on the borrow, not on the value read through it, which a value type may still copy into a fresh slot. - **Canonical home:** [`memory.md`](memory.md) §2.9 @@ -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, and it may be minted only from an `&T` parameter or a field access whose base is a place and whose base chain does not pass through a `'T` borrow (§3.36). 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 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). - **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 @@ -239,18 +239,18 @@ This file gives short, reusable names to concepts that appear across multiple sp - **Why this name:** "Consume" names taking the value for good; "relay" names passing the hosting role through and handing it back out. - **Canonical home:** [`lifetimes.md`](lifetimes.md) §1.8 -### 3.36 guest source restriction -- **Meaning:** A new `&` may be minted only from an `&T` parameter, or from a field access whose base is a place and whose base chain does not pass through a `'T` borrow parameter. A **bare symbol** — an identifier standing alone rather than as the base of a field access — is a place expression but never a guest source, so no guest can point at a local's own hosting slot and that slot stays free to be overwritten or moved from. A bare symbol may still be swallowed by a plain `T` parameter; `'T` is the only **non-swallowing** mode that accepts one (§3.27, §3.37). The borrow exclusion runs the same way: a guest minted from a borrowed object's field would escape the call just as surely as the borrow itself. -- **Why this name:** The rule constrains the *source* of a guest — where one may come from — and nothing about what a guest can survive once minted; a guest to a field still follows its host across overwrites and rehosting. -- **Canonical home:** [`memory.md`](memory.md) §2.8.1 +### 3.36 guest source +- **Meaning:** A place expression a new `&` may be minted from: a **bare symbol**, a field access whose base is a place, or an `&T` parameter. Only a `[]` expression is a place excluded, and temporaries are not places at all. The guest names the object hosted at that source when it is minted; if the object is moved the guest follows it, and if the object is destroyed by an overwrite of its slot the guest carries forward to the replacement. +- **Why this name:** The term names the *source* end — where a guest may come from — separately from what a guest survives once minted, which is the anchor system's business. +- **Canonical home:** [`memory.md`](memory.md) §2.8 ### 3.37 passing mode -- **Meaning:** Which of three 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** (storable and returnable; requires a guest source), `'T` **borrows** it (read and `mut` for the call only; accepts any place, bare symbols included). The subject parameter (§3.38) selects between the borrow and `&T` only: a bare `this T` is the borrow and `'` 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 all three, and only the caller's obligations and resulting state differ. +- **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. +- **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 ### 3.38 subject / subject parameter / subject expression -- **Meaning:** The **subject** is the object a method is called on. The **subject parameter** is `this`, the declaration's first parameter, whose surface form fixes the passing mode (§3.37) — bare for the borrow, `this &T` for the guest, never `'`. The **subject expression** is what stands left of `:` or `!` at the call site and supplies the object; it must satisfy what that form requires, which is why a bare symbol works for a bare `this` but not for `this &T` (§3.36). +- **Meaning:** The **subject** is the object a method is called on. The **subject parameter** is `this`, the declaration's first parameter, always written bare: a reference-type subject is an implicit guest, a value-type subject a borrow (§3.27), and no marker is written on `this` for either. The **subject expression** is what stands left of `:` or `!` at the call site and supplies the object. - **Why this name:** Grammar, matching `verb` (§3.22): a call reads *subject–verb–object*, and the subject is what the verb acts from. The three senses are one word in ordinary use because they usually coincide; the spec separates them where a rule holds of the declaration but not the object, or the other way round. - **Canonical home:** [`functions.md`](functions.md) §2.1 @@ -265,7 +265,7 @@ This file gives short, reusable names to concepts that appear across multiple sp - **Canonical home:** [`memory.md`](memory.md) §2.3 ### 3.41 move-source -- **Meaning:** An expression denoting a hosting value that the expression is entitled to consume, and therefore the only thing that may be moved into a hosting position. Three forms qualify: a **direct host symbol**; a **hosting verb result**, from a verb whose return type is a hosting `T`; and a **`#variant` case form**, `Variant.case(payload)` on a **reference** sum, which is built-in syntax rather than a verb but produces a fresh value nothing hosts yet. A *value* `variant` case form is not one — a value sum is copied rather than hosted, so there is no hosting to transfer. Neither is an `&` value, a `'T` borrow, a field access, nor a container element access. +- **Meaning:** An expression denoting a hosting value that the expression is entitled to consume, and therefore the only thing that may be moved into a hosting position. Three forms qualify: a **direct host symbol**; a **hosting verb result**, from a verb whose return type is a hosting `T`; and a **`#variant` case form**, `Variant.case(payload)` on a **reference** sum, which is built-in syntax rather than a verb but produces a fresh value nothing hosts yet. A *value* `variant` case form is not one — a value sum is copied rather than hosted, so there is no hosting to transfer. Neither is an `&` value, a value-type borrow, a field access, nor a container element access. - **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 diff --git a/spec/lexical.md b/spec/lexical.md index 6e44780..eaecedd 100644 --- a/spec/lexical.md +++ b/spec/lexical.md @@ -98,7 +98,6 @@ Certain leading characters are reserved and are not ordinary identifier starts: | Sigil | Meaning | Canonical home | |---|---|---| | `&` | Guest type (`&Node`) | [`memory.md`](memory.md) §2 | -| `'` | Borrow type (`'Node`), parameter positions only | [`memory.md`](memory.md) §2.9 | | `@` | Reserved compiler namespace (`@primitives$`, `@concepts$`) | [`syntax.md`](syntax.md) §2.7 | | `$` | Package-member separator (`packageName$member`) | [`packages.md`](packages.md) §1 | @@ -185,7 +184,7 @@ Because the parser always knows whether it is inside a type-expression body or a | Type parameter | An uppercase name (`T`) declared `T Type` (in a type's `<>` header or inline in a verb); referenced bare | | Digits | Legal in a name except as the first character; carry no special meaning | | Leading `_` | A field is private to `this` methods for its type; a named package-scope declaration is private to its package | -| Leading `&` / `'` | `&Node` is a guest type (storage, parameter, and return positions); `'Node` is a borrow type (parameter positions only); mutually exclusive on one type | +| Leading `&` | `&Node` is a guest type, legal in storage, parameter, and return positions; it is the only marker a type may carry, and it is never written on `this` | | `<>` disambiguation | A type (uppercase) on the left means a type argument list; a value (lowercase) means comparison | | Member terminator | `;` terminates every member of a `struct`/`variant` body (marked or unmarked with `#`) and every arm of a `match` block; always trailing, inline or multiline; newlines are insignificant there | | Value separator | `,` separates elements of a value collection (arrays, `enum`, call/constructor args, `init{}` fields, generic args, `match` case groups); never trailing | diff --git a/spec/lifetimes.md b/spec/lifetimes.md index 6a2d1cc..db5c8d3 100644 --- a/spec/lifetimes.md +++ b/spec/lifetimes.md @@ -20,11 +20,11 @@ r &Node = outerTree.root } ``` -The two conditions are independent, and the second only ever arises for sources the first admits. A bare symbol fails the first condition outright: +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: ```zane node Node() -r &Node = node // ILLEGAL: a bare symbol is not a guest source +r &Node = node // legal: same scope ``` The compiler compares declaration scopes. It does not perform borrow inference or lifetime annotation solving. @@ -43,7 +43,7 @@ A verb that returns a hosting `T`, and a case form that builds a `#variant`, bot The following are **not** move-sources: - an `&` value, including a verb that returns `&T` (guests are non-hosting and cannot transfer hosting; see [`memory.md`](memory.md) §2.4) -- a `'T` borrow parameter (a borrow neither hosts the object nor outlives the call; see [`memory.md`](memory.md) §2.9) +- a value-type parameter or a value-type `mut` subject, both of which are borrows of the caller's slot (see [`memory.md`](memory.md) §2.9) - a field access such as `car.engine` - a container element access such as `cars[1]` - any other access path that projects into an existing host @@ -115,7 +115,7 @@ A parameter's value is exempt. Because a parameter belongs to the call-site scop ### 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. -This is stated for the swallowing mode because that is the only mode where hosting crosses the call boundary at all. A `&T` guest parameter and a `'T` borrow parameter never take hosting ([`memory.md`](memory.md) §2.9), so nothing about the argument's lifetime changes when either is used; the call-site scope keeps hosting throughout. +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. @@ -129,7 +129,7 @@ Unit enterMatch(player Player) { `startMatch` puts `player` into the local `island`. Because `player` belongs to the call site, `island` draining does not destroy it; the value lives until `enterMatch`'s own scope drains. Inside `enterMatch`, `player` was passed to `startMatch` by hosting access, so `enterMatch`'s `player` symbol is now a guest to it (§1.8) — and so is the argument symbol in whatever called `enterMatch`. -For `&` fields specifically, the callee must declare the corresponding parameter as `&T` ([`memory.md`](memory.md) §2.9). Binding a plain `T` parameter into `&` storage is a compile-time error, because a swallowed value is hosted at the call site while an `&` field lives with the object that holds it, which may outlive the call. Binding a `'T` parameter into `&` storage is a compile-time error for a stronger reason: a borrow ends with the call. The callee's signature therefore signals which mode applies, and so whether a guest source ([`memory.md`](memory.md) §2.8) is required at the call site. +For `&` fields specifically, the callee must declare the corresponding parameter as `&T` ([`memory.md`](memory.md) §2.9). Binding a plain `T` parameter into `&` storage is a compile-time error, because a swallowed value is hosted at the call site while an `&` field lives with the object that holds it, which may outlive the call. The callee's signature therefore signals which mode applies, and so what the caller gives up. > **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#consumed-or-borrowed-the-parameter-that-lives-at-the-call-site) — "Consumed or borrowed: the parameter that lives at the call site". @@ -149,21 +149,21 @@ 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 guest parameter -A function may return an `&T` only when the returned guest is rooted in one of the function's **`&T` parameters** and is itself a guest source ([`memory.md`](memory.md) §2.8) — the parameter used bare, or a field access whose base chain reaches it. `this` counts as a parameter for this rule when it is declared `this &T`. +### 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. ```zane -&Weapon getWeapon(this &Player) => this.weapon +&Weapon getWeapon(this Player) => this.weapon ``` -The other two parameter modes are not roots. A `'T` borrow ends with the call, so a guest rooted in one would outlive the access it was granted. A swallowing `T` parameter is a bare symbol in the call-site scope, and a bare symbol is not a guest source at all. +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). -```zane -&Weapon fromBorrow(this Player) => this.weapon // ILLEGAL: a borrow is not a guest root +A **local** is the case this rule excludes, and it is excluded by lifetime rather than by what may mint a guest: +```zane &Node bad() { value Node() - return value // ILLEGAL: a local is neither a parameter nor a guest source + return value // ILLEGAL: value is hosted by the body scope, which drains at the return } ``` @@ -171,7 +171,7 @@ The other two parameter modes are not roots. A `'T` borrow ends with the call, s > **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#where-a-guest-may-be-rooted) — "Where a guest may be rooted". ### 1.8 Passing a host to a `T` parameter downgrades it to a guest -A plain reference-type parameter `T` takes its argument by **hosting access**. Passing a hosting value to such a parameter uses that value as a move-source (§1.2), so the caller's symbol downgrades to a guest (§1.6) — **whatever the callee does with the value**. The parameter's declared type is the whole contract: `T` means the caller gives up hosting; `&T` and `'T` ([`memory.md`](memory.md) §2.9) both mean the caller stays a full host. Nothing in the callee's body changes the outcome the signature already states. +A plain reference-type parameter `T` takes its argument by **hosting access**. Passing a hosting value to such a parameter uses that value as a move-source (§1.2), so the caller's symbol downgrades to a guest (§1.6) — **whatever the callee does with the value**. The parameter's declared type is the whole contract: `T` means the caller gives up hosting; `&T` ([`memory.md`](memory.md) §2.9) means the caller stays a full host. Nothing in the callee's body changes the outcome the signature already states. ```zane car Car() @@ -182,14 +182,13 @@ truck Truck(car) // ILLEGAL: car is a guest, not a move-source The value outlives the call (§1.5), so the downgraded guest always resolves to a live object. Where the value comes to rest — moved into another parameter's hosting storage, moved into the return, or held in the call-site scope — the guest follows through the anchor ([`memory.md`](memory.md) §4.5). -A verb treats a reference-type host argument in one of four ways, each fixed by its signature: +A verb treats a reference-type host argument in one of three ways, each fixed by its signature: -- it **borrows** the object — declares the parameter `'T` ([`memory.md`](memory.md) §2.9); the caller stays a full host and the callee gets read and `mut` access for the call only. This is the mode for a bare symbol, which no other non-swallowing mode accepts (§2.8.1 of [`memory.md`](memory.md)). -- it takes a **guest** — declares the parameter `&T`; the caller stays a full host, and the callee may keep the guest past the call by storing or returning it. Only a guest source can supply one. +- 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 **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. -Borrowing and taking a guest leave the caller as host; relaying and consuming both downgrade it, differing only in whether a hosting handle is handed back. So to keep or recover hosting, pass `'T` or `&T`, or bind a relayed return: +Taking a guest leaves the caller as host; relaying and consuming both downgrade it, differing only in whether a hosting handle is handed back. So to keep or recover hosting, pass `&T` or bind a relayed return: ```zane weapon Weapon() @@ -214,7 +213,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` — or `&T`, when the callee needs to keep it — 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. 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". @@ -270,14 +269,14 @@ Because scope rules (§1.1) prevent guests from outliving their hosts, the runti | Concept | Rule | |---|---| -| `&` return | Returned `&T` must be rooted in an `&T` parameter and be a guest source; `this &T` counts; a `'T` borrow and a swallowing `T` are not roots | -| 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 never a guest source | -| Move-source | A direct host symbol (local or parameter), a hosting verb result, or a `#variant` case form; not an `&`, a `'T` borrow, a field, a container element, or any other access path | +| `&` 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 | +| 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 | | 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 **borrows** it (`'T`, caller keeps it; the only non-swallowing mode a bare symbol may feed), 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 | +| 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 | | Return value | A return need not be bound; an unbound reference-type result floats to the enclosing scope as an anonymous host, while an ignored value-type result is discarded | | Destruction | Deterministic and delayed until the hosting scope drains | diff --git a/spec/memory.md b/spec/memory.md index be0d1ba..23b0c83 100644 --- a/spec/memory.md +++ b/spec/memory.md @@ -13,8 +13,8 @@ Zane eliminates dangling guests by combining single hosting, lexical lifetime ru - **`Overwritable hosts`.** A reference-type host is directly initialized and may later be overwritten. - **`Guests ride on reference types`.** An `&` — a **guest** — is a non-hosting handle to a **reference type** (a `#`-marked type); a value type has no identity to anchor, so it is shared by copy or scoped borrow, never by a stored guest. - **`A value copy is deep`.** A value owns whatever it holds out of line, so copying one copies its boxed payloads into fresh storage instead of sharing them. That is what lets a value type recurse without ever aliasing (§2.3, §2.10). -- **`Bare symbols are not guest sources`.** A new guest may be minted only from a field access or from an `&T` parameter — never from a bare symbol (§2.8). A local's own hosting slot is therefore never the thing a guest points at. -- **`Three passing modes`.** A reference-type parameter is written `T` to **swallow** it, `&T` to take a **guest**, or `'T` to **borrow** it for the call (§2.9). +- **`A guest follows its object`.** A new guest may be minted from any place expression that names hosted storage — a bare symbol, a field access, or an `&T` parameter (§2.8). When that object is moved, its guests travel with it; when a slot's occupant is destroyed by an overwrite, guests to that slot observe the replacement (§2.8.1, §4.5). +- **`Two passing modes`.** A reference-type parameter is written `T` to **swallow** it or `&T` to take a **guest** (§2.9). - **`Repointable guests`.** A guest is non-hosting storage that can point at different hosts over time. - **`Lexical lifetime enforcement`.** Guest assignment and rehosting are checked using declaration scope alone (see [`lifetimes.md`](lifetimes.md) §1). - **`Deterministic destruction`.** Objects are destroyed when their hosting scope drains; there is no tracing garbage collector (see [`lifetimes.md`](lifetimes.md) §2). @@ -23,7 +23,7 @@ Zane eliminates dangling guests by combining single hosting, lexical lifetime ru The source language and runtime use separate terms: an object lives in a **host**, and a **guest** (`&T`) may access it without storing it or controlling its lifetime. Internally, each guest is represented by a **tether** that resolves through an **anchor**. Moving the object updates its terminal anchor or links an older anchor to the destination anchor, so existing tethers — and therefore guests — continue to reach it. -These rules fit together mechanically. Hosts are the only storage that controls destruction. A guest may be minted only from a field or an `&` parameter — never from a temporary, and never from a bare symbol. Lexical scope checks ensure the host outlives every guest derived from it. When an object is rehosted or a host is overwritten, guests stay valid. Internally, their tethers follow the host's anchor rather than a fixed object address. +These rules fit together mechanically. Hosts are the only storage that controls destruction. A guest may be minted only from a place that names hosted storage — never from a temporary. Lexical scope checks ensure the host outlives every guest derived from it. When an object is rehosted or a host is overwritten, guests stay valid. Internally, their tethers follow the host's anchor rather than a fixed object address. > **Story:** [`stories/memory.md`](../stories/memory.md#safety-without-a-collector-and-without-lifetimes) — "Safety without a collector and without lifetimes". @@ -99,9 +99,9 @@ A guest may be declared as: - a function or constructor parameter - a function return type -An `&` type is legal in storage sites (local symbols, fields, nested storage types), function parameter positions, and function return-type positions. The borrow type `'T` (§2.9) is legal in parameter positions only: a borrow is not storage and never escapes its call. +An `&` type is legal in storage sites (local symbols, fields, nested storage types), function parameter positions, and function return-type positions. -Declaring an `&` symbol is legal, but the restriction in §2.8 governs what may initialize it: a guest is minted from a field or from an `&T` parameter, not from a bare symbol. +Declaring an `&` symbol is legal; §2.8 governs what may initialize it. > **Story:** [`stories/memory.md`](../stories/memory.md#two-vocabularies-host-and-guest-above-anchor-and-tether) — "Two vocabularies: host and guest above anchor and tether". @@ -126,18 +126,17 @@ The following are place expressions: - a named local, field-backed, or hosting/`&` storage symbol such as `engine` - a field access whose base is a place, such as `car.engine` or `this.engine` - a subscript expression `list[index]` when `list` is a place expression and `[]` is defined as a place projection for that subject type -- an `&T` guest parameter or a `'T` borrow parameter inside the callee body (§2.9) +- an `&T` guest parameter inside the callee body (§2.9) -Only some place expressions may mint a new guest. A new `&` value may be minted from: +Almost every place expression may mint a new guest. A new `&` value may be minted from: -- a field access whose base is a place **and whose base chain does not pass through a `'T` borrow parameter**, such as `car.engine` or `this.engine` on a guest subject +- a **bare symbol** naming hosted storage — a local, a parameter, or a package constant +- a field access whose base is a place, such as `car.engine` or `this.engine` - an `&T` parameter -Everything else is rejected. In particular: +Two things are rejected: -- A **bare symbol** is never a guest source, even though it is a place expression (§2.8.1). - A `[]` expression is never a guest source, even though it is a place expression. -- A field access rooted in a `'T` borrow parameter is never a guest source. A borrow does not escape its call (§2.9), and it would escape just as surely inside a guest minted from one of its fields as it would on its own. - Temporaries and other value-only expressions are not place expressions at all. Constructor calls and ordinary function results such as `Engine()` and `makeEngine()` are not places. ```zane @@ -147,6 +146,7 @@ engine &Engine = Engine() // ILLEGAL: Engine() is a temporary, not a place exp ```zane car Car() r &Engine = car.engine // legal: field access on a place +s &Car = car // legal: a bare symbol naming a host ``` ```zane @@ -163,62 +163,69 @@ Non-`&` host bindings may be initialized from any expression, including temporar engine Engine() // legal: plain host binding; Engine() temporary is materialized into engine ``` -### 2.8.1 A bare symbol is not a guest source +### 2.8.1 A guest follows the object; an overwritten slot carries its guests forward -A **bare symbol** — an identifier naming a local, a parameter, or a package constant, standing alone rather than as the base of a field access — **MUST NOT** be used to mint a new `&`. +Minting a guest from a place gives a guest to **the object hosted there at that moment**, and the guest tracks that object's identity rather than the storage it was read from (§4). Two things can subsequently happen to that object, and the program text says which: -```zane -engine Engine() -r &Engine = engine // ILLEGAL: a bare symbol is not a guest source -inspect(engine) // ILLEGAL if inspect takes `&Engine` -``` +- The object is **moved** — some other host takes it (see [`lifetimes.md`](lifetimes.md) §1.2). The object is alive at its new home, and every guest to it follows it there through the anchor path (§4.5). +- The object is **destroyed** by an overwrite of the slot it lived in (§2.2). The slot's hosting identity continues across the replacement, so guests minted from that slot observe the new occupant. + +The two cases never compete, because an object cannot both leave and die in the same step. A guest is therefore always reading something live, and which live thing it reads is decidable from the source. -The reason is that a bare symbol's hosting slot is exactly the storage the language lets you overwrite most freely (§2.2, [`lifetimes.md`](lifetimes.md) §1). Without this rule a program can write: +The case worth spelling out is a bare symbol, because a symbol's hosting slot is the storage the language lets you overwrite and move from most freely: ```zane main Player() second Player() -guest &Player = main // ILLEGAL under this rule -second = main +guest &Player = main // legal: a bare symbol is a guest source +second = main // the object moves out of main's slot into second's ``` -`second = main` moves the object out of `main`'s slot, and `main` downgrades to guest state ([`lifetimes.md`](lifetimes.md) §1.6). What `guest` should then denote — the object that left, or the slot it left from — has no answer that is right in both directions, and every candidate answer costs either a rule the programmer has to carry or machinery the runtime has to pay for. Removing the source removes the question: line 3 is a compile-time error, so no guest ever depends on a bare symbol's slot. +After the move, `guest` denotes the object, which now lives in `second`. So does `main` itself, which downgrades to a guest to that same object ([`lifetimes.md`](lifetimes.md) §1.6). The two names agree; there is nothing to choose between. -Nothing is lost by it. A guest exists so that storage which does not own an object can still reach it — an `&` field, a container element, an `&T` parameter inside a callee. A bare symbol is *already* in scope wherever a guest to it could be declared, so the guest never buys reach that the symbol itself did not already have. What a bare symbol is genuinely needed for is passing an object into a call. It may still be swallowed by a plain `T` parameter, which takes hosting outright; where the call must *not* take hosting, the borrow mode `'T` is what carries it (§2.9), reading and mutating the caller's object for the duration of the call without minting a guest to it. +They part company only if the emptied slot is put back to work: -A **field** is a different matter and stays a legal source. A field belongs to an object whose own lifetime the host system already tracks, and a guest to `car.engine` follows that field's host through the anchor path (§4.5) when the field is overwritten or the containing object is rehosted. This is what makes the restriction narrow: it constrains where guests come from, not what they can survive. +```zane +main = Player() // main's slot hosts a new, unrelated object +``` -> **Story:** [`stories/memory.md`](../stories/memory.md#where-a-new-ref-may-come-from) — "Where a new ref may come from". -> **Story:** [`stories/memory.md`](../stories/memory.md#the-slot-that-could-not-be-pointed-at) — "The slot that could not be pointed at". +`main` now names the new object and `guest` still names the moved one, which is alive in `second`. That is the first bullet doing its job: the move carried the old object's identity away with it, so re-hosting the slot begins a fresh identity rather than capturing the guests of the old one. Contrast an overwrite, where nothing moves: + +```zane +car Car() +r &Engine = car.engine +car.engine = Engine() // the old engine is destroyed in place; r observes the new one +``` -### 2.9 Function parameters: swallow, guest, and borrow +Here the occupant of `car.engine` died and the slot's identity continued, so `r` carries forward to the replacement. The question a reader has to answer is only ever *did the object leave, or did it die* — and a move and an overwrite are different statements. -A **borrow** is non-hosting, non-escaping access to a caller's storage for the duration of a call. A borrow is never itself storage: unlike a guest (§2.4) it has no anchor, and it **MUST NOT** be stored in a field or returned. It exists only while the call runs. +> **Story:** [`stories/memory.md`](../stories/memory.md#where-a-new-ref-may-come-from) — "Where a new ref may come from". +> **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". -That restriction is on the borrow, not on what is read through one. A value type is *always* passed this way — a value-type parameter is a **read-only borrow** of the caller's slot — and binding through that borrow into a fresh slot (an assignment, a new declaration, or a field or return store) **copies** the value. The copy is a new value that outlives the call perfectly well; what does not escape is the borrow. A reference type has no such copy, so a `'T` borrow leaves nothing behind at all. +### 2.9 Function parameters: swallow and guest -A **reference type** parameter has three passing modes, one per surface form. The subject parameter `this` is not one of these positions and has its own rule, below: +A **reference type** parameter has two passing modes, one per surface form. The subject parameter `this` is not one of these positions and has its own rule, below: | 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) | store it in `&` storage or return it as `&T` | -| Borrow | `'T` | any place expression, **including a bare symbol** | read and mutate it for the duration of the call only | +| Guest | `&T` | a guest source (§2.8) — any place but a `[]` expression | read and mutate it, store it in `&` storage, or return it as `&T` | - 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. Because a bare symbol is not a guest source, an `&T` parameter can only be fed from a field, a container's stored guest, or another `&T` parameter. -- A parameter declared as `'T` is a **borrow**: the caller may supply any place expression, a bare symbol included, and the callee gets read and `mut` access for the call and nothing more. A `'T` parameter **MUST NOT** be stored in `&` storage, returned as `&T`, or used as a move-source, and neither may a field reached through it (§2.8); `'T` is not a legal storage, field, or return type. Passing a host to a `'T` parameter leaves the caller a full host: nothing downgrades. +- 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. -`'T` is the mode that keeps ordinary calls ordinary. Under §2.8.1 a bare local cannot feed an `&T` parameter, so a verb that merely wants to read or mutate a caller's object declares that object `'T`: +`&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: ```zane -Float topSpeed(engine 'Engine) => engine.speed +Float topSpeed(engine &Engine) => engine.speed engine Engine() -s Float = topSpeed(engine) // legal: a bare symbol may be borrowed +s Float = topSpeed(engine) // legal: engine stays a full host ``` -Passing a value by borrow is the semantic model for both worlds; where a read-only borrow is indistinguishable from a copy, the compiler may still pass a small value by copy, the same latitude placement has (§3.5). The distinction becomes observable under concurrent sharing, where a spawned reader sees the borrowed value live (see [`concurrency.md`](concurrency.md) §4.4). +A **value type** parameter has no such choice, because there is nothing to host and no identity to guest. It is a **borrow**: non-hosting, non-escaping read-only access to the caller's slot for the duration of the call. A borrow is not storage — it has no anchor and cannot be stored in a field or returned — but that restriction is on the borrow, not on what is read through one. Binding through a borrow into a fresh slot (an assignment, a new declaration, or a field or return store) **copies** the value (§2.3). The copy outlives the call perfectly well; what does not escape is the borrow. A value-type `mut` subject is a **mutable** borrow on the same terms (see [`functions.md`](functions.md) §2.4). Neither `&` nor any other marker is written on a value-type parameter: the borrow is what such a parameter is. + +Passing a value by borrow is the semantic model rather than an optimization; where a read-only borrow is indistinguishable from a copy, the compiler may still pass a small value by copy, the same latitude placement has (§3.5). The distinction becomes observable under concurrent sharing, where a spawned reader sees the borrowed value live (see [`concurrency.md`](concurrency.md) §4.4). ```zane type Car = #struct { @@ -239,33 +246,31 @@ Unit setSpare(this Car, engine Engine) mut { return Unit() } -// borrow parameter: a reference-type object read without consuming it and without minting a guest -Int inspect(this Car, engine 'Engine) { +// `&` parameter used only to read: the caller keeps hosting and nothing is stored +Int inspect(this Car, engine &Engine) { return this._value + engine.speed } ``` -**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) — chooses between two of the three modes rather than all three: it is a **borrow** written bare, or a **guest** written `this &T` when the method stores the subject past the call or returns it as `&T` (see [`functions.md`](functions.md) §2.4). `'` is **never** written on `this`. +**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. -So bare `T` does not mean the same thing in both positions — on an ordinary parameter it swallows, on `this` it borrows — because `this` was never a swallow position to begin with. That much predates the borrow mode: a bare reference-type `this` used to be an implicit **guest**, likewise never swallowed. What changed is only *which* non-swallowing mode it is, and it moved to the borrow because the subject expression at a call site is usually a bare symbol, which §2.8.1 no longer admits as a guest source. Value and reference subjects now agree: `this` carries at most one marker, `&`, and its absence means borrow. +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 or borrowed parameter into `&` storage is illegal. A swallowed value is hosted at the call site while an `&` field lives with the object that holds it — which may outlive the call. A borrow does not survive the call at all: +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: ```zane Unit setEngineSwallowed(this Car, engine Engine) mut { - this.engine = engine // ILLEGAL: a swallowed host is not a guest source - return Unit() -} - -Unit setEngineBorrowed(this Car, engine 'Engine) mut { - this.engine = engine // ILLEGAL: a borrow is not a guest source and does not escape the call + this.engine = engine // ILLEGAL: a swallowed host may not be bound into `&` storage return Unit() } ``` +Returning one as `&T` is a different matter and is legal ([`lifetimes.md`](lifetimes.md) §1.7): the returned guest lands in the caller, which is the very scope the swallowed value belongs to, so §1.1 compares the two directly at the call site. + This rule preserves uniform call syntax. The call site writes `consume(e)`, `inspect(e)`, or `setEngine(e)` identically; only the callee's signature says which mode applies and therefore what the caller must supply and what state the caller is left in. > **Story:** [`stories/memory.md`](../stories/memory.md#three-ways-to-hand-over-an-object) — "Three ways to hand over an object". +> **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". ### 2.10 Value-downstream enforcement (transitive value-only field restriction) @@ -612,15 +617,15 @@ A single global free stack and frontier require synchronization under concurrent | Value copy | Copies the whole existing value: inline bytes, plus a fresh allocation and recursive copy of every boxed payload the value owns, so two values never share storage | | `&` (guest) | Guest-only non-hosting storage; stores one tether, may be repointed, copied by value, and returned, but can never directly host a `T` | | Host-capable guest state | After rehosting, the old hosted bytes cease to be live and a slot declared as `T` stores the terminal tether as a guest while retaining enough storage to host another `T` later | -| Place expression | Existing stable storage: a named symbol, a field access of a place, a place-projection subscript of a place, or an `&`/`'` parameter | -| New `&` value | May be minted only from a field access of a place or an `&` parameter; bare symbols, `[]` expressions, and temporaries are rejected | -| Guest source restriction | A bare symbol is a place but never a guest source (§2.8.1); a guest to a local's own hosting slot cannot be written, so overwriting that slot leaves no guest behind | +| Place expression | Existing stable storage: a named symbol, a field access of a place, a place-projection subscript of a place, or an `&` parameter | +| New `&` value | May be minted from a bare symbol, a field access of a place, or an `&` parameter; `[]` expressions and temporaries are rejected | +| What a guest follows | The object hosted at the source when the guest was minted: it travels with that object when the object is moved, and carries forward to the replacement when the object is destroyed by an overwrite of its slot (§2.8.1) | | `&` parameter | Declares that the caller must supply a guest source; the parameter is place-like inside the callee and may be stored or returned | -| Borrow | Non-hosting, non-escaping access to a caller's storage for the duration of a call; no anchor, not storable, not returnable, not a move-source | +| Borrow | Non-hosting, non-escaping access to a caller's value storage for the duration of a call; no anchor, not storable, not returnable, not a move-source | | Value-type parameter | Always a read-only borrow; caller need not supply a place; copied only when the parameter — an existing place — is itself bound into a fresh slot (assignment, declaration, field or return store), never merely by being passed | -| Reference-type parameter | `T` swallows (hosting access; passing a host downgrades the caller's symbol to a guest whatever the body does — see [`lifetimes.md`](lifetimes.md) §1.8); `&T` takes a guest, which only a guest source can supply; `'T` borrows any place, bare symbols included, and leaves the caller a full host | -| `'T` position | Parameter positions only; never a storage, field, or return type | -| Reference-type `this` | Never a swallow position: bare `this T` is the borrow subject — `'` is never written on `this` — and `this &T` is a guest subject a method may store or return | +| Reference-type parameter | `T` swallows (hosting access; passing a host downgrades the caller's symbol to a guest whatever the body does — see [`lifetimes.md`](lifetimes.md) §1.8); `&T` takes a guest, and leaves the caller a full host | +| Swallowed parameter into `&` storage | Illegal — not for want of a guest source but because the swallowed value is hosted at the call site while an `&` field may outlive the call; returning it as `&T` is legal | +| Reference-type `this` | Never a swallow position: it is an implicit guest, and `&` is never written on `this` | | Value-downstream enforcement | Value types may contain only primitives and other value types, transitively — never a reference (`#`) or `&` field, because a reference type is made to be moved rather than copied; recursion is **not** barred, since a boxed member is placement rather than a reference-type field | | `&` targets reference types | An `&T` requires `T` to be a reference type; a value is shared by copy or scoped borrow, never by a stored `&` | | Symbol declaration | Must be directly initialized | diff --git a/spec/syntax.md b/spec/syntax.md index a868b60..c988aa7 100644 --- a/spec/syntax.md +++ b/spec/syntax.md @@ -145,26 +145,15 @@ TypeName ```zane &TypeName -'TypeName ``` -`&TypeName` is a **guest** type. It is legal in storage sites (local-variable declarations, fields, and nested storage types such as the example below), as well as in function and constructor parameter positions and return-type positions. +`&TypeName` is a **guest** type. It is legal in storage sites (local-variable declarations, fields, and nested storage types such as the example below), as well as in function and constructor parameter positions and return-type positions. It is the only marker a type may carry. ```zane Array<&Node, n> ``` -`'TypeName` is a **borrow** type. It is legal in **parameter positions only** — including the `this` position — and never as a storage, field, element, or return type. - -```zane -Float topSpeed(engine 'Engine) => engine.speed - -held 'Engine = ... // ILLEGAL: a borrow is not storage -'Engine makeEngine() // ILLEGAL: a borrow is not a return type -Array<'Node, n> // ILLEGAL: a borrow is not an element type -``` - -`&` and `'` are mutually exclusive on one type: `&'Node` and `'&Node` are not type forms. See [`memory.md`](memory.md) §2.9 for the semantics of the three passing modes. +See [`memory.md`](memory.md) §2.9 for the semantics of the two passing modes. ### 2.4 Type expressions @@ -255,17 +244,12 @@ ReturnType?AbortType[this SubjectType, ParamType, ...] mut The abort type stays attached to the return type, exactly as in a declaration's `ReturnType?AbortType name(...)` header. -Reference-typed parameters and returns use the ordinary type form. A parameter slot accepts all three passing modes — `ParamType`, `&ParamType`, and `'ParamType` — while a return slot accepts a bare or `&` type only (§2.3): +Reference-typed parameters and returns use the ordinary type form. A parameter slot accepts both passing modes — `ParamType` and `&ParamType` — and a return slot accepts a bare or `&` type (§2.3): ```zane ReturnType[&ParamType, ...] -ReturnType['ParamType, ...] -&ReturnType[this &SubjectType, &ParamType, ...] -ReturnType[this SubjectType, 'ParamType, ...] mut -``` - -```zane -'ReturnType[ParamType] // ILLEGAL: a borrow is not a return type +&ReturnType[this SubjectType, &ParamType, ...] +ReturnType[this SubjectType, ParamType, ...] mut ``` `mut` is legal only when the first parameter is `this`. @@ -297,17 +281,15 @@ type Tree = #variant { leaf Int; node Tree; } // reference sum type; `node` r ```zane ReturnType name(param ParamType, ...) { body } ReturnType name(param &ParamType, ...) { body } -ReturnType name(param 'ParamType, ...) { body } ReturnType?AbortType name(param ParamType, ...) { body } ReturnType name(param ParamType, ...) => expr ReturnType name(param &ParamType, ...) => expr -ReturnType name(param 'ParamType, ...) => expr ReturnType?AbortType name(param ParamType, ...) => expr ReturnType name(param T Type, ...) { body } ReturnType name(param Container, ...) { body } ``` -A **reference-type** parameter independently selects one of the three passing modes (see [`memory.md`](memory.md) §2.9): bare `ParamType` swallows, `&ParamType` takes a guest, `'ParamType` borrows. A **value-type** parameter has no such choice — it is always a read-only borrow — so `&` and `'` are not written on one. +A **reference-type** parameter independently selects one of the two passing modes (see [`memory.md`](memory.md) §2.9): bare `ParamType` swallows, `&ParamType` takes a guest. A **value-type** parameter has no such choice — it is always a read-only borrow — so `&` is not written on one. A function, method, or constructor has no `<>` parameter header. It introduces a type or number parameter inline within its value parameters, at the parameter's first **marked** occurrence — on a value parameter's type (`param T Type`) or inside a value parameter's nested type (`param Container`) — and references it bare elsewhere, including in positions written earlier such as the return type. Inline parameters are inferred from the value arguments at the call; the same `Type` / `Number` concepts are used as in a type definition's header (§2.5). See [`generics.md`](generics.md) §3 and §5. @@ -327,12 +309,11 @@ ReturnType name(this SubjectType, param &ParamType, ...) mut => expr ReturnType?AbortType name(this SubjectType, param ParamType, ...) => expr ReturnType?AbortType name(this SubjectType, param ParamType, ...) mut => expr ReturnType name(this SubjectType, param ParamType, ...) { body } -ReturnType name(this &SubjectType, param ParamType, ...) { body } ``` `this` is legal only in the first parameter position. A declaration is a method if and only if its first parameter is named `this`. -The subject takes at most one marker, `&`. A bare `this SubjectType` is the **borrow** subject, and `this &SubjectType` is written when the method stores or returns the subject as a guest; `'` is **never** written on `this`, for either kind of type. A value subject is likewise a borrow of the caller's slot, mutable when the method is `mut`, and always written bare. See [`functions.md`](functions.md) §2.4. +The subject takes **no** marker, for either kind of type: `&` is never written on `this`. A reference-type subject is an implicit guest, which may be stored or returned as `&T` without asking; a value subject is a borrow of the caller's slot, mutable when the method is `mut`. See [`functions.md`](functions.md) §2.4. `=> expr` returns `expr`, including when `expr` has type `Unit`. @@ -454,7 +435,6 @@ A lambda literal is a function declaration with the name removed. It writes its ReturnType() { body } ReturnType(param ParamType, ...) { body } ReturnType(param &ParamType, ...) { body } -ReturnType(param 'ParamType, ...) { body } ReturnType() => expr ReturnType(param ParamType, ...) => expr ReturnType?AbortType(param ParamType, ...) { body } @@ -466,7 +446,7 @@ ReturnType(this SubjectType, param ParamType, ...) => expr ReturnType(this SubjectType, param ParamType, ...) mut => expr ``` -A lambda literal omits only the function name. `this` is legal only in the first parameter position. `mut` is legal only when the first parameter is `this`. Parameters and the subject carry the same three passing modes as a named verb (§3.1–§3.2). +A lambda literal omits only the function name. `this` is legal only in the first parameter position. `mut` is legal only when the first parameter is `this`. Parameters and the subject carry the same passing modes as a named verb (§3.1–§3.2). Examples: diff --git a/spec/types.md b/spec/types.md index 7c20604..cb07add 100644 --- a/spec/types.md +++ b/spec/types.md @@ -281,7 +281,7 @@ Every field of the target type **MUST** be assigned exactly once, either explici Constructors are not methods. They create new values rather than mutating an existing subject, so `mut` does not apply. ### 3.9 `&` fields require `&` constructor parameters -An `&` field is legal only in a reference type (`#struct`/`#variant`), since a value type is transitively value (§2.2). A constructor that assigns a value to an `&` field must declare the corresponding parameter as `&T` — a `'T` borrow will not do, because a borrow ends with the call while the field outlives it. The caller must then supply a **guest source** under [`memory.md`](memory.md) §2.8: a field access on a place, or an `&T` parameter. A bare symbol, a temporary, and a `[]` expression are all rejected. +An `&` field is legal only in a reference type (`#struct`/`#variant`), since a value type is transitively value (§2.2). A constructor that assigns a value to an `&` field must declare the corresponding parameter as `&T` — a swallowing `T` will not do, because the swallowed value is hosted at the call site while the field outlives it ([`memory.md`](memory.md) §2.9). The caller must then supply a **guest source** under [`memory.md`](memory.md) §2.8: a bare symbol, a field access on a place, or an `&T` parameter. A temporary and a `[]` expression are rejected. ```zane package Vehicle @@ -312,13 +312,13 @@ car Car(garage.spare) // legal: a field access is a guest source ```zane engine Engine() -car Car(engine) // ILLEGAL: a bare symbol is not a guest source +car Car(engine) // legal: a bare symbol is a guest source car Car(Engine()) // ILLEGAL: a temporary cannot initialize an `&` field ``` -The object an `&` field points at therefore has to be hosted somewhere that outlives the bare local — in another object's field, most often. Recursion is not one of these cases: a recursive member is an ordinary owning field the compiler boxes, so it needs no `&` and no guest source at all (see [`adt.md`](adt.md) §4). +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). -> **Story:** [`stories/memory.md`](../stories/memory.md#the-slot-that-could-not-be-pointed-at) — "The slot that could not be pointed at". +> **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". A reference type whose fields are all plain hosts does not require `&` parameters: From eca621a38e6b161471fdfeeb9cfabed298fca79f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 16:05:40 +0000 Subject: [PATCH 2/5] docs(stories): the ban that cost more than the question it closed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two chapters: one in `stories/memory.md` retiring the bare-symbol guest source ban and the borrow mode that was built to route around it, and one in `stories/lifetimes.md` for the return-root rule that went back to "any parameter" once both were gone. The memory chapter names what the ban's own ledger left out — it counted one rejected program and not the sigil, the third passing mode, or the value/reference split in `this` that the next chapter spent to make the ban survivable. It then answers the five-liner instead of outlawing it, using the ban chapter's own argument turned around: a guest to a bare symbol buys no reach, so nobody minting one meant "watch the slot". Both costs we are accepting are stated — merging stays reachable, and a signature no longer promises non-escape. Records what we declined: the proposal that prompted this also made a moved-from symbol spent. The two readings coincide at the move rather than compete, so the downgrade stays. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017AYiJXjzCPEDfYRW1ZtxH7 --- README.md | 4 ++-- spec/functions.md | 1 + spec/lifetimes.md | 2 ++ stories/lifetimes.md | 12 ++++++++++++ stories/memory.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 60 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 958808f..8e21fea 100644 --- a/README.md +++ b/README.md @@ -66,8 +66,8 @@ The spec states *what* the language does; the **why** lives in a parallel set of | [`stories/adt.md`](stories/adt.md) | [`spec/adt.md`](spec/adt.md) — splitting `enum` from `variant` against the hype, the shared struct body, escaping the matcher machine with case overloads and the turn to a central `match` block, matching variants rather than patterns, keeping enum data outside the members, reducing a match group to sugar for one arm per case, building a variant by naming a case rather than calling a constructor, the bindings that existed only to be pointed at, and making a recursive member an owned child boxed through a hosting handle rather than an `&`, and the sum that could not contain itself until a value copy became deep | | [`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, 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, and narrowing a returned guest's root to a guest parameter once borrows arrived | +| [`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/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/functions.md b/spec/functions.md index a252159..b5d7e7f 100644 --- a/spec/functions.md +++ b/spec/functions.md @@ -35,6 +35,7 @@ Int scaledId(this Node, factor Int) { ``` > **Story:** [`stories/functions.md`](../stories/functions.md#what-does-a-receiver-receive) — "What does a receiver receive?". +> **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". ### 2.2 `this` grants private-field access Naming the first parameter `this` is the only thing that makes a declaration a method. That token grants access to `_`-prefixed fields on the subject type regardless of which package declares the method; home-package status does not matter. The same parameter type written with another name is a function and does not grant private-field access. diff --git a/spec/lifetimes.md b/spec/lifetimes.md index db5c8d3..45079c1 100644 --- a/spec/lifetimes.md +++ b/spec/lifetimes.md @@ -31,6 +31,7 @@ The compiler compares declaration scopes. It does not perform borrow inference o > **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". ### 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: @@ -169,6 +170,7 @@ A **local** is the case this rule excludes, and it is excluded by lifetime rathe > **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". ### 1.8 Passing a host to a `T` parameter downgrades it to a guest A plain reference-type parameter `T` takes its argument by **hosting access**. Passing a hosting value to such a parameter uses that value as a move-source (§1.2), so the caller's symbol downgrades to a guest (§1.6) — **whatever the callee does with the value**. The parameter's declared type is the whole contract: `T` means the caller gives up hosting; `&T` ([`memory.md`](memory.md) §2.9) means the caller stays a full host. Nothing in the callee's body changes the outcome the signature already states. diff --git a/stories/lifetimes.md b/stories/lifetimes.md index 1e0d235..344872f 100644 --- a/stories/lifetimes.md +++ b/stories/lifetimes.md @@ -183,3 +183,15 @@ The same reasoning had to be pushed one step further than the rule's own text, a Nothing else moved. The downgrade rule ([§1.6](https://github.com/zane-lang/spec/blob/b10eaed/spec/lifetimes.md#16-moved-symbols-downgrade-to--values-and-are-no-longer-movable)) still turns a moved-from symbol into a readable guest, and it is worth being clear that this is not in tension with the new source rule: the downgrade is something the language does to a slot, not a guest a program mints, and the reader never writes it. The signature-is-the-whole-contract rule ([§1.8](https://github.com/zane-lang/spec/blob/b10eaed/spec/lifetimes.md#18-passing-a-host-to-a-t-parameter-downgrades-it-to-a-guest)) simply grew a fourth entry — a verb may now *borrow* an argument as well as take a guest, relay, or consume — and the entry it grew is, satisfyingly, the one that leaves the caller in the strongest position: still the host, with the callee unable to keep anything. The honest cost here is not a rejected program but a redistribution of where the reader's attention has to go. Before, a reference parameter's mode was visible in one bit — `&` or not — and the question "what happens to my object" had two answers. Now there are three signatures to read and three answers, and the difference between two of them (`&T` and `'T`) is invisible at the call site by design, because the whole point is that the call site should not have to care. The programmer who wants to know whether their host survives a call still reads exactly one thing, the signature; there is just more in it than there used to be. + +## The root rule that got shorter + +[The previous chapter](#where-a-guest-may-be-rooted) tightened the return rule and was pleased with itself for it. It had said, of the older and looser version, that "a parameter" was the right rule "when there was only one kind of reference parameter to be rooted in" — and with three kinds, it named the guest parameter specifically and gave each rejected mode its own reason. Both of those reasons have since been withdrawn, and the rule they were propping up went back to the sentence it started as. + +The withdrawal is told in [the memory story](memory.md#the-ban-that-cost-more-than-the-question-it-closed): a bare symbol is a guest source again and the borrow mode is gone. Follow the consequences through §1.7 and both exclusions evaporate. A swallowing `T` parameter was excluded because it is a bare symbol and a bare symbol was not a source — but it is one now. A `'T` borrow was excluded because it expires with the call — but there is no `'T`. + +What is interesting is that the rule which survives is not the old one restored by accident. It has a reason the old one never articulated, and the reason is stronger than either exclusion it replaces: **a parameter belongs to the call-site scope** ([§1.5](https://github.com/zane-lang/spec/blob/c36ef08/spec/lifetimes.md#15-parameters-belong-to-the-call-site)). Both modes have that property, which is why both are roots. A guest rooted in a parameter names something hosted in the very scope the returned value is landing in, so [the scope comparison](https://github.com/zane-lang/spec/blob/c36ef08/spec/lifetimes.md#11--assignment-uses-host-scope) has both declarations in front of it at the call site and rejects the cases that would dangle — including the one that looks alarming, where the argument was declared in a block nested below the binding that takes the return. Nothing about that check reaches across the call boundary, which is the property this document is organised around. + +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. diff --git a/stories/memory.md b/stories/memory.md index c21bd3a..1699328 100644 --- a/stories/memory.md +++ b/stories/memory.md @@ -229,3 +229,46 @@ The residuals we do accept, and they are stated where they land rather than buri The interesting part is the line that did **not** move. Deep copy dissolves the *stated* reason for banning a reference-type field in a value type — "copying would silently duplicate hosting" — and for a while it looked as though `List` inside a `struct` should follow value recursion through the same door. It should not, and the reason is sharper than the one it replaces: **a reference type exists in order not to be copied.** That is not a side effect of hosting; it is the whole point of the mark. A reference type has one host at a time, a stable identity that guests resolve through, and it reaches a new place by being *moved*. A copy of a value holding one could only duplicate the object — minting a second identity, so guests tethered to the original quietly fail to follow — or share it, so two values reach one host and "exactly one host" stops describing anything. Both outcomes destroy the thing the `#` was for. `List` and `String` are reference types, so they are covered by the same sentence, and they stay out ([`memory.md` §2.10](https://github.com/zane-lang/spec/blob/804631ffa94d4f7df08b38f057e2e4bb95add545/spec/memory.md#210-value-downstream-enforcement-transitive-value-only-field-restriction)). A boxed member never had that problem, which is why it walks through while a `List` field does not. A box holds an instance of the member's **own** type — it is placement, not a change of type, and placement was already unobservable. Nothing about it says "copy me by sharing." So the `#` axis comes out of this meaning something tighter than it did: identity, aliasing, and copy-versus-move. Not "may contain itself." That question turned out to belong to layout, and layout answered it the same way for both kinds. + +## The ban that cost more than the question it closed + +Two chapters up, we removed a guest source and then immediately built a passing mode to replace what removing it broke. That is a shape worth being suspicious of at the time, and we were not suspicious enough. This chapter is about going back. + +The ban's own ledger, written when it landed, said the cost was "one surprising rejection at the root of a tree" — you could no longer write `Expr.flip(leaf)` and had to root your structure in a field. That accounting was honest about the thing it was looking at and blind to everything else, because the rest of the cost had already been spent one chapter later and was not being counted as cost. [The borrow mode](#three-ways-to-hand-over-an-object) exists for exactly one reason: the ban made `topSpeed(engine)` unwritable, and something had to make it writable again. So the true bill was a new sigil, a third passing mode on every signature, a `this` whose meaning depended on which kind of type it named, and an `&`-return rule that had to explain separately why each of the two rejected modes was not a root. None of that appears in the chapter that took the credit for the ban being cheap. + +Then the one item that *was* counted disappeared on its own. [Making a recursive member an owned child](#the-region-takes-the-boxes-and-a-box-asks-for-what-it-is) took `&` out of recursive spines entirely: a recursive case is boxed through a handle, so a tree is built from owning edges and needs no guest source anywhere. The rule that a recursive structure must be rooted in a field went with it. The single cost the ban's ledger had named was gone, and what remained on the page was the machinery built to pay a bill nobody was charging any more. + +That is the pressure that reopened it, and the judgment that settled it was blunt: the `'` semantic was not worth having as a separate thing to learn. Not "the borrow is wrong" — the borrow is exactly right for value types, where it has always been what a parameter *is*. The objection is to a second sigil whose whole job is to route around a restriction we chose. Delete the restriction and the sigil has no work left. + +### Answering the question instead of deleting it + +Reverting means the five-liner comes back, and this time it has to be answered rather than made illegal: + +```zane +main Player() +second Player() +guest &Player = main +second = main +``` + +The old chapter said the anchor system gives an answer — the guest follows the object — and that the answer is "defensible but not obviously right", because a reader who wrote `&Player = main` might have meant *watch that variable*. The flaw in that worry is sitting in the same chapter, three paragraphs down, in the argument that was supposed to make the ban cheap: **a guest to a bare symbol never buys any reach.** A bare symbol is already in scope everywhere a guest to it could be declared. If you wanted to watch the variable, you can watch the variable — write `main`. The only reason to mint a guest at all is to carry the reference somewhere the symbol does not go: into a field, into a container, into a callee that keeps it. Every one of those is the *object* reading. The ambiguity we were protecting against was between one intention that needs a guest and one that has a shorter spelling, and we had removed the wrong one. + +So [`memory.md` §2.8.1](https://github.com/zane-lang/spec/blob/c36ef08/spec/memory.md#281-a-guest-follows-the-object-an-overwritten-slot-carries-its-guests-forward) now states it as a rule with two branches, which is what the runtime was doing all along. A guest names the object hosted at its source when it was minted. If that object is **moved**, the guest goes with it. If that object is **destroyed** by an overwrite of the slot it lived in, the slot's hosting identity continues and the guest carries forward to the replacement. The two never compete, because an object cannot both leave and die in one step, and the program text says which happened: a move is one statement and an overwrite is another. What a reader has to carry is a single question — *did it leave, or did it die* — and the answer is legible at the line. + +The case that looks like a puzzle and is not is re-hosting an emptied slot. After `second = main`, both `guest` and `main` denote the moved object; `main` downgraded to a guest to it, so the two agree and there is nothing to choose. Write `main = Player()` afterwards and they part: `main` names the new object, `guest` still names the old one, which is alive in `second`. That is not an ambiguity, it is two names for two objects — the move carried the old identity away, so the slot begins a fresh one rather than inheriting the guests of the thing that left. + +This is where we declined the rest of the package that was on the table. The proposal that accompanied the revert also made a moved-from symbol **spent** — unreadable after the move — on the grounds that a readable moved-from symbol keeps the "watch that variable" reading alive to compete with the object reading. We do not think it does. At the moment of the move the two readings *coincide*, which is the opposite of competing; they only diverge after a subsequent re-host, and by then the divergence is the ordinary fact that two different names denote two different objects. Making the symbol spent to prevent that would cost [downgrade, not poison](../stories/lifetimes.md#downgrade-not-poison-why-there-is-no-use-after-move-read), a rule we like for its own sake and that has nothing to do with guest sources. So the downgrade stays and the revert is narrower than the proposal that prompted it. + +### What comes back, and what it costs + +With bare symbols pointable again, `'T` has nothing left to do and goes. A reference-type parameter is `T` or `&T` ([`memory.md` §2.9](https://github.com/zane-lang/spec/blob/c36ef08/spec/memory.md#29-function-parameters-swallow-and-guest)) — swallow it, or take a guest — and `topSpeed(engine)` works by declaring `engine &Engine` and passing the local as it stands. The `'` character returns to being unused, which is where it was before we borrowed it from Rust to name something Rust uses it for a different reason. + +The subject goes back to being an implicit guest. `this` now takes no marker at all, for either kind of type ([`functions.md` §2.4](https://github.com/zane-lang/spec/blob/c36ef08/spec/functions.md#24-mutating-methods-use-mut)), and `this &T` is gone rather than merely redundant — a guest subject may already be read, mutated, stored, or returned, so a marker selecting it would select the only option. This is the one place where we are giving something up that we said we liked. The borrow-mode chapter called it "the pleasing part": a `mut` subject was a mutable borrow of the caller's slot whether the type was a value or a reference, and the two worlds agreed. They no longer do. A value subject is a borrow, a reference subject is a guest, and they are written identically because in both cases the subject is simply the object you called the method on and neither kind has a choice to express. The unification was real and it is lost; what it was bought with was a sigil on every non-consuming reference parameter in the language, and that is the wrong exchange rate. + +One rule got shorter rather than longer, and it is the one we did not expect. Returning an `&` used to require a root in an `&T` parameter specifically, with the other two modes excluded for two different reasons — a borrow expires with the call, a swallowed parameter is a bare symbol and therefore not a source at all. Both exclusions are gone, and the rule underneath them turns out to be simpler than either: **any parameter is a root, because a parameter belongs to the call-site scope** ([`lifetimes.md` §1.7](https://github.com/zane-lang/spec/blob/c36ef08/spec/lifetimes.md#17-returned--values-must-be-rooted-in-a-parameter)). A guest rooted in one therefore names something hosted in the very scope the return value lands in, so [the scope comparison](https://github.com/zane-lang/spec/blob/c36ef08/spec/lifetimes.md#11--assignment-uses-host-scope) settles it at the call site with no special case. A local is still not a root, and now for the honest reason — its host is the body scope, which drains at the return — rather than for a reason about where guests may come from. + +Two costs are real and we are choosing them with our eyes open. + +The first is that **merging stays reachable**. The whole reason the bare-symbol slot was awkward is that it can be moved from while something points at it, and that is exactly the situation that puts two live anchor identities on one payload. Forwarding anchors, the retirement stacks, and the target-kind discriminator are therefore permanent rather than provisional — the alternatives that would have deleted them all worked by restricting the source language, and we have just decided the source language is where we least want to pay. That is a defensible trade in the specific direction Zane leans: the merge machinery is written once, by the person implementing the compiler, and the restriction was paid by every program that builds a structure and hands out a reference to it. + +The second is that **a signature no longer promises non-escape**. With three modes, `'T` told a caller that the callee could not keep the reference; `&T` told them it might. With two, `&T` covers both and the caller cannot tell them apart by reading the header. Nothing about this is unsafe — [the scope rule](https://github.com/zane-lang/spec/blob/c36ef08/spec/lifetimes.md#11--assignment-uses-host-scope) is what keeps a guest from outliving its host, and it does not care what the callee intended — but it is a genuine loss of legibility, and it is the one argument for keeping `'T` that survives the ban being lifted. We are not keeping it, because a mode that exists purely to document intent is a heavy way to document intent. If that turns out to be the wrong call, the thing to reach for is a non-escaping *annotation* on an `&T` parameter, not a third way of passing an argument. From 9a49fbd87253bf79fec3dab573fca6601c8f97d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 16:20:39 +0000 Subject: [PATCH 3/5] docs: fix a stale summary row and three overclaims from review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `functions.md` summary still called `this` a mutable borrow for both subject kinds, contradicting §2.4's implicit guest. The exact drift CLAUDE.md warns about, missed on self-review. - `memory.md` §1 said a guest is minted from a place that "names hosted storage", which an `&T` parameter does not; list the three forms. - `memory.md` §2.9 glossed the guest-source column as "any place but a `[]` expression", which reads as forbidding `inspect(weapons[1])` on a stored guest. §2.8 excludes `[]` from *minting*, not from being read; drop the gloss rather than restate it badly. - `CLAUDE.md` claimed every line the retired bare-symbol guard matched is now correct Zane. It isn't — §1.1 still rejects one whose target host is declared deeper than the `&`. The point is that a match proves nothing either way, which is why the pattern is gone. Also renumbers the guard list, which still said "both greps" and "a third guard" after the bare-symbol guard was removed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017AYiJXjzCPEDfYRW1ZtxH7 --- CLAUDE.md | 17 ++++++++++------- spec/functions.md | 2 +- spec/memory.md | 4 ++-- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 418630f..a9c7c23 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,19 +64,22 @@ The only legitimate stray `<...>` is `Result` in `spec/error-handling.md` There used to be a second guard here, matching `&X = bareSymbol` to catch spec examples that mint an `&` from a bare symbol. It is **gone**, and must not be -restored: a bare symbol is a guest source again (`spec/memory.md` §2.8), so -every line that guard was written to find is now correct Zane. What still -governs an `&` assignment is the scope comparison in `spec/lifetimes.md` §1.1, -and no grep can check that — it needs the declaration scopes of both sides. +restored: a bare symbol is a guest source again (`spec/memory.md` §2.8), so a +match no longer indicates a defect. It does not indicate a correct line either — +what still governs an `&` assignment is the scope comparison in +`spec/lifetimes.md` §1.1, and a bare-symbol assignment still fails it when the +target's host is declared deeper than the `&`. No grep can decide that; it needs +the declaration scopes of both sides. The pattern separated nothing worth +separating, which is why it is gone rather than reworded. Run the retired-forms grep with `-R` on the directory, not a `spec/*.md` glob plus a bare directory argument: `grep` prints `bench/: Is a directory` and silently skips it otherwise. -Stories are exempt from both greps: `stories/` records the language as it was -at each turn and is never rewritten to match the present spec. +Stories are exempt from every grep here: `stories/` records the language as it +was at each turn and is never rewritten to match the present spec. -A third guard covers one term. The subject of a method — the object it is +A second guard covers one term. The subject of a method — the object it is called on — is the **subject**, never the *receiver*; `receiver` was Smalltalk residue naming a message Zane does not have, and it was renamed throughout `spec/` (canonical home `functions.md` §2.1, glossary §3.38). The word survives diff --git a/spec/functions.md b/spec/functions.md index b5d7e7f..c9592e3 100644 --- a/spec/functions.md +++ b/spec/functions.md @@ -412,7 +412,7 @@ Read-only methods and functions are effect-free with respect to their subject un | Verb | A callable; its kind is selected by markers, and each marker unlocks a capability | | Capability markers | `this` first → method (private access); name is a type → constructor (`init{ }`, implicit return); symbol name → operator; no name → lambda | | Method | Package-scope verb whose first parameter is `this` | -| `mut` method | Called with `!`; `this` is a mutable borrow of the caller's slot for both value and reference subjects; may mutate state reachable through `this` | +| `mut` method | Called with `!`; may mutate state reachable through `this`, which is a mutable borrow of the caller's slot for a value subject and an implicit guest for a reference subject | | Read-only method | Called with `:`; may read but not write `this` | | Function | Identifier-named package-scope verb without `this`; no private-field privilege | | Block-bodied return | Every returning path uses `return expr`; `Unit` receives no fallthrough or bare-return exception | diff --git a/spec/memory.md b/spec/memory.md index 23b0c83..b01c844 100644 --- a/spec/memory.md +++ b/spec/memory.md @@ -23,7 +23,7 @@ Zane eliminates dangling guests by combining single hosting, lexical lifetime ru The source language and runtime use separate terms: an object lives in a **host**, and a **guest** (`&T`) may access it without storing it or controlling its lifetime. Internally, each guest is represented by a **tether** that resolves through an **anchor**. Moving the object updates its terminal anchor or links an older anchor to the destination anchor, so existing tethers — and therefore guests — continue to reach it. -These rules fit together mechanically. Hosts are the only storage that controls destruction. A guest may be minted only from a place that names hosted storage — never from a temporary. Lexical scope checks ensure the host outlives every guest derived from it. When an object is rehosted or a host is overwritten, guests stay valid. Internally, their tethers follow the host's anchor rather than a fixed object address. +These rules fit together mechanically. Hosts are the only storage that controls destruction. A guest may be minted from a stable place that reaches a hosted object — a bare symbol, a field access, or an `&T` parameter — never from a temporary. Lexical scope checks ensure the host outlives every guest derived from it. When an object is rehosted or a host is overwritten, guests stay valid. Internally, their tethers follow the host's anchor rather than a fixed object address. > **Story:** [`stories/memory.md`](../stories/memory.md#safety-without-a-collector-and-without-lifetimes) — "Safety without a collector and without lifetimes". @@ -209,7 +209,7 @@ 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) — any place but a `[]` expression | read and mutate it, store it in `&` storage, or return it as `&T` | +| Guest | `&T` | a guest source (§2.8) | read and mutate it, store it in `&` storage, or return it as `&T` | - 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. From 03c30771774292ce4f6bfb31721a90fce57f9b0b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 06:20:44 +0000 Subject: [PATCH 4/5] docs(stories): the ban never removed the merge it was standing next to MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the argument that makes the revert obvious rather than merely defensible: the bare-symbol ban did not eliminate the situation the anchor-merge machinery exists for. #152's canonical example reaches two live anchor identities on one payload using only field accesses, which the ban explicitly kept legal — so every line of it was writable on the day the ban shipped, and forwarding anchors were permanent regardless. The ban bought a narrower explanation, not a smaller runtime. That corrects this chapter's own cost list, which had booked "merging stays reachable" as something the revert spends. It was already reachable. What the revert actually forecloses is the class of proposals that would have deleted the machinery by restricting the source language further, which is a real decision but a different one. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017AYiJXjzCPEDfYRW1ZtxH7 --- stories/memory.md | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/stories/memory.md b/stories/memory.md index 1699328..a9d5589 100644 --- a/stories/memory.md +++ b/stories/memory.md @@ -240,6 +240,47 @@ Then the one item that *was* counted disappeared on its own. [Making a recursive That is the pressure that reopened it, and the judgment that settled it was blunt: the `'` semantic was not worth having as a separate thing to learn. Not "the borrow is wrong" — the borrow is exactly right for value types, where it has always been what a parameter *is*. The objection is to a second sigil whose whole job is to route around a restriction we chose. Delete the restriction and the sigil has no work left. +### The ban did not close the hole it was standing next to + +There is a harder version of that objection, and it is the one that makes the revert obvious rather than merely defensible. **The ban did not remove the situation the machinery exists for.** Not "removed it at a cost we later judged too high" — did not remove it at all. + +The situation is two live anchor identities resolving to one payload. A payload carries a single backpointer, so it can be found from one cell; a second cell naming it cannot be updated on the next relocation. That is what forwarding anchors, the retirement stacks, and the target-kind discriminator are all for. It arises when a move has anchored objects on **both** sides. And here it is, reached without a bare symbol anywhere: + +```zane +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() +} +``` + +Both guests are minted from **field accesses**. Fields were never banned — the ban's own chapter is emphatic that "a **field** is a different matter and stays a legal source", and treats that as the reason the restriction is narrow. So every line here was legal on the day the ban shipped, and the last one still puts `parkedAnchor` and `arrivingAnchor` on one payload: `guestToParked` follows the *slot* and observes `arrivingEngine` after the overwrite; `guestToArriving` follows the *object* and observes it too. `arrivingAnchor` becomes a forwarding cell targeting `parkedAnchor`, exactly as before. + +So the ban was never buying the merge machinery's removal. It bought one *question* — what a guest to a bare symbol's own slot denotes after that slot is moved from — and left the underlying mechanism fully reachable by the ordinary route. We had been carrying it as though it were load-bearing on the runtime, and it was load-bearing on nothing but a documentation problem. + +That reframes the whole ledger. It is not that we paid a sigil and a passing mode for a smaller runtime; the runtime is identical either way. We paid them for a narrower *explanation*, and then discovered the explanation has a perfectly good answer that fits in two bullets — which is the next section. + ### Answering the question instead of deleting it Reverting means the five-liner comes back, and this time it has to be answered rather than made illegal: @@ -267,8 +308,8 @@ The subject goes back to being an implicit guest. `this` now takes no marker at One rule got shorter rather than longer, and it is the one we did not expect. Returning an `&` used to require a root in an `&T` parameter specifically, with the other two modes excluded for two different reasons — a borrow expires with the call, a swallowed parameter is a bare symbol and therefore not a source at all. Both exclusions are gone, and the rule underneath them turns out to be simpler than either: **any parameter is a root, because a parameter belongs to the call-site scope** ([`lifetimes.md` §1.7](https://github.com/zane-lang/spec/blob/c36ef08/spec/lifetimes.md#17-returned--values-must-be-rooted-in-a-parameter)). A guest rooted in one therefore names something hosted in the very scope the return value lands in, so [the scope comparison](https://github.com/zane-lang/spec/blob/c36ef08/spec/lifetimes.md#11--assignment-uses-host-scope) settles it at the call site with no special case. A local is still not a root, and now for the honest reason — its host is the body scope, which drains at the return — rather than for a reason about where guests may come from. -Two costs are real and we are choosing them with our eyes open. +One cost is real and we are choosing it with our eyes open, and one thing that looks like a cost is not. -The first is that **merging stays reachable**. The whole reason the bare-symbol slot was awkward is that it can be moved from while something points at it, and that is exactly the situation that puts two live anchor identities on one payload. Forwarding anchors, the retirement stacks, and the target-kind discriminator are therefore permanent rather than provisional — the alternatives that would have deleted them all worked by restricting the source language, and we have just decided the source language is where we least want to pay. That is a defensible trade in the specific direction Zane leans: the merge machinery is written once, by the person implementing the compiler, and the restriction was paid by every program that builds a structure and hands out a reference to it. +The one that is not: **merging stays reachable** — but it was reachable before, through the field route above, so this is not something the revert spends. Forwarding anchors, the retirement stacks, and the target-kind discriminator are permanent, and they were already permanent. What the revert does close off is a *different* set of proposals, the ones that would have deleted the machinery by restricting the source language further — declaring which locals may be pointed at, or inferring it from where a guest is minted. Those are now off the table, and that is a real decision, just not the one it is easy to mistake it for. It goes the way Zane leans: the merge machinery is written once, by the person implementing the compiler, and a source restriction is paid by every program that builds a structure and hands out a reference to it. -The second is that **a signature no longer promises non-escape**. With three modes, `'T` told a caller that the callee could not keep the reference; `&T` told them it might. With two, `&T` covers both and the caller cannot tell them apart by reading the header. Nothing about this is unsafe — [the scope rule](https://github.com/zane-lang/spec/blob/c36ef08/spec/lifetimes.md#11--assignment-uses-host-scope) is what keeps a guest from outliving its host, and it does not care what the callee intended — but it is a genuine loss of legibility, and it is the one argument for keeping `'T` that survives the ban being lifted. We are not keeping it, because a mode that exists purely to document intent is a heavy way to document intent. If that turns out to be the wrong call, the thing to reach for is a non-escaping *annotation* on an `&T` parameter, not a third way of passing an argument. +The one that is: **a signature no longer promises non-escape**. With three modes, `'T` told a caller that the callee could not keep the reference; `&T` told them it might. With two, `&T` covers both and the caller cannot tell them apart by reading the header. Nothing about this is unsafe — [the scope rule](https://github.com/zane-lang/spec/blob/c36ef08/spec/lifetimes.md#11--assignment-uses-host-scope) is what keeps a guest from outliving its host, and it does not care what the callee intended — but it is a genuine loss of legibility, and it is the one argument for keeping `'T` that survives the ban being lifted. We are not keeping it, because a mode that exists purely to document intent is a heavy way to document intent. If that turns out to be the wrong call, the thing to reach for is a non-escaping *annotation* on an `&T` parameter, not a third way of passing an argument. From bd306509b9233bc95c3b3c5d48c85abaf7944bce Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 12:34:50 +0000 Subject: [PATCH 5/5] docs: a swallowed temporary has no symbol to downgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `functions.md` summary row read "caller may supply a temporary and downgrades to a guest", which describes something that cannot happen — `lifetimes.md` §1.6 says a hosting verb result has no symbol to downgrade, and the double-move question never arises for one. Name both move-source forms and what each leaves behind. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017AYiJXjzCPEDfYRW1ZtxH7 --- spec/functions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/functions.md b/spec/functions.md index c9592e3..4deab01 100644 --- a/spec/functions.md +++ b/spec/functions.md @@ -417,7 +417,7 @@ Read-only methods and functions are effect-free with respect to their subject un | Function | Identifier-named package-scope verb without `this`; no private-field privilege | | Block-bodied return | Every returning path uses `return expr`; `Unit` receives no fallthrough or bare-return exception | | `&` method parameter | Caller must supply a guest source, which a bare symbol satisfies; callee may read, mutate, store it into `&` fields, or return it | -| Plain `T` method parameter | Swallows; caller may supply a temporary and downgrades to a guest; callee **MUST NOT** bind it into `&` storage | +| Plain `T` method parameter | Swallows; caller supplies a move-source — a host symbol, which downgrades to a guest, or a temporary, which has no symbol to downgrade; callee **MUST NOT** bind it into `&` storage | | Reference-type `this` | Never a swallow position: it is an implicit guest, and `&` is never written on `this` | | Subscript | Package-scope place projection written `(this T)[...] => placeExpr`; no explicit return type | | Overload identity | Parameter types only; not names, return type, or `mut`; overloads differing only by the passing mode (`T` / `&T`) at one position are illegal |