diff --git a/README.md b/README.md index 47b76d3..4edd3ee 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ The specification lives in [`spec/`](spec/) and is organized by topic. Each docu | Document | Purpose | |---|---| -| [`spec/types.md`](spec/types.md) | Classes, structs, fields, constructors, implicit conversions, and `type`/`alias` declarations | +| [`spec/types.md`](spec/types.md) | Fundamental types, value/reference types, fields, constructors, implicit conversions, and `type`/`alias` declarations | | [`spec/adt.md`](spec/adt.md) | Enums, variants, the struct/variant symmetry, pattern matching, `match`, and enum maps | | [`spec/functions.md`](spec/functions.md) | Methods, free functions, subscripts, overload resolution, function values, and lambdas | | [`spec/generics.md`](spec/generics.md) | Unified type parameters, `<>` type expressions, constructor calls, and the `Array` primitive | @@ -62,17 +62,17 @@ The spec states *what* the language does; the **why** lives in a parallel set of |---|---| | [`stories/foundations.md`](stories/foundations.md) | [`spec/foundations.md`](spec/foundations.md) — the bets behind captured intent, staged compilation, casing-determines-kind, strictness-as-performance, and the collapse of value/reference into one `#` axis | | [`stories/lexical.md`](stories/lexical.md) | [`spec/lexical.md`](spec/lexical.md) — casing as a hard-error kind signal and the `<>` disambiguation it buys, why field privacy lives in the name and reaches across packages, and a delimiter for each kind of separated thing | -| [`stories/types.md`](stories/types.md) | [`spec/types.md`](spec/types.md) — the two-axis value/reference model, why `#` marks a distinct type rather than a box, constructing a type by what it is, confining `#` to the body forms, why every type must be named, dropping the `tuple` mould once a named `struct` does the job better, adding named (dart-style) constructors on the syntax variants introduced, and naming the `enum`'s shape the peer mould | +| [`stories/types.md`](stories/types.md) | [`spec/types.md`](spec/types.md) — the two-axis value/reference model, why `#` marks a distinct type rather than a box, constructing a type by what it is, confining `#` to the body forms, why every user-defined type must be named, dropping the `tuple` mould once a named `struct` does the job better, adding named constructors, naming the `enum`'s shape the peer mould, and making `Unit` a real fundamental unit type | | [`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, and the opt-in remapping model | +| [`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/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, and keeping abortability orthogonal to effects | -| [`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, and one-based counting after the loop that forced the question | -| [`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, and why callables are call-only while self-typed lambdas are values | +| [`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/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/spec/control-flow.md b/spec/control-flow.md index 92d3c87..534b817 100644 --- a/spec/control-flow.md +++ b/spec/control-flow.md @@ -10,7 +10,7 @@ This document specifies Zane's control-flow constructs: conditional branching, s Zane keeps control flow small and explicit. Branching uses `if`/`elif`/`else`, early scope exit uses `guard`, and repetition uses bounded `loop`. -- **`Bool conditions`.** `if`, `elif`, and `guard` conditions are `Bool` expressions. +- **`Bool conditions`.** `if`, `elif`, and `guard` conditions coerce to `Bool`. - **`Guard exits the current scope`.** `guard` leaves the enclosing lexical scope instead of introducing another nested branch. - **`Bounded loops`.** `loop` always has a written upper bound; Zane does not define a separate `while`. - **`1-based ordinals`.** Counted loops and positional indexing start at `1`, not `0`. @@ -22,7 +22,7 @@ Zane keeps control flow small and explicit. Branching uses `if`/`elif`/`else`, e ### 2.1 `if` chains evaluate top to bottom An `if` chain evaluates its conditions from top to bottom. The first branch whose condition is `true` runs. If no prior condition is `true` and an `else` branch is present, the `else` branch runs. -All `if` and `elif` conditions **MUST** have type `Bool`. +Every `if` and `elif` condition is a coercion site with destination type `Bool`. After any applicable single-step implicit constructor is inserted, the condition **MUST** have type `Bool`. Zane defines no general truthiness rule: another type is accepted only when an applicable `implicit Bool(...)` constructor exists. ```zane if ready { @@ -34,6 +34,8 @@ if ready { } ``` +> **Story:** [`stories/control-flow.md`](../stories/control-flow.md#control-flow-speaks-in-language-types) — "Control flow speaks in language types". + ### 2.2 `elif` is the continuation form Zane uses the single keyword `elif` for chained conditions. `else` is the unconditional fallback branch and appears only at the end of the chain. @@ -54,6 +56,8 @@ if firstChoice { ### 3.1 `guard` exits when its condition is true `guard condition` immediately exits the current lexical scope when `condition` evaluates to `true`. When the condition is `false`, execution continues with the next statement in the same scope. +The condition is a coercion site with destination type `Bool`, under the same rule as `if` and `elif` (§2.1). + ```zane { value Int(3) @@ -87,7 +91,7 @@ In the example above, `print(value)` runs only when `finished` is `false`. ## 4. Counted Loops ### 4.1 `loop from ... to ...` is inclusive -`loop name from start to end { ... }` iterates over an inclusive integer range. On each iteration, `name` is bound to the current `Int` value, starting at `start`, increasing by `1`, and ending at `end`. +`loop name from start to end { ... }` iterates over an inclusive integer range. `start` and `end` are coercion sites with destination type `Int`; after coercion, both **MUST** have type `Int`. On each iteration, `name` is bound with exact type `Int`, starting at `start`, increasing by `1`, and ending at `end`. ```zane loop i from 1 to 3 { @@ -97,6 +101,7 @@ loop i from 1 to 3 { The loop above visits `i = 1`, then `2`, then `3`. +> **Story:** [`stories/control-flow.md`](../stories/control-flow.md#control-flow-speaks-in-language-types) — "Control flow speaks in language types". > **Story:** [`stories/control-flow.md`](../stories/control-flow.md#counting-from-one) — "Counting from one". ### 4.2 `loop ... to ...` starts at `1` diff --git a/spec/dependencies.md b/spec/dependencies.md index 11d7ee3..62706c4 100644 --- a/spec/dependencies.md +++ b/spec/dependencies.md @@ -268,13 +268,13 @@ At a high level, dependency resolution proceeds in this order: ## 14. Toolchain Version -The `zane-version` field in `zane.coda` pins the toolchain tag used to build the project. It selects the compiler alone; the reserved `zane` key in `zane-versions.coda` records the commit that tag must resolve to. +The `zane-version` field in `zane.coda` pins the toolchain tag used to build the project. It selects the compiler and its bundled `core` implementation; the reserved `zane` key in `zane-versions.coda` records the commit that tag must resolve to. -- The compiler is released under its own tag, so a project always builds with a known compiler. This frees the toolchain to evolve without preserving backward compatibility across versions: each project states the compiler version it builds with. -- The standard library is **not** special, and no part of it is coupled to the toolchain tag. `core`, `std`, and every other library are ordinary packages, each fetched, versioned, pinned, and remapped like any other dependency, with its own `deps` row in `zane.coda` and entry in `zane-versions.coda`. `core` is installed like any package; a project that never adds it cannot name the core surface types. +- The compiler and `core` implementation are released under one toolchain tag, so a project always builds with matching definitions of the fundamental language types. This frees the toolchain to evolve without preserving backward compatibility across versions: each project states the toolchain version it builds with. +- The standard library is **not** special, and no ordinary library is coupled to the toolchain tag. `std` and every other source library are ordinary packages, each fetched, versioned, pinned, and remapped like any other dependency, with its own `deps` row in `zane.coda` and entry in `zane-versions.coda`. The bundled `core` implementation is compiler infrastructure rather than a manifest dependency; see [`types.md`](types.md) §2.6. - The reserved `zane` key is subject to the same tag/commit verification as every other entry (§4): a moved toolchain tag is detected, not silently trusted. -> **Story:** [`stories/dependencies.md`](../stories/dependencies.md#cutting-core-loose-from-the-toolchain) — "Cutting core loose from the toolchain" explains why `core` is an ordinary package rather than a toolchain-coupled one. +> **Story:** [`stories/dependencies.md`](../stories/dependencies.md#the-package-that-was-the-language) — "The package that was the language". --- diff --git a/spec/error-handling.md b/spec/error-handling.md index d32dc8e..4b864b4 100644 --- a/spec/error-handling.md +++ b/spec/error-handling.md @@ -29,8 +29,13 @@ ReturnType?AbortType A declaration with no `?AbortType` cannot abort. -### 2.2 `Void` abort type -If failure carries no payload, the abort type is `Void`. In that case `abort` is written without an argument. +### 2.2 `Unit` abort type + +If failure carries no meaningful payload, the abort type is `Unit`. The abort path still carries a value explicitly: + +```zane +abort Unit() +``` ### 2.3 Abortability is orthogonal to `mut` Abortability and mutation are independent. A method may be: @@ -66,10 +71,10 @@ value Int = parse("42") ? err { } ``` -When the abort type is `Void`, the binder is omitted: +The binder is required even when the abort type is `Unit`: ```zane -done Bool = tryFinish() ? { +done Bool = tryFinish() ? ignored { resolve false } ``` @@ -102,8 +107,17 @@ Falling through a handler block is a compile-time error. count Int = parse("abc") ?? Int(0) ``` -### 3.4 `Void` primary returns are not assignable -Calls whose primary return type is `Void` may not be assigned to variables. When such calls are abortable, the handler still attaches to the call expression itself. +### 3.4 `Unit` primary returns are values + +A call whose primary return type is `Unit` produces the `Unit` value returned by the callee. The result may be assigned or passed like any other value. + +```zane +completed Unit = performWork() +``` + +When such a call is abortable, the handler attaches to the call expression exactly as for any other primary return type. + +> **Story:** [`stories/error-handling.md`](../stories/error-handling.md#the-empty-door-still-carries-a-value) — "The empty door still carries a value". ### 3.5 `match` is abort-transparent A `match` expression passes the output of its selected arm straight up. If the arms are abortable, the whole `match` is abortable and takes a `?` (or `??`) handler exactly like any other abortable expression. Abortability is not introduced or swallowed by `match`; it simply flows through. @@ -134,7 +148,7 @@ result Int = match token { | `return` | parent function | leave via the primary return path | | `abort` | parent function | leave via the abort path | -When the primary return type is `Void`, `resolve` takes no value. +When the primary return type is `Unit`, the handler writes `resolve Unit()`. `resolve` always carries an explicit value. > **Story:** [`stories/error-handling.md`](../stories/error-handling.md#handling-a-fork-resolve-and-why-it-isnt-assignment) — "Handling a fork: `resolve`, and why it isn't assignment". @@ -276,7 +290,7 @@ Zig also keeps failure explicit and avoids stack unwinding, but the surface mode |---|---|---| | Signature order | `Error!Value` | `Value?Abort` | | Recovery syntax | `catch` with labeled-block patterns | `?` with `resolve`/`return`/`abort` | -| Payload-free failure | inferred error sets and union mechanics | explicit `Void` abort type | +| Payload-free failure | inferred error sets and union mechanics | explicit `Unit` abort type | | Integration with effects | no corresponding `mut`-based effect layer | abortability and effects are analyzed separately; both must be satisfied at call sites | --- @@ -290,4 +304,5 @@ Zig also keeps failure explicit and avoids stack unwinding, but the surface mode | Handler paths | Must end with `resolve`, `return`, or `abort` | | `??` | Shorthand for resolve-with-default | | `resolve` | Exits only the handler block | +| `Unit` path value | `Unit()` is a real primary or abort-path value and is written explicitly with `return`, `resolve`, or `abort` | | Abort-free function | Statically guaranteed not to abort | diff --git a/spec/functions.md b/spec/functions.md index 0bccbe5..deeb7da 100644 --- a/spec/functions.md +++ b/spec/functions.md @@ -59,14 +59,16 @@ A write to `this` lands on the caller's object; how `this` reaches the caller di - 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. ```zane -Void setScale(this Node, scale Float) mut { // reference receiver +Unit setScale(this Node, scale Float) mut { // reference receiver this.scale = scale + return Unit() } ``` ```zane -Void setY(this Vec2, y Float) mut { // value receiver: in-place through the borrow +Unit setY(this Vec2, y Float) mut { // value receiver: in-place through the borrow this.y = y + return Unit() } pos!setY(Float(3)) @@ -106,8 +108,9 @@ type Car = #struct { } // `&` parameter: may be stored into an `&` field -Void setEngine(this Car, engine &Engine) mut { +Unit setEngine(this Car, engine &Engine) mut { this.engine = engine // legal + return Unit() } // `&` parameter, read only @@ -116,8 +119,9 @@ Int calculate(this Car, engine &Engine) { } // plain reference-type parameter swallows; a swallowed host is not an `&` source -Void setEngineWrong(this Car, engine Engine) mut { +Unit setEngineWrong(this Car, engine Engine) mut { this.engine = engine // ILLEGAL: cannot store a swallowed host into an `&` field + return Unit() } ``` @@ -173,14 +177,29 @@ Functions may access only fields whose names do not begin with `_`. This rule is Functions are called as `name(args...)` or `packageName$name(args...)`. ### 3.4 Expression-bodied verbs -A verb that returns a value may use `=>` for its body. Functions, methods, operators, constructors, and lambdas all support this shorthand (operators are covered in [`operators.md`](operators.md), constructors in [`types.md`](types.md) §3.2): +A verb may use `=>` for its body. Functions, methods, operators, constructors, and lambdas all support this shorthand (operators are covered in [`operators.md`](operators.md), constructors in [`types.md`](types.md) §3.2): ```zane Int double(value Int) => value * 2 Int scaledId(this Node, factor Int) => this._id * factor +Unit noOperation() => Unit() ``` -`=> expr` is **purely a surface shorthand**: it means exactly `{ return expr }` and adds no other behavior. A constructor's `=> init{...}` is the same rewrite — `Vec2(x Float, y Float) => init{x, y}` is shorthand for `{ return init{x, y} }`. Because the shorthand always returns its expression, it is illegal for declarations whose return type is `Void`. +`=> expr` is **purely a surface shorthand**: it means exactly `{ return expr }` and adds no other behavior. A constructor's `=> init{...}` is the same rewrite — `Vec2(x Float, y Float) => init{x, y}` is shorthand for `{ return init{x, y} }`. + +### 3.5 Block-bodied verbs return explicitly + +Every block-bodied verb must return a value explicitly on every returning path, including a verb whose return type is `Unit`: + +```zane +Unit noOperation() { + return Unit() +} +``` + +The return checker does not synthesize a constructor call for `Unit` or any other singleton type. + +> **Story:** [`stories/functions.md`](../stories/functions.md#every-return-carries-a-value) — "Every return carries a value". --- @@ -192,8 +211,8 @@ Two declarations in the same package conflict when they have the same ordered pa 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." ```zane -Void consume(this Car, engine Engine) -Void consume(this Car, engine &Engine) // ERROR +Unit consume(this Car, engine Engine) +Unit consume(this Car, engine &Engine) // ERROR ``` ### 4.2 Consequences of the overload identity rules @@ -231,10 +250,10 @@ These phases describe **static** overload resolution. Matching a `variant` on it ### 6.1 Unqualified method lookup For `receiver:methodName(...)` or `receiver!methodName(...)`, the compiler resolves candidates in this order: -1. the receiver type's home package +1. the receiver 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 home package 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 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. ### 6.2 Qualified method calls Cross-package extension methods are written explicitly: @@ -284,12 +303,14 @@ Because a lambda carries its complete type, it is a single value with one exact `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: ```zane -onEventCallback Void[this Node, EventData] mut = Void(this Node, data EventData) { +onEventCallback Unit[this Node, EventData] mut = Unit(this Node, data EventData) { ... + return Unit() } // OK: non-`mut` lambda assigned to a `mut` function type -readonlyCallback Void[this Node, EventData] = Void(this Node, data EventData) mut { +readonlyCallback Unit[this Node, EventData] = Unit(this Node, data EventData) mut { ... + return Unit() } // ILLEGAL: expected a non-`mut` function value ``` @@ -372,6 +393,7 @@ Read-only methods and functions are effect-free with respect to their receiver u | `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` | | 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 | | Subscript | Package-scope place projection written `(this T)[...] => placeExpr`; no explicit return type | @@ -381,5 +403,5 @@ Read-only methods and functions are effect-free with respect to their receiver u | 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 home package, then current package | +| Unqualified method lookup | Searches the receiver'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 09379a1..1060aef 100644 --- a/spec/generics.md +++ b/spec/generics.md @@ -85,9 +85,9 @@ A type is itself a symbol of exactly this kind. The symbol `T` has a value — a That is the whole parameter model. The two parameter forms differ only in which part you supply: ```zane -Void func (x Int) // x : value Int(3), type Int -Void func2(x T Type) // x : value Int(3), type T — and T : value Int, type Type -Void func3( T Type) // T : value Int, type Type +Unit func (x Int) // x : value Int(3), type Int +Unit func2(x T Type) // x : value Int(3), type T — and T : value Int, type Type +Unit func3( T Type) // T : value Int, type Type ``` - `func3(T Type)` declares the type-symbol `T` directly. The caller supplies its value: `func3(Int)` gives `T` the value `Int`. This is **explicit passing**. diff --git a/spec/glossary.md b/spec/glossary.md index 3bbb136..c879ed1 100644 --- a/spec/glossary.md +++ b/spec/glossary.md @@ -200,8 +200,8 @@ This file gives short, reusable names to concepts that appear across multiple sp - **Canonical home:** [`memory.md`](memory.md) §2.9 ### 3.28 coercion site -- **Meaning:** A position where the compiler inserts an applicable `implicit` constructor automatically: a positional argument of a function or constructor call, or a named field entry of a field-constructor call. It is *not* inserted where a value is written to a locally-fixed destination — a symbol declaration, an assignment or store, a `return`, or an `init{ }` — where the conversion is written explicitly. -- **Why this name:** "Coercion" is the standard term for an implicit, compiler-inserted type conversion, as opposed to an explicit cast; a *coercion site* names a position where that conversion is permitted. Each coercion is still backed by a user-declared `implicit` constructor — the site says where one may be inserted, not that the conversion is built in. +- **Meaning:** A position where the compiler inserts an applicable implicit conversion automatically: a callable argument, a named field entry of a field-constructor call, a condition, or a counted-loop bound. It is *not* inserted where a value is written to a locally-fixed destination — a symbol declaration, an assignment or store, a `return`, or an `init{ }` — where the conversion is written explicitly. +- **Why this name:** "Coercion" is the standard term for an implicit, compiler-inserted type conversion, as opposed to an explicit cast; a *coercion site* names a position where that conversion is permitted. A coercion is backed by an `implicit` constructor, including the literal constructors supplied by the bundled `core` implementation — the site says where one may be inserted, not that arbitrary conversion is built in. - **Canonical home:** [`types.md`](types.md) §4.2 ### 3.29 mould @@ -244,7 +244,7 @@ This file gives short, reusable names to concepts that appear across multiple sp ## 4. Packages, Operators, and Versioning ### 4.1 home-package operator rule -- **Meaning:** An operator implementation may be declared only in the home package of one of its operand types. +- **Meaning:** A source operator implementation may be declared only in the home package of one of its user-defined operand types. Fundamental-only operators live in the bundled `core` implementation. - **Why this name:** The rule ties operator declarations to the package that "owns" one operand type and prevents unrelated helper imports from changing operator meaning. - **Canonical home:** [`operators.md`](operators.md) §2.2 diff --git a/spec/lexical.md b/spec/lexical.md index d5f07df..9bf5cd7 100644 --- a/spec/lexical.md +++ b/spec/lexical.md @@ -154,9 +154,10 @@ type Colors = enum [ red, green, blue ] A newline separates statements in a function body or a control-flow block. Zane has no statement separator, so two statements cannot share a line. This is the one place a newline is structural. ```zane -Void main() { +Unit main() { x Int(5) print(x) + return Unit() } ``` diff --git a/spec/lifetimes.md b/spec/lifetimes.md index 1c9cde9..4eea393 100644 --- a/spec/lifetimes.md +++ b/spec/lifetimes.md @@ -75,8 +75,9 @@ car Car() ``` ```zane -Void loadCar(this Boat, car Car) mut { +Unit loadCar(this Boat, car Car) mut { this.cars!append(car) // legal: car is moved into this.cars at the top level of the body + return Unit() } ``` @@ -107,9 +108,10 @@ A reference-type parameter is **not part of the callee's body scope**. It behave 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 -Void enterMatch(player Player) { +Unit enterMatch(player Player) { island Island = makeIsland() island!startMatch(player) // player is lent into the local island + return Unit() } ``` @@ -187,9 +189,10 @@ Player enterMatch(player Player) { return player } -Void main() { +Unit main() { player Player = makePlayer() player = enterMatch(player) // bind to regain hosting privilege; unbound, the host floats (§1.9) + return Unit() } ``` @@ -198,7 +201,7 @@ A verb that only reads its reference argument may still declare it plain `T`: re > **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#the-signature-is-the-whole-contract-retiring-inferred-consumption) — "The signature is the whole contract: retiring inferred consumption". ### 1.9 An ignored hosting result floats to the enclosing scope -A non-`Void` return 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). +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. Binding the return is how the caller takes **hosting privilege**. A bound host may be moved again; a floated one may not — the caller reaches it only through whatever guest it already holds (§1.8). @@ -254,7 +257,7 @@ Because scope rules (§1.1) prevent guests from outliving their hosts, the runti | 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 | -| Return value | A non-`Void` return need not be bound; an unbound reference-type result floats to the enclosing scope as an anonymous host, and the caller keeps only a guest to it | +| 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 | > **Story:** [`stories/lifetimes.md`](../stories/lifetimes.md#no-rule-to-spare-the-specific-hole-each-restriction-plugs) — "No rule to spare: the specific hole each restriction plugs". diff --git a/spec/memory.md b/spec/memory.md index f471d75..64281c0 100644 --- a/spec/memory.md +++ b/spec/memory.md @@ -153,13 +153,15 @@ type Car = #struct { } // `&` parameter is a guest; it may be stored into an `&` field -Void setEngine(this Car, engine &Engine) mut { +Unit setEngine(this Car, engine &Engine) mut { this.engine = engine + return Unit() } // plain reference-type parameter: taken by hosting access, then moved into a hosting field of this -Void setSpare(this Car, engine Engine) mut { +Unit setSpare(this Car, engine Engine) mut { this.spare = engine + return Unit() } // `&` parameter, read only: a reference-type object passed without consuming it @@ -171,8 +173,9 @@ Int inspect(this Car, engine &Engine) { 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: ```zane -Void setEngineWrong(this Car, engine Engine) mut { +Unit setEngineWrong(this Car, engine Engine) mut { this.engine = engine // ILLEGAL: a swallowed host is not an `&` source + return Unit() } ``` @@ -292,8 +295,9 @@ When an instance escapes — it is moved into a longer-lived host in a parent sc > **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 core reference types have fixed footprint -The core dynamically-sized reference types — `List`, `String`, and similar types — 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. +### 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. 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. diff --git a/spec/operators.md b/spec/operators.md index 673cc1d..be003ee 100644 --- a/spec/operators.md +++ b/spec/operators.md @@ -36,7 +36,7 @@ Primitive operators are implementable and define the operator surface area: ### 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. -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`. See [`functions.md`](functions.md) §6.1 for the home-package concept used by method resolution. +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. Imported packages do not contribute new implicit operator candidates. This prevents the meaning of `a + b` or `a < b` from changing just because a different helper package was imported. @@ -46,7 +46,7 @@ Vec2 +(left Int, right Vec2) { } ``` -The example above is legal only in the home package of `Int` or `Vec2`. +Because `Int` is fundamental, the example above is legal only in the home package of `Vec2`. > **Story:** [`stories/operators.md`](../stories/operators.md#imports-may-add-names-not-meanings) — "Imports may add names, not meanings". @@ -163,7 +163,7 @@ An operator token may appear only in operator position; it has no value form. Th | Operator vocabulary | Only the fixed built-in operator set may be overloaded; programs cannot declare new tokens or precedence. | | Primitive operators | `~`, `*`, `/`, `+`, `==`, and `<` are independently implementable. | | Derived operators | `-`, `~=`, `>`, `<=`, and `>=` have fixed desugarings and cannot be implemented independently. | -| Operator definitions | An implementation must live in the home package of at least one operand type. | +| Operator definitions | A source implementation must live in the home package of at least one user-defined operand type; fundamental-only operators live in the bundled `core` implementation. | | Grouping | Precedence and left associativity are fixed by syntax; parentheses group explicitly. | | Boolean logic | `and` and `or` are short-circuiting keywords rather than overloadable operators. | | Callability | Operator tokens are call-only; behavior is passed as a value through a lambda-variable. | diff --git a/spec/packages.md b/spec/packages.md index 8b41472..e379e52 100644 --- a/spec/packages.md +++ b/spec/packages.md @@ -14,7 +14,7 @@ Zane packages are directory-defined namespaces and compilation units that contai - **`Declaration check`.** Every source file declares that package name, allowing the compiler to detect a file copied or moved into the wrong directory. - **`One compilation unit`.** All source files in a package compile together without source-order dependencies. - **`Explicit cross-package access`.** An import makes one package namespace available to one source file; its members remain qualified as `packageName$member`. -- **`No implicit packages`.** No package is available without an import, and none injects unqualified names. Every package a file uses—`core` included—is imported explicitly and reached by qualification. +- **`No implicit source packages`.** A file's own package is established by its `package` declaration and its members remain unqualified. No other source package is available without an import, and imported members never become unqualified. The bundled `core` implementation is outside the source package and manifest system; it provides the predeclared fundamental names. - **`No hidden ambient state`.** Packages expose immutable constants and verbs; time-varying state lives in values. --- @@ -53,7 +53,7 @@ All source files directly in one package directory form a single compilation uni The package must be available through the dependency rules in [`dependencies.md`](dependencies.md). -No package is available without such an import. There is no ambient or automatically-imported package: every package a file uses—`core` included, the home of the core surface types (see [`syntax.md`](syntax.md) §2.1)—is imported explicitly and its members reached by the same `packageName$member` qualification as any other. This keeps every package on equal footing, so how a name is reached never depends on which package defines it. +No other source package is available without such an import. A file's own package is established by its `package` declaration, and its members remain available unqualified (§3.2). There is no ambient or automatically-imported source package: every other source package a file uses is imported explicitly and its members are reached by the same `packageName$member` qualification. The compiler's bundled `core` implementation is not exposed through this package system; its fundamental type names are predeclared instead (see [`types.md`](types.md) §2.6). ### 3.2 Current-package members are unqualified @@ -116,7 +116,7 @@ State that changes over time must live in a value, such as a `struct` or referen | Compilation unit | All files in one package compile together; file and declaration order are irrelevant | | Same-package access | Members are available unqualified across all files in the package | | Import scope | One source file only | -| Implicit packages | None; every package, `core` included, requires an explicit import | +| Implicit source packages | None; the current package comes from the file's declaration, every other source package requires an explicit import, and bundled `core` provides predeclared fundamental names outside the source package and manifest system | | Imported member access | Always qualified as `packageName$member` | | Package separator | `$`; distinct from field access and method-call markers | | Package-private member | Any named package-scope declaration beginning with `_` | diff --git a/spec/syntax.md b/spec/syntax.md index 73e0a30..3782e61 100644 --- a/spec/syntax.md +++ b/spec/syntax.md @@ -128,12 +128,11 @@ EnumName.property FieldType [ ## 2. Types -### 2.1 Core surface types -`Int`, `Float`, `Bool`, `String`, `Void` +### 2.1 Fundamental types -These are the public types the `core` package defines over the compiler's storage primitives, not storage primitives themselves. `core` is an ordinary package with no special status: a file reaches these types by importing `core` and qualifying them as `core$Int`, exactly as for any other package (see [`packages.md`](packages.md) §3). `Int`, `Float`, and `Bool` are nominal wrapper structs over machine storage primitives in the `@primitives$` namespace. `String` and other runtime-managed core types use the same wrapper pattern over opaque runtime primitives. `Void` is the exception: it is a core surface type with no storage payload. +`Int`, `Float`, `Bool`, `String`, `Unit` -For brevity, the examples throughout this specification write the core surface types unqualified—`Int` rather than `core$Int`—and omit the `import core` line; real code reaches them by the ordinary package rules above. +These type names are available unqualified in every source file. Their bundled `core` implementation has no source-level package qualifier or import form. See [`types.md`](types.md) §2.6 for their semantics. ### 2.2 Named types @@ -213,7 +212,7 @@ Array @concepts$name ``` -The `@primitives$` namespace contains storage primitives such as machine-word scalar types and opaque runtime primitives used by core wrapper types. The `@concepts$` namespace contains compiler concept types used for source literals. +The `@primitives$` namespace contains storage primitives such as machine-word scalar types and opaque runtime primitives used by fundamental types. The `@concepts$` namespace contains compiler concept types used for source literals. ### 2.8 Compiler concept types for literals @@ -223,7 +222,7 @@ The `@primitives$` namespace contains storage primitives such as machine-word sc @concepts$Collection ``` -These compiler-provided concept types represent source literals before they are lowered into storage types. Concept types may appear in parameter positions but **MUST NOT** be used as storage types such as local variables, fields, or nested storage positions. Functions and constructors may use concept-typed parameters to accept literals and lower them into the corresponding core surface type. +These compiler-provided concept types represent source literals before they are lowered into storage types. Concept types may appear in parameter positions but **MUST NOT** be used as storage types such as local variables, fields, or nested storage positions. Functions and constructors may use concept-typed parameters to accept literals and lower them into the corresponding fundamental type. The concept types `Type` and `Number` declare the type and number parameters of a parameterized declaration (see [`generics.md`](generics.md) §3). They follow the same rule: legal in parameter positions, never as storage. A `Type` parameter accepts a type; a `Number` parameter accepts a compile-time number. @@ -254,7 +253,7 @@ ReturnType[&ParamType, ...] ```zane Int[Node, Int] mut // ILLEGAL: mut requires this as first parameter -Void[Int, this Node] // ILLEGAL: this must be the first parameter +Unit[Int, this Node] // ILLEGAL: this must be the first parameter ``` ### 2.10 The `#` reference modifier @@ -307,7 +306,7 @@ ReturnType name(this ReceiverType, param ParamType, ...) { bod `this` is legal only in the first parameter position. A declaration is a method if and only if its first parameter is named `this`. -`=> expr` is legal only when the declared return type is not `Void`. +`=> expr` returns `expr`, including when `expr` has type `Unit`. ### 3.3 Positional constructors @@ -437,17 +436,19 @@ ReturnType(this ReceiverType, param ParamType, ...) => expr ReturnType(this ReceiverType, 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`. `=> expr` is legal only when the declared return type is not `Void`. +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`. Examples: ```zane -element!onClick(Void(eventData EventData) { +element!onClick(Unit(eventData EventData) { ... + return Unit() }) -element!onClick(Void(this Element, data EventData) mut { +element!onClick(Unit(this Element, data EventData) mut { ... + return Unit() }) ``` @@ -463,12 +464,14 @@ name ReturnType(this ReceiverType, 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: ```zane -callback Void[this Player] mut = Void(this Player) mut { +callback Unit[this Player] mut = Unit(this Player) mut { this.shooting = false + return Unit() } -callback Void(this Player) mut { // shorthand for the line above +callback Unit(this Player) mut { // shorthand for the line above this.shooting = false + return Unit() } ``` @@ -485,6 +488,14 @@ Bool <(leftParam LeftType, rightParam RightType) { body } Operator definitions are package-scope verb declarations whose names are operator tokens. They never declare `this`, so they are not methods and cannot use `mut`. +### 3.10 Return statements + +```zane +return expr +``` + +See [`functions.md`](functions.md) §3.5 for return-path requirements. + --- ## 4. Calls and Function Values @@ -540,11 +551,11 @@ spawn functionName(args...) spawn receiver:methodName(args...) spawn receiver!methodName(args...) spawn functionName(args...) ? binder { ... } -spawn receiver:methodName(args...) ? { ... } +spawn receiver:methodName(args...) ? binder { ... } spawn receiver!methodName(args...) ?? fallbackExpr name VarType = spawn functionName(args...) name VarType = spawn receiver:methodName(args...) ? binder { ... } -name VarType = spawn receiver!methodName(args...) ? { ... } +name VarType = spawn receiver!methodName(args...) ? binder { ... } name VarType = spawn functionName(args...) ?? fallbackExpr ``` @@ -658,17 +669,14 @@ ReturnType?AbortType ```zane expr ? binder { ... } -expr ? { ... } ``` Every path inside the handler must end with one of: ```zane resolve expr -resolve return expr abort expr -abort ``` ### 6.3 `??` shorthand diff --git a/spec/types.md b/spec/types.md index b61c562..b9c9bf7 100644 --- a/spec/types.md +++ b/spec/types.md @@ -1,6 +1,6 @@ # Zane Types -This document specifies Zane's data types: value and reference types, the `#` modifier, fields, constructors, `type` and `alias` declarations, and the `init{ }` expression. Methods and other behavior live in [`functions.md`](functions.md). +This document specifies Zane's data types: fundamental, value, and reference types; the `#` modifier; fields; constructors; `type` and `alias` declarations; and the `init{ }` expression. Methods and other behavior live in [`functions.md`](functions.md). > **See also:** [`memory.md`](memory.md) §2 for hosting rules. [`functions.md`](functions.md) for methods and functions. [`syntax.md`](syntax.md) §1 and §3 for declaration grammar. @@ -14,7 +14,7 @@ Zane keeps data layout and construction separate from behavior. - **`One kind axis`.** A type is a **value type** unless its mould is marked `#`, which makes it a **reference type** — identity-bearing, aliasable through `&`, and able to hold reference-type and `&` fields and recurse. `struct` is a value mould; `#struct` a reference mould. - **`Package-scope constructors`.** A constructor is a verb at package scope; the body builds the value with `init{ }`. - **`Name-based field privacy`.** A leading `_` makes a field private to methods whose first parameter is `this` for that type. -- **`Named types and aliases`.** `type` introduces a new distinct named type; `alias` introduces an interchangeable name for a type expression. +- **`Fundamental and declared types`.** `Int`, `Float`, `Bool`, `String`, and `Unit` belong to the language; `type` introduces a new distinct named type and `alias` an interchangeable name. --- @@ -84,6 +84,32 @@ The `#` modifier (§2.1) is the other axis: `struct`/`#struct` are the product p > **See also:** [`adt.md`](adt.md) for the canonical rules on `variant`, `enum`, pattern matching, and enum maps. [`adt.md`](adt.md) §3 for the full struct-versus-variant symmetry. +### 2.6 Fundamental language types + +`Int`, `Float`, `Bool`, `String`, and `Unit` are **fundamental language types**. Their names are available unqualified in every source file. The compiler distribution supplies their declarations through a bundled `core` implementation package. That package is not part of the source package system: programs do not import it, qualify its members, list it as a dependency, or replace it independently of the compiler version. + +Control-flow constructs refer to these semantic types rather than to their storage primitives. Conditions expect `Bool`, counted-loop bounds expect `Int`, and the loop variable has type `Int`; see [`control-flow.md`](control-flow.md) §2–§4. Programs never unwrap a fundamental type to feed a primitive into control flow. + +The bundled `core` package defines the constructors and methods of fundamental types over storage primitives in the `@primitives$` namespace. Compiler concept types represent source literals until coercion selects one of those constructors: `true` becomes `Bool` in a condition and `20` becomes `Int` at a counted-loop bound. Explicit `Bool(true)` and `Int(20)` construction remains legal. This does not create general truthiness or numeric narrowing. + +`Unit` is the unit type. Its bundled declaration is an empty value `struct`, so it has exactly one logical value and zero-sized storage. `Unit()` is its ordinary `core` constructor. It may appear wherever any other value type may appear, including symbols, fields, arrays, generic arguments, function parameters, and return types. + +```zane +type Player = #struct { + name String; + extraSettings T; +} + +Player(name String, extraSettings T Type) => init{name, extraSettings} + +player Player = Player("Manuel", Unit()) +completed Unit = performWork() +``` + +An implementation may erase only the runtime storage of `Unit` values, including fields, array elements, and constructor results. It **MUST** still evaluate every expression that produces a `Unit` value at its original program point and in its original order. Storage erasure never removes side effects or otherwise changes observable evaluation. + +> **Story:** [`stories/types.md`](../stories/types.md#unit-exposes-the-package-that-wasnt-one) — "Unit exposes the package that wasn't one". + --- ## 3. Constructors and Initialization @@ -344,10 +370,13 @@ type Feet = struct { // implicit conversion from Feet to Meters implicit Meters(feet Feet) => init{value = feet.value * Float(0.3048)} -Void printDistance(d Meters) { ... } +Unit printDistance(d Meters) { + ... + return Unit() +} ``` -At a **coercion site** — a positional argument of a function or constructor call, or a named field entry of a field-constructor call (see §4.2) — if the source expression has a different type from the parameter or field and exactly one applicable implicit constructor exists, the compiler inserts that constructor call automatically. +At a **coercion site** — a position whose destination type is fixed by a callable or language construct (see §4.2) — if the source expression has a different type and exactly one applicable implicit constructor exists, the compiler inserts that constructor call automatically. ```zane printDistance(Feet(Float(10))) // coercion site: parameter expects Meters, Feet provided @@ -374,14 +403,17 @@ distance Meters = Meters(Feet(Float(10))) // legal: explicit conversion ``` ### 4.2 Coercion sites -A coercion site is a position that passes a value into a **call or constructor** whose corresponding parameter or field type is known. These are the only positions where the compiler inserts an implicit constructor: +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 positional constructor call `Type(...)` +- Positional arguments of a named-constructor call `Type.name(...)` - Named field entries of a field-constructor call `Type{ field = expr }` +- Condition expressions of `if`, `elif`, and `guard`, whose destination type is `Bool` +- The `start` and `end` expressions of a counted `loop`, whose destination type is `Int` -A field-constructor call entry fills the constructor's declared slot, exactly as a positional argument fills a slot whose type is fixed by the callee's signature, so the two coerce alike. +Anonymous and named positional constructors use their declared parameter types identically, so `Type(...)` and `Type.name(...)` arguments receive the same implicit conversions. A field-constructor call entry fills the constructor's declared slot in the same way. A control-flow expression fills a slot fixed by the language instead: `Bool` for a condition and `Int` for a counted-loop bound. An implicit constructor is **never** inserted at any other position. In particular, the following are **not** coercion sites: @@ -391,18 +423,20 @@ An implicit constructor is **never** inserted at any other position. In particul - `return` expressions, even when the return type is declared - Named field entries of an `init{ field = expr }` initializer inside a constructor body -At each of these positions the destination type is one you fix yourself — a local declaration, existing storage, the return type in the enclosing signature, or the fields the constructor builds through `init{ }` — rather than a contract you pass a value into, so the conversion must be written explicitly. +At each of these positions the destination type is one you fix yourself — a local declaration, existing storage, the return type in the enclosing signature, or the fields the constructor builds through `init{ }` — rather than a contract supplied by a callee or language construct, so the conversion must be written explicitly. Operator operands **are** coercion sites, because operators desugar to ordinary function calls (see [operators.md](operators.md) §2.2); each operand is a positional argument of that call. -At one coercion site requiring destination type `T` (a parameter or field type), given an argument with static type `U`, the compiler resolves the site locally: +At one coercion site requiring destination type `T`, given an argument with static type `U`, the compiler resolves the site locally: 1. If `U` is exactly `T`, accept the argument with no insertion. -2. Otherwise, collect all visible applicable implicit constructors from `U` to `T`. +2. Otherwise, collect all visible applicable `implicit` constructors from `U` to `T`. 3. If exactly one applicable implicit constructor exists, rewrite the argument as `T(arg)`. 4. If multiple applicable implicit constructors exist, the site is an ambiguity error. 5. If none exist, the site is a normal type error. +For candidate collection, implicit constructors declared in the bundled `core` package are automatically visible and applicable at every coercion site. They require no source import or qualification. In particular, `core` declares the implicit constructors from compiler concept types to the corresponding fundamental types, so literal coercion follows this same algorithm rather than a separate compiler-only lowering rule. + ### 4.3 No chaining Implicit conversions are never chained. If no single-step implicit constructor exists from source type `U` to destination type `T`, the compiler does not search for a path `U → V → T`. The call is a type error. @@ -437,7 +471,7 @@ implicit Destination(s Source) { // ILLEGAL: source type is a reference type ``` ### 4.5 Coherence and the orphan rule -An implicit constructor from type `U` to type `T` **MUST** be declared in the home package of either `T` or `U`. A third-party package **MUST NOT** declare an implicit constructor between two imported types. +An implicit constructor from type `U` to type `T` **MUST** be declared in the home package of either type. A third-party package **MUST NOT** declare an implicit constructor between two imported types. The bundled `core` implementation is the home package of fundamental types, but source packages cannot add declarations to it; a fundamental endpoint therefore does not by itself grant a source package permission to declare a conversion. This rule prevents conflicts when multiple packages independently define the same implicit conversion and ensures that the owner of at least one type controls the conversion behavior. @@ -462,7 +496,10 @@ implicit Units$Meters(feet Units$Feet) => init{value = feet.value * Float(0.3048 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. ```zane -Void logDistance(this Meters) { ... } +Unit logDistance(this Meters) { + ... + return Unit() +} feet Feet(Float(10)) feet:logDistance() // ILLEGAL: receiver type is Feet, not Meters @@ -505,7 +542,7 @@ type Wrapper = struct { A named type is therefore always declared this way: `type Name = struct { ... }` or `type Name = #struct { ... }` (and likewise `variant`/`#variant`/`enum`). There is no standalone `struct Name { ... }` declaration form — a mould is a type expression that only names a type through a `type` (or `alias`) declaration. -These three forms — `struct`, `variant`, and `enum` — are the **moulds**: the constructs that give a type its shape. Each has a value form and a `#` reference form (§2.1), and a mould **MUST** appear only as the right-hand side of a `type` or `alias` declaration. Every other type position — a field, a parameter, a return type — names a declared type or an instantiation of one (`Weapon`, `Vector`, `Array`, `&Node`). Every constructible type therefore has a name, and that name is what its constructor is called by (§3.1). A mould reaches all the way down: even a core type such as `Int` is *declared with* one — `Int`, `Float`, and `Bool` are wrapper `struct`s over machine-storage primitives in the `@primitives$` namespace (see [`syntax.md`](syntax.md) §2.1). +These three forms — `struct`, `variant`, and `enum` — are the **moulds**: the constructs that give a type its shape. Each has a value form and a `#` reference form (§2.1), and a mould **MUST** appear only as the right-hand side of a `type` or `alias` declaration. Every other type position — a field, a parameter, a return type — names a declared type or an instantiation of one (`Weapon`, `Vector`, `Array`, `&Node`). Every constructible type therefore has a name, and that name is what its constructor is called by (§3.1). Fundamental types follow the same declaration model inside the bundled `core` implementation; see §2.6. > **Story:** [`stories/types.md`](../stories/types.md#every-type-has-a-name-because-construction-needs-one) — "Every type has a name, because construction needs one". > **Story:** [`stories/types.md`](../stories/types.md#naming-the-moulds-and-marking-every-one) — "Naming the moulds, and marking every one". @@ -527,10 +564,12 @@ Intent lives entirely in the keyword — `type` versus `alias` — not in the pu | 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 | | 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 | | 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 automatically at positional arguments of function and constructor calls and at named field entries of a `Type{field = value}` call — never at declarations, assignments, stores, `return`, or the `init{field = value}` inside a constructor body; no field-constructor form; source type must be struct or compiler concept; orphan rule applies | +| 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 | | `&` constructor parameter | Caller must supply an allowed `&` source; callee may store into `&` fields | | Plain `T` constructor parameter | Value-only; caller may supply a temporary; callee **MUST NOT** bind it into `&` storage | | `Type` / `Number` constructor parameter | Accepts a type or a compile-time number; inferred from inline introduction or passed explicitly as a value parameter | diff --git a/stories/control-flow.md b/stories/control-flow.md index 7ed7289..b742754 100644 --- a/stories/control-flow.md +++ b/stories/control-flow.md @@ -90,3 +90,11 @@ The road not taken is the one nearly every systems language walks, and we should We were willing to pay that because of what Zane is, and is not. The language is fast, but it does not buy its speed by *pretending to be low-level* — it buys it by [trading low-level control for information](https://github.com/zane-lang/spec/blob/6882e129f144607e300367684d640d1d79bc41f4/stories/foundations.md#strictness-is-the-performance-model), handing the compiler enough guaranteed structure to generate good code rather than handing the programmer raw offsets and hoping. In a language whose whole stance is that source captures *intent* rather than machine mechanism, the number in `list[k]` should be the ordinal a person means — *the k-th thing* — and the offset arithmetic underneath is exactly the sort of machine detail the compiler exists to absorb. The `- 1` at the boundary is the compiler doing its job, not a tax the programmer pays. The cost is not only that hidden subtraction. It is friction at every seam with the zero-based world: data laid out by other systems, wire formats, algorithms transcribed from zero-based pseudocode, all of which count from zero and now need a translation — mental or actual — crossing into Zane. A programmer who has spent a career with `0..n-1` has a real habit to unlearn, and an off-by-one that a zero-based language made impossible is now possible in the other direction. We judged the intuitive win worth the standing friction: that `loop i to n` runs `n` times, that `list[list:size()]` is the last element, that the index and the count are finally one number. (We fixed only the ordinal base here; what happens when an index falls *outside* `1..size` is a separate question, left to the sections that own bounds behavior.) + +## Control flow speaks in language types + +The first versions of these chapters could say a condition "is `Bool`" and a loop variable "is `Int`" without confronting where those types came from. Once `core` was made an ordinary package, that omission became a contradiction: an ordinary dependency may be absent, renamed, or independently versioned, while `if` and `loop` still need stable answers before any library contract can help them. We tried to follow the package claim to its logical surface — `if Bool(true)._v`, `loop i to Int(20)._v` — and the result exposed why it was wrong. Control flow would depend on private wrapper layout, and every useful `Bool` or `Int` returned by an API would have to be torn open before the language could understand it. + +So control flow speaks in the semantic language types themselves. A condition supplies a slot whose expected type is `Bool`; loop bounds supply slots whose expected type is `Int`; the loop variable is exactly `Int`. The same one-step implicit-constructor mechanism used at calls adapts an expression to those slots. This is coercion, not truthiness: no integer, string, reference, or collection becomes true merely because a runtime convention says so. A type enters a condition only when it is already `Bool` or deliberately declares a single applicable conversion to `Bool`, and the corresponding rule holds for loop bounds and `Int`. + +The alternative was to require exact types and give literals a separate compiler-only exception. That would make `if true` and `loop i to 20` work, but it would create two conversion mechanisms — ordinary implicit constructors at calls and special literal lowering in control flow — precisely where one contract-shaped mechanism covers both. Treating language-defined expected types as coercion sites keeps the model local and lets safe, deliberate conversions participate without adding broad truthiness. The cost is that adding an implicit conversion can change whether an expression is accepted by control flow, the same cost it already carries at a call; coherence and the no-chaining rule keep that reach bounded. diff --git a/stories/dependencies.md b/stories/dependencies.md index 6f85867..ebba35a 100644 --- a/stories/dependencies.md +++ b/stories/dependencies.md @@ -71,3 +71,13 @@ We weighed dissolving `core` the other way, into the compiler, so `Int` and `Str The deeper reason to prefer this shape is a feature we have not built yet but mean to: an omni-import, the `from numpy import *` of Zane, that pulls a package's names in unqualified in one file. The whole point of such a thing is that it is *uniform* — one rule that works the same on `math`, on `std`, on anything. A `core` that was a compiler built-in, or a blessed always-present package, would be the single carve-out that rule had to apologize for; unqualified `Int` would come from a different mechanism than unqualified anything-else, and there would be two stories to learn instead of one. By making `core` ordinary now, we make sure the ergonomic path to bare `Int` — when it lands — is the very same path that reaches every other package's names ([`packages.md` §3.1](https://github.com/zane-lang/spec/blob/88b22f00189d910265310b922fffef0b02b8e0e3/spec/packages.md#31-imports-are-file-scoped-namespace-availability) fixes the rule that keeps them qualified until then). This is why the spec's examples already write `Int` bare and elide the `import core`: they are written against the world where that import, plus the omni-import, sits at the top of the file out of frame, not against a world where `core` is magic. The cost we accept is the one the [previous chapter](#the-toolchain-rides-one-tag-the-standard-library-does-not) named as the reason to couple in the first place, now turned face-up: a compiler and a `core` are versioned independently, so a project *can* pin a `core` that does not match what its compiler emits calls into, a mismatch the single tag made impossible by construction. We do not make that impossible any more; we make it *ordinary* — a version incompatibility like any other, caught, if it is caught, by the same pinning and compatibility machinery that governs every dependency, not prevented outright by a release process. That is a genuine guard the coupling gave for free and this design gives up. We judge it the right price: the alternative was a permanent exception to the package model, and an exception that has to be re-explained at every feature — imports, versioning, and the omni-import still to come — is a heavier long-run tax than a version mismatch that a project pins its way out of. + +## The package that was the language + +The [previous chapter](#cutting-core-loose-from-the-toolchain) made `core` ordinary to protect the uniformity of packages. It was a coherent local answer and the wrong global one. The contradiction appeared when we stopped asking how `Int` is imported and asked what an `if` accepts. The answer was already the canonical `Bool`; a counted loop already depended on canonical `Int`; literals, `String`, and the unit type had roles beside them. An ordinary package can be omitted or replaced. These identities could not be. Calling their container ordinary did not make the dependency optional; it only hid a language dependency inside the package graph. + +We briefly tried to preserve the package by pushing control flow down to the underlying primitives. That would make the package wrapper genuinely optional, at the price of writing conditions and bounds through private representation fields and tying syntax to one storage layout. The abstraction failed in the opposite direction: instead of the language depending on `core`, every program would depend on how `core` happened to wrap a bit or a word. We also no longer found the proposed omni-import persuasive as a reason to keep the package. A future convenience for importing ordinary library names should not decide the identity of types the language itself requires, and forcing fundamental types through that mechanism would make the general import feature carry a special burden rather than remove one. + +The answer is not to erase the implementation package. `core` still gives the fundamental types ordinary source declarations, constructors, methods, and primitive-backed layouts, but it becomes part of the compiler distribution rather than the program's package graph. Programs see `Int`, `Float`, `Bool`, `String`, and `Unit` as predeclared names: they neither import `core` nor write `core$Int`, and no manifest can omit or replace it. Reusable functionality remains in ordinary, specifically named packages and follows the dependency rules without privilege. + +This reverses the previous chapter without turning the type implementations into compiler AST exceptions. The language fixes which declarations `Bool` and `Int` name because its control-flow contracts require those identities; `core` still implements their behavior through the same package machinery used to compile source. The cost is a privileged package that cannot evolve on an independent library cadence or participate in imports and dependency resolution. That is real coupling, but it is confined to implementation and release rather than leaking primitive layout into source code or special cases into every fundamental type. diff --git a/stories/error-handling.md b/stories/error-handling.md index f1e9baf..a66ac70 100644 --- a/stories/error-handling.md +++ b/stories/error-handling.md @@ -12,7 +12,7 @@ What we disliked in the two contenders was subtler, and it decided the shape. Go So we asked the question that fixed the model: how does a function fail in the first place? Said plainly, the answer was that failing is a way of *exiting* — very close kin to `return`. Here is the picture that made it concrete. Rust receives a package, does its work, and sends one package back out through a single door: a green box on success, a red box carrying a message on failure, the color telling the caller which it got. Go sends one package too — a box with two compartments inside. But a function has only ever needed one thing from failure: a way *out* that is not the normal way out. A package with a color, or a box with two compartments, is an elaborate way to smuggle a second exit through one door. So we gave the function two doors. The primary door carries the ordinary return; the abort door carries the failure. A function that fails does not build a differently-colored package — it walks out the other door. -The surface falls out of that picture. If failure leaves by its own door, that door needs a declared type just as the primary one does, and it belongs right where the signature already says what comes back: `ReturnType?AbortType`, the `?` marking the fork between the two doors. When the abort door carries no payload — failure with nothing to report — its type is `Void`, and `abort` is written bare. And the same `?` reappears at the call site, where the caller must say what happens if the function takes the abort door: `call() ? err { ... }`. That reuse is deliberate: the token means one thing in both places — *the other door* — so annotating a fork and handling a fork spend one symbol between them rather than two. Because an abort is just an exit and nothing more exotic, the constructs that compose expressions need no special case for it: a [`match`](https://github.com/zane-lang/spec/blob/54ac140005b0f4f330b24e86e0351bfd74b8fa25/spec/adt.md#54-abort-flows-through) whose chosen arm can fail is itself a fallible expression, the abort door of the selected arm flowing straight through the `match` to the caller, neither introduced nor swallowed by it. +The surface fell out of that picture. If failure left by its own door, that door needed a declared type just as the primary one did, and it belonged right where the signature already said what came back: `ReturnType?AbortType`, the `?` marking the fork between the two doors. At this stage, an abort door carrying no payload used `Void`, and `abort` was written bare; [the later unit-type turn](#the-empty-door-still-carries-a-value) replaced that contract with `Unit`, an explicit `abort Unit()`, and a handler binder for the value. The same `?` reappeared at the call site, where the caller had to say what happened if the function took the abort door: `call() ? err { ... }`. That reuse was deliberate: the token meant one thing in both places — *the other door* — so annotating a fork and handling a fork spent one symbol between them rather than two. Because an abort was just an exit and nothing more exotic, the constructs that compose expressions needed no special case for it: a [`match`](https://github.com/zane-lang/spec/blob/54ac140005b0f4f330b24e86e0351bfd74b8fa25/spec/adt.md#54-abort-flows-through) whose chosen arm could fail was itself a fallible expression, the abort door of the selected arm flowing straight through the `match` to the caller, neither introduced nor swallowed by it. The cost here is the one a reader expects us to name, and honesty runs the other way: it is very small. Because failure is a second exit rather than a value, you cannot pick up a success-or-failure outcome and carry it around as a first-class thing — hold it in a list, stash it in a field, hand it back later. We went looking for where that bites and found little. A function's failure is almost always dealt with where it happens; the one time you want to send it further along is to re-fail with it, and that is just taking the abort door yourself. The cases that look like a stored error usually are not errors at all — an unconfigured setting that falls back to a default, a lookup that legitimately finds nothing — and those read better as [variants](https://github.com/zane-lang/spec/blob/54ac140005b0f4f330b24e86e0351bfd74b8fa25/spec/adt.md#3-variants), which say out loud "this is not a failure, it is one of several normal outcomes." So the one thing the two-doors model gives up, the language already covers better elsewhere, and the apparent cost mostly dissolves on inspection. @@ -51,3 +51,11 @@ The cost is real and we name it: propagating a failure is more to type in Zane t One question sits underneath all of this: is failing a *side effect*? Zane has an effect system that tracks whether a function mutates, reads, or stays pure, and abortability looks, at a glance, like one more thing that system might track — one more admission a signature makes about what a function can do. We keep the two entirely separate, and the history is part of the reason: error handling was designed first and the effect model came later, so when effects arrived there was nothing to reconcile, because the two had never described the same thing. They answer different questions. The effect system asks what a function does *to the world* — does it write memory, touch a capability, stay pure. Abortability asks what a function's *return* looks like — can it leave by the abort door. Those axes are independent. A pure function that derives a result from its inputs and touches nothing else can still fail, the way parsing pure text can fail; a function that mutates the world freely can be infallible. All four combinations are ordinary, and none is stranger than the others. Folding "can fail" into the effect lattice would have forced a false coupling — it would seat failure on the same axis as mutation and imply some ordering or containment between them that simply is not there. Instead the compiler runs the two analyses independently and a call must satisfy both: the effect check and the abort check are separate gates the one call passes through. The cost is that a function's contract now has two axes to read instead of one — but they are genuinely two things, and collapsing them would have bought a tidier signature that described a less true design. The effect system names this orthogonality [in its own words](https://github.com/zane-lang/spec/blob/54ac140005b0f4f330b24e86e0351bfd74b8fa25/spec/effects.md#73-abortability-is-orthogonal). + +## The empty door still carries a value + +The original error rules treated `Void` as absence. A `Void` primary result could not be assigned, a `Void` abort had no binder, and bare `resolve` or `abort` carried nothing. Once the type became the real `Unit` value, that interpretation stopped holding: a successful call returns `Unit()`, and a failure with no information carries that same sole value down the abort path. Forbidding assignment would have recreated the pseudo-type distinction the type change removed. + +We briefly kept the bare forms as convenience, then rejected them for the same reason bare returns went away. `abort` and `resolve` are AST exits with value expressions for every other type; omitting the expression for `Unit` would require each node and its type checker to know the nominal unit type and synthesize its constructor. A `Unit` abort therefore writes `abort Unit()`, a `Unit` recovery writes `resolve Unit()`, and a handler binds the abort value even when it contains no information. + +The cost is visible ceremony on an empty door. The gain is that "no meaningful payload" no longer means "no typed value": every path carries exactly the value its declared type promises, and error handling needs no special form for one bundled type. diff --git a/stories/functions.md b/stories/functions.md index 4a17074..96300a8 100644 --- a/stories/functions.md +++ b/stories/functions.md @@ -45,3 +45,11 @@ Everything so far treats a callable as something you *invoke*; the last decision But callbacks are real, and a language needs a way to pass behavior as data. The lambda is that way, and it works precisely because it is built to be the thing an overloaded name is not: a single value with one exact type. A lambda literal writes its own complete type — parameter types, return type, abort type, and `mut` — and infers nothing from its surroundings ([`functions.md` §7.2](https://github.com/zane-lang/spec/blob/54ac140005b0f4f330b24e86e0351bfd74b8fa25/spec/functions.md#72-lambdas-are-self-typed-function-values)). Because it carries its whole type, it is already collapsed to one member; there is no set to resolve, so it can be passed straight into an overloaded receiver by exact shape match, no circularity anywhere. To *name* a function value you declare a lambda-variable — a symbol bound to a lambda literal — and because a symbol has one type and can never accumulate an overload set, the name is always unambiguous in value position where a bare callable name would not be ([`functions.md` §7.3](https://github.com/zane-lang/spec/blob/54ac140005b0f4f330b24e86e0351bfd74b8fa25/spec/functions.md#73-lambda-variables-hold-function-values)). The self-typing is not ceremony; it is the property that lets a function value coexist with overloading at all. Two consequences of the lambda's design are worth naming as costs, because they are the parts a later reader will want to have been warned about. Lambdas do not capture: every value a lambda uses must be passed as a parameter or reached through explicit storage, never closed over from the surrounding scope ([`functions.md` §7.4](https://github.com/zane-lang/spec/blob/54ac140005b0f4f330b24e86e0351bfd74b8fa25/spec/functions.md#74-lambdas-do-not-capture)). That keeps data flow explicit and keeps effect and concurrency analysis tractable — a function value cannot smuggle in hidden mutable state for a parallel task to trip over ([`concurrency.md` §5.2](https://github.com/zane-lang/spec/blob/54ac140005b0f4f330b24e86e0351bfd74b8fa25/spec/concurrency.md#52-lambdas-do-not-capture)) — but it is a real ergonomic price: closures are a convenience most languages grant, and here every dependency rides in the open. It also means there are no bound method references: because nothing is captured, there is no syntax that quietly tucks a receiver inside a function value, so code that wants a receiver later must keep it in ordinary storage and pass it back explicitly at the call. And a function value that is itself *generic* — polymorphic over its own type or number parameters — is left unspecified in this version, not because overloading cannot cope with it (a generic function type is still one unique parameter shape) but because its runtime representation, monomorphization versus dictionary passing, is a memory-model question we have not answered; that gap is owned by the [generics story's deferred account](generics.md#deferred-what-the-model-promises-but-does-not-yet-deliver) alongside the other places the model promises more than it yet delivers. + +## Every return carries a value + +Making `Unit` a real value raised a choice between convenient procedure syntax and a uniform return checker. We first leaned toward convenience: a `Unit` verb could fall through or use bare `return`, with the compiler supplying `Unit()` because no other result was possible. A user-defined empty type would still need `return MyUnit()` because its constructor could execute arbitrary code. That separation was locally defensible and globally the wrong shape. + +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. diff --git a/stories/types.md b/stories/types.md index 0b2c8ab..573dc57 100644 --- a/stories/types.md +++ b/stories/types.md @@ -103,3 +103,15 @@ That it is a real category, and not just an underspecified sum, is a point worth Finding the name took a couple of wrong turns worth recording, because each was tempting. The first was **scalar**, from the cardinality framing — product multiplies, sum adds, and this one just counts. Elegant on paper, and dead on arrival at the reader: "scalar" means *a single primitive value* in nearly every language a Zane user will have met, the very opposite of a set of *n* constants, and a term that fights its own reader is one we had already refused once when we turned down "matrix" for `mould`. The second was **case**, naming each member a case — but "case" was long since spent on a `variant`'s members, where a value holds one case and `match` selects them, so lending it to `enum` too would smudge the one line the whole two-construct design exists to keep sharp. We took a genuine detour through probability, where an `enum` really does look like a sample space Ω — a finite set of mutually-exclusive, exhaustive, atomic outcomes — but "outcome" and "event" drag in randomness and measure an `enum` has nothing to do with. What every dead end had circled was the same property: the members are *atomic*, structureless, equal in rank. And the language already had a word for that, sitting in the enum chapter since it was written — its members are "uniform, interchangeable **peers**." So `peer mould` was less coined than promoted: it names the category from the `enum`'s own defining trait, collides with nothing, and imports no baggage. The honest note is what the name gives up next to its siblings. "Product" and "sum" advertise an operation — ×, + — and "peer" does not; it describes the members, not an arithmetic. But that is the truthful trade, because the `enum`'s shape is not a third operation beside the other two, only the all-units corner of sum, and a name promising an algebra it does not extend would be the same overclaim we spent the naming on avoiding. `peer mould` says what the category *is* — a flat set of uniform peers — and leaves the arithmetic to the two shapes that actually have one. + +## Unit exposes the package that wasn't one + +The question arrived through a generic field. If `Player` carries optional extra settings, `Player` is the obvious application when there are no settings to carry. But the field still has to be constructed: what value can be passed for a type that exists only as a return-position marker? That pressure made `Void` an actual zero-sized type with an actual constructor, `Void()`, so its value could sit in a symbol, field, array, or type argument like any other value. Once it had exactly one value, the old name no longer described it — a void has none, while a unit type has one — so we renamed it to `Unit`, making the final application `Player` and its field value `Unit()`. The rename says what the model became and leaves `Never` as the natural future name for a type with no values. + +A user can already declare an empty `struct`, which forced the sharper question: what distinguishes `Unit` from `MyUnit`? Layout does not. Both may take zero bytes, both have one logical value, and both constructors are ordinary verbs supplied by their defining package. We initially tried to privilege `Unit` anyway: reaching the end of a `Unit` verb and bare `return` would synthesize `Unit()`. That looked harmless because there is only one result, but it required the return checker and AST lowering to recognize one nominal type and invent a call for it. The comparison with `Bool` in an `if` did not save the exception. An `if` already has to check its condition against some destination type; naming that type `Bool` changes no unrelated rule. Synthesizing `Unit()` would add behavior that returns do not otherwise have. + +So `Unit` receives no completion privilege. A verb returning it writes `return Unit()`, and abort and resolve paths write the same value explicitly. This makes the return checker uniform and keeps the bundled constructor observable as an ordinary constructor rather than a compiler rewrite. The cost is familiar procedure boilerplate: an operation with no informative result still spells the sole value at every returning path. We accept that cost because the alternative would make the type ordinary everywhere except the AST node where uniformity matters most. + +Following that distinction led past `Unit` into the types surrounding it. We had been calling `Int`, `Float`, `Bool`, `String`, and `Void` ordinary types from an ordinary `core` package, while giving the compiler no way to treat them as ordinary. Conditions already meant `Bool`; counted loops already meant `Int`; literals lowered toward these names. The package claim was not an abstraction the language actually honored. It was a layer of qualification drawn around types whose identities the language syntax already required. + +We considered exposing the storage primitives instead — unwrap `Bool(true)._v` for an `if`, unwrap `Int(20)._v` for a loop — and rejected it because it turns representation into semantics. A field rename would change what control flow accepts, every API returning `Bool` would need manual unwrapping, and abstraction would end exactly where the language needs it most. The resolution keeps `core` as the compiler's implementation package while removing it from the source package model: the five fundamental names are predeclared, their definitions ship with the compiler, and `@primitives$` remains below them as storage machinery. The cost is a deliberately privileged implementation package, but not five compiler-defined type bodies or constructors. `Bool` and `Int` are named by the control-flow type rules; `Unit` is merely another bundled declaration.