diff --git a/CLAUDE.md b/CLAUDE.md index 239091d..0204a53 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,6 +28,12 @@ next agent gets up to speed — keep it to durable, agent-facing facts. internal contradictions on this codebase before: re-read the *un-updated* spec files and `bench/zane_bench.c` against the new design before opening a PR, not just the file you changed. +3. **A rule correction is not done until `glossary.md` carries it.** The + glossary summarizes rules it does not own, so fixing a rule in its canonical + home and leaving the entry paraphrasing the superseded version produces a + spec that contradicts itself — this has happened twice, both times caught in + review rather than by the author. After editing any normative rule, grep + `spec/glossary.md` for the concept and update the entry in the same commit. ## The `bench/` harness `bench/` is a reference **C** harness for runtime experiments — **not** Zane @@ -43,13 +49,51 @@ The generics system was unified into a `<>`-header / `()`-call model (canonical 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 -nE "Array\[|\[size\]|Array[0-9]+|Matrix10|\[rows\]|\[cols\]|'[A-Z]|inferred type generic|type-parameter symbol|root form" spec/*.md -``` + +`'[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. 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: + +```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. + +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. + If the grep hits an old form, stop and rewrite it in the unified system. If a cross-reference target moved (renumbered `§`), fix the reference in every doc that uses it, then re-grep for the old numbers. If the change conflicts with @@ -66,6 +110,34 @@ quality bar — dense, opinionated, long-form prose. Writing a story is two halves: write the narrative, then integrate it into the spec. Don't skip the second half. +### Append-only: run the check, and read the rule where it lives +**Story guide §5 owns this rule** — what may be edited, what the PR-versus-commit +distinction means, and the rare consolidation exception. Read it there; it is +the source of truth for contributors and agents alike, and this section adds +only what a session keeps getting wrong. + +Nothing enforces it automatically — no CI, no hook. Run the check yourself +before every commit that touches `stories/`: + +```sh +git diff origin/main -- stories/.md | grep -E "^-[^-]" +``` + +Additions only is the passing result. + +Two failure modes, both from real sessions on this repo: + +- **Too loose.** Editing a merged chapter to fix a retired claim, or bolting a + forward pointer onto one. Say what stopped being true from the *new* chapter + instead, naming the older chapter's claim. Caught in review, not by the author. +- **Too strict.** Refusing to touch chapters *your own branch* added, because + they were already written. They are drafts until the PR merges — rewrite, + reorder, and insert among them freely; a decision reached late in review often + belongs before them. The grep is quiet through all of that by design. + +If the grep is clean, you have not violated the rule, whatever your instinct +says. + ### Interview the maintainer — you cannot reconstruct the real reasoning The actual thread — which roads were tried and rejected, in what order the realizations came, what pressure forced each turn — lives only in the diff --git a/README.md b/README.md index 4edd3ee..9f587e7 100644 --- a/README.md +++ b/README.md @@ -66,13 +66,13 @@ 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, and building a variant by naming a case rather than calling a constructor | | [`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 receiver, and the shift to segmented chunked bump arenas | -| [`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, and why each strict rule is the minimal guard against one specific memory corruption | -| [`stories/effects.md`](stories/effects.md) | [`spec/effects.md`](spec/effects.md) — inferring effects instead of annotating them, receiver-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 receiver | +| [`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, and the three passing modes that split out of 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/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` | | [`stories/control-flow.md`](stories/control-flow.md) | [`spec/control-flow.md`](spec/control-flow.md) — `guard` as an active exit that opens no scope of its own, doing without `while` behind a written loop bound, one-based counting after the loop that forced the question, and why control-flow contracts use fundamental semantic types | -| [`stories/functions.md`](stories/functions.md) | [`spec/functions.md`](spec/functions.md) — pulling methods out of the type body and the verb model that revealed, mutation made visible with `:`/`!`, overloading on parameter shape alone, why callables are call-only while self-typed lambdas are values, and why every return carries an explicit value | +| [`stories/functions.md`](stories/functions.md) | [`spec/functions.md`](spec/functions.md) — pulling methods out of the type body and the verb model that revealed, mutation made visible with `:`/`!`, overloading on parameter shape alone, why callables are call-only while self-typed lambdas are values, why every return carries an explicit value, and dropping the inherited word "receiver" for `subject` | | [`stories/operators.md`](stories/operators.md) | [`spec/operators.md`](spec/operators.md) — the fixed vocabulary worth overloading, `~` as the universal flip, laws enforced through derived operators, grammar-only grouping, and home-package coherence | | [`stories/packages.md`](stories/packages.md) | [`spec/packages.md`](spec/packages.md) — the directory as namespace and compilation unit, declarations as move checks, explicit qualified access through `$`, and keeping mutable state inside values so the effect model can see it | diff --git a/contributing/naming-terms.md b/contributing/naming-terms.md index 878f965..ff71ff5 100644 --- a/contributing/naming-terms.md +++ b/contributing/naming-terms.md @@ -2,7 +2,7 @@ This guide describes how the spec chooses the coined terms it reuses — the named concepts recorded in [`glossary.md`](../spec/glossary.md), such as `verb`, -`mould`, `borrow`, `host`, `guest`, `anchor`, and `tether`. It governs the *terms of art* the +`subject`, `mould`, `borrow`, `host`, `guest`, `anchor`, and `tether`. It governs the *terms of art* the documentation leans on, not the surface keywords of the language itself. Terminology is worth naming deliberately because a good term is used on nearly @@ -20,6 +20,9 @@ does the teaching before the definition is even read. - **`verb`** — a function, method, operator, constructor, or lambda. In grammar a verb is the word that *acts*; a callable is the construct that *does* work. +- **`subject`** — the object a method is called on. Grammar again, and the same + sentence: the subject is what the verb acts from, so `player!setScale(...)` + reads subject–verb–object down the line. - **`mould`** — a `struct`/`variant`/`enum` form. A mould gives shapeless material a fixed form; these forms give a type its shape, and the type is what is cast from them. @@ -99,7 +102,7 @@ seen rarely and gains its meaning slowly, so an oblique reference like *Ariadne* (the thread through the labyrinth) is a strength. A **term** is the opposite case: read constantly, and needed to teach on contact. -Terms therefore lean plain and everyday — `verb`, `mould`, `borrow`, `host`, +Terms therefore lean plain and everyday — `verb`, `subject`, `mould`, `borrow`, `host`, `guest`, `anchor`, `tether` — even when the underlying instinct (name by metaphor, keep the link oblique) is the same. When in doubt for a term, choose the ordinary word over the exotic one. diff --git a/contributing/writing-stories-docs.md b/contributing/writing-stories-docs.md index ec4e5f0..20cf437 100644 --- a/contributing/writing-stories-docs.md +++ b/contributing/writing-stories-docs.md @@ -120,7 +120,24 @@ The href ends in the chapter's heading **anchor** so the link scrolls straight t This is the discipline that makes the folder a *history* rather than a stale snapshot. -**Append, don't overwrite.** When the design changes, the old reasoning did not become false — it became *the previous chapter*. So when the spec moves, add to the story: open a new chapter (or extend the relevant one) that names the cause and what it forced — *"The shift to X meant the old Y no longer held, so we…"* — and pin its spec references to the new commit (§4.2). The discarded path stays on the page as the record of why the design used to be one way and is now another; that causal trail is often the most illuminating thing in the file, and rewriting it away destroys it. +**Append, don't overwrite.** When the design changes, the old reasoning did not become false — it became *the previous chapter*. So when the spec moves, add to the story: open a **new chapter at the end of the file** — after everything already published, see the publication note below — that names the cause and what it forced — *"The shift to X meant the old Y no longer held, so we…"* — and pin its spec references to the new commit (§4.2). The discarded path stays on the page as the record of why the design used to be one way and is now another; that causal trail is often the most illuminating thing in the file, and rewriting it away destroys it. + +"Append" is meant literally, and it has two teeth: + +- **Do not touch a published chapter — at all.** Not to correct a claim the design has since retired, and not to bolt a forward pointer onto the end of it. A chapter records what was true when it was written; a later chapter is where you say what stopped being true and why. Naming the superseded claim explicitly *from the new chapter* — "the segmented-offset chapter had promotion rewrite the one anchor cell; that holds only while…" — does the same job for the reader without editing history. +- **A new chapter goes after every published one**, never slotted between chapters that already exist. Chapter order is the order the thinking moved, and the file's tail is the present. Inserting into the middle rewrites the sequence even when no existing character changes. + +**The unit of publication is the pull request, not the commit.** "Published" means merged — what is on the default branch. The chapters a PR is *itself* adding are still draft until it lands, so within that PR they may be rewritten, reordered, or have a new chapter inserted among them, however many commits it takes. A design decision reached late in review often belongs *before* the chapters already drafted on the branch, and putting it there is not a violation. What must not move is anything that was already merged. + +**Verify it by diffing.** Before committing a story change, check it against the branch you are merging into: + +```sh +git diff origin/main -- stories/.md | grep -E "^-[^-]" +``` + +Any output is a violation: a removed or rewritten line means a published chapter was edited, and a `-` next to a chapter heading means a chapter was inserted ahead of one that had already merged. The clean result is additions only — which is also why the check is the right one to run: it compares against what is published, so it stays silent while you rearrange your own branch's new chapters and speaks up the moment you disturb a merged one. + +Nothing runs this for you. There is no CI job and no hook; the rule is enforced by the author running the diff before committing and by the reviewer running it again on the branch. That is deliberate — the "consolidate dead threads" exception below is a judgement call no check could make, so a green check would have to be overridable anyway — but it does mean a violation reaches `main` if both people skip it. Treat the command as part of the commit, not as an optional audit. **Consolidate dead threads, sparingly.** Appending forever would bury the present under history. So a chapter *may* be rewritten or folded down — but only when its narrative has become pure dead weight: it no longer illuminates the present design *and* is not interesting as history. That is a high bar. The default is to append; consolidation is the rare exception, not routine cleanup, and when in doubt you keep the history. diff --git a/spec/adt.md b/spec/adt.md index a38b401..8914aeb 100644 --- a/spec/adt.md +++ b/spec/adt.md @@ -115,7 +115,7 @@ Naming a case takes its payload whole; to reach a nested case, write another cas An `enum` member is the payloadless degenerate of the same form: `Colors.red` selects a case that carries no payload, so it is written with no argument list (§2). A payload-carrying case is called; a payloadless one is selected. -A recursive `#variant` case boxes through `&` (§4), and its construction follows the ordinary reference rules: `Expr.flip(r)` takes an `&Expr`, and its argument must be a source that may create a new `&` (see [`memory.md`](memory.md) §2.8), exactly as an `&` field of a `#struct` requires (see [`types.md`](types.md) §3.9). +A recursive `#variant` case boxes through `&` (§4), and its construction follows the ordinary reference rules: `Expr.flip(r)` takes an `&Expr`, and its argument must be a guest source (see [`memory.md`](memory.md) §2.8), exactly as an `&` field of a `#struct` requires (see [`types.md`](types.md) §3.9). A bare symbol is not one, so the child a recursive case points at is reached through a field or through an `&Expr` parameter (§4.1). **Shared surface, different mechanism.** The `Type.member(args)` form — in both its long (`e Expr = Expr.intLit("5")`) and short (`e Expr.intLit("5")`) declaration — is exactly the surface a **named constructor** on a product type uses (see [`types.md`](types.md) §3.4): `v Vector2.diagonal(Float(3))` reads and declares just like `e Expr.intLit("5")`. The resemblance is purely **syntactic**. A named constructor is a declared *verb* that builds through `init{ }`; naming a variant case is built-in syntax with no verb behind it. They share a spelling, not a mechanism. @@ -131,6 +131,32 @@ A directly inline self-reference would have infinite size, which the uniform-str - The `#` modifier is what carries recursion: a plain `variant` is the sum mould's value form, laid out inline, while a `#variant` is its reference form — carrying a tag, boxing its recursive cases through `&`, and placed by the ordinary reference-type rules ([`memory.md`](memory.md) §3.5). A recursive sum such as `Expr` is a `#variant`. - Indirection is always **explicit `&`**. There is no hidden auto-boxing, matching Zane's stance that hosting and guests are explicit. +### 4.1 A recursive structure is rooted in a field + +Because a recursive member is an `&`, filling it needs a **guest source**, and a bare symbol is not one ([`memory.md`](memory.md) §2.8.1). A recursive structure is therefore rooted in a field rather than in a bare local: the node a case points at is hosted by a field, and the guest is minted from that field access. + +```zane +type Tree = #struct { + root Expr; +} + +tree Tree(Expr.intLit("5")) // the first node is hosted by a field +outer Expr.flip(tree.root) // legal: `tree.root` is a field access on a place + +leaf Expr.intLit("5") +bad Expr.flip(leaf) // ILLEGAL: `leaf` is a bare symbol, not a guest source +``` + +A verb that builds recursively takes its child as an `&` parameter, which is itself a guest source, so the chain continues without further ceremony: + +```zane +Expr negate(inner &Expr) => Expr.flip(inner) +``` + +This is the same requirement an `&` field of any `#struct` carries; recursion is not a special case. What it means in practice is that the *root* of a recursive structure lives in a field of the type that owns the structure — which is where a root belongs anyway, since that field is what keeps the whole shape alive. + +> **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/adt.md`](../stories/adt.md#one-body-product-or-sum) — "One body, product or sum". --- diff --git a/spec/concurrency.md b/spec/concurrency.md index dfbca3f..5c354e2 100644 --- a/spec/concurrency.md +++ b/spec/concurrency.md @@ -13,7 +13,7 @@ Zane separates **parallelism** (compiler-managed, unobservable) from **concurren - **`Implicit parallelism`.** The compiler may run provably independent work in parallel when it cannot change program results. - **`Explicit concurrency`.** `spawn` starts a concurrent function or method call; ordering is the programmer’s responsibility. - **`Water-tower lifetimes`.** A scope’s hosted objects live until all spawned work in that scope completes. -- **`Mutation needs a value receiver`.** A spawned call may mutate only a value-typed receiver; a value type's transitive alias-freedom lets the compiler rule out a data race from the receiver's type, and at most one spawn may mutably borrow a given location. +- **`Mutation needs a value subject`.** A spawned call may mutate only a value-typed subject; a value type's transitive alias-freedom lets the compiler rule out a data race from the subject's type, and at most one spawn may mutably borrow a given location. - **`No async coloring`.** Concurrency is chosen at the call site rather than encoded into function signatures. --- @@ -115,13 +115,13 @@ Each time one spawned call finishes, one plate is removed. The water level drops > **Story:** [`stories/concurrency.md`](../stories/concurrency.md#the-water-tower-lifetimes-that-survive-the-spawn) — "The water tower: lifetimes that survive the spawn". -### 4.2 Concurrent mutation requires a value-typed receiver -A spawned call may **mutate** state only through a value-typed receiver. A spawned `mut` call whose receiver is a reference type (a `#`-marked type) is a compile-time error. The rule is sound because a value type is transitively alias-free — it contains no reference-type or `&` field anywhere downstream (see [`memory.md`](memory.md) §2.10) — so no two names can reach the same mutated object by different paths. The compiler therefore rules out an aliased data race from the receiver's *type* alone, with no whole-program alias analysis. +### 4.2 Concurrent mutation requires a value-typed subject +A spawned call may **mutate** state only through a value-typed subject. A spawned `mut` call whose subject is a reference type (a `#`-marked type) is a compile-time error. The rule is sound because a value type is transitively alias-free — it contains no reference-type or `&` field anywhere downstream (see [`memory.md`](memory.md) §2.10) — so no two names can reach the same mutated object by different paths. The compiler therefore rules out an aliased data race from the subject's *type* alone, with no whole-program alias analysis. 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 `!` receiver of a spawned `mut` call. 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 receivers denote the same slot — so this disjointness is checked at the spawn site by inspecting the receivers, 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 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). ### 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). @@ -152,7 +152,7 @@ The language does not provide cancellation, kill groups, or shutdown ordering. A > **Story:** [`stories/concurrency.md`](../stories/concurrency.md#what-the-core-deliberately-leaves-out) — "What the core deliberately leaves out". ### 5.2 Lambdas do not capture -Lambdas (and blocks used as values) **MUST NOT** capture outer variables. All dependencies must be passed explicitly. This keeps effect tracking and the value-receiver check (§4.2) tractable. +Lambdas (and blocks used as values) **MUST NOT** capture outer variables. All dependencies must be passed explicitly. This keeps effect tracking and the value-subject check (§4.2) tractable. > **Story:** [`stories/concurrency.md`](../stories/concurrency.md#safety-the-compiler-proves-from-signatures-not-locks) — "Safety the compiler proves from signatures, not locks". @@ -176,4 +176,4 @@ Zane does not define a dedicated `Process` type, actor primitive, or channel pri | `spawn` | Starts a concurrent function or method call; blocks only when results are read | | Abortable `spawn` | Must attach `?` or `??` directly to the spawn expression | | Water tower | A scope exits only after all spawned work completes | -| Mutation | A spawned mutating call requires a value-typed receiver; at most one mutable borrow per storage location; concurrent reads take a coherent snapshot | +| Mutation | A spawned mutating call requires a value-typed subject; at most one mutable borrow per storage location; concurrent reads take a coherent snapshot | diff --git a/spec/effects.md b/spec/effects.md index 1a1bf7b..9414015 100644 --- a/spec/effects.md +++ b/spec/effects.md @@ -11,7 +11,7 @@ This document specifies Zane's effect model: `mut`, inferred effect levels, capa Zane uses a structural effect model with a single user-facing effect modifier: `mut`. - **`No purity keywords`.** Users do not write `pure`, `readonly`, or capability qualifiers. -- **`Receiver-local mutation`.** `mut` grants write access to state reachable through `this`, including through guests. +- **`Subject-local mutation`.** `mut` grants write access to state reachable through `this`, including through guests. - **`Compiler-inferred effect levels`.** The compiler classifies code by what state it can read or write. - **`Capability-based external effects`.** I/O and external state remain explicit because capability objects must be passed or stored. @@ -31,10 +31,10 @@ 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. A value-type `this` is a **borrow** of the caller's slot; a reference-type `this` is an implicit `&` reference to the object (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 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). ### 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 receiver. 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. +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. > **Story:** [`stories/effects.md`](../stories/effects.md#where-mutation-is-allowed-to-reach) — "Where mutation is allowed to reach". @@ -42,7 +42,7 @@ Parameters other than `this` are read-only. Mutation of another object must be e ## 3. Inferred Effect Levels -The compiler assigns a function to the strongest effect level required by any operation in its body or any function it calls transitively. Reading capability-backed state raises a function out of the pure levels; writes through a receiver or to external state raise it to Write Impure. +The compiler assigns a function to the strongest effect level required by any operation in its body or any function it calls transitively. Reading capability-backed state raises a function out of the pure levels; writes through a subject or to external state raise it to Write Impure. ### 3.1 Level 1 — Total Pure Total Pure functions depend only on explicit parameters and immutable package constants. They have no side effects and are guaranteed to terminate for all inputs. @@ -66,16 +66,16 @@ Write Impure functions mutate `this`, mutate capability-backed state, or otherwi A method without `mut` may not assign through `this` or call `mut` methods on state reached through `this`. ### 4.2 `mut` does not authorize arbitrary writes -Even a `mut` method may write only through `this`. It does not gain permission to mutate unrelated parameters. This applies whether the receiver is a value type or a reference type: a value receiver is mutated in place through its borrow (see [`functions.md`](functions.md) §2.4), not by returning a replacement. +Even a `mut` method may write only through `this`. It does not gain permission to mutate unrelated parameters. This applies whether the subject is a value type or a reference type: a value subject is mutated in place through its borrow (see [`functions.md`](functions.md) §2.4), not by returning a replacement. ### 4.3 `&` use sites follow ordinary call rules -Reading through a guest is not a side effect by itself. At use sites, guests follow the same field-access and method-call rules as hosts. Mutation of the hosted object's state must still be expressed through a `mut` method call with that object as the receiver. +Reading through a guest is not a side effect by itself. At use sites, guests follow the same field-access and method-call rules as hosts. Mutation of the hosted object's state must still be expressed through a `mut` method call with that object as the subject. --- ## 5. Structural Inference -### 5.1 Receiver reachability drives effects +### 5.1 Subject reachability drives effects The compiler uses reachability from `this` to determine which state is writable in a `mut` method and readable in any method. ### 5.2 Call-graph propagation @@ -115,10 +115,10 @@ Passing capabilities through constructors and methods is part of the design. It ## 7. Constructors, Allocation, and Abortability ### 7.1 Constructors may allocate but are not `mut` -Constructors create values and therefore participate in allocation, but they do not mutate an existing receiver. +Constructors create values and therefore participate in allocation, but they do not mutate an existing subject. ### 7.2 Allocation and destruction do not by themselves raise effect level -Heap allocation and destruction are runtime implementation events, but they are not side effects by themselves for effect classification. A function stays in the pure levels unless it also mutates receiver-reachable state or reads/writes through capabilities. +Heap allocation and destruction are runtime implementation events, but they are not side effects by themselves for effect classification. A function stays in the pure levels unless it also mutates subject-reachable state or reads/writes through capabilities. ### 7.3 Abortability is orthogonal A function's abort type and effect level are independent. An abortable function may be Total Pure, Read-Only Impure, or Write Impure depending on what else it does. @@ -136,13 +136,13 @@ Because they do not write mutable state, they can be reordered and parallelized Multiple concurrent reads are legal. For external, capability-backed state a read that conflicts with a concurrent write is serialized by the compiler/runtime. For in-memory value state, a concurrent read instead takes a coherent snapshot rather than blocking (see [`concurrency.md`](concurrency.md) §4.4). ### 8.3 Concurrent mutation is governed by the spawn rules -Concurrent mutation is not a per-`mut`-call property; it is governed by the spawn rules in [`concurrency.md`](concurrency.md) §4. A spawned mutating call's receiver **MUST** be a value type, and no two concurrent spawns may mutably borrow the same storage. A value type's transitive alias-freedom (see [`memory.md`](memory.md) §2.10) is what lets the compiler settle the absence of a data race from the receiver's type alone. +Concurrent mutation is not a per-`mut`-call property; it is governed by the spawn rules in [`concurrency.md`](concurrency.md) §4. A spawned mutating call's subject **MUST** be a value type, and no two concurrent spawns may mutably borrow the same storage. A value type's transitive alias-freedom (see [`memory.md`](memory.md) §2.10) is what lets the compiler settle the absence of a data race from the subject's type alone. --- ## 9. Effect Level Matrix -| Level | Reads capability-backed state | Writes receiver-reachable state | May write external state | Compile-time evaluation | +| Level | Reads capability-backed state | Writes subject-reachable state | May write external state | Compile-time evaluation | |---|---|---|---|---| | Total Pure | ❌ | ❌ | ❌ | ✅ | | Pure | ❌ | ❌ | ❌ | ❌ | diff --git a/spec/foundations.md b/spec/foundations.md index 7505cb4..b36c300 100644 --- a/spec/foundations.md +++ b/spec/foundations.md @@ -89,12 +89,13 @@ Every type is a **value type** unless it is marked with `#`, which makes it a ** A value type is copied on assignment, has no identity, and — the load-bearing restriction — is *transitively* a value: it may contain only other value types, never a reference-type or `&` field. Nothing reachable from a value can be aliased, which is why a value can be copied and shared by snapshot with no bookkeeping, and why a value type cannot recurse (a self-reference would need indirection, and indirection is a reference). A reference type is the opposite in each respect: it has stable identity, may be aliased through `&`, may hold reference-type and `&` fields, and may recurse. -Both kinds are mutated in place through a `mut` method, but the receiver reaches the caller differently: a value-type `this` is a *borrow* of the caller's slot (so a value is mutable without gaining identity), while a reference-type `this` is an implicit `&` to the object. Borrowing is the value world's device; the reference world already has `&`. +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. - **`#` 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. - **`&` 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. -- **Concurrency reads this axis.** A spawned call may mutate only a value-typed receiver, 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. +- **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. +- **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 deeb7da..84fc502 100644 --- a/spec/functions.md +++ b/spec/functions.md @@ -12,7 +12,7 @@ Zane unifies methods, functions, and lambdas under one model: a callable is a pa - **`Verb`.** A **verb** is a callable whose body is a sequence of statements that executes to do work: functions, methods, operators, constructors, and lambdas (a lambda being an anonymous verb). The spec uses "verb" whenever a rule applies to all of these as a group, and reserves "function" for the narrow form — an ordinary identifier-named verb with no `this`. A subscript is not a verb — its body must be a place expression that projects a place rather than running computation (§2.9). - **`Package-scope behavior`.** All methods, functions, and constructors are declared at package scope; type bodies never contain behavior. -- **`Methods as verbs`.** A method is a verb whose first parameter is `this`, so methods and functions share one model and differ only by the receiver. +- **`Methods as verbs`.** A method is a verb whose first parameter is `this`, so methods and functions share one model and differ only by the subject. - **`Capability markers`.** A verb's kind is selected by surface markers, and each marker unlocks a capability: naming the first parameter `this` grants private-field access (a method); naming the verb after a type grants `init{ }` and an implicit return type (a constructor). See §8. - **`Explicit mutation at the call site`.** `:` calls are read-only; `!` calls invoke `mut` methods. - **`Overload identity is parameter types only`.** Names, return type, and `mut` do not distinguish overloads. @@ -26,14 +26,18 @@ Zane unifies methods, functions, and lambdas under one model: a callable is a pa ### 2.1 Methods are verbs whose first parameter is `this` A method is any package-scope verb whose first parameter is named `this`. `this` **MUST** be the first parameter and **MUST NOT** appear in any other parameter position. +The **subject** is the object a method is called on. Two things are named after it and are not interchangeable: `this` is the **subject parameter** — the declaration's first parameter, whose surface form fixes how the object reaches the body ([`memory.md`](memory.md) §2.9) — and the expression to the left of `:` or `!` at a call site is the **subject expression**, which supplies the object and must satisfy whatever that form requires. Unqualified, "the subject" means the object itself. + ```zane Int scaledId(this Node, factor Int) { return this._id * factor } ``` +> **Story:** [`stories/functions.md`](../stories/functions.md#what-does-a-receiver-receive) — "What does a receiver receive?". + ### 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 receiver 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. +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. ```zane Int scaledId(this Node, factor Int) { @@ -55,18 +59,24 @@ 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** receiver, `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** receiver, `this` is an implicit **`&` reference** to the object (never swallowed). A `mut` method mutates through it as through any `&`, and `this` composes with the `&` system — it may be passed where an `&T` is expected. +- 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. + +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. ```zane -Unit setScale(this Node, scale Float) mut { // reference receiver +Unit setScale(this Node, scale Float) mut { // reference subject: the implicit borrow this.scale = scale return Unit() } ``` ```zane -Unit setY(this Vec2, y Float) mut { // value receiver: in-place through the borrow +&Weapon mainWeapon(this &Player) => this.weapon // guest subject: may be returned as `&` +``` + +```zane +Unit setY(this Vec2, y Float) mut { // value subject: in-place through the borrow this.y = y return Unit() } @@ -85,21 +95,28 @@ node!setScale(Float(3)) Calling a `mut` method with `:` is illegal. Calling a non-`mut` method with `!` is also illegal. > **Story:** [`stories/functions.md`](../stories/functions.md#mutation-you-can-see-at-the-call-site) — "Mutation you can see at the call site". +> **Story:** [`stories/memory.md`](../stories/memory.md#three-ways-to-hand-over-an-object) — "Three ways to hand over an object". ### 2.6 Method desugaring ```zane -receiver:method(arg) → ResolvedPkg$method(receiver, arg) -receiver!method(arg) → ResolvedPkg$method(receiver, arg) -receiver:Pkg$method(arg) → Pkg$method(receiver, arg) -receiver!Pkg$method(arg) → Pkg$method(receiver, arg) +subject:method(arg) → ResolvedPkg$method(subject, arg) +subject!method(arg) → ResolvedPkg$method(subject, arg) +subject:Pkg$method(arg) → Pkg$method(subject, arg) +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 receiver. How each parameter is passed — a value borrow, or a reference `&`/swallow — 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 three 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): + +- 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 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). -### 2.8 `&` and swallowing method parameters -A method parameter declared as `&T` is a **reference**: the caller supplies a source that may create a new `&` under [`memory.md`](memory.md) §2.8, and the callee may store it into an `&` field. 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) — so it cannot be bound into `&` storage, because a swallowed value is hosted at the call site while an `&` field may outlive the call (see [`memory.md`](memory.md) §2.9). A value-type parameter is a read-only borrow. To pass a reference object for reading only, use `&T`. +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`. ```zane type Car = #struct { @@ -113,29 +130,32 @@ Unit setEngine(this Car, engine &Engine) mut { return Unit() } -// `&` parameter, read only -Int calculate(this Car, engine &Engine) { - return this._value + engine.speed // legal: reading through the reference +// borrow parameter, read only +Int calculate(this Car, engine 'Engine) { + return this._value + engine.speed // legal: reading through the borrow } -// plain reference-type parameter swallows; a swallowed host is not an `&` source +// plain reference-type parameter swallows; a swallowed host is not a guest source Unit setEngineWrong(this Car, engine Engine) mut { this.engine = engine // ILLEGAL: cannot store a swallowed host into an `&` field return Unit() } ``` -Call syntax is uniform regardless of the parameter mode: +Call syntax is uniform regardless of the parameter mode; only what the caller may supply differs: ```zane engine Engine() -car!setEngine(engine) // legal: engine may create a new `&` -car:calculate(engine) // legal: read-only reference to engine -car!setEngine(Engine()) // ILLEGAL: temporary cannot bind to `&` parameter +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!setEngine(garage.spare) // legal: a field access is a guest source +car!setEngine(Engine()) // ILLEGAL: a temporary is not a place expression ``` ### 2.9 Subscripts are place projections -Subscripts are package-scope declarations with the receiver first: +Subscripts are package-scope declarations with the subject first: ```zane (this CustomList)[index Int] => this._data[index] @@ -144,7 +164,7 @@ Subscripts are package-scope declarations with the receiver first: The body of a subscript definition **MUST** be a place expression. `[]` is not a general function call and cannot return a computed value. Its result is always inferred from the projected place, so subscripts have no explicit return type annotation. A subscript may declare any number of comma-separated parameters inside `[]`; it is not limited to one or two. -When a receiver interprets an `Int` subscript as an ordinal position in an ordered sequence, that position is 1-based. The first element is at `1`, and a sequence with `n` elements uses `1` through `n` as its positional range. +When a subject interprets an `Int` subscript as an ordinal position in an ordered sequence, that position is 1-based. The first element is at `1`, and a sequence with `n` elements uses `1` through `n` as its positional range. > **See also:** [`memory.md`](memory.md) §2.8 for when a place expression may create a new `&`. @@ -208,18 +228,21 @@ 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 whether the same parameter position is `T` versus `&T`. Such declarations are illegal and the compiler **MUST** reject them with a compile-time error, for example: "illegal overload set: differs only by `&` 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`, `&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." ```zane Unit consume(this Car, engine Engine) -Unit consume(this Car, engine &Engine) // ERROR +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. + ### 4.2 Consequences of the overload identity rules Declarations that differ only by return type, parameter names, `this`, or `mut` are compile-time conflicts. ### 4.3 Valid overloads differ by arity or parameter type -Legal overload sets must differ in the number of parameters or in at least one parameter type other than bare `&`-ness at the same position. +Legal overload sets must differ in the number of parameters or in at least one parameter type at the same position, ignoring the passing mode. > **Story:** [`stories/functions.md`](../stories/functions.md#overloading-on-shapes-and-only-shapes) — "Overloading on shapes, and only shapes". @@ -248,12 +271,12 @@ These phases describe **static** overload resolution. Matching a `variant` on it ## 6. Method Name Resolution and Extension Methods ### 6.1 Unqualified method lookup -For `receiver:methodName(...)` or `receiver!methodName(...)`, the compiler resolves candidates in this order: +For `subject:methodName(...)` or `subject!methodName(...)`, the compiler resolves candidates in this order: -1. the receiver type's home package; for a fundamental type, the bundled `core` implementation package fills this role +1. the subject type's home package; for a fundamental type, the bundled `core` implementation package fills this role 2. the current package -If no candidate matches, the call is a compile-time error. If multiple candidates remain after overload resolution, the call is a compile-time error and must be written with an explicit package qualifier. Searching the receiver type's defining declarations first makes an unqualified call resolve the same way wherever it is written, independent of which packages the caller has imported. +If no candidate matches, the call is a compile-time error. If multiple candidates remain after overload resolution, the call is a compile-time error and must be written with an explicit package qualifier. Searching the subject type's defining declarations first makes an unqualified call resolve the same way wherever it is written, independent of which packages the caller has imported. ### 6.2 Qualified method calls Cross-package extension methods are written explicitly: @@ -263,7 +286,7 @@ vec:Physics$kineticEnergy() ``` ### 6.3 Extension methods may be declared in any package -Because methods are package-scope verbs, any package may define methods on imported types. This follows the same rule as [`types.md`](types.md) §2.3 and §2.2 above: if the first parameter is `this`, the declaration is a method and gets the same private-field access as any other method on that receiver type. +Because methods are package-scope verbs, any package may define methods on imported types. This follows the same rule as [`types.md`](types.md) §2.3 and §2.2 above: if the first parameter is `this`, the declaration is a method and gets the same private-field access as any other method on that subject type. > **Story:** [`stories/functions.md`](../stories/functions.md#pulling-methods-out-of-the-type-body) — "Pulling methods out of the type body". @@ -289,7 +312,7 @@ The reason is the same one that makes operators safe to overload. An overloaded A lambda literal is a function declaration with the name removed. It writes its own parameter types, return type, abort type, and `mut` (see [`syntax.md`](syntax.md) §3.8). Nothing is inferred from context. ```zane -receiver(Float(x Int) { +callee(Float(x Int) { if x < Int(10) { return Float(0) } else { @@ -298,7 +321,7 @@ receiver(Float(x Int) { }) ``` -Because a lambda carries its complete type, it is a single value with one exact type. It can therefore be passed to an **overloaded** receiver without ambiguity: the lambda fixes its own type, so overload resolution on the receiver proceeds with ordinary argument types and no circularity. Its complete written type also allows it to be defined and passed directly in the same expression without depending on surrounding context. +Because a lambda carries its complete type, it is a single value with one exact type. It can therefore be passed to an **overloaded** callee without ambiguity: the lambda fixes its own type, so overload resolution on that callee proceeds with ordinary argument types and no circularity. Its complete written type also allows it to be defined and passed directly in the same expression without depending on surrounding context. `mut` is part of the lambda's written type. A lambda that does not declare `mut` may still be assigned to a `mut` function type — it simply does not use the mutation permission — but a `mut` lambda may not be assigned to a non-`mut` function type: @@ -322,7 +345,7 @@ Float callback(x Int) { ... } // function declaration callback Float(x Int) { ... } // lambda-variable declaration ``` -A lambda-variable is an ordinary symbol with a single function type. Because a symbol cannot be redeclared with a different type, a lambda-variable name can never accumulate an overload set, so it is always unambiguous in value position. This is what makes `receiver(callback)` well-defined where referencing an overloaded callable would not be. +A lambda-variable is an ordinary symbol with a single function type. Because a symbol cannot be redeclared with a different type, a lambda-variable name can never accumulate an overload set, so it is always unambiguous in value position. This is what makes `callee(callback)` well-defined where referencing an overloaded callable would not be. > **See also:** [`syntax.md`](syntax.md) §2.9 for function types and §3.8 for lambda literals and lambda-variable declarations. @@ -332,7 +355,7 @@ Lambdas **MUST NOT** capture outer variables. Every dependency must be passed as > **Story:** [`stories/functions.md`](../stories/functions.md#names-that-are-not-values) — "Names that are not values". ### 7.5 No bound method references -Zane does not provide bound method references as a separate feature. Because lambdas do not capture, there is no syntax that implicitly stores a receiver inside a function value. Code that needs a receiver later must keep that receiver in ordinary storage and pass it explicitly when the function value is invoked. +Zane does not provide bound method references as a separate feature. Because lambdas do not capture, there is no syntax that implicitly stores a subject inside a function value. Code that needs a subject later must keep that subject in ordinary storage and pass it explicitly when the function value is invoked. ### 7.6 Generics are orthogonal to overloading for function values A lambda is a single value with one exact type, even when that type is a function type (§7.2). Overload identity is parameter types only (§4.1), so a function type is a single, unique parameter shape. Passing a lambda to an overloaded callable is therefore an exact shape match at that parameter position, not a contest the lambda must win. @@ -351,13 +374,13 @@ Every callable in Zane is a verb (§1). What *kind* of verb a declaration is — | Marker | Verb kind | Capability unlocked | |---|---|---| -| First parameter named `this` | Method | Private-field access on the receiver; `:` / `!` call syntax | +| First parameter named `this` | Method | Private-field access on the subject; `:` / `!` call syntax | | Name is a type | Constructor | Return type is the named type (no return annotation); `init{ }` for field **initialization** | | Symbol name (operator token) | Operator | Operator-position calls | | No name | Lambda | Anonymous function value | | Plain identifier, none of the above | Function | No special capability | -The markers are largely independent — a lambda may still declare a `this` receiver (§7.2), for example — but the kinds above are distinguished by which markers are present. A constructor body and a method body are otherwise ordinary verb bodies (§2, §3). +The markers are largely independent — a lambda may still declare a `this` subject (§7.2), for example — but the kinds above are distinguished by which markers are present. A constructor body and a method body are otherwise ordinary verb bodies (§2, §3). ### 8.2 `init{ }` is to constructors what `this` is to methods @@ -377,7 +400,7 @@ All verbs share one parameter system (see [`generics.md`](generics.md) §3), one ## 9. Connection to the Effect Model -Read-only methods and functions are effect-free with respect to their receiver unless they touch guests or capabilities. `mut` marks the path for writing state reachable through `this`. This is why overload identity ignores `mut`: the call contract is structurally the same even though the behavioral permissions differ. +Read-only methods and functions are effect-free with respect to their subject unless they touch guests or capabilities. `mut` marks the path for writing state reachable through `this`. This is why overload identity ignores `mut`: the call contract is structurally the same even though the behavioral permissions differ. > **See also:** [`effects.md`](effects.md) for the complete effect model and concurrency implications. @@ -390,18 +413,20 @@ Read-only methods and functions are effect-free with respect to their receiver u | 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 `!`; a value-type `this` is a mutable borrow of the caller's slot, a reference-type `this` is an implicit `&` reference; may mutate state reachable through `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` | | 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 an allowed `&` source; callee may store into `&` fields | -| Plain `T` method parameter | Value-only; caller may supply a temporary; callee **MUST NOT** bind it into `&` storage | +| `&` 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 | +| 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 | | 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 `&` 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` / `'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 | | Lambda-variable | Symbol bound to a lambda literal; has one function type; the only way to hold a function value | | Generic function value | Not specified in this version; deferred on runtime-representation grounds, not overloading (see [`generics.md`](generics.md) §9) | -| Unqualified method lookup | Searches the receiver's home package (the bundled `core` implementation for a fundamental type), then the current package | +| Unqualified method lookup | Searches the subject's home package (the bundled `core` implementation for a fundamental type), then the current package | | Extension methods | Any package may declare methods on imported types by naming the first parameter `this` | diff --git a/spec/generics.md b/spec/generics.md index 1060aef..ac8aed3 100644 --- a/spec/generics.md +++ b/spec/generics.md @@ -117,7 +117,7 @@ A **verb** — a function, method, or constructor — has no header. It *introdu ```zane Vector(x T Type, y T Type) { ... } // T introduced on a value parameter T head(arr Array) { ... } // T, n introduced inside a nested type -Int size(this Buffer) { ... } // T, n introduced on the receiver +Int size(this Buffer) { ... } // T, n introduced on the subject ``` A verb has no header because it never needs one: its parameters are always inferred (§5) and never applied positionally, so there is no order to fix and nothing for a header to declare. @@ -153,7 +153,7 @@ Int size(this Buffer) { } ``` -Here `n` in the return position is the number the use site supplied for that parameter. The receiver type `Buffer` introduces `T` and `n` inline; the return position references `n`. The `Array` layout inside `Buffer` uses the same `n` to fix the storage size. +Here `n` in the return position is the number the use site supplied for that parameter. The subject type `Buffer` introduces `T` and `n` inline; the return position references `n`. The `Array` layout inside `Buffer` uses the same `n` to fix the storage size. > **See also:** [`effects.md`](effects.md) §2 — a number parameter read in a body position is a read-only value-like binding. @@ -355,7 +355,7 @@ The following are intentionally not specified in this version: - dynamic container types such as lists and maps - bounds-checking rules for element access APIs - named lane access (`.x`, `.y`, `.z`, `.w`) -- phantom type parameters — an introduced parameter (a type's header parameter, or a verb's inline parameter) with no path from any value argument, receiver, or literal that fixes it +- phantom type parameters — an introduced parameter (a type's header parameter, or a verb's inline parameter) with no path from any value argument, subject, or literal that fixes it - generic function values — a function *value* that is itself polymorphic over type or number parameters; the open question is runtime representation (monomorphization versus dictionary passing), not overload resolution or type checking, since a generic function type is a unique parameter shape (see [`functions.md`](functions.md) §7.6) > **Story:** [`stories/generics.md`](../stories/generics.md#deferred-what-the-model-promises-but-does-not-yet-deliver) — "Deferred: what the model promises but does not yet deliver" records why each item is open, including the constraints/bounds gap and the type-level equality problem. diff --git a/spec/glossary.md b/spec/glossary.md index c879ed1..16766dc 100644 --- a/spec/glossary.md +++ b/spec/glossary.md @@ -36,8 +36,8 @@ This file gives short, reusable names to concepts that appear across multiple sp - **Canonical home:** [`control-flow.md`](control-flow.md) §3 ### 2.4 value-typed mutation rule -- **Meaning:** A spawned call may mutate only a value-typed receiver, and at most one live spawn may mutably borrow a given storage location. A value type is transitively alias-free, so the rule rules out an aliased data race from the receiver's type alone; concurrent reads take a coherent snapshot instead of serializing. -- **Why this name:** Concurrent mutation is gated on the receiver being a value type — the property that makes race-freedom checkable without whole-program alias analysis. +- **Meaning:** A spawned call may mutate only a value-typed subject, and at most one live spawn may mutably borrow a given storage location. A value type is transitively alias-free, so the rule rules out an aliased data race from the subject's type alone; concurrent reads take a coherent snapshot instead of serializing. +- **Why this name:** Concurrent mutation is gated on the subject being a value type — the property that makes race-freedom checkable without whole-program alias analysis. - **Canonical home:** [`concurrency.md`](concurrency.md) §4.2 and §4.3 ### 2.5 water-tower lifetimes @@ -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. Some place expressions may create new `&` values, while `[]` expressions remain excluded from that rule. +- **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). - **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 @@ -75,7 +75,7 @@ This file gives short, reusable names to concepts that appear across multiple sp - **Canonical home:** [`memory.md`](memory.md) §2.10 ### 3.3 unified type parameters -- **Meaning:** A type or number parameter is a *type parameter* (`name Type`, an uppercase name such as `T`, ranging over types) or a *number parameter* (`name Number`, a lowercase name such as `n`, ranging over compile-time numbers and resolving to a number value in body positions). A type definition declares its parameters in a `<>` header (their order is applied positionally at use sites); a verb — function, method, or constructor — has no header and introduces each parameter inline within its value parameters, at the parameter's first marked occurrence. Parameters are referenced by bare name; casing carries the kind. +- **Meaning:** A type or number parameter is a *type parameter* (`name Type`, an uppercase name such as `T`, ranging over types) or a *number parameter* (`name Number`, a lowercase name such as `n`, ranging over compile-time numbers and resolving to a number value in body positions). A type definition declares its parameters in a `<>` header (their order is applied positionally at use sites); a verb — function, method, operator, constructor, or lambda — has no header and introduces each parameter inline within its value parameters, at the parameter's first marked occurrence. Parameters are referenced by bare name; casing carries the kind. - **Why this name:** Type and number parameters share one concept-and-reference system (the `Type`/`Number` concepts, bare references, and the casing rule) across types and verbs; only the introduction site differs — a header for types, which are applied positionally, and inline for verbs, whose parameters are always inferred. - **Canonical home:** [`generics.md`](generics.md) §3 @@ -91,7 +91,7 @@ This file gives short, reusable names to concepts that appear across multiple sp ### 3.6 method-based privacy - **Meaning:** `_` fields are private to methods whose first parameter is `this` for that type, rather than to a package boundary. -- **Why this name:** Privacy is granted by the method/receiver relationship, not by where the function is declared. +- **Why this name:** Privacy is granted by the method/subject relationship, not by where the function is declared. - **Canonical home:** [`types.md`](types.md) §2.3 ### 3.7 direct initialization @@ -175,18 +175,18 @@ This file gives short, reusable names to concepts that appear across multiple sp - **Canonical home:** [`functions.md`](functions.md) §1 ### 3.23 anchor cell -- **Meaning:** A runtime `u32` cell holding the current segmented offset of one hosted object — the stable indirection point through which tethers resolve. It is bump-allocated when the first guest is created, in a dedicated anchor-cell region of the host's scope arena. -- **Why this name:** The cell is the fixed point that lets a moving object remain reachable: rehosting updates the cell while existing tethers keep pointing to it. +- **Meaning:** An 8-byte runtime cell in the global anchor pool containing a `u32` target and a kind. A payload anchor targets a hosted object's segmented offset; a forwarding anchor targets another anchor after two hosting identities merge. A guest's `u32` tether names an anchor cell and follows forwarding cells until it reaches the terminal payload anchor. +- **Why this name:** The cell is a stable point through which an older guest identity can remain attached to a moving value, either directly or through another anchor. - **Canonical home:** [`memory.md`](memory.md) §4.1 ### 3.24 segmented-offset tether -- **Meaning:** The internal representation of a guest: a `u32` segmented offset pointing at the host's anchor cell, not a raw pointer. The value `0` means no tether. A tether is a runtime mechanism, distinct from the source-facing `&T` guest (§3.33). +- **Meaning:** The internal representation of a guest: a `u32` segmented offset pointing at an anchor cell, not a raw pointer. The cell may directly target the hosted payload or forward to another anchor. The value `0` means no tether. A tether is a runtime mechanism, distinct from the source-facing `&T` guest (§3.33). - **Why this name:** The tether connects a guest's stored representation to the anchor through which it reaches the hosted object. - **Canonical home:** [`memory.md`](memory.md) §4.2 ### 3.25 arena placement -- **Meaning:** A reference-type instance is bump-allocated in the arena of the scope that creates it, and is copied (promoted) into a parent arena only if it escapes that scope. Only dynamic size or escape changes where an instance lives. Placement is an unobservable implementation choice. -- **Why this name:** Placement is a choice among **arenas** — the per-scope bump regions — rather than between a stack and a heap; the creating scope's arena is the default, a parent arena the fallback on escape. +- **Meaning:** A scope's arena has two regions: statically sized storage — value slots, reference-type hosts, and dynamic handles — is bump-allocated inline in the fixed-size region of the scope that creates it, while a resizable backing store goes in that scope's dynamic region. Rehosting copies the complete hosted representation into destination-owned storage: inline bytes move into the destination fixed-size region, each dynamic backing store is relocated into an equal-size destination-region allocation, and the old source storage ceases to be live. The source host-capable slot then stores the terminal tether as a guest. Placement is an unobservable implementation choice. +- **Why this name:** Placement is a choice among **arenas** — the per-scope regions — rather than between a stack and a heap; the creating scope's arena is the default, a parent arena the fallback on escape. - **Canonical home:** [`memory.md`](memory.md) §3.5 ### 3.26 capability marker @@ -195,8 +195,8 @@ 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 — the passing mode for **value types**, which have no `&` of their own. A value parameter is a read-only borrow and a value-type `mut` receiver is a mutable borrow; a value is copied only when bound into a fresh slot. Reference types are passed as guests or swallowed instead, and a reference-type `this` is an implicit guest. -- **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, a borrow has no anchor or tether and cannot be stored or returned. +- **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`. +- **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 ### 3.28 coercion site @@ -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. 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, 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). - **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,6 +239,21 @@ 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.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. +- **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). +- **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 + --- ## 4. Packages, Operators, and Versioning diff --git a/spec/lexical.md b/spec/lexical.md index 9bf5cd7..6e44780 100644 --- a/spec/lexical.md +++ b/spec/lexical.md @@ -97,7 +97,8 @@ Certain leading characters are reserved and are not ordinary identifier starts: | Sigil | Meaning | Canonical home | |---|---|---| -| `&` | Reference type (`&Node`) | [`memory.md`](memory.md) §2 | +| `&` | 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 | @@ -184,6 +185,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 | | `<>` 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 4eea393..f7d83d0 100644 --- a/spec/lifetimes.md +++ b/spec/lifetimes.md @@ -9,20 +9,28 @@ This document specifies Zane's lexical lifetime rules: `&` assignment scope chec ## 1. Scope Rules and Moves ### 1.1 `&` assignment uses host scope -An `&` assignment is legal only when the target's host is declared in the same or a higher lexical scope than the `&` itself. +An `&` assignment is legal only when the source is a guest source ([`memory.md`](memory.md) §2.8) **and** the target's host is declared in the same or a higher lexical scope than the `&` itself. ```zane -outer Node() -r &Node = outer +outerTree Tree() +r &Node = outerTree.root { - innerNode Node() - r = innerNode // ILLEGAL: host's scope is nested relative to the guest + innerTree Tree() + r = innerTree.root // ILLEGAL: the host's scope is nested relative to the guest } ``` +The two conditions are independent, and the second only ever arises for sources the first admits. A bare symbol fails the first condition outright: + +```zane +node Node() +r &Node = node // ILLEGAL: a bare symbol is not a guest source +``` + The compiler compares declaration scopes. It does not perform borrow inference or lifetime annotation solving. > **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#inheriting-a-debt-safety-without-a-borrow-checker) — "Inheriting a debt: safety without a borrow checker". +> **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#where-a-guest-may-be-rooted) — "Where a guest may be rooted". ### 1.2 Move-sources are host symbols or hosting verb results A move-source must denote a **hosting value the expression is entitled to consume**. Two forms qualify: @@ -34,6 +42,7 @@ A verb that returns a hosting `T` produces a fresh value that no symbol, field, 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 field access such as `car.engine` - a container element access such as `cars[1]` - any other access path that projects into an existing host @@ -105,6 +114,8 @@ 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 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. ```zane @@ -117,7 +128,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). Attempting to bind 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 whether an `&`-creating 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. 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. > **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". @@ -137,24 +148,29 @@ This also applies across calls. Passing a hosting value to a plain `T` parameter A hosting verb result (§1.2) has no symbol to downgrade. The temporary is consumed by the move and cannot be named again, so the double-move question never arises for it. -### 1.7 Returned `&` values must be rooted in a parameter -A function may return an `&T` only when the returned reference is rooted in one of the function's parameters. `this` counts as a parameter for this rule. +### 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`. ```zane &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. + ```zane +&Weapon fromBorrow(this Player) => this.weapon // ILLEGAL: a borrow is not a guest root + &Node bad() { value Node() - return value // ILLEGAL: returned `&` is not rooted in a parameter + return value // ILLEGAL: a local is neither a parameter nor a guest source } ``` > **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". ### 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` (a guest, [`memory.md`](memory.md) §2.9) means the caller lends the value and 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` 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. ```zane car Car() @@ -165,13 +181,14 @@ 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 three ways, each fixed by its signature: +A verb treats a reference-type host argument in one of four ways, each fixed by its signature: -- it takes a **guest** — declares the parameter `&T` ([`memory.md`](memory.md) §2.9); the caller stays a full host and lends only a guest for the call. +- 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 **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. -Passing 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, either pass `&T` or bind a relayed return: +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: ```zane weapon Weapon() @@ -196,9 +213,10 @@ Unit main() { } ``` -A verb that only reads its reference argument may still declare it plain `T`: reading does not change the fact that the signature asked for hosting access, so the caller downgrades all the same. Declaring the parameter `&T` is what keeps the caller as host. Because the signature alone decides the caller's state, there is no interprocedural consumption inference: whether a passed host downgrades never depends on the callee's body or on the build. Using hosting access only to read a value is legal. Leaving a parameter entirely unused is a separate, general matter — a release build rejects an unused parameter whether it hosts a value or not. +A verb that only reads its reference argument may still declare it plain `T`: reading does not change the fact that the signature asked for hosting access, so the caller downgrades all the same. Declaring the parameter `'T` — 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. > **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". ### 1.9 An ignored hosting result floats to the enclosing scope A return value need not be bound. When a call's result is a reference-type host and the call stands as a bare statement, that host is not destroyed at the end of the statement — it **floats**: it becomes an anonymous host in the enclosing scope and lives until that scope drains, like any object hosted by that scope (§2.1). An ignored value-type result, including `Unit()`, is simply discarded. @@ -249,14 +267,14 @@ Because scope rules (§1.1) prevent guests from outliving their hosts, the runti | Concept | Rule | |---|---| -| `&` return | Returned `&T` must be rooted in a parameter; `this` counts | -| Guest assignment | Only from a place expression whose host is in the same or a higher lexical scope than the guest | -| Move-source | A direct host symbol (local or parameter) or a hosting verb result; not an `&`, field, container element, or other access path | +| `&` 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) or a hosting verb result; not an `&`, a `'T` 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 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 **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 | | 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 64281c0..f2ea506 100644 --- a/spec/memory.md +++ b/spec/memory.md @@ -12,15 +12,17 @@ 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. +- **`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). - **`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). -- **`Arena placement`.** A reference-type instance is bump-allocated in the arena of the scope that creates it, and is copied into a parent arena only if it escapes that scope (see §3.5). -- **`Segmented-offset tethers`.** Internally, each guest is represented by a `u32` tether — a chunk id plus an in-chunk offset — that points at the host's anchor cell, not a raw pointer (see §4.2). +- **`Regioned arena placement`.** Every scope owns separate fixed-size and dynamic-backing-store regions. Statically sized storage is placed inline in the fixed-size region; resizable data uses the dynamic region. Anchors live outside scope arenas in one runtime-global fixed-slot pool (see §3 and §4). +- **`Segmented-offset tethers`.** Internally, each guest is represented by a `u32` tether — a chunk id plus an in-chunk offset — that points at an anchor cell in the host's identity path, not a raw pointer (see §4.2). -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 the anchor, so existing tethers — and therefore guests — continue to reach it. +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 point only at an existing place, never 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 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. > **Story:** [`stories/memory.md`](../stories/memory.md#safety-without-a-collector-and-without-lifetimes) — "Safety without a collector and without lifetimes". @@ -29,9 +31,11 @@ These rules fit together mechanically. Hosts are the only storage that controls ## 2. Hosting and Storage ### 2.1 Every reference-type instance has exactly one host + Every instance of a reference type (a `#`-marked type, see [`types.md`](types.md) §2.1) is hosted by exactly one symbol, field, or container slot at a time. Hosting is the default storage mode for reference values. ### 2.2 Reference-type hosts are overwritable after initialization + Any hosting storage position for a reference-type instance—a symbol, field, or container slot—**MUST** be directly initialized, and **MAY** later be overwritten. ```zane @@ -50,7 +54,8 @@ hosts Array = [Node(), Node()] Rewriting `hosts[1]` replaces the hosted reference-type instance in that slot. Guests to that slot observe the new value because guests follow the host/anchor path, not the original object. ### 2.3 Value types are mutable in place and freely overwritable -Value types have no anchor and no heap identity. A value is mutated in place through a `mut` method whose receiver is a borrow of the value's storage (see [`effects.md`](effects.md) §2.3, [`functions.md`](functions.md) §2.4), and its storage slot may also be reassigned wholesale. Neither operation goes through the anchor system, because a value has no identity to track. + +Value types have no anchor and no heap identity. A value is mutated in place through a `mut` method whose `this` is a borrow of the value's storage (see [`effects.md`](effects.md) §2.3, [`functions.md`](functions.md) §2.4), and its storage slot may also be reassigned wholesale. Neither operation goes through the anchor system, because a value has no identity to track. ```zane pos Vec2(1, 2) @@ -59,8 +64,11 @@ pos = Vec2(3, 4) // whole-slot overwrite ``` ### 2.4 `&` is a guest: non-hosting storage + `&` creates a **guest**: non-hosting storage that points at a **reference type** only. An `&T` requires `T` to be a reference type — a declared `#struct`/`#variant`/`#enum` — because only a reference type carries the identity (the anchor, §4) that a stable, move-surviving guest needs. A value type is shared by copying it or by a scoped borrow (see [`functions.md`](functions.md) §2.4), never by a stored guest. Writing `&Node` names a guest to a reference type; a bare `&Int` over a value type is ill-formed. +An explicitly declared `&T` slot is **guest-only**: it stores only a tether and can never directly host a `T`. A slot declared as `T` is **host-capable**. After its value is rehosted, that same full-size slot may remain readable in guest state, but it retains the storage needed to host another `T` later. Guest-only and host-capable guest states use the same access semantics, but only the latter can become a host again. + A guest may be declared as: - a local symbol @@ -69,81 +77,126 @@ 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. +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. + +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. > **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". ### 2.5 Guests are repointable -An `&` symbol or `&` field may be assigned a different target later, as long as the scope rule in [`lifetimes.md`](lifetimes.md) §1.1 is satisfied. + +An `&` symbol or `&` field may be assigned a different target later, as long as the new target is a guest source (§2.8) and the scope rule in [`lifetimes.md`](lifetimes.md) §1.1 is satisfied. ### 2.6 Guests are independent -Assigning or passing a guest gives the destination its own guest to the same host. Rebinding one guest's storage site later changes only that storage site; it does not retarget other guests that already point to that host. + +Assigning or passing a guest gives the destination its own guest to the same host. The runtime may resolve a forwarding tether and store the terminal anchor identity in the new guest; this canonicalization is unobservable. Rebinding one guest's storage site later changes only that storage site; it does not retarget other guests that already point to that host. ### 2.7 Guests and hosts use the same surface operations + At use sites, a guest is used with the same surface syntax as a direct host. Method calls, field access, and `mut` calls use the ordinary syntax. The distinction between host and guest matters only at the storage site: a guest stores a non-hosting link, while a host stores the object itself or its hosting slot. ### 2.8 Place expressions and new `&` values + A **place expression** is an expression that denotes an existing, stable storage location. 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 receiver type -- an `&T` parameter inside the callee body (§2.9) +- 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) -Only some place expressions may create a new guest. A new `&` binding may be initialized from: +Only some place expressions may mint a new guest. A new `&` value may be minted from: -- a named symbol -- a field access whose base is a place +- 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 - an `&T` parameter -A `[]` expression is never a source for creating a new `&`, even when it is a place expression. +Everything else is rejected. In particular: -Temporaries and other value-only expressions are not place expressions. Constructor calls and ordinary function results such as `Engine()` and `makeEngine()` are not places and cannot be bound to an `&`. +- 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 engine &Engine = Engine() // ILLEGAL: Engine() is a temporary, not a place expression ``` ```zane -engine Engine() -r &Engine = engine // legal: engine is a named, stable storage location +car Car() +r &Engine = car.engine // legal: field access on a place ``` ```zane -weapons List = [Weapon(), Weapon()] -current &Weapon = weapons[1] // ILLEGAL: `[]` cannot create a new `&` +armory Armory() +weapons List<&Weapon> = [armory.primary, armory.backup] +current &Weapon = weapons[1] // legal: reads an `&Weapon` already stored in the list ``` +The last line works because `weapons[1]` reads an `&Weapon` value the list already holds. It does not mint a new `&` from a hosting element. Those stored guests are stable because the language does not let `[]` mint guests from host storage in the first place. + +Non-`&` host bindings may be initialized from any expression, including temporaries. The host materializes the value into stable storage. + ```zane -first Weapon() -second Weapon() -weapons List = [first, second] -current &Weapon = weapons[1] // legal: uses the existing stored `&Weapon` +engine Engine() // legal: plain host binding; Engine() temporary is materialized into engine ``` -This works because `weapons[1]` reads an `&Weapon` value that is already stored in the list. It does not create a new `&` from a hosting element. Those stored guests are stable because the language does not let `[]` create guests from host storage in the first place. +### 2.8.1 A bare symbol is not a guest source -Non-`&` host bindings may be initialized from any expression, including temporaries. The host materializes the value into stable storage. +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 `&`. ```zane -engine Engine() // legal: plain host binding; Engine() temporary is materialized into engine +engine Engine() +r &Engine = engine // ILLEGAL: a bare symbol is not a guest source +inspect(engine) // ILLEGAL if inspect takes `&Engine` ``` +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: + +```zane +main Player() +second Player() +guest &Player = main // ILLEGAL under this rule +second = main +``` + +`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. + +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. + +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. + > **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". + +### 2.9 Function parameters: swallow, guest, and borrow + +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. + +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: borrows and `&` -A **borrow** is non-hosting, non-escaping access to a caller's storage for the duration of a call. Unlike a guest (§2.4), a borrow has no anchor, cannot be stored in a field, and cannot be returned; it exists only while the call runs. Borrowing is the passing mode for **value types**, which have no `&` of their own. A value-type parameter is a **read-only borrow** of the caller's slot, and a value is **copied** only when it is bound into a fresh slot — an assignment, a new declaration, or a field or return store. The one writable borrow is a value-type `mut` receiver (see [`functions.md`](functions.md) §2.4). +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** is passed through the hosting/`&` system instead, in one of two modes: +| 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 | -- 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 swallowing parameter the callee only reads downgrades the caller's host all the same; declaring it `&T` (a guest) is what keeps the caller as host. -- A parameter declared as `&T` is a **guest**: the caller supplies a source that may create 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. To read a reference-type object *without* taking hosting access, pass 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 reference-type `mut` receiver is neither of these: `this` is an implicit guest to the object, never swallowed, so it composes with `&T` parameters (see [`functions.md`](functions.md) §2.4). +`'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`: -Passing a value by borrow is the semantic model; 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 +Float topSpeed(engine 'Engine) => engine.speed + +engine Engine() +s Float = topSpeed(engine) // legal: a bare symbol may be borrowed +``` + +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). ```zane type Car = #struct { @@ -164,24 +217,36 @@ Unit setSpare(this Car, engine Engine) mut { return Unit() } -// `&` parameter, read only: a reference-type object passed without consuming it -Int inspect(this Car, engine &Engine) { +// borrow parameter: a reference-type object read without consuming it and without minting a guest +Int inspect(this Car, engine 'Engine) { return this._value + engine.speed } ``` -Binding a plain (swallowed) parameter into `&` storage is illegal, 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, leaving the `&` dangling: +**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`. + +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. + +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: ```zane -Unit setEngineWrong(this Car, engine Engine) mut { - this.engine = engine // ILLEGAL: a swallowed host is not an `&` source +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 return Unit() } ``` -This rule preserves uniform call syntax. The call site writes `consume(e)` or `inspect(e)` regardless of whether the parameter is `&`. The callee's signature determines whether an `&`-creating source is required from the caller. +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". ### 2.10 Value-downstream enforcement (transitive value-only field restriction) + Value types form a closed world of plain value storage. A value-type field may contain primitives (see [`syntax.md`](syntax.md) §2.1) and other value types, but it **MUST NOT** contain a reference type (a `#`-marked type) or an `&`. This rule applies transitively: a value type containing another value type that eventually contains a reference-type or `&` field is also illegal. The same closure forbids a value type from recursing, since a self-reference would need indirection and indirection is a reference. Here, **downstream** means "through nested value-type fields." The restriction is checked recursively through the full value graph. @@ -211,6 +276,7 @@ type BadRef = struct { > **Story:** [`stories/memory.md`](../stories/memory.md#the-value-world-stays-closed-and-placement-stays-the-compilers) — "The value world stays closed, and placement stays the compiler's". ### 2.11 Symbols require direct initialization + Every symbol declaration **MUST** provide its initial value in the declaration itself. Zane does not permit bare symbol declarations followed by conditional or delayed first assignment. ```zane @@ -228,129 +294,155 @@ if runtimeBool() { ## 3. Memory Layout -### 3.1 Scope arenas and segmented offsets -The runtime does not reserve one flat region. Each lexical scope owns a **bump arena**: a chain of fixed-size **1 MiB chunks** mapped from the OS on demand. Allocation advances one frontier pointer inside the current chunk; when a chunk fills, the runtime maps a fresh 1 MiB chunk, assigns it the next **chunk id**, and makes it current. Growing an arena never copies or relocates live data. +### 3.1 Scope arenas, the global anchor pool, and segmented offsets + +Each lexical scope owns an **arena** made from two independent allocation regions: -Scopes nest last-in-first-out, and their arenas nest with them: a scope's chunks are unmapped in full the moment the scope drains (§3.2, [`lifetimes.md`](lifetimes.md) §2.1). Arena granularity is an implementation choice, like boolean packing (§3.4) and placement (§3.5) — the compiler may fold several lexical scopes into one arena. What the language fixes is the observable behavior: memory a scope allocates outlives every guest that can reach it and is released together when the scope drains. +- The **fixed-size region** stores materialized value-type slots, statically sized reference-type hosts, and the fixed-size handles of dynamically-sized reference types. +- The **dynamic region** stores the resizable backing stores behind handles such as `List` and `String`. -Within an arena, payloads and anchor cells (§4.1) occupy **separate regions** — distinct chunk chains — so a scan over payloads never strides across interleaved cell metadata. Both chains draw chunk ids from the same directory, so a segmented offset addresses either identically. +Each region is a separate chain of fixed-size **1 MiB chunks** mapped from the OS on demand. A chunk belongs to exactly one region: fixed-size slots and dynamic backing stores never coexist in the same chunk. A region maps no chunk until its first allocation. When its current chunk cannot satisfy an allocation, the runtime maps another chunk for that region, assigns it the next **chunk id**, and makes it current. + +Scopes nest last-in-first-out, and their arenas nest with them: both regions of a scope are unmapped in full the moment the scope drains (§3.2, [`lifetimes.md`](lifetimes.md) §2.1). Arena granularity is an implementation choice, like boolean packing (§3.4) and placement (§3.5) — the compiler may fold several lexical scopes into one arena. What the language fixes is the observable behavior: a scope's memory is released together when that scope drains, and no guest ever resolves into released memory. A value that escapes is promoted out of the draining scope first (§3.5, §3.7), and its guests reach the promoted value through the terminal anchor path (§4.5). + +Anchors do not belong to any scope arena. The runtime owns one **global anchor pool**, implemented as a lazy chain of anchor-only 1 MiB pages. Every anchor occupies an **8-byte-aligned, 8-byte physical slot**: the first four bytes hold a `u32` target segmented offset and the remaining four bytes identify whether that target is a hosted payload or another anchor. An anchor page therefore contains 131072 addressable slots. The pool maps its first page only when the program creates its first guest and adds another page only when its current frontier and free-address stack cannot satisfy an allocation. ```text -one scope arena - -payload region anchor-cell region -────────────────── ────────────────── - -payload chunk anchor-cell chunk -+--------------------+ +--------------------+ -| Weapon payload | | Weapon's cell | -| Player payload | | Player's cell | -| Enemy payload | | Enemy's cell | -| ... | | ... | -+--------------------+ +--------------------+ - -payload chunk anchor-cell chunk -+--------------------+ +--------------------+ -| more payloads | | more cells | -+--------------------+ +--------------------+ +one scope arena runtime-global anchor pool +────────────────────────────── ────────────────────────── +fixed-size region dynamic region [anchor page] → [anchor page] → ... +[F1] → [F2] [D1] → [D2] ``` -The two regions are separate allocation streams: a scan of payloads does -not step across anchor-cell metadata. Their chunks need not be adjacent in -native memory. Both kinds of chunk have ordinary chunk ids and are resolved -through the same chunk directory. +An ordinary dynamic allocation never straddles a chunk boundary. A dynamic block of at most 1 MiB is wholly contained in one dynamic chunk; if the remaining bytes in the current chunk cannot hold it, allocation continues in a fresh dynamic chunk. -Every in-arena location is a **`u32` segmented offset**, never a native pointer. The `u32` splits into two fields: +A dynamic block larger than 1 MiB is an **oversized span**: a dedicated contiguous OS mapping made from `block_size / 1 MiB` consecutive dynamic chunks, all belonging exclusively to that block and assigned consecutive chunk ids. Its handle stores the segmented offset of the span's first byte and its size class. After resolving that base, element addressing uses an ordinary byte offset across the contiguous mapping. Every constituent chunk also has a directory entry. Returning an oversized span pushes only its base offset onto the exact-size stack; the complete span remains mapped for reuse until the scope drains. -``` +Scope chunks and global anchor pages draw ids from the same chunk directory, so payload locations, dynamic handles, tethers, backpointers, anchor cells, and size-stack entries all use one **`u32` segmented offset**: + +```text u32 segmented offset - ┌───────────────┬─────────────────────────┐ + ┌───────────────┬──────────────────────────┐ │ chunk id │ in-chunk word offset │ │ (high bits) │ (low bits) │ - └───────────────┴─────────────────────────┘ + └───────────────┴──────────────────────────┘ ``` -Allocations are 8-byte aligned, so the low bits count 8-byte words: a 1 MiB chunk holds 2¹⁷ words, so **17 low bits** address any slot in a chunk and the remaining **15 high bits** select one of up to 32768 live chunks — a reach of 32 GiB. A small **chunk directory** maps a chunk id to that chunk's native base address, so an address is materialized only at use, as `directory[chunk id] + word offset × 8`: splitting the `u32` is a shift and a mask, and the directory lookup is one load. Tethers (§4.2), the per-host backpointer (§4.2), and the anchor cells (§4.1) are all `u32` segmented offsets. The value `0` — chunk `0`, word `0` — is the *untethered* sentinel. It costs no reserved memory: because anchor cells are allocated only in the anchor-cell region (§4.1), which never includes that slot, no cell is ever at `0`, so a `0` backpointer or tether can never name a real cell. Host payloads carry no such restriction and may occupy offset `0` — so a scope's first payload sits at a chunk base, which is why the frontier needs no reserved gap. +Allocations are at least 8-byte aligned, so the low bits count 8-byte words: a 1 MiB chunk holds 2¹⁷ words, so **17 low bits** address any slot in a chunk and the remaining **15 high bits** select one of up to 32768 live chunks — a reach of 32 GiB. The chunk directory maps a chunk id to the chunk's native base address, so an address is materialized only at use, as `directory[chunk id] + word offset × 8`: splitting the `u32` is a shift and a mask, and the directory lookup is one load. + +Tethers (§4.2), per-host backpointers (§4.2), anchor cells (§4.1), dynamic handles, and size-stack entries (§3.2) use segmented offsets. The value `0` is the *untethered* sentinel wherever an anchor identity is expected. The global anchor pool never issues `0` as an anchor identity: if its first page is assigned chunk id `0`, that page's first slot is left permanently unused. Payloads carry no such restriction and may occupy segmented offset `0`, so a region's first allocation sits at a chunk base. > **Story:** [`stories/memory.md`](../stories/memory.md#the-last-table-problem-and-the-segmented-offset) — "The last table problem, and the segmented offset". +> **Story:** [`stories/memory.md`](../stories/memory.md#two-payload-streams-and-the-anchor-that-leaves-the-scope) — "Two payload streams, and the anchor that leaves the scope". + +### 3.2 Allocation, reuse, and teardown + +The fixed-size region is a pure bump allocator: no size classes, no free list, no coalescing. A host has a fixed-size storage slot, so overwriting it destroys the current occupant and initializes the replacement directly in the same slot (§2.2, §3.7); the overwrite consumes no new space in the fixed-size region. Any dynamic backing stores owned by the destroyed occupant are returned to their exact-size stacks before the replacement becomes live. Nothing in the fixed-size region is reclaimed individually — bytes in a slot that cease to be live before the scope drains remain dead space until teardown. + +The dynamic region adds exact-size reuse on top of its bump frontier. Dynamic blocks use power-of-two byte sizes beginning at **128 bytes**. Each scope maintains one LIFO **size stack** for every block size that has become reusable. To allocate a dynamic block of size `S`, the runtime first pops `size_stack[S]`; only when that stack is empty does it bump the dynamic frontier. It never satisfies a request from another size stack and never coalesces neighbouring blocks. -### 3.2 Allocation is a bump; teardown is an unmap -Within a scope's arena, allocation is a single frontier bump — no size classes, no free list, no coalescing. Hosts are not reclaimed one at a time: a host that dies or is overwritten mid-scope (§2.2) becomes dead space in the arena until the scope drains. Reclamation is bulk. When the scope drains — after all its spawned work completes ([`concurrency.md`](concurrency.md) §4.1) — the runtime unmaps the scope's chunks and every byte the scope held is released at once, with no per-object teardown pass threaded through the exit. Logical destruction timing is unchanged — a host dies when its host, container, or scope does ([`lifetimes.md`](lifetimes.md) §2.1); it is the *memory* that is reclaimed together at drain. +Returning a dynamic block pushes its base segmented offset onto the stack for that exact byte size. The stacks are shared by all dynamic types in the scope: a 128-byte block previously used by a `List` may later hold string bytes or another list's elements. An oversized span participates in the same exact-size policy. + +The global anchor pool has one LIFO **free-address stack**, because every anchor slot has the same size. Creating an anchor pops that stack first; only when it is empty does allocation bump the global anchor frontier, mapping another anchor page as needed. Returning an anchor pushes its segmented offset onto the same stack. Anchor pages remain mapped and retain their chunk-directory entries until runtime shutdown, including when every slot on a page is free; consequently every offset retained by the stack always resolves to its original anchor slot and anchor chunk ids are never repurposed during the run. + +When a scope drains — after all its spawned work completes ([`concurrency.md`](concurrency.md) §4.1) — the runtime unmaps its fixed-size and dynamic chunks in bulk, with no per-object teardown pass threaded through the exit. Logical destruction timing is independent of this: a value dies when its host, container, or scope does ([`lifetimes.md`](lifetimes.md) §2.1); it is the *memory* that is reclaimed together at drain. Global anchor pages are not tied to scope teardown: terminal payload anchors are returned when their hosting lineages end, while forwarding anchors are returned from the former source scope's retirement stack when that scope drains (§4.6). The pages themselves remain mapped until runtime shutdown. > **Story:** [`stories/memory.md`](../stories/memory.md#when-the-free-stacks-fragment-and-the-arena-takes-the-scope) — "When the free stacks fragment, and the arena takes the scope". +> **Story:** [`stories/memory.md`](../stories/memory.md#two-payload-streams-and-the-anchor-that-leaves-the-scope) — "Two payload streams, and the anchor that leaves the scope". ### 3.3 Value and reference layout follow declaration order -Fields are laid out in declaration order. Value types are stored inline. A reference-type instance has stable identity and carries one `u32` backpointer field of anchor metadata (a segmented offset, §4.2) that stays `0` until the instance is first tethered. Arena placement of a reference-type instance is covered in §3.5. + +Fields are laid out in declaration order. Value types are stored inline. A statically sized reference-type instance is also stored inline in a fixed-size host slot, so value-type slots and reference-type host slots may sit directly beside each other in the fixed-size region. Reference types differ by identity and hosting semantics, not by requiring a separate indirect allocation. + +A reference-type instance carries one `u32` backpointer field of anchor metadata (a segmented offset, §4.2) that remains `0` until the instance is first tethered. A dynamically-sized reference type such as `List` occupies a fixed-size handle inline in the same region; only the backing store named by that handle occupies the dynamic region (§3.6). ### 3.4 Booleans may be packed + The compiler may pack booleans in structs and arena frames when doing so does not change language semantics. -### 3.5 Reference-type instances are placed in their scope's arena -Placement is an implementation decision, not a language-visible property. A reference-type instance is bump-allocated in the arena of the scope that creates it when both hold: +### 3.5 Statically sized storage uses the fixed-size region + +Placement is an implementation decision, not a language-visible property. The arena model places every materialized, statically sized scope slot — value-type storage, a reference-type host, or a dynamic type's fixed-size handle — inline in that scope's fixed-size region. The compiler may keep an unobservable value in registers or otherwise optimize its physical placement, but reference types do not require a separate heap allocation merely because they carry identity. -- its size is statically known, and -- it does not escape that scope in a way a move cannot satisfy. +When a reference-type instance is rehosted, all storage owned by that host is relocated into storage owned by the destination. Its statically sized inline bytes are copied into the destination host's fixed-size slot (§3.7). For every dynamic backing store, the runtime allocates an equal-size block or oversized span in the destination scope's dynamic region, relocates the live contents into it according to their ordinary move rules, updates the copied handle, and then returns the old source block or span to its exact-size stack. Anchored reference-type hosts inside a relocated backing store apply the same identity-merging rule as the outer host: a destination identity remains terminal and a distinct source identity forwards to it (§4.5). A promotion therefore completes before the source scope may drain and leaves no destination handle pointing into source-scope memory. -When an instance escapes — it is moved into a longer-lived host in a parent scope — it is **promoted**: its payload is copied into the destination scope's arena (§3.7). A dynamically-sized instance forces its backing store into the arena the same way (§3.6). Placement never changes observable semantics: destruction stays deterministic (see [`lifetimes.md`](lifetimes.md) §2), and tethers resolve identically regardless of which arena the instance lives in (§4), because a tether resolves through the host's anchor cell rather than a fixed address. This freedom mirrors the boolean-packing rule (§3.4): the compiler may choose the cheaper arena whenever doing so cannot change program meaning. +Placement never changes observable semantics: destruction stays deterministic (see [`lifetimes.md`](lifetimes.md) §2), and tethers resolve identically regardless of physical placement (§4), because a tether follows the host's anchor rather than a fixed address. > **Story:** [`stories/memory.md`](../stories/memory.md#the-value-world-stays-closed-and-placement-stays-the-compilers) — "The value world stays closed, and placement stays the compiler's". ### 3.6 Handle-typed dynamic reference types have fixed footprint -Dynamically-sized reference types such as `List` and `String` are represented as a fixed-size **handle**: a small header (or single segmented offset) whose dynamic backing store lives in the arena. The handle occupies a statically known footprint wherever it is stored. +Dynamically-sized reference types such as `List`, `String`, and similar types are represented as fixed-size **handles**. A handle records the backing store's segmented offset and the metadata needed by the type, such as length and size class. The handle occupies a statically known footprint inline in the fixed-size region; its resizable backing store is a separate allocation in the dynamic region. -A type that contains a handle-typed field therefore stays statically sized. A type holding a `List` field does not become dynamically sized; it stores the fixed handle inline, and only the backing store behind the handle is a separate arena allocation. +A type that contains a handle-typed field therefore stays statically sized: ```zane type Inventory = #struct { - items List; // fixed-size handle inline; backing store in the arena + items List; // fixed-size handle inline; elements in the dynamic region count Int; } ``` -This is what keeps arena placement (§3.5) broadly applicable: almost every value is statically sized at its own level, so dynamic size appears only inside the backing stores of handle types. +Dynamic block sizes are byte-based rather than element-type-based. A new list starts with a **128-byte block** — equivalent to sixteen 64-bit words — regardless of `T`. Its element capacity is `floor(block_bytes / stride(T))`. If one element does not fit in 128 bytes, the initial block is the smallest power-of-two block that can hold one element. Keeping the byte classes common allows blocks to be reused across lists with different element types and across other dynamically-sized reference types. + +A list grows according to the following rules: + +1. When its capacity is exhausted, the requested block size is exactly twice its current block size. +2. The allocator first checks the size stack for that doubled size. If a block or oversized span is available, it is popped and the live elements are relocated into it. +3. If that stack is empty, the current backing store is the dynamic frontier allocation, the doubled size is at most 1 MiB, and the additional bytes fit before the current chunk boundary, the frontier is bumped by the additional bytes and the store grows in place. +4. Otherwise, a doubled block of at most 1 MiB is bump-allocated wholly inside one dynamic chunk. A doubled block larger than 1 MiB is allocated as a fresh dedicated oversized span (§3.1). The live elements are relocated into the new block or span. +5. After relocation, the handle's backing-store offset and size class are updated and the old block's base offset is pushed onto the stack for its exact old byte size. + +A block never grows in place across a chunk boundary, and an oversized span is never extended in place: further growth relocates into a doubled oversized span after checking that exact-size stack first. Relocation moves or copies elements according to their type's ordinary move rules; the old block becomes reusable only after its previous occupants are no longer live. Guests to the list remain valid because they reach the list's host, whose fixed-size handle now names the current backing store. -A backing store is allocated **cache-line-aligned**: before it is placed the arena frontier is advanced to the next cache-line boundary. A backing store is streamed and grown in bulk, and an unaligned base would let its elements straddle cache lines, so sequential access would touch a line more than it needs. Aligning the base packs whole elements within lines. Small inline allocations keep the ordinary 8-byte alignment (§3.1) — cache-line-aligning every small object would waste most of a line per object for no locality gain, since the cost only arises when streaming across many elements. The padding to reach the boundary is at most one line, negligible against a backing store's size. +Dynamic chunks, ordinary power-of-two blocks, and oversized spans begin at cache-line-aligned addresses. Because the minimum block is 128 bytes and every larger block doubles, frontier allocations, reused blocks, and dedicated spans preserve cache-line alignment without mixing backing stores into fixed-size chunks. > **Story:** [`stories/memory.md`](../stories/memory.md#the-sentinel-that-costs-nothing-and-the-buffer-that-wanted-a-line) — "The sentinel that costs nothing, and the buffer that wanted a line". +> **Story:** [`stories/memory.md`](../stories/memory.md#two-payload-streams-and-the-anchor-that-leaves-the-scope) — "Two payload streams, and the anchor that leaves the scope". ### 3.7 Moving a value reuses the destination slot + A move transfers hosting into a destination host of the **same type** (see [`lifetimes.md`](lifetimes.md) §1). Because both sides have identical, statically known size, a move is a fixed-size overwrite of the destination slot: - Moving into a fresh declaration or a return slot is in-place initialization. - Moving into an already-initialized host first destroys the current occupant, then overwrites the same-size slot. -Moves only ever target the same or a higher scope ([`lifetimes.md`](lifetimes.md) §1.4), so the destination always outlives the source and its slot already exists. A move into a higher scope copies the inline bytes into the destination scope's arena — a promotion (§3.5). Because handle-typed fields (§3.6) keep the moved footprint small, a move relocates only the inline bytes — a handle's backing store never moves. If the moved value is tethered, the move also updates its one anchor cell (§4.5), never the tethers themselves. +Moves only ever target the same or a higher scope ([`lifetimes.md`](lifetimes.md) §1.4), so the destination always outlives the source and its slot already exists. Rehosting copies the complete hosted representation into destination-owned storage. The inline payload or handle is copied into the destination's fixed-size slot. Each dynamic backing store is relocated into an equal-size destination-region block or oversized span as specified in §3.5; after its live contents and any contained host identities have been updated, the old store is returned to the source scope's exact-size stack. The source payload bytes then cease to be live. Its host-capable slot is rewritten into guest state and stores a tether to the terminal anchor; the rest of that full-size slot is dead until the slot is overwritten or its scope drains. If both source and destination already have distinct anchor identities, the destination identity remains terminal and the source identity becomes a forwarding anchor (§4.5). Existing tethers are never enumerated or rewritten. --- ## 4. Anchors and Tethers -### 4.1 The anchor cell -Tethers are tracked through per-host **anchor cells** rather than one shared table. An anchor cell is a single **`u32`** holding the current segmented offset (§3.1) of one hosted object; it stores nothing else. A cell is an ordinary arena allocation — bump-allocated on the host's first tether (§4.3) — so there is no monolithic table to relocate as anchors accumulate: minting an anchor is one bump, never a resize. +### 4.1 The global anchor pool + +Tethers are tracked through **anchor cells** in one runtime-global pool. An anchor cell occupies one 8-byte-aligned physical slot so every cell identity is representable by the shared 8-byte-word offset encoding. Its first `u32` is a segmented target offset; its second `u32` identifies the target as either a hosted payload or another anchor. A **payload anchor** terminates at the currently hosted value. A **forwarding anchor** preserves an older guest identity after two hosting identities merge, without changing any existing tether. -Cells are bump-allocated in a **dedicated anchor-cell region** of the scope's arena, a separate chunk chain from the one holding payloads (§3.1). Keeping cells out of the payload stream means a scan over payloads never strides across interleaved cell metadata, so iteration stays dense and a payload's placement never depends on how many of its neighbours were tethered first. The cell region is itself compact and heavily reused, so resolving through a cell (§4.4) is a load into hot, cache-resident memory. +Anchor pages contain only equal-sized 8-byte slots. The pool therefore needs one free-address stack and one bump frontier rather than size classes. Pages are allocated lazily, never move, and remain mapped until runtime shutdown. > **Story:** [`stories/memory.md`](../stories/memory.md#where-the-cells-live-and-the-scan-that-pays-for-them) — "Where the cells live, and the scan that pays for them". +> **Story:** [`stories/memory.md`](../stories/memory.md#two-payload-streams-and-the-anchor-that-leaves-the-scope) — "Two payload streams, and the anchor that leaves the scope". ### 4.2 Tethers are segmented offsets, not pointers -A tether is a **`u32` segmented offset** (§3.1) pointing at the host's anchor cell — not a raw pointer and not a table index. At half the width of a 64-bit pointer, twice as many tethers fit in a cache line, and the 32-bit encoding keeps resolution on cheap 32-bit CPU math. A cell is allocated only on the first tether of a host (§4.3), so cells stay a small fraction of live memory, and the `u32`'s 32 GiB reach (§3.1) sits far beyond any realistic working set. -The value `0` (chunk `0`, word `0`, §3.1) means *untethered*. A cell is never placed at `0` (§4.1, §3.1), so `0` is never a real cell, and a stray resolution of an untethered `0` traps rather than reading live memory. +A tether is a **`u32` segmented offset** (§3.1) naming one global anchor cell — not a raw pointer and not a table index. At half the width of a 64-bit pointer, twice as many tethers fit in a cache line, and the 32-bit encoding keeps resolution on cheap 32-bit CPU math. -Every reference-type instance reserves a **`u32` backpointer** field, initialized to `0`; the first tether records the segmented offset of the instance's anchor cell there. The cell is allocated lazily (§4.3), whereas the backpointer field is always present in the layout, so object size is fixed and array layout stays uniform. The backpointer lets a host mint new tethers from the object — `&x` copies the offset — and lets a move locate and update the object's cell (§4.5). It is a single offset, not a list of tethers: the runtime never enumerates the tethers that point at the object, which is what keeps moves O(1) (§4.5). +Every reference-type payload reserves a `u32` backpointer field initialized to `0`. Once an anchor exists, that field stores the terminal payload-anchor identity. Guests may store either that terminal identity or an older identity that forwards to it. Neither guests nor backpointers store the payload address directly. -A tethered reference-type instance therefore costs **12 bytes** across the whole chain: the 4-byte tether (wherever it is stored), the 4-byte anchor cell, and the 4-byte backpointer in the payload. +An explicitly declared `&T` slot contains only this tether. A host-capable `T` slot that has been rehosted may use the same tether representation while it is in guest state, but retains enough storage to host another `T` later (§2.4). + +The minimum physical footprint attributable to one directly tethered hosting lineage is **16 bytes**: one 4-byte tether, one 8-byte physical anchor slot, and one 4-byte payload backpointer. Each additional guest adds another 4-byte tether. Merging two already-anchored hosting identities allocates no new cell: the destination cell remains the payload anchor and the existing source cell becomes a forwarder until its former source scope drains. > **Story:** [`stories/memory.md`](../stories/memory.md#the-last-table-problem-and-the-segmented-offset) — "The last table problem, and the segmented offset". ### 4.3 Anchors are created lazily -A hosted object that never gains a tether consumes no cell: its backpointer field stays `0` and no cell is allocated (it still carries the 4-byte field, §4.2). The first `&` taken on its host bump-allocates a cell in the arena, writes the hosted object's current segmented offset into it, and records the cell's own segmented offset in the object's backpointer. Every subsequent `&` from that host copies the backpointer. + +A hosting lineage that never gains a guest consumes no cell: its payload backpointer remains `0`. The first `&` taken on its host pops the global free-address stack if possible; otherwise it bump-allocates a cell at the global anchor frontier. The runtime writes the payload's current segmented offset into the cell and the cell's identity into the payload backpointer. Every later `&` from that host copies the backpointer. > **Story:** [`stories/memory.md`](../stories/memory.md#finding-the-anchor-and-not-paying-when-there-are-no-refs) — "Finding the anchor, and not paying when there are no refs". ### 4.4 Resolving a tether -Resolving a tether reads the anchor cell it points at, reads the hosted object's segmented offset from that cell, materializes the object's address through the chunk directory (§3.1), then accesses the field. Because a cell is never at `0`, a resolution of an untethered `0` never reads a live cell. + +Resolving a tether uses the chunk directory to locate its global anchor cell. If the cell is a forwarder, resolution repeats with the target anchor until it reaches a payload anchor; it then resolves that cell's payload offset through the same directory and accesses the field. Forwarding chains cannot cycle because a move redirects a superseded source identity toward the same- or longer-lived destination identity. The runtime may path-compress visited forwarding cells. Because the pool never allocates cell identity `0`, resolving an untethered `0` traps rather than reading a live cell. Consider reading a field through a tether, where `mainWeapon` is an `&Weapon`: @@ -358,74 +450,75 @@ Consider reading a field through a tether, where `mainWeapon` is an `&Weapon`: dps Float = mainWeapon.dps ``` -`mainWeapon` holds a segmented offset to an anchor cell, not the Weapon's address. Field access uses `.`: it resolves the cell, reads the hosted object's current offset from it, resolves that offset to the object's address, then adds the field offset. The walk is tether → cell → payload offset → payload address → field: - -#### Illustrative resolution walkthrough - -A tether contains a segmented offset to an anchor cell, not directly to the -hosted object. Reading `mainWeapon.dps` therefore resolves two segmented offsets. +The terminal case is tether → global anchor cell → payload offset → payload address → field. An older tether may first cross one or more forwarding anchor cells: ```text mainWeapon: &Weapon │ -│ tether segmented offset -│ [ anchor-cell chunk id | anchor-cell word offset ] +│ anchor identity ▼ chunk directory │ ▼ -anchor-cell chunk +global anchor page │ ▼ anchor cell │ -│ hosted object segmented offset -│ [ payload chunk id | payload word offset ] +│ current payload segmented offset ▼ chunk directory │ ▼ -payload chunk +fixed-size chunk │ ▼ Weapon payload │ -│ ordinary field offset within Weapon +│ ordinary field offset ▼ Weapon.dps ``` -The first segmented offset locates the anchor cell. The cell contains the -second segmented offset, which locates the hosted object's current payload. -Both use the same chunk-directory resolution rule. +Ordinary overwrites and moves into untethered destinations update one payload anchor. When two anchored hosting identities merge, the destination anchor remains terminal and the source anchor forwards to it. Existing source guests therefore gain a forwarding hop, while destination guests and newly minted guests continue to use the terminal anchor directly. Assigning or passing a guest resolves its tether and stores the terminal identity in the new guest, so an obsolete identity cannot newly escape its former source-host scope. + +The added cost over direct host access is one dependent anchor-cell load for a terminal tether and one load per uncompressed forwarding hop for an older tether. Across repeated accesses through the same guest with no intervening move or overwrite, the compiler may resolve the host address once and reuse it; the runtime may also compress the anchor path. + +### 4.5 Moves and overwrites may merge anchor identities + +An overwrite from a newly materialized value and a move from another host are distinct cases. + +- **Ordinary overwrite:** if the destination hosting slot already has a payload anchor, the replacement payload inherits that backpointer and the cell is updated to the replacement's location. Existing destination guests therefore observe the new occupant. Destroying the old occupant does not return the cell, because the destination hosting identity continues. +- **Move into a fresh or untethered destination:** if the source already has a payload anchor, that cell follows the value into the destination and remains terminal. If no anchor exists but the moved-from source slot must remain readable as a guest, the runtime allocates one for the value after relocation. The source host-capable slot stores a tether to the terminal anchor. +- **Move into an anchored destination:** the destination payload anchor remains terminal, because the destination host identity survives replacement. If the source has a different payload anchor, the runtime changes that source cell into a forwarding anchor targeting the destination cell and records the forwarder on the former source scope's retirement stack. Existing source guests continue through the forwarding cell; existing destination guests continue directly through the destination cell. The moved payload stores the destination identity in its backpointer, and the moved-from source slot stores that same terminal tether. If resolving both identities already reaches the same terminal anchor, no new forwarding edge is installed. +- **Consumed untethered temporaries:** a temporary with no source slot that must remain readable may materialize into an untethered destination with backpointer `0` and allocate no anchor. + +The same rules apply recursively to reference-type hosts contained in a relocated representation, including hosts inside dynamic backing stores. Their destination host identities survive replacement, and any distinct source identities forward to them. Anchor bookkeeping is O(1) for each merged host and never enumerates guests; physical rehosting remains proportional to the bytes, elements, and contained hosts relocated. + +This is also how a moved-from symbol stays readable: after a move the host-capable symbol enters guest state and stores the terminal tether, so reads resolve through the anchor path to the value's new home (see [`lifetimes.md`](lifetimes.md) §1.6). -If the `Weapon` moves or its host is overwritten, the runtime updates only -the hosted object's offset stored in the anchor cell. `mainWeapon` continues -to point at the same cell, and its next access reaches the payload's new location. +> **Story:** [`stories/memory.md`](../stories/memory.md#the-move-problem-and-the-anchor-that-never-moves) — "The move problem, and the anchor that never moves". +> **Story:** [`stories/memory.md`](../stories/memory.md#two-payload-streams-and-the-anchor-that-leaves-the-scope) — "Two payload streams, and the anchor that leaves the scope". -The `.` reads the field; it never reassigns the Weapon. Rebinding `mainWeapon` itself would only repoint the tether at a different host's cell (subject to the scope rule in [`lifetimes.md`](lifetimes.md) §1.1) — it would not overwrite any field. Splitting each segmented offset is a shift and a mask that fold into machine addressing once the chunk base is in hand, so the encoding costs no arithmetic over a raw-pointer dereference. The added cost is one dependent load: the cell read between the tether and the field, and because cells live packed together in the arena's compact anchor-cell region (§4.1) that load normally lands in hot, cache-resident memory. See §4.8. +### 4.6 Payload and forwarding anchors retire at different events -### 4.5 Moves, overwrites, and promotion update one cell, not all tethers -A tether follows the host/anchor path rather than pointing at a fixed object address. When a host is overwritten in place (§2.2) or its object is moved within the scope, the runtime writes the payload's new segmented offset into the object's one anchor cell, located through the backpointer (§4.2). The cell itself does not move, so every existing tether — which points at the cell, not the payload — observes the hosted object's current location on its next resolution with no per-tether fixup. +A terminal payload anchor is returned to the global free-address stack when its **hosting identity** ends. Overwriting only the current occupant does not end that identity, because the destination host remains and existing destination guests follow the replacement. Rehosting transfers teardown responsibility to the destination host. -**Promotion** on escape (§3.5) carries one extra step, because the hosted object's anchor cell lives in the anchor-cell region of the scope that minted it (§4.1) — a scope that is about to drain. Every tether that already points at that cell was taken in that scope or deeper ([`lifetimes.md`](lifetimes.md) §1.1), so none of them outlives the cell. On promotion the runtime therefore does two things: it updates the old cell to the payload's new location, so those existing tethers keep resolving to the live promoted copy for the remainder of the source scope, and it **resets the payload's backpointer to `0`**. The reset re-arms lazy allocation (§4.3): the next tether taken in the destination scope mints a fresh cell in the destination arena's cell region — one that lives exactly as long as the promoted value. The old cell and the tethers reading it then expire together when the source scope drains. +A source anchor converted into a forwarder may still be named by guests created before the move, so it is not returned when the source stops hosting. Instead, the runtime pushes it onto a retirement stack owned by the lexical scope of that former source host and returns it when that scope drains. Every guest that could already contain that obsolete identity is then dead by the ordinary scope rules. Assigning or passing such a guest stores the terminal identity (§2.6, §4.4), so the forwarding identity cannot newly escape its retirement scope. -This is why relocation, overwrite, and promotion are all **O(1) with respect to the number of tethers**. It is also how a moved-from symbol stays readable: after a move the symbol downgrades to an `&` — a segmented offset to the cell — and reads resolve through the cell to the value's new home (see [`lifetimes.md`](lifetimes.md) §1.6). +Forwarding edges always point from a former source identity toward a destination identity in the same or a higher lexical scope. A forwarder therefore never depends on an anchor retired before it; at a shared scope drain all identities from that scope may be returned together. These retirement rules require neither reference counting nor guest enumeration. When either kind of anchor is returned, no live guest can still name it, so immediate reuse needs no generation counter, delayed reuse, or ABA protection. -> **Story:** [`stories/memory.md`](../stories/memory.md#the-move-problem-and-the-anchor-that-never-moves) — "The move problem, and the anchor that never moves". [`stories/memory.md`](../stories/memory.md#where-the-cells-live-and-the-scan-that-pays-for-them) — "Where the cells live, and the scan that pays for them". +> **Story:** [`stories/memory.md`](../stories/memory.md#two-payload-streams-and-the-anchor-that-leaves-the-scope) — "Two payload streams, and the anchor that leaves the scope". -### 4.6 Teardown releases cells in bulk -Anchor cells are arena allocations, so they are never individually freed. When a scope drains, its chunks — payload and anchor-cell regions alike — are unmapped (§3.2) and its cells vanish together with the hosts and payloads they served. +### 4.7 Why tethers never dangle or misdirect -Because scope rules keep every tether inside its host's lifetime ([`lifetimes.md`](lifetimes.md) §1.1, §1.4), no live tether can point at a cell that has been unmapped. Destruction therefore creates no dangling-tether state. +A dangling or misdirected tether would require a guest to outlive the hosted value, a forwarding chain to cycle, or an anchor slot to be reused while an old guest remains. The model forbids all three. Scope checking prevents the first; forwarding always points from a superseded source identity toward a same- or longer-lived destination identity, preventing cycles; and the separate payload-anchor and forwarding-anchor retirement rules make slot reuse safe. -### 4.7 Why tethers never dangle -A dangling tether would require one of three failures: a host overwrite breaking existing tethers, a tether outliving the host's scope, or an object move leaving tethers pointed at a dead address. The model eliminates each. Host/anchor indirection makes overwrite and move follow the current cell value instead of a stale address (§4.5). The same-or-higher-scope rule keeps every tether inside the host's lifetime envelope ([`lifetimes.md`](lifetimes.md) §1.1). The model is enforced by storage shape and lexical scope, not by runtime borrow tracking. +### 4.8 Resolution and allocation cost -### 4.8 Resolution cost -The segmented encoding adds no arithmetic cost: the shift and mask that split a `u32` into a chunk id and a word offset fold into machine addressing once the chunk base is loaded. The chunk directory is the hottest table in the program — tiny, and normally resident in registers or L1 — so materializing an address from a segmented offset is effectively a single indexed load and add. +The segmented encoding adds no meaningful arithmetic cost: the shift and mask that split a `u32` fold into machine addressing once the chunk base is loaded. A terminal tether pays one dependent anchor-cell load beyond direct host access; an older identity pays one additional load per uncompressed forwarding hop. Rehosting never enumerates guests, and path compression makes repeated traversal of a chain approach the terminal case. Physical relocation cost remains proportional to the representation moved. -The genuine cost of any anchor scheme is **one extra dependent load per tether resolution** — the cell read — versus an idealized raw pointer that cannot survive moves. Because cells are packed together in the arena's compact anchor-cell region (§4.1), that load usually lands in hot, cache-resident memory, a few cycles at most. It is paid only when resolving a tether; direct access through a host never consults a cell. Across a run of accesses through the same tether with no intervening move, overwrite, or promotion, the compiler resolves the host address once and reuses it, so hot loops do not re-pay the load. +A single global free stack and frontier require synchronization under concurrent allocation and teardown. Implementations may use thread-local anchor caches backed by the same global pool. The LIFO discipline of §3.2 describes how the central pool behaves, not a guarantee the language makes: which free slot a given anchor allocation receives is unobservable from the source language, so a cache that hands out slots in another order — or holds a returned slot until it flushes — changes nothing a program can detect. What such a cache **MUST NOT** change is anchor identity uniqueness, the retirement events of §4.6, or the lifetime guarantees that rest on them. --- @@ -446,7 +539,7 @@ The genuine cost of any anchor scheme is **one extra dependent load per tether r | Property | Zane | GC languages | Rust | C/C++ | |---|---|---|---|---| -| Allocation strategy | per-scope bump arenas, bulk teardown | runtime-managed | allocator-dependent | allocator-dependent | +| Allocation strategy | per-scope fixed/dynamic arenas plus a global recyclable anchor pool | runtime-managed | allocator-dependent | allocator-dependent | > **See also:** [`lifetimes.md`](lifetimes.md) §3 for the lifetime and destruction behavior comparison. @@ -457,26 +550,32 @@ The genuine cost of any anchor scheme is **one extra dependent load per tether r | Concept | Rule | |---|---| | Hosting storage | Reference-typed symbols, fields, and container elements are directly initialized and may later be overwritten | -| Value type | Mutable in place through a borrowed `mut` receiver; storage may also be overwritten freely | -| `&` (guest) | Non-hosting storage; may be repointed, copied by value, and returned from functions | -| 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 initialized only from a named symbol, a field access of a place, or an `&` parameter; temporaries and `[]` expressions are rejected | -| `&` parameter | Declares that the caller must supply an `&`-creating source; the parameter is place-like inside the callee | -| Borrow | Non-hosting, non-escaping access to a caller's storage for the duration of a call; the passing mode for value types; no anchor, not storable, not returnable | -| Value-type parameter | A read-only borrow; caller need not supply a place; copied only when bound into a fresh slot (assignment, declaration, field or return store) | -| Reference-type parameter | Plain `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` is a guest the caller lends while remaining host (may be stored into `&` storage or returned) | -| Reference-type `mut` receiver | `this` is an implicit `&` reference, never swallowed; composes with `&T` parameters | +| Value type | Mutable in place through a borrowed `mut` subject; storage may also be overwritten freely | +| `&` (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 | +| `&` 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 | +| Value-type parameter | Always a read-only borrow; caller need not supply a place; copied only when bound into a fresh slot (assignment, declaration, field or return store) | +| 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 | | Value-downstream enforcement | Value types may contain only primitives and other value types, transitively — never a reference (`#`) or `&` 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 | -| Reference-type placement | Bump-allocated in the creating scope's arena; promoted to a parent arena only on escape — an unobservable choice | -| `&` representation | A guest is represented internally by a `u32` tether: a segmented offset (chunk id + in-chunk offset) to the host's anchor cell; `0` means no tether | -| Addressing | Every location is a `u32` segmented offset resolved through the chunk directory; 8-byte-aligned offsets reach 32 GiB across up to 32768 1 MiB chunks | -| Untethered sentinel | `0` (chunk `0`, word `0`); costs no reserved memory because cells never occupy it — payloads may sit at offset `0` | -| Backing-store alignment | Dynamically-sized backing stores (§3.6) are cache-line-aligned so sequential element access does not straddle lines; small inline allocations stay 8-byte aligned | -| Anchor cell | One `u32` per hosted object that has at least one tether, holding the object's current segmented offset; bump-allocated in the scope's dedicated anchor-cell region, kept out of the payload stream so payload iteration stays dense | -| Backpointer | Each hosted object stores the `u32` segmented offset of its anchor cell for move updates and tether minting; `0` means no cell has been allocated | -| Anchor lifecycle | Lazily allocated on first guest; on promotion the payload re-anchors in the destination arena; released in bulk when the host's scope drains | -| Tethered-instance cost | 12 bytes total: the 4-byte tether, the 4-byte anchor cell, and the 4-byte backpointer | +| Reference-type placement | Inline storage is bump-allocated in the creating scope's fixed-size region; rehosting copies inline bytes and every owned dynamic backing store into destination-owned regions before source storage is retired | +| `&` representation | A guest is represented internally by a `u32` tether: a segmented offset to a global anchor cell, which may terminate at a payload or forward to another anchor; `0` means no tether | +| Addressing | Scope chunks and global anchor pages share one `u32` segmented-offset directory; 8-byte-aligned offsets reach 32 GiB across up to 32768 1 MiB chunks | +| Untethered sentinel | `0`; the global anchor pool reserves this identity, while payloads may still occupy segmented offset `0` | +| Dynamic allocation | Power-of-two byte classes beginning at 128 bytes; exact-size stack first, frontier second; blocks above 1 MiB use dedicated contiguous oversized spans | +| Backing-store alignment | Dynamically-sized backing stores (§3.6) are cache-line-aligned; small inline allocations stay 8-byte aligned | +| Anchor cell | One global-pool 8-byte physical slot containing a `u32` target and a payload/forwarding kind; a forwarding cell targets another anchor | +| Backpointer | Each hosted payload stores the terminal payload-anchor identity for move updates and tether minting; `0` means no cell has been allocated | +| Anchor merging | Moving into an anchored destination preserves the destination anchor and converts a distinct source anchor into a forwarder; no guest is enumerated | +| Anchor lifecycle | A payload anchor returns when its hosting identity ends; a forwarding anchor returns when its former source-host scope drains | +| Anchor reuse safety | Guest canonicalization and lexical scope rules ensure no live tether names a returned slot | +| Tethered-instance cost | Minimum 16-byte direct footprint: one 4-byte tether, one 8-byte anchor slot, and one 4-byte backpointer; each retained historical identity uses one existing 8-byte forwarding slot until its retirement scope drains | > **See also:** [`lifetimes.md`](lifetimes.md) §4 for the summary of scope, move, and destruction rules. diff --git a/spec/operators.md b/spec/operators.md index be003ee..57bd534 100644 --- a/spec/operators.md +++ b/spec/operators.md @@ -34,7 +34,7 @@ Primitive operators are implementable and define the operator surface area: | `<` | binary | `Bool <(left T, right T)` | ### 2.2 Where operators may be defined -Operator implementations are package-scope verb declarations whose names are operator tokens. They are ordinary non-`mut` verbs with special names, not methods: an operator declaration never has a `this` receiver parameter. +Operator implementations are package-scope verb declarations whose names are operator tokens. They are ordinary non-`mut` verbs with special names, not methods: an operator declaration never has a `this` subject parameter. A unary operator is legal only in the home package of its operand type. A binary operator `(left T, right U)` is legal only in the home package of `T` or `U`. The bundled `core` implementation is the home package of fundamental types, but source packages cannot add declarations to it; a fundamental operand therefore does not by itself grant a source package permission to declare an operator. See [`functions.md`](functions.md) §6.1 for the corresponding method-resolution rule. diff --git a/spec/packages.md b/spec/packages.md index e379e52..06a4a1d 100644 --- a/spec/packages.md +++ b/spec/packages.md @@ -69,7 +69,7 @@ import math result Float = math$sqrt(value) ``` -The method-call lookup rules in [`functions.md`](functions.md) §6 are a distinct resolution mechanism. A qualified extension-method call writes the package name explicitly as `receiver:packageName$method(...)`. +The method-call lookup rules in [`functions.md`](functions.md) §6 are a distinct resolution mechanism. A qualified extension-method call writes the package name explicitly as `subject:packageName$method(...)`. ### 3.4 `$` separates a package namespace from its member @@ -101,7 +101,7 @@ Operators are symbol-named rather than identifier-named and cannot carry a leadi Package scope may contain immutable constants and verbs. It **MUST NOT** contain mutable variables or any other time-varying package state. -State that changes over time must live in a value, such as a `struct` or reference-typed object, and reach operations through ordinary parameters, receivers, or capability wiring. This keeps mutation visible to the effect model in [`effects.md`](effects.md). +State that changes over time must live in a value, such as a `struct` or reference-typed object, and reach operations through ordinary parameters, subjects, or capability wiring. This keeps mutation visible to the effect model in [`effects.md`](effects.md). > **Story:** [`stories/packages.md`](../stories/packages.md#state-has-to-be-a-value) — "State has to be a value". diff --git a/spec/syntax.md b/spec/syntax.md index 3782e61..186d52e 100644 --- a/spec/syntax.md +++ b/spec/syntax.md @@ -145,14 +145,27 @@ TypeName ```zane &TypeName +'TypeName ``` -`&TypeName` 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. ```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. + ### 2.4 Type expressions A type expression applies arguments to a parameterized type with `<>`. Arguments are positional. @@ -233,20 +246,26 @@ A function type leads with its return type, then lists parameter types inside `[ ```zane ReturnType[ParamType, ...] ReturnType?AbortType[ParamType, ...] -ReturnType[this ReceiverType, ParamType, ...] -ReturnType[this ReceiverType, ParamType, ...] mut -&ReturnType[this ReceiverType, ParamType, ...] -ReturnType?AbortType[this ReceiverType, ParamType, ...] -ReturnType?AbortType[this ReceiverType, ParamType, ...] mut +ReturnType[this SubjectType, ParamType, ...] +ReturnType[this SubjectType, ParamType, ...] mut +&ReturnType[this SubjectType, ParamType, ...] +ReturnType?AbortType[this SubjectType, ParamType, ...] +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: +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): ```zane ReturnType[&ParamType, ...] -&ReturnType[this ReceiverType, &ParamType, ...] +ReturnType['ParamType, ...] +&ReturnType[this &SubjectType, &ParamType, ...] +ReturnType[this SubjectType, 'ParamType, ...] mut +``` + +```zane +'ReturnType[ParamType] // ILLEGAL: a borrow is not a return type ``` `mut` is legal only when the first parameter is `this`. @@ -276,36 +295,43 @@ type Tree = #variant { leaf Int; node &Tree; } // reference sum type ```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 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. ### 3.2 Methods ```zane -ReturnType name(this ReceiverType, param ParamType, ...) { body } -ReturnType name(this ReceiverType, param &ParamType, ...) { body } -ReturnType name(this ReceiverType, param ParamType, ...) mut { body } -ReturnType name(this ReceiverType, param &ParamType, ...) mut { body } -ReturnType?AbortType name(this ReceiverType, param ParamType, ...) { body } -ReturnType?AbortType name(this ReceiverType, param ParamType, ...) mut { body } -ReturnType name(this ReceiverType, param ParamType, ...) => expr -ReturnType name(this ReceiverType, param &ParamType, ...) => expr -ReturnType name(this ReceiverType, param ParamType, ...) mut => expr -ReturnType name(this ReceiverType, param &ParamType, ...) mut => expr -ReturnType?AbortType name(this ReceiverType, param ParamType, ...) => expr -ReturnType?AbortType name(this ReceiverType, param ParamType, ...) mut => expr -ReturnType name(this ReceiverType, param ParamType, ...) { body } +ReturnType name(this SubjectType, param ParamType, ...) { body } +ReturnType name(this SubjectType, param &ParamType, ...) { body } +ReturnType name(this SubjectType, param ParamType, ...) mut { body } +ReturnType name(this SubjectType, param &ParamType, ...) mut { body } +ReturnType?AbortType name(this SubjectType, param ParamType, ...) { body } +ReturnType?AbortType name(this SubjectType, param ParamType, ...) mut { body } +ReturnType name(this SubjectType, param ParamType, ...) => expr +ReturnType name(this SubjectType, param &ParamType, ...) => expr +ReturnType name(this SubjectType, param ParamType, ...) mut => expr +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. + `=> expr` returns `expr`, including when `expr` has type `Unit`. ### 3.3 Positional constructors @@ -391,7 +417,7 @@ implicit TypeName{field FieldType} { ... } // ILLEGAL: field-constructor form is ### 3.6 Subscript definitions ```zane -(this ReceiverType)[param ParamType, ...] => placeExpr +(this SubjectType)[param ParamType, ...] => placeExpr ``` Subscript definitions have no explicit return type annotation. The body **MUST** be a place expression. If the body is not a place expression, the declaration is a compile-time error. @@ -401,10 +427,10 @@ A subscript definition may declare any number of comma-separated parameters insi The following forms are not part of the grammar: ```zane -ReturnType (this ReceiverType)[index ParamType] => expr +ReturnType (this SubjectType)[index ParamType] => expr ``` -`[]` is not a general function call form. A subscript definition always declares a place projection that references existing storage within the receiver. +`[]` is not a general function call form. A subscript definition always declares a place projection that references existing storage within the subject. ### 3.7 `init{ }` @@ -425,18 +451,20 @@ A lambda literal is a function declaration with the name removed. It writes its ```zane 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 } -ReturnType(this ReceiverType) { body } -ReturnType(this ReceiverType) mut { body } -ReturnType(this ReceiverType, param ParamType, ...) { body } -ReturnType(this ReceiverType, param ParamType, ...) mut { body } -ReturnType(this ReceiverType, param ParamType, ...) => expr -ReturnType(this ReceiverType, param ParamType, ...) mut => expr +ReturnType(this SubjectType) { body } +ReturnType(this SubjectType) mut { body } +ReturnType(this SubjectType, param ParamType, ...) { body } +ReturnType(this SubjectType, param ParamType, ...) mut { body } +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`. +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). Examples: @@ -458,7 +486,7 @@ A lambda-variable declaration binds a lambda literal to a symbol. The shorthand name ReturnType(param ParamType, ...) { body } name ReturnType(param ParamType, ...) => expr name ReturnType?AbortType(param ParamType, ...) { body } -name ReturnType(this ReceiverType, param ParamType, ...) mut { body } +name ReturnType(this SubjectType, param ParamType, ...) mut { body } ``` The shorthand expands to a symbol declaration whose type is the function type (§2.9) and whose value is the lambda literal: @@ -510,10 +538,10 @@ packageName$name(args...) ### 4.2 Method calls ```zane -receiver:method(args...) -receiver!method(args...) -receiver:packageName$method(args...) -receiver!packageName$method(args...) +subject:method(args...) +subject!method(args...) +subject:packageName$method(args...) +subject!packageName$method(args...) ``` ### 4.3 Callables are call-only @@ -548,14 +576,14 @@ Vec2(2)|100 // groups as Vec2(2)|100 ```zane spawn functionName(args...) -spawn receiver:methodName(args...) -spawn receiver!methodName(args...) +spawn subject:methodName(args...) +spawn subject!methodName(args...) spawn functionName(args...) ? binder { ... } -spawn receiver:methodName(args...) ? binder { ... } -spawn receiver!methodName(args...) ?? fallbackExpr +spawn subject:methodName(args...) ? binder { ... } +spawn subject!methodName(args...) ?? fallbackExpr name VarType = spawn functionName(args...) -name VarType = spawn receiver:methodName(args...) ? binder { ... } -name VarType = spawn receiver!methodName(args...) ? binder { ... } +name VarType = spawn subject:methodName(args...) ? binder { ... } +name VarType = spawn subject!methodName(args...) ? binder { ... } name VarType = spawn functionName(args...) ?? fallbackExpr ``` @@ -567,7 +595,7 @@ name VarType = spawn functionName(args...) ?? fallbackExpr placeExpr[argExpr, ...] ``` -`[]` is legal only when the receiver type defines a subscript declaration. A subscript expression is a place projection, not a general function call, so it is legal only when its base is a place expression. +`[]` is legal only when the subject type defines a subscript declaration. A subscript expression is a place projection, not a general function call, so it is legal only when its base is a place expression. Examples: diff --git a/spec/types.md b/spec/types.md index b9c9bf7..5bc8264 100644 --- a/spec/types.md +++ b/spec/types.md @@ -41,7 +41,7 @@ type Node = #struct { // reference type: identity, may hold `&`, may recurs ### 2.2 Value types are transitive and mutable in place A value-type body contains only field declarations, stored inline. A value type **MUST NOT** contain a reference-type or `&` field, and this holds transitively: a value type reachable through a value type must itself be a value type (see [`memory.md`](memory.md) §2.10). The restriction is what makes a value copyable and shareable-by-snapshot with no hosting or anchor bookkeeping. -A value is **mutable in place**: a `mut` method may write its fields, because the receiver is a *borrow* of the caller's storage rather than a copy (see [`effects.md`](effects.md) §2.3 and [`functions.md`](functions.md) §2.4). A value's storage slot may also be overwritten wholesale. +A value is **mutable in place**: a `mut` method may write its fields, because the subject is a *borrow* of the caller's storage rather than a copy (see [`effects.md`](effects.md) §2.3 and [`functions.md`](functions.md) §2.4). A value's storage slot may also be overwritten wholesale. ```zane package Math @@ -59,7 +59,7 @@ pos = Vec2(3, 4) // legal: overwrites the whole value ### 2.3 Field visibility is name-based Fields whose names begin with `_` are private to methods whose first parameter is `this` for that type, regardless of which package declares the method. -The same receiver type written under any other parameter name is a non-receiver parameter and does not gain private-field access. +The same subject type written under any other parameter name is a non-subject parameter and does not gain private-field access. All fields whose names do not begin with `_` are public. @@ -223,7 +223,7 @@ A named constructor is an ordinary constructor in every other respect. Naming a - overloads by parameter types, alongside the anonymous constructor and the other named ones; - is called by its qualified name and yields the **base type** — `Vector2.zeros()` is a `Vector2`, never a `Vector2.zeros` type. -The casing rule (see [`lexical.md`](lexical.md) §3) keeps the call unambiguous: `Vector2.zeros()` has an uppercase receiver, so `.zeros` is a member of the *type* — a constructor — while `v.zeros` has a lowercase receiver, so `.zeros` is a field or method of a *value*. The two never collide. +The casing rule (see [`lexical.md`](lexical.md) §3) keeps the call unambiguous: `Vector2.zeros()` has an uppercase subject, so `.zeros` is a member of the *type* — a constructor — while `v.zeros` has a lowercase subject, so `.zeros` is a field or method of a *value*. The two never collide. A named constructor **MUST NOT** be marked `implicit`: an implicit constructor is an anonymous single-argument conversion the compiler inserts at a coercion site (§4), and a name has nothing to insert. @@ -278,10 +278,10 @@ Vector{x Int, y Int} { Every field of the target type **MUST** be assigned exactly once, either explicitly or through implicit field access shorthand. ### 3.8 Constructors do not use `mut` -Constructors are not methods. They create new values rather than mutating an existing receiver, so `mut` does not apply. +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`. The caller must then supply a source that may create a new `&` under [`memory.md`](memory.md) §2.8 — not a temporary or `[]` expression. +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. ```zane package Vehicle @@ -306,14 +306,20 @@ Car(engine Engine) { Call sites: ```zane -engine Engine() -car Car(engine) // legal: engine may create a new `&` +garage Garage() +car Car(garage.spare) // legal: a field access is a guest source ``` ```zane -car Car(Engine()) // ILLEGAL: temporary cannot initialize an `&` field +engine Engine() +car Car(engine) // ILLEGAL: a bare symbol is not 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. See [`adt.md`](adt.md) §4.1 for the same requirement seen from a recursive type's side. + +> **Story:** [`stories/memory.md`](../stories/memory.md#the-slot-that-could-not-be-pointed-at) — "The slot that could not be pointed at". + A reference type whose fields are all plain hosts does not require `&` parameters: ```zane @@ -406,7 +412,7 @@ distance Meters = Meters(Feet(Float(10))) // legal: explicit conversion A coercion site is a position that passes a value into a contract whose destination type is fixed by a callable or language construct. These are the only positions where the compiler inserts an implicit constructor: - Positional arguments of a function call -- Positional arguments of a method call (the receiver is excluded; see §4.6) +- Positional arguments of a method call (the subject is excluded; see §4.6) - Positional arguments of a positional constructor call `Type(...)` - Positional arguments of a named-constructor call `Type.name(...)` - Named field entries of a field-constructor call `Type{ field = expr }` @@ -492,8 +498,8 @@ import Units implicit Units$Meters(feet Units$Feet) => init{value = feet.value * Float(0.3048)} ``` -### 4.6 Method receivers are never implicitly converted -The receiver expression (`this`) in a method call is never subject to implicit conversion. This remains true even though method calls desugar to ordinary function calls. If the receiver type does not match, the call is a type error. +### 4.6 Method subjects are never implicitly converted +The subject expression (`this`) in a method call is never subject to implicit conversion. This remains true even though method calls desugar to ordinary function calls. If the subject type does not match, the call is a type error. ```zane Unit logDistance(this Meters) { @@ -502,7 +508,7 @@ Unit logDistance(this Meters) { } feet Feet(Float(10)) -feet:logDistance() // ILLEGAL: receiver type is Feet, not Meters +feet:logDistance() // ILLEGAL: subject type is Feet, not Meters ``` > **See also:** [`functions.md`](functions.md) §5 for how implicit constructors interact with overload resolution. @@ -562,11 +568,11 @@ Intent lives entirely in the keyword — `type` versus `alias` — not in the pu | Value/reference axis | A type is a value type unless marked `#`; `#` marks only a mould — `#struct`/`#variant`/`#enum` (declared and named), each a distinct reference type with identity, `&`-aliasing, and recursion; the unmarked moulds declare value types | | Mould | One of the three type-shaping forms — `struct`, `variant`, or `enum`; each has a value form and a `#` reference form; appears only as a `type`/`alias` right-hand side, so every constructible type is named | | Use-site types | A field, parameter, or return type names a declared type or an instantiation (`Weapon`, `Vector`, `&Node`); a mould appears only as a `type`/`alias` right-hand side | -| Value type | Copied on assignment; transitively value (no reference-type or `&` field, anywhere downstream); mutable in place through a borrowed `mut` receiver; storage may also be overwritten wholesale | +| Value type | Copied on assignment; transitively value (no reference-type or `&` field, anywhere downstream); mutable in place through a borrowed `mut` subject; storage may also be overwritten wholesale | | Reference type (`#`) | Single hosting and stable identity; may hold reference-type and `&` fields; may recurse; placement is unobservable | | Fundamental type | `Int`, `Float`, `Bool`, `String`, or `Unit`; declared by the bundled `core` implementation and available unqualified | | `Unit` | Empty `core` value type; `Unit()` constructs its sole value, which may be stored or used as a generic argument | -| Field visibility | Names starting with `_` are private to `this`-parameter methods on the receiver type; all other names are public | +| Field visibility | Names starting with `_` are private to `this`-parameter methods on the subject type; all other names are public | | Constructor | Package-scope verb named after the type; the written type name is the return type; no `this`; may use block or `=> init{...}` form | | Field constructor | Declares field parameters directly, may assign default values, and may use `init{field}` shorthand | | Implicit constructor | Single-parameter constructor marked `implicit`; inserted at callable arguments, named field-constructor entries, conditions, and counted-loop bounds — never at declarations, assignments, stores, `return`, or the `init{field = value}` inside a constructor body; no field-constructor form; source type must be a value type or compiler concept; orphan rule applies | diff --git a/stories/functions.md b/stories/functions.md index 96300a8..e611de5 100644 --- a/stories/functions.md +++ b/stories/functions.md @@ -53,3 +53,21 @@ Making `Unit` a real value raised a choice between convenient procedure syntax a The problem was not whether synthesizing `Unit()` was safe; it was where the knowledge had to live. `Unit` is declared by the bundled `core` implementation like the other fundamental types. Giving it fallthrough would require a special case in return-path analysis and AST lowering solely for that nominal declaration. `Bool` does not set a precedent: an `if` node already checks a condition, so choosing `Bool` as the expected type only fills an existing slot. A return node with no expression has no such slot to check; the compiler would have to create the expression. We therefore took the strict rule we had initially rejected. Every returning path carries an explicit value, so a `Unit` verb ends in `return Unit()` and an expression body writes `Unit noOperation() => Unit()`. The cost is repetition where the signature already proves there is only one possible value. What it buys is more fundamental: `Unit` stays ordinary all the way through the compiler, and every return AST has the same shape regardless of the type travelling through it. + +## What does a receiver receive? + +The rules in this document had said "receiver" from the first draft, and we never looked at the word once. It arrived free with the object model, the way it arrives free in Go, Swift, Ruby, and Java's documentation, and a word that every neighbouring language already uses does not attract the scrutiny a coined one does. It survived the whole of the passing-mode work by being invisible. + +What eventually broke it was a rule that had to talk about `this` specifically. Taking the bare symbol away as a guest source forced us to say what a bare `this T` means on a reference type, and the sentence we reached for — *the receiver is never a swallow position* — turned out not to parse. A reader asked which receiver was meant, and the honest answer was that the word had been covering three things at once: the object a method is called on, the `this` parameter whose surface form decides how that object arrives, and the expression standing left of `:` or `!` at the call site. The rule was about the second; the reason behind it was about the third; "stores or returns the receiver" was about the first. All three were true, and the sentence let a reader pick the wrong one. So we split them — subject, subject parameter, subject expression ([`glossary.md` §3.38](https://github.com/zane-lang/spec/blob/97e5bf2/spec/glossary.md#338-subject--subject-parameter--subject-expression)) — and thought that was the end of it. + +It was not, because the next question was the one that actually landed: *what does a receiver receive?* In Smalltalk the answer is a message. A call **was** a message, sent to an object, and the object that took delivery was its receiver — the metaphor was exact, and it was load-bearing, because the message could be forwarded, reified, or not understood. Every language that copied the object-dot-method shape kept the noun and dropped the machinery underneath it. Zane has no messages. A method here is a package-scope verb whose first parameter is `this`, and `player!setScale(...)` desugars to a plain call with the object in first position ([`functions.md` §2.1](https://github.com/zane-lang/spec/blob/97e5bf2/spec/functions.md#21-methods-are-verbs-whose-first-parameter-is-this)). Nothing is sent, so nothing is received. + +That is a worse failure than it first sounds, and our own naming guide is what makes it legible. The test we apply to a candidate term is whether a fresh reader's existing sense of the word points toward the concept or away from it — whether it feeds or fights. `receiver` did neither. It is a dead metaphor: a label with no analogy left underneath, teaching nothing on contact and quietly inviting the reader to look for a delivery that does not exist. We had rejected `origin form` for being flat and descriptive; `receiver` is flatter, and it describes something untrue. + +The replacement was sitting in the register we had already committed to. This document calls callables **verbs** because a verb is the word that acts — and grammar, having lent us that, has the rest of the sentence to lend as well. A call reads *subject–verb–object*: in `player!setScale(scale)` the subject is `player`, the verb is `setScale`, the object is what it acts on. The subject is what the verb acts *from*, which is precisely what `this` is, and a reader who has met the word in a sentence already knows that much before reading a definition. It is short, ordinary, unremarkable in dense prose, and it makes the whole parameter list nameable in one register instead of one word borrowed from grammar and one from a message-passing model we do not implement. + +We weighed keeping `receiver` anyway, on the strength of it being what every other language says — a real argument, since a term's job is to be understood, and familiar-but-hollow may beat apt-but-strange. We decided the familiarity was worth less than it looks: a reader who knows `receiver` from Go knows it as a *position*, not as a metaphor, so nothing is actually lost when the position keeps its meaning under a better name. `target` was the other candidate and was rejected quickly — "call target" already means the function being called, so it would have collided with the one thing in a call it must never be confused with. + +`subject` is not free of prior claims. It is the observer pattern's `Subject`, and in security it is the principal a policy is written about. That is a weaker collision than the one that sank `matrix` — neither of those meanings is likely to be in a reader's mind while reading a method signature — but it is not nothing, and it is the honest cost of the choice. + +The larger cost is one this file is currently demonstrating. The spec is rewritten to the present, so every rule now says *subject*; the stories are not, so every chapter above this one still says *receiver*, and always will. A reader working through the design history meets both words for one concept and must carry the mapping themselves. We accepted that deliberately rather than quietly reaching back to rewrite the earlier chapters: an accurate record of the reasoning we actually had, in the words we actually had it in, is worth more than a uniform vocabulary. This chapter is the mapping. diff --git a/stories/lifetimes.md b/stories/lifetimes.md index 62bc10f..1e0d235 100644 --- a/stories/lifetimes.md +++ b/stories/lifetimes.md @@ -169,3 +169,17 @@ Move the engine out and `car` is left hosting a gap the type system still swears Laid side by side, the five stop looking like a taste for blunt rules and start looking like what they are: five different doors onto the same room, each the minimal lock on one specific way a host and its value come apart — a guest outliving its host (§1.1, §1.7), a value sinking below a guest that still tracks it (§1.4), a value consumed on some paths but not others (§1.3), a value stolen from a host that still counts it (§1.2). None substitutes for another; each closes a gap the others leave open. That is the answer to the reader who finds them rigid and wonders where the slack is: there is none to give, because loosening any one is not a gentler version of the same safety but a specific, nameable crash let back in. This is [restriction as information](foundations.md#restriction-is-information-and-the-test-of-a-good-one) at the level of a single document — every rule that forbids a program is carrying a fact the compiler would otherwise have to prove — and [that the readable rule and the fast language coincide](foundations.md#strictness-is-the-performance-model) is the standing bet, not a coincidence. The cost of buying safety this way is real, and it is not the false rejections — those the first chapter already owned. It is that the safety arrives as a *list* rather than a principle. A borrow checker derives every one of these cases from a single notion, a lifetime outliving a borrow; learn that idea and you hold all of it at once. Zane asks its reader to carry five separate rules instead, and nothing but a chapter like this one tells them the five are a set with no member removable. We think that is the right trade — five rules you can each check by eye against the code in front of you beat one proof you must trust a solver to have carried — but it is a genuine trade, and the chapter that pretends the rules are self-evidently a system, rather than a hard-won set each earning its place, is the chapter we had before this one. + +## Where a guest may be rooted + +The five rules of the previous chapter were each a lock on one way a host and its value come apart. Narrowing the guest source, over in [the memory story](memory.md#the-slot-that-could-not-be-pointed-at), turned out to change what two of those locks are guarding, and the adjustment is small enough to state precisely and worth stating because the reasoning is easy to get backwards. + +The scope check ([§1.1](https://github.com/zane-lang/spec/blob/b10eaed/spec/lifetimes.md#11--assignment-uses-host-scope)) did not change at all — it still asks only whether the target's host is declared in the same or a higher lexical scope than the guest — but it now sits behind a prior question, because a source that is not a guest source never reaches the scope check in the first place. The visible effect is that the canonical illegal example is no longer the interesting one. `r = innerNode`, rejected for scope, used to be the rule's whole face; now the more common rejection is `r &Node = node`, refused before any scope is compared, and the example that still exercises the scope rule has to reach through a field to get there. That is a small loss of pedagogical clarity in exchange for a large one of exposure: the rule that used to be the only thing standing between a program and a dangling guest is now the second line of defence. + +The rule that did change is the one governing returns ([§1.7](https://github.com/zane-lang/spec/blob/b10eaed/spec/lifetimes.md#17-returned--values-must-be-rooted-in-a-guest-parameter)). It used to say that a returned `&T` must be rooted in *a parameter*, which was the right rule when there was only one kind of reference parameter to be rooted in. With three, "a parameter" is no longer specific enough, and each of the other two fails for its own reason. A swallowing `T` parameter is a bare symbol in the call-site scope, and a bare symbol is not a guest source — so the returned guest could not have been minted in the first place. A `'T` borrow fails harder: the borrow ends when the call does, and a guest rooted in one would outlive the very access it was derived from. So the rule now names the guest parameter specifically. What makes this feel right rather than merely tighter is that the three modes each answer the question the rule is really asking — *may this outlive the call?* — and only one of them answers yes. + +The same reasoning had to be pushed one step further than the rule's own text, and this is the part that is easy to miss. Refusing to return a `'T` parameter is pointless if you may instead return a guest minted from one of its **fields**: `this.weapon` on a borrowed subject would escape just as surely as `this` would, wrapped in one layer of indirection. So a field access rooted in a borrow is not a guest source either. We debated allowing it — the caller's object does outlive the call, so the guest would in fact be live — and rejected it on the grounds that "in fact live" is not the standard. The compiler would have to reason about the relative scopes of two objects across a call boundary to know it, which is the interprocedural analysis this whole document exists to avoid. A borrow that does not escape, with no exceptions and nothing to check, is worth more than a borrow that escapes safely under an argument only the compiler can follow. + +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. diff --git a/stories/memory.md b/stories/memory.md index 6a8afec..cb84f4d 100644 --- a/stories/memory.md +++ b/stories/memory.md @@ -126,3 +126,66 @@ The source pair is now **host** and **guest**. A host is the symbol, field, or c The runtime keeps **anchor** and **tether**. Each guest is represented by a tether that resolves through an anchor; moving or rehosting the object updates the anchor, so existing tethers keep working. That vocabulary remains a natural mechanical picture, but it no longer leaks upward into source semantics. The concise model is: *an object lives in a host; a guest may access it; internally, the guest's tether follows the object through its anchor.* The alternatives each blurred something we wanted to keep sharp. **Owner/tether** named the two halves accurately in isolation but paired source semantics with implementation. **Owner/guest** worked, though “owner” stressed rights and destruction more than residence. **Owner/view** was technically reasonable without being a convincing lived relationship. Proxy, keyholder, delegate, and licensee were variously technical, overloaded, or indirect; “key” also collided with dictionary keys. CC/email language suggested secondary participation, but a CC recipient receives an independent copy rather than live access to one moving object. And keeping **tether** as the name of `&T` remained expressive, but preserved the very overload this split was meant to remove. + +## Two payload streams, and the anchor that leaves the scope + +With the vocabulary settled, the allocator came back open. The scope arena survived, but the pure-bump conclusion did not survive unchanged. Fixed-size values, reference-type hosts, and dynamic handles still fit the original rule: append them densely and reclaim their chunks when the scope drains. Resizable backing stores do not. A list can abandon several buffers while its scope remains alive, so treating those buffers like ordinary fixed-size payloads strands exactly the kind of reusable holes that matter. The arena therefore split into two lazy chunk chains per scope: one fixed-size region that remains a pure bump allocator, and one dynamic region that may reuse backing-store blocks. A chunk belongs to exactly one region, and a scope that never allocates a backing store never maps a dynamic chunk. + +The dynamic region brings back free stacks in the one place where their fragmentation is controlled rather than global. Blocks use shared power-of-two byte classes beginning at 128 bytes, independent of element type. Allocation checks the exact-size LIFO stack first and bumps the frontier only when that stack is empty. A full list requests exactly twice its current byte size; it grows in place only when it is the frontier allocation and the added bytes fit before the chunk boundary. Otherwise its elements relocate into a reusable doubled block or a newly bumped one, and the old block enters its exact-size stack. Blocks above 1 MiB become dedicated contiguous oversized spans, addressed by one base segmented offset and reused through the same exact-size rule. The cost is dead space between size classes and until scope teardown, but reuse is confined to the buffers whose repeated growth creates it. + +The scope-local anchor region turned out to have a deeper problem than teardown, and it retires a claim two chapters back. [The segmented-offset chapter](#the-last-table-problem-and-the-segmented-offset) had promotion follow the backpointer to *the* one anchor cell and rewrite it, so no tether ever learns the payload moved. That holds only while a payload has exactly one cell to rewrite. It stops the moment both sides of a move are already anchored: promotion could leave source guests naming the old cell while destination guests named a new one, and a later move had only one payload backpointer and could update only one path. Moving either cell would merely force every existing guest on that side to be repointed — the work anchors exist to avoid. The final design therefore takes anchors out of scopes entirely. One runtime-global pool grows through anchor-only pages, and an anchor cell may target either a payload or another anchor. + +That second target kind makes identity merging mechanical. A move into an already-anchored destination destroys its old occupant but preserves the destination host identity, so the destination anchor remains the terminal payload anchor. A distinct source anchor changes into a forwarding cell that targets it. Old source guests walk source anchor to destination anchor to payload; destination guests and newly created guests go directly to the destination anchor. Nothing in the source language exposes the chain, and no guest is enumerated or rewritten. + +The same rule applies inside dynamic values. Rehosting copies the inline payload or handle into the destination host and relocates every owned backing store into an equal-size block or oversized span in the destination scope's dynamic region. Contained reference-type hosts merge their anchor identities in the same way. The old source payload bytes cease to be live, and its host-capable slot stores the terminal tether as a guest. Anchor work remains O(1) per merged host, while the physical move is proportional to the bytes, elements, and contained hosts relocated. + +Each anchor occupies an 8-byte-aligned physical slot: four bytes hold a target segmented offset and four identify whether that target is a payload or another anchor. The pool uses one free-address stack because all slots have the same size; allocation pops it before bumping the global frontier. Anchor pages remain mapped until runtime shutdown, even when wholly free, so an offset retained by the stack can never name an unmapped or repurposed page. The minimum physical machinery for one directly tethered identity remains sixteen bytes: a four-byte tether, an eight-byte anchor slot, and a four-byte payload backpointer. + +The chain has a natural direction. A superseded source identity always forwards toward the destination identity, and moves only target the same or a higher scope, so a forwarding edge never points toward a shorter-lived host. Resolution may compress the path. Copying or rebinding an old guest stores the terminal identity rather than propagating the obsolete one, which means a forwarding anchor cannot newly escape the lexical scope that originally contained its source host. + +That gives the two cell kinds different retirement events. A terminal payload anchor returns when its final hosting identity ends. A forwarding anchor is pushed onto its former source scope's retirement stack and remains only until that scope drains, when every old guest that could still contain the identity is dead. Neither case requires reference counting or guest enumeration, and both permit immediate free-stack reuse. The earlier restriction on moves with guests on both sides disappears entirely: the two identities simply become a runtime chain. An explicit `&T` remains guest-only and holds only a tether, while a slot declared `T` keeps its full storage after rehosting and may later host another `T`. + +The sentinel changes by one small accounting detail. Segmented offset zero remains a valid payload location, but the global pool never issues anchor identity zero. The sentinel therefore costs one unusable anchor-slot identity rather than forcing either payload region away from its naturally aligned chunk base. Dynamic blocks begin at 128 bytes and preserve cache-line alignment through doubling, reuse, and oversized spans. + +## The slot that could not be pointed at + +The whole of the preceding machinery — hosts, guests, anchors, forwarding cells, retirement stacks — was built to answer one question, and it took a five-line program to show that we had been answering the wrong one: + +```zane +main Player() +second Player() +guest &Player = main +second = main +``` + +Line 4 moves the object out of `main`'s slot and into `second`'s. Line 3 had already handed out a guest. So what does `guest` denote afterwards? The anchor system gives an answer — it follows the object, because that is exactly what anchors are for — and the answer is defensible. But it is not *obviously* right. A reader who wrote `&Player = main` may well have meant "watch that variable," in which case following the object is wrong; a reader who meant "watch that player" is served correctly. The spelling does not distinguish them, and the model had quietly picked one reading and made the other unsayable. Worse, the reader who wanted the other reading had no way to find out except by discovering that their program did something they did not expect. That is the shape of a design flaw, not a documentation gap. + +The first instinct was to make the machinery smarter. If a guest tracked the *name* rather than the object, `guest` would keep denoting `main`'s slot; but `main`'s slot is now empty in every sense that matters, so a guest to it is a guest to nothing and we are back to needing a null state we had spent the whole model avoiding. If it tracked the object, we had what we already had. So we tried a third layer: name → location → object, with the location as a stable middle that either end could be re-pointed at independently. That is a genuine idea and it is also, on inspection, the anchor system with an extra name — it moves the ambiguity from "which does a guest follow" to "which does a location follow," and it charges another indirection for the privilege. We dropped it. + +Then we tried the direction that looks decisive: make the object immovable. If nothing relocates, the question cannot be asked. We built that model out in full and it failed twice over, in ways worth recording because they are not obvious from the outside. It could not express a move whose destination is decided at runtime — `if someIO() { boat.bottom!append(car) } else { boat.top!append(car) }` — and its answer was to forbid the program rather than to place the car. And `append` moves into a list's backing store, which relocates when the list grows; the model was claiming immovability in one section and admitting relocation in another. Anchors exist precisely so that objects *can* move. Removing them to buy an invariant the design cannot actually hold was the wrong turn, and we took it far enough to be sure. + +The opposite extreme — no guests at all, so nothing can be left pointing anywhere — fails for a duller reason: a function that only wants to read an object would have to swallow it, and a language where reading costs you ownership is not one anyone would enjoy writing. + +What finally resolved it was narrowing the question instead of answering it. The trouble is not that guests exist, and not that objects move; it is that a **bare symbol's own hosting slot** is both the thing you may most freely overwrite and, until now, a thing you could point at. Those two properties are what generate the ambiguity, and only one of them is load-bearing. So [`memory.md` §2.8.1](https://github.com/zane-lang/spec/blob/b10eaed/spec/memory.md#281-a-bare-symbol-is-not-a-guest-source) removes the other: a bare symbol stays a place expression — you may read it, mutate it, move from it, borrow it — but it is no longer a **guest source**. A new `&` is minted from a field access or from an `&T` parameter, and from nothing else. Line 3 of the program above is now a compile error, and it is an error at the line that creates the problem rather than a puzzle at the line that reveals it. + +The reason this costs so little is worth stating plainly, because it is the argument that decided it: **a guest to a bare symbol never buys any reach.** A guest exists to let storage that does not own an object nevertheless get at it — a field in another object, an element of a container, a callee's parameter. But a bare symbol is, by construction, already in scope everywhere a guest to it could be declared; you can simply use the symbol. The only thing the removed source was genuinely doing was carrying an object into a call, and that job now belongs to [the borrow mode](#three-ways-to-hand-over-an-object). + +Fields are a different matter, and they stay. A field belongs to an object whose lifetime the host system already tracks, so a guest to `car.engine` has something meaningful to follow when the field is overwritten or the containing object is rehosted — and follow it does, through the anchor path exactly as before. That asymmetry is the whole content of the rule: it constrains where a guest may *come from*, and changes nothing about what a guest can *survive*. The anchor system is untouched. + +The cost is real and lands in one place: a structure that needs guests must be **rooted in a field** rather than in a bare local. A recursive `#variant` boxes its recursive case through `&`, so building one now starts from a node hosted in a field — `Expr.flip(tree.root)` rather than `Expr.flip(leaf)` — as [`adt.md` §4.1](https://github.com/zane-lang/spec/blob/b10eaed/spec/adt.md#41-a-recursive-structure-is-rooted-in-a-field) spells out. We spent some time deciding whether that was a defect. It is not, quite: the root of a recursive structure is what keeps the whole shape alive, and a field of the type that owns the structure is where such a thing belongs anyway. But it *is* a constraint the previous design did not impose, and the programmer who reaches for a bare local to hold their first node will meet it as a surprise before they meet it as a principle. That is the honest ledger: one surprising rejection at the root of a tree, bought with the disappearance of an entire class of question about what a guest means after a move. + +## Three ways to hand over an object + +Taking away the bare-symbol guest source left a hole immediately, and it is the most ordinary hole imaginable: `topSpeed(engine)`, where `engine` is a local and the function only wants to read it. Under the old model that parameter was `&Engine` and the argument was a bare symbol — the exact pairing now forbidden. Swallowing it instead is absurd for a read. So the rule as stated made a common program unwritable, and needed a companion. + +We considered giving `&T` parameters a special exemption: a bare symbol may not initialize `&` *storage*, but may still feed an `&T` *parameter*. That is nearly right, and it is where the thinking sat for a while. What sank it is that an `&T` parameter is not merely a way in — it is a guest, and a callee may store it in a field or return it. Exempting the argument would mean a bare symbol's slot could still end up with something pointing at it, arriving by the one route the rule did not check. The exemption would have to be conditional on what the callee does with the parameter, which is interprocedural inference — the thing [`lifetimes.md` §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) was written specifically to retire. + +So the passing mode had to split. A parameter that merely reads or mutates the caller's object for the duration of the call is a genuinely different contract from one that keeps a guest past the call, and the old `&T` was carrying both. Separating them gives the three modes in [`memory.md` §2.9](https://github.com/zane-lang/spec/blob/b10eaed/spec/memory.md#29-function-parameters-swallow-guest-and-borrow): `T` **swallows**, taking hosting access and downgrading the caller; `&T` takes a **guest**, which the callee may store or return and which only a guest source can supply; `'T` **borrows**, accepting any place at all — bare symbols included — and granting read and `mut` access that expires with the call. The borrow was not a new concept: value types had always been passed exactly this way, and the reference world had simply never been given the same option. + +That third mode turned out to pay for itself immediately in a place we had not been aiming at. A reference-type subject had been an implicit guest, which under the new source rule would have made `node!setScale(...)` illegal on a bare local — an absurdity. But a subject almost never needs to be kept; it needs to be read and written for the duration of the call. So a bare `this T` on a reference type is now an implicit `'T` borrow ([`functions.md` §2.4](https://github.com/zane-lang/spec/blob/b10eaed/spec/functions.md#24-mutating-methods-use-mut)), and `this &T` is what a method writes in the rarer case where it stores or returns the subject. The pleasing part is that this makes the two type worlds agree: a `mut` subject is a mutable borrow of the caller's slot whether the type is a value or a reference, and the special-casing that used to sit in that sentence is gone. + +Naming the mode took longer than designing it. The tempting move was to hand the constrained meaning to the bare `&` and mark the escaping one, on the general principle that the marked form should be the restricted form. That principle does not apply here, and noticing why was the turn: **both** forms are marked. The unmarked form is `T`, the swallow. Between `&` and a new sigil there is no asymmetry of markedness to appeal to, so the argument has to be made on continuity instead — and there `&` has a large incumbent claim. It means *guest* in a field type, in a storage declaration, in a return type, in the glossary, and across every chapter above this one. Redefining it in the parameter position alone would make the same character mean two things depending on where it sits, which is precisely the kind of context-dependence [the two-vocabulary chapter](#two-vocabularies-host-and-guest-above-anchor-and-tether) had just finished removing from `tether`. So `&` keeps meaning guest everywhere, and the new concept takes the new mark. + +`'` won it on two counts. The character was unused in Zane's lexis — no character literals, no operator, no identifier start — so it cost nothing to reserve ([`lexical.md` §4.3](https://github.com/zane-lang/spec/blob/b10eaed/spec/lexical.md#43-reserved-sigils)). And its existing association in the reader's mind is with Rust's lifetimes, which is closer to right than to wrong: a Rust lifetime annotates precisely the thing a Zane borrow *is* — access valid for a bounded duration and no longer. The difference is that Zane's version has nothing to name, because the duration is always the call. Borrowing the character while dropping the parameter it usually carries is, if anything, a decent summary of what the language does with lifetimes generally. We weighed a keyword — `borrow Engine` — and rejected it as too heavy for something that appears on most parameters; and we weighed reusing `#`, which is taken by the reference mould and would have been genuinely confusing. + +The cost of three modes is that there are three of them. A programmer now chooses a passing mode per parameter, and the choice is not always forced: a parameter that only reads could be written `'T` or `&T`, and only the question "does the callee keep it?" separates them. We do not think this is a real burden — the modes correspond to three things people already distinguish when they think about a function's contract, and the signature now states which one applies rather than leaving it to the body — but it is one more axis in every signature, and it is fair to count it. Against that, one thing genuinely got simpler: an overload set may not differ only by the passing mode ([`functions.md` §4.1](https://github.com/zane-lang/spec/blob/b10eaed/spec/functions.md#41-overload-identity-is-parameter-types-only)), because the mode changes what the caller must supply and what state the call leaves them in, with nothing at the call site to tell two such overloads apart. That rule already existed for `T` versus `&T`; widening it to all three modes cost nothing and closed the door before anyone tried it.