Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 21 additions & 36 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,51 +50,36 @@ home `spec/generics.md`, casing rules `spec/lexical.md`). Several pre-redesign
forms are now illegal and must never reappear. Grep for them — none should hit:

```sh
grep -RIn -E "Array\[|\[size\]|Array[0-9]+|Matrix10|\[rows\]|\[cols\]|inferred type generic|type-parameter symbol|root form" spec/
grep -RIn -E "Array\[|\[size\]|Array[0-9]+|Matrix10|\[rows\]|\[cols\]|inferred type generic|type-parameter symbol|root form|'[A-Z]" spec/
```

`'[A-Z]` used to be on that list — it is **not** any more. A leading `'` is now
the **borrow** type marker (`'Node`), canonical home `spec/memory.md` §2.9,
surface form `spec/syntax.md` §2.3. Do not re-add it to the retired-forms grep.
`'[A-Z]` is back on that list. A leading `'` was the **borrow** type marker
(`'Node`) for one release of the design; the borrow mode for reference types has
since been removed, so `'` is again a character Zane's lexis does not use. A
reference-type parameter has exactly two modes, `T` and `&T` (`spec/memory.md`
§2.9), and `&` is the only marker a type may carry (`spec/syntax.md` §2.3).

The only legitimate stray `<...>` is `Result<T, E>` in `spec/error-handling.md`
— Rust's type named as a comparison, not Zane's.

A second guard covers the memory model. A **bare symbol is not a guest source**
(`spec/memory.md` §2.8.1), so a spec example that mints an `&` from one is a
bug. Eyeball every hit of:
There used to be a second guard here, matching `&X = bareSymbol` to catch spec
examples that mint an `&` from a bare symbol. It is **gone**, and must not be
restored: a bare symbol is a guest source again (`spec/memory.md` §2.8), so a
match no longer indicates a defect. It does not indicate a correct line either —
what still governs an `&` assignment is the scope comparison in
`spec/lifetimes.md` §1.1, and a bare-symbol assignment still fails it when the
target's host is declared deeper than the `&`. No grep can decide that; it needs
the declaration scopes of both sides. The pattern separated nothing worth
separating, which is why it is gone rather than reworded.

```sh
grep -RIn -E "&[A-Z][A-Za-z0-9]*[[:space:]]*=[[:space:]]*_?[a-z][A-Za-z0-9]*[[:space:]]*(//.*)?[[:space:]]*$" spec/
```

The pattern matches a **bare-symbol** right-hand side. Only one legal source is
excluded syntactically: a field access (`= car.engine`) never matches, because
`.` is outside the character class. The other legal source **does** match — an
`&T` parameter is written bare, so `r &Node = source` inside a callee is a hit
even though it is correct. Read every hit and keep it if any of these hold:

- the right-hand side is an `&T` parameter of the enclosing verb (check the
signature, not the line);
- it is a deliberate `// ILLEGAL:` example;
- it is a grammar metavariable, as in `syntax.md`.

Anything else is a real one to fix.

Two details are load-bearing. The trailing `(//.*)?[[:space:]]*$` is what makes
the guard see the `// ILLEGAL: ...` examples; without it the end anchor skipped
every commented line, which is most of them. The `_?` catches a private
lowercase name (`_engine`) — Zane allows `_` only as a leading character, never
inside a name (`lexical.md` §4.1–4.2), so nothing more is needed there.

Run both with `-R` on the directory, not a `spec/*.md` glob plus a bare
directory argument: `grep` prints `bench/: Is a directory` and silently skips
it otherwise.
Run the retired-forms grep with `-R` on the directory, not a `spec/*.md` glob
plus a bare directory argument: `grep` prints `bench/: Is a directory` and
silently skips it otherwise.

Stories are exempt from both greps: `stories/` records the language as it was
at each turn and is never rewritten to match the present spec.
Stories are exempt from every grep here: `stories/` records the language as it
was at each turn and is never rewritten to match the present spec.

A third guard covers one term. The subject of a method — the object it is
A second guard covers one term. The subject of a method — the object it is
called on — is the **subject**, never the *receiver*; `receiver` was Smalltalk
residue naming a message Zane does not have, and it was renamed throughout
`spec/` (canonical home `functions.md` §2.1, glossary §3.38). The word survives
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ The spec states *what* the language does; the **why** lives in a parallel set of
| [`stories/adt.md`](stories/adt.md) | [`spec/adt.md`](spec/adt.md) — splitting `enum` from `variant` against the hype, the shared struct body, escaping the matcher machine with case overloads and the turn to a central `match` block, matching variants rather than patterns, keeping enum data outside the members, reducing a match group to sugar for one arm per case, building a variant by naming a case rather than calling a constructor, the bindings that existed only to be pointed at, and making a recursive member an owned child boxed through a hosting handle rather than an `&`, and the sum that could not contain itself until a value copy became deep |
| [`stories/generics.md`](stories/generics.md) | [`spec/generics.md`](spec/generics.md) — the parameter model, the `<>`/`()` split, size-in-the-type, and the deferred features |
| [`stories/dependencies.md`](stories/dependencies.md) | [`spec/dependencies.md`](spec/dependencies.md) — URL identity, the manifest/resolution split, prebuilt distribution, symbol-rewriting, the browsable global cache, the package-graph acyclicity rule, opt-in remapping, and why `core` became a bundled implementation package |
| [`stories/memory.md`](stories/memory.md) | [`spec/memory.md`](spec/memory.md) — the no-GC-no-lifetimes goal, the move problem and the anchor, lazy backpointer creation, the indexed heap table, the rooted-guest rules and the host/guest terminology split, the collapse to one value/reference axis with a borrowed subject, the shift to segmented chunked bump arenas, the split into fixed-size and dynamic regions with anchors moved to a runtime-global recyclable pool, taking the bare symbol away as a guest source, the three passing modes that split out of it, and the dynamic region taking the boxes, each asking for exactly its own size, once recursion became hosting, and what a copy is for — the deep value copy that lets a value type recurse, and the reference-field ban that survived it |
| [`stories/lifetimes.md`](stories/lifetimes.md) | [`spec/lifetimes.md`](spec/lifetimes.md) — lexical scope in place of a borrow checker, what may be moved, the declaration-block rule that kills flow analysis, downgrade instead of use-after-move, parameter-rooted returned guests, why each strict rule is the minimal guard against one specific memory corruption, and narrowing a returned guest's root to a guest parameter once borrows arrived |
| [`stories/memory.md`](stories/memory.md) | [`spec/memory.md`](spec/memory.md) — the no-GC-no-lifetimes goal, the move problem and the anchor, lazy backpointer creation, the indexed heap table, the rooted-guest rules and the host/guest terminology split, the collapse to one value/reference axis with a borrowed subject, the shift to segmented chunked bump arenas, the split into fixed-size and dynamic regions with anchors moved to a runtime-global recyclable pool, taking the bare symbol away as a guest source, the three passing modes that split out of it, giving both back when the ban cost more than the question it closed, and the dynamic region taking the boxes, each asking for exactly its own size, once recursion became hosting, and what a copy is for — the deep value copy that lets a value type recurse, and the reference-field ban that survived it |
| [`stories/lifetimes.md`](stories/lifetimes.md) | [`spec/lifetimes.md`](spec/lifetimes.md) — lexical scope in place of a borrow checker, what may be moved, the declaration-block rule that kills flow analysis, downgrade instead of use-after-move, parameter-rooted returned guests, why each strict rule is the minimal guard against one specific memory corruption, narrowing a returned guest's root to a guest parameter once borrows arrived, and the root rule going back to "any parameter" once they left |
| [`stories/effects.md`](stories/effects.md) | [`spec/effects.md`](spec/effects.md) — inferring effects instead of annotating them, subject-scoped `mut`, capabilities in place of ambient I/O, the four-level ladder and the Total-Pure/Pure split, what deliberately is not an effect, and mutation through a borrowed subject |
| [`stories/concurrency.md`](stories/concurrency.md) | [`spec/concurrency.md`](spec/concurrency.md) — the parallelism/concurrency split and the refusal of `async` coloring, why `spawn` marks only a call, water-tower lifetimes, signature-based safety without locks, and value-typed mutation closing the aliased-write gap |
| [`stories/error-handling.md`](stories/error-handling.md) | [`spec/error-handling.md`](spec/error-handling.md) — the two-doors model and why failure is control flow rather than a `Result` value, `resolve` as expression-substitution rather than assignment, typed abort paths and the deliberately-absent propagate operator, keeping abortability orthogonal to effects, and explicit path values through `Unit` |
Expand Down
2 changes: 1 addition & 1 deletion spec/adt.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ program Expr = Expr.op(Operation(Expr.intLit("3"), Expr.intLit("2"), Operator.ad
- **Boxing is required on a cycle and permitted off one.** The graph this rule reads is one of **owning** edges — members that store an instance of their type, inline or boxed. An `&` member is not one of them: a guest is fixed-size storage whatever it points at, so a cycle closed through `&` needs no boxing and never triggers this rule. A member **MUST** be boxed when its declared type can lead back to the enclosing type along owning edges — when its edge lies on a cycle in that graph — because no finite inline layout exists for it. Every such edge is boxed, so nothing depends on declaration order or on choosing where to cut the cycle: above, `Expr.op`, `Operation.left`, and `Operation.right` are all boxed, and every type on the cycle has a finite, statically known size. Off a cycle, an implementation **MAY** box or inline as it judges best. Inline is the ordinary choice, but a sum whose widest case dwarfs its common ones is the case an implementation may want to place out of line, and nothing here forecloses that. The choice is made per type, not per instance, so every value of a type is still the same size and uniform stride holds (see [`generics.md`](generics.md) §7); and a program cannot observe which was chosen, because placement is not a language-visible property (see [`memory.md`](memory.md) §3.5) and no operator exposes a type's footprint.
- **Nothing is written for it.** The programmer writes `left Expr`; the compiler boxes it because inline placement is impossible, exactly as it places list elements out of line. There is no `Box<T>` type and no marker, because placement was never a language-visible property (see [`memory.md`](memory.md) §3.5).
- **Both kinds may recurse.** A recursive value type is legal, because a box is placement rather than a reference-type field. The body syntax is symmetric across all four kinds, and so is recursion: `#` decides identity, aliasing, and copy-versus-move, not whether a type may contain itself. What a copy of a recursive value costs, and why it shares no node with its original, is the deep-copy rule (see [`memory.md`](memory.md) §2.3).
- **`&` is for aliasing, not for recursion.** A guest expresses non-hosting access — a parent back-pointer, a symbol table naming nodes, a genuine graph edge — and a cycle of guests is a shape hosting could not express in the first place. An `&` member follows the ordinary guest rules, including the guest-source restriction (see [`memory.md`](memory.md) §2.8); an owning member, boxed or not, does not.
- **`&` is for aliasing, not for recursion.** A guest expresses non-hosting access — a parent back-pointer, a symbol table naming nodes, a genuine graph edge — and a cycle of guests is a shape hosting could not express in the first place. An `&` member follows the ordinary guest rules — it must be assigned from a guest source ([`memory.md`](memory.md) §2.8) whose host is in the same or a higher scope ([`lifetimes.md`](lifetimes.md) §1.1); an owning member, boxed or not, is subject to neither.

Boxing carries costs, all of them the price of the child actually being owned. Reaching a boxed child costs one indirection, which recursion cannot avoid. Rehosting a reference-type node relocates every boxed descendant into the destination scope's dynamic region, so moving a tree costs time proportional to the tree rather than to its root (see [`memory.md`](memory.md) §3.5) — the same price a `List` already pays for its backing store. A recursive **value** type pays that cost more often, because it is copied rather than moved — but only where a copy actually happens. Binding an existing **place** into another slot walks and reallocates the whole structure. Building a fresh one does not: a non-place result is constructed directly in its eventual destination, recursively, so `Countdown.more(Countdown.more(Countdown.done(Unit())))` builds each node once where it will live rather than copying each completed prefix into the next (see [`memory.md`](memory.md) §2.3). Passing one to a parameter does not either, since a value-type parameter is a read-only borrow (§2.9). The O(structure) case is the one that reads like a copy: `b Countdown = a`, or a field, container, or return store whose source is an **existing** value. A store whose source is a fresh non-place result constructs in place instead and costs nothing extra, returns included. Where a tree is large and shared handling is wanted, that is the signal to reach for the reference form.

Expand Down
2 changes: 1 addition & 1 deletion spec/concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ A spawned call may **mutate** state only through a value-typed subject. A spawne
A direct consequence is that reference types are never mutated by spawned work, so every concurrent **read** of the reference-typed object graph is safe by construction.

### 4.3 Single writer per storage location
For any one storage location, at most one live spawned call may hold a **mutable borrow** — the `!` subject of a spawned `mut` call. By §4.2 that subject is always value-typed, so the borrows this rule counts are value borrows; a reference-type `'T` borrow ([`memory.md`](memory.md) §2.9) never reaches a spawned `mut` subject. Two spawned calls that mutably borrow the same location are a compile-time error. Because value types carry no `&`, a location's identity is unambiguous — there is no hidden alias to obscure that two subjects denote the same slot — so this disjointness is checked at the spawn site by inspecting the subjects, not by tracing the program. The hosting scope may not access a location while a live spawn holds its mutable borrow; the borrow is released when that spawn completes (§4.1).
For any one storage location, at most one live spawned call may hold a **mutable borrow** — the `!` subject of a spawned `mut` call. By §4.2 that subject is always value-typed, so every borrow this rule counts is a value borrow — the only kind there is ([`memory.md`](memory.md) §2.9). Two spawned calls that mutably borrow the same location are a compile-time error. Because value types carry no `&`, a location's identity is unambiguous — there is no hidden alias to obscure that two subjects denote the same slot — so this disjointness is checked at the spawn site by inspecting the subjects, not by tracing the program. The hosting scope may not access a location while a live spawn holds its mutable borrow; the borrow is released when that spawn completes (§4.1).
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### 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).
Expand Down
2 changes: 1 addition & 1 deletion spec/effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ A side effect is any observable interaction beyond returning a value, including:
A capability is an object whose methods model access to external state, such as a filesystem, logger, socket, clock, or random source.

### 2.3 `mut`
`mut` is the only effect modifier in the language. It appears on methods and grants write access to state reachable through `this`; the write lands on the caller's object or on state reachable from it. `this` is a **borrow** of the caller's slot for both kinds: a value-type `this` borrows the value, and a reference-type `this` written bare is a borrow of the object, `'` never being written on `this` (see [`functions.md`](functions.md) §2.4).
`mut` is the only effect modifier in the language. It appears on methods and grants write access to state reachable through `this`; the write lands on the caller's object or on state reachable from it. `this` is written bare for both kinds and carries no marker: a value-type `this` is a **borrow** of the caller's slot, and a reference-type `this` is an implicit **guest** to the object (see [`functions.md`](functions.md) §2.4). Neither takes hosting, so a `mut` call leaves the caller exactly as it found it.

### 2.4 Parameters are not mutable by default
Parameters other than `this` are read-only. Mutation of another object must be expressed by calling a `mut` method on that object as the subject. A number parameter that resolves to a number value in body positions (see [`generics.md`](generics.md) §3.5) is a value-like binding and is read-only by default; mutating it requires a `mut` declaration.
Expand Down
Loading