From 6e638a08ec4850a76ac9b05fe4cbb029294aca08 Mon Sep 17 00:00:00 2001 From: Manuel Stieger <149385373+TheLazyCat00@users.noreply.github.com> Date: Sun, 26 Jul 2026 19:23:40 +0200 Subject: [PATCH 1/5] docs: make fundamental types part of the language --- README.md | 12 ++++---- spec/control-flow.md | 11 ++++++-- spec/dependencies.md | 4 +-- spec/error-handling.md | 23 ++++++++++++---- spec/functions.md | 32 +++++++++++++++++---- spec/glossary.md | 6 ++-- spec/lifetimes.md | 4 +-- spec/memory.md | 4 +-- spec/operators.md | 6 ++-- spec/packages.md | 6 ++-- spec/syntax.md | 24 ++++++++++------ spec/types.md | 58 +++++++++++++++++++++++++++++---------- stories/control-flow.md | 8 ++++++ stories/dependencies.md | 10 +++++++ stories/error-handling.md | 8 ++++++ stories/functions.md | 8 ++++++ stories/types.md | 10 +++++++ 17 files changed, 177 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 47b76d3..04858b7 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 `Void` 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` ultimately stopped being a 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 payloadless syntax over the real `Void` value | +| [`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 `Void`'s one-value return shorthand | | [`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..b605b1c 100644 --- a/spec/dependencies.md +++ b/spec/dependencies.md @@ -271,10 +271,10 @@ At a high level, dependency resolution proceeds in this order: 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 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 standard library is **not** special, and no part of it is coupled to the toolchain tag. `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`. Fundamental language types are part of the language rather than a library 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..75ae9c0 100644 --- a/spec/error-handling.md +++ b/spec/error-handling.md @@ -30,7 +30,7 @@ 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. +If failure carries no meaningful payload, the abort type is `Void`. Both `abort` and `abort Void()` are legal. The bare form is shorthand for aborting with the canonical `Void()` value. ### 2.3 Abortability is orthogonal to `mut` Abortability and mutation are independent. A method may be: @@ -66,7 +66,7 @@ value Int = parse("42") ? err { } ``` -When the abort type is `Void`, the binder is omitted: +When the abort type is `Void`, the binder may be omitted: ```zane done Bool = tryFinish() ? { @@ -74,7 +74,9 @@ done Bool = tryFinish() ? { } ``` -There is no propagation-without-a-handler form. To pass failure outward, the handler itself uses `abort ...`. +A handler may still bind the `Void` value when uniform generic code needs a name for it. + +There is no propagation-without-a-handler form. To pass failure outward, the handler itself uses `abort ...` or bare `abort` for `Void`. ```zane value Int = parse(input) ? err { @@ -102,8 +104,16 @@ 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 `Void` primary returns are values +A call whose primary return type is `Void` produces the canonical `Void()` value. The result may be assigned or passed like any other value. + +```zane +completed Void = 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#payloadless-syntax-over-a-real-value) — "Payloadless syntax over a real 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 +144,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 `Void`, both `resolve` and `resolve Void()` are legal. The bare form is shorthand for resolving the canonical `Void()` 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". @@ -290,4 +300,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 | +| `Void` path value | `Void()` is a real primary or abort-path value; bare `resolve` and `abort` are shorthand for passing it | | Abort-free function | Statically guaranteed not to abort | diff --git a/spec/functions.md b/spec/functions.md index 0bccbe5..83e97b8 100644 --- a/spec/functions.md +++ b/spec/functions.md @@ -173,14 +173,35 @@ 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 +Void noOperation() => 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} }`. 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 `Void` completion +A verb whose primary return type is `Void` may complete in any of three equivalent ways: + +```zane +Void implicitCompletion() { +} + +Void bareReturn() { + return +} + +Void explicitReturn() { + return Void() +} +``` + +Reaching the end of the body and a bare `return` both return the canonical `Void()` value. This shorthand applies only to the fundamental `Void` type. A user-defined empty value type still requires an explicit returned value because its constructor may execute arbitrary code. + +> **Story:** [`stories/functions.md`](../stories/functions.md#the-one-value-return-door) — "The one-value return door". --- @@ -231,10 +252,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, or its compiler-provided method set when the receiver type is fundamental 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: @@ -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 | +| `Void` completion | Fallthrough and bare `return` return `Void()`; explicit `return Void()` is also legal | | `&` 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 or fundamental method set, then the current package | | Extension methods | Any package may declare methods on imported types by naming the first parameter `this` | diff --git a/spec/glossary.md b/spec/glossary.md index 3bbb136..5589ed7 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 or by the compiler-provided literal lowering of a fundamental type — 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 user-defined operator implementation may be declared only in the home package of one of its user-defined operand types. Fundamental-only operators are compiler-provided. - **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/lifetimes.md b/spec/lifetimes.md index 1c9cde9..0df160d 100644 --- a/spec/lifetimes.md +++ b/spec/lifetimes.md @@ -198,7 +198,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 `Void()`, 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 +254,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..aa41c3d 100644 --- a/spec/memory.md +++ b/spec/memory.md @@ -292,8 +292,8 @@ 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..91ca25b 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`. Fundamental types have no home package, so they do not by themselves grant a package permission to declare an operator; their compiler-provided operators are part of the language. 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 user implementation must live in the home package of at least one user-defined operand type; fundamental-only operators are compiler-provided. | | 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..9962b2d 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 packages`.** No package is available without an import, and none injects unqualified names. Every package a file uses is imported explicitly and reached by qualification. - **`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 package is available without such an import. There is no ambient or automatically-imported package: every package a file uses is imported explicitly and its members are reached by the same `packageName$member` qualification. Fundamental language types are not package members; 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 packages | None; every package requires an explicit import | | 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..24ee42e 100644 --- a/spec/syntax.md +++ b/spec/syntax.md @@ -128,12 +128,10 @@ EnumName.property FieldType [ ## 2. Types -### 2.1 Core surface types +### 2.1 Fundamental types `Int`, `Float`, `Bool`, `String`, `Void` -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. - -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 part of the language and are available unqualified in every source file. They are not members of a package. See [`types.md`](types.md) §2.6 for their semantics. ### 2.2 Named types @@ -213,7 +211,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 +221,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. @@ -307,7 +305,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 `Void`. ### 3.3 Positional constructors @@ -437,7 +435,7 @@ 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: @@ -485,6 +483,15 @@ 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 +return +``` + +Bare `return` is legal only in a verb whose primary return type is `Void`. See [`functions.md`](functions.md) §3.5. + --- ## 4. Calls and Function Values @@ -667,6 +674,7 @@ Every path inside the handler must end with one of: resolve expr resolve return expr +return abort expr abort ``` diff --git a/spec/types.md b/spec/types.md index b61c562..bf366d4 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 `Void` belong to the language; `type` introduces a new distinct named type and `alias` an interchangeable name. --- @@ -84,6 +84,31 @@ 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 `Void` are **fundamental language types**. Their names are available unqualified in every source file and do not belong to a package. The compiler defines their canonical identities and lowers their operations to the storage primitives in the `@primitives$` namespace. + +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 compiler provides the canonical constructors and literal lowering for fundamental types. At a coercion site, a matching source literal may therefore become the expected fundamental type: `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 literal lowering does not create general truthiness or numeric narrowing. + +`Void` is the canonical unit type. It is a zero-sized value type with exactly one value, constructed as `Void()`. 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", Void()) +completed Void = performWork() +``` + +An implementation may erase `Void` values and fields from runtime storage. Their type-level presence and evaluation order remain observable to the type checker. + +> **Story:** [`stories/types.md`](../stories/types.md#void-exposes-the-package-that-wasnt-one) — "Void exposes the package that wasn't one". + --- ## 3. Constructors and Initialization @@ -347,7 +372,7 @@ implicit Meters(feet Feet) => init{value = feet.value * Float(0.3048)} Void printDistance(d Meters) { ... } ``` -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 +399,16 @@ 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(...)` - 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. +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. 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,17 +418,18 @@ 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`. -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. +2. If `U` is a compiler concept type and `T` is a fundamental type with matching canonical literal lowering, rewrite the argument as `T(arg)`. +3. Otherwise, collect all visible applicable `implicit` constructors from `U` to `T`. +4. If exactly one applicable implicit constructor exists, rewrite the argument as `T(arg)`. +5. If multiple applicable implicit constructors exist, the site is an ambiguity error. +6. If none exist, the site is a normal type error. ### 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 +465,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 user-defined type. A third-party package **MUST NOT** declare an implicit constructor between two imported types. Fundamental types have no home package, so they do not by themselves grant a package permission to declare a conversion; their compiler-provided constructors are part of the language. 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. @@ -505,7 +533,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 user-defined 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 user-defined constructible type therefore has a name, and that name is what its constructor is called by (§3.1). The fundamental types are compiler-defined named types rather than source declarations; 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 +555,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 `Void`; compiler-defined, package-free, and available unqualified | +| `Void` | Zero-sized singleton value type; `Void()` is its sole value and 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..86ecbdf 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 `Void` had compiler-defined 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. + +So `core` disappears as a package. `Int`, `Float`, `Bool`, `String`, and `Void` belong directly to the language; reusable functionality belongs in ordinary, specifically named packages and follows the dependency rules without privilege. This reverses the previous chapter rather than erasing it: first we tried coupling `core` to the compiler, then making it fully ordinary, and only the control-flow question showed that both answers assumed a package boundary that was not real. + +The cost is that the language has a fixed fundamental vocabulary which no dependency may replace, and implementations cannot evolve those types on an independent library cadence. That is not incidental coupling; it is the coupling the syntax had already committed to. Owning it in the language makes the dependency graph simpler and the semantics honest. diff --git a/stories/error-handling.md b/stories/error-handling.md index f1e9baf..803d3ea 100644 --- a/stories/error-handling.md +++ b/stories/error-handling.md @@ -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). + +## Payloadless syntax over a real 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 `Void` became the real unit value, that interpretation stopped holding: a successful call returns `Void()`, and a payloadless failure carries that same sole value down the abort path. Forbidding assignment would have recreated the pseudo-type distinction the type change removed. + +We kept the bare forms because writing the only possible payload adds no information. `abort` means `abort Void()` and `resolve` means `resolve Void()` when the relevant door is typed `Void`; a handler may omit its binder or bind the value when uniform code benefits from a name. The explicit forms remain legal, so generic expansions never need a special case that deletes an argument merely because its instantiated type is `Void`. + +The cost is mostly conceptual: "payloadless" no longer means literally no typed value flows through the door, only that the one flowing value contains no information and occupies no storage. That is a more precise model, and it lets error handling keep its concise surface without placing `Void` outside the type system. diff --git a/stories/functions.md b/stories/functions.md index 4a17074..3ebeb92 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. + +## The one-value return door + +Making `Void` a real value raised an apparent choice between orthogonality and convenient procedure syntax. If every function returns a value, the strict reading says a `Void` function must end in `return Void()` just as a `MyUnit` function must end in `return MyUnit()`. We considered that rule seriously because it makes the return checker beautifully uniform, and rejected it because the extra expression carries no choice: `Void` has exactly one value, so spelling it at every early return repeats information the signature already fixes. + +The shorthand is safe only because `Void()` is compiler-known and effect-free. Generalizing fallthrough to every zero-sized or single-valued type would silently call user constructors, which may log, abort, mutate, or do anything else a verb may do. We therefore keep the privilege nominal and narrow: reaching the end of a `Void` verb and bare `return` both mean `return Void()`. An explicit `return Void()` remains legal, and once the value exists, `Void noOperation() => Void()` follows from the ordinary expression-body rewrite rather than requiring another exception. + +The cost is that `Void` is not interchangeable with a user-defined unit type in control flow even when their layouts are identical. That difference is intentional: one is the language's canonical completion type, the other is an ordinary nominal type with ordinary construction. Uniform storage wins on both; only the return door distinguishes them. diff --git a/stories/types.md b/stories/types.md index 0b2c8ab..edea655 100644 --- a/stories/types.md +++ b/stories/types.md @@ -103,3 +103,13 @@ 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. + +## Void exposes the package that wasn't one + +The question arrived through a generic field. If `Player` carries optional extra settings, `Player` is the clean application when there are no settings to carry — but only if `Void` is a real type with a real value, rather than a return-position marker the type system pretends is a type until someone tries to store it. Making that use legal means `Void()` exists, occupies no storage, and can sit in a symbol, field, array, or type argument exactly as any other zero-sized value can. The first alternative was to keep the old half-state: let `Void` name the absence of a return while forbidding values of it. That preserved familiar procedure syntax and broke the generic model precisely where a unit type is most useful, so we rejected it. + +A user can already declare an empty `struct`, which forced the sharper question: what distinguishes the language's unit type from `MyUnit`? Layout does not. Both may take zero bytes, and both have one logical value. The distinction is construction. `MyUnit()` is an ordinary user-written constructor and may execute arbitrary code before returning `init{ }`; the compiler cannot silently call it when a function falls through without inventing an effect. `Void()` is the compiler-known canonical value and cannot be overloaded. That makes storage behavior general and completion shorthand specific: both types are ordinary values, but only `Void` can be returned by a bare `return` or by reaching the end of a `Void` verb. + +Following that distinction led past `Void` 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; `Void` controlled payloadless returns. 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 is to make the five names fundamental language types and keep `@primitives$` below them as compiler machinery. The cost is a deliberate exception to the rule that user-defined types originate in mould declarations: these five have compiler-defined identities and are simply present. That exception is smaller and more honest than a package that can supposedly be removed or replaced even though the language cannot say what an `if` means without it. From dd154900139482b8a2b7bd67000456224589d8fd Mon Sep 17 00:00:00 2001 From: Manuel Stieger <149385373+TheLazyCat00@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:45:06 +0200 Subject: [PATCH 2/5] docs: make Unit returns explicit --- README.md | 8 +++---- spec/dependencies.md | 6 ++--- spec/error-handling.md | 30 +++++++++++++----------- spec/functions.md | 49 +++++++++++++++++++-------------------- spec/generics.md | 6 ++--- spec/glossary.md | 4 ++-- spec/lexical.md | 3 ++- spec/lifetimes.md | 11 +++++---- spec/memory.md | 9 ++++--- spec/operators.md | 4 ++-- spec/packages.md | 2 +- spec/syntax.md | 31 ++++++++++++------------- spec/types.md | 45 ++++++++++++++++++++--------------- stories/dependencies.md | 6 ++--- stories/error-handling.md | 8 +++---- stories/functions.md | 8 +++---- stories/types.md | 12 ++++++---- 17 files changed, 129 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 04858b7..4edd3ee 100644 --- a/README.md +++ b/README.md @@ -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 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 `Void` a real fundamental unit type | +| [`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, opt-in remapping, and why `core` ultimately stopped being a package | +| [`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, keeping abortability orthogonal to effects, and payloadless syntax over the real `Void` value | +| [`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 `Void`'s one-value return shorthand | +| [`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/dependencies.md b/spec/dependencies.md index b605b1c..62706c4 100644 --- a/spec/dependencies.md +++ b/spec/dependencies.md @@ -268,10 +268,10 @@ 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. `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`. Fundamental language types are part of the language rather than a library dependency; see [`types.md`](types.md) §2.6. +- 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#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 75ae9c0..131eb9b 100644 --- a/spec/error-handling.md +++ b/spec/error-handling.md @@ -29,8 +29,12 @@ ReturnType?AbortType A declaration with no `?AbortType` cannot abort. -### 2.2 `Void` abort type -If failure carries no meaningful payload, the abort type is `Void`. Both `abort` and `abort Void()` are legal. The bare form is shorthand for aborting with the canonical `Void()` value. +### 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,17 +70,15 @@ value Int = parse("42") ? err { } ``` -When the abort type is `Void`, the binder may be omitted: +The binder is required even when the abort type is `Unit`: ```zane -done Bool = tryFinish() ? { +done Bool = tryFinish() ? ignored { resolve false } ``` -A handler may still bind the `Void` value when uniform generic code needs a name for it. - -There is no propagation-without-a-handler form. To pass failure outward, the handler itself uses `abort ...` or bare `abort` for `Void`. +There is no propagation-without-a-handler form. To pass failure outward, the handler itself uses `abort ...`. ```zane value Int = parse(input) ? err { @@ -104,16 +106,16 @@ Falling through a handler block is a compile-time error. count Int = parse("abc") ?? Int(0) ``` -### 3.4 `Void` primary returns are values -A call whose primary return type is `Void` produces the canonical `Void()` value. The result may be assigned or passed like any other value. +### 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 Void = performWork() +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#payloadless-syntax-over-a-real-value) — "Payloadless syntax over a real value". +> **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. @@ -144,7 +146,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`, both `resolve` and `resolve Void()` are legal. The bare form is shorthand for resolving the canonical `Void()` 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". @@ -286,7 +288,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 | --- @@ -300,5 +302,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 | -| `Void` path value | `Void()` is a real primary or abort-path value; bare `resolve` and `abort` are shorthand for passing it | +| `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 83e97b8..7685b87 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() } ``` @@ -178,30 +182,23 @@ A verb may use `=>` for its body. Functions, methods, operators, constructors, a ```zane Int double(value Int) => value * 2 Int scaledId(this Node, factor Int) => this._id * factor -Void noOperation() => Void() +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} }`. -### 3.5 `Void` completion -A verb whose primary return type is `Void` may complete in any of three equivalent ways: +### 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 -Void implicitCompletion() { -} - -Void bareReturn() { - return -} - -Void explicitReturn() { - return Void() +Unit noOperation() { + return Unit() } ``` -Reaching the end of the body and a bare `return` both return the canonical `Void()` value. This shorthand applies only to the fundamental `Void` type. A user-defined empty value type still requires an explicit returned value because its constructor may execute arbitrary code. +The return checker does not synthesize a constructor call for `Unit` or any other singleton type. -> **Story:** [`stories/functions.md`](../stories/functions.md#the-one-value-return-door) — "The one-value return door". +> **Story:** [`stories/functions.md`](../stories/functions.md#every-return-carries-a-value) — "Every return carries a value". --- @@ -213,8 +210,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 @@ -252,7 +249,7 @@ 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, or its compiler-provided method set when the receiver type is fundamental +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 defining declarations first makes an unqualified call resolve the same way wherever it is written, independent of which packages the caller has imported. @@ -305,12 +302,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 ``` @@ -393,7 +392,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 | -| `Void` completion | Fallthrough and bare `return` return `Void()`; explicit `return Void()` is also legal | +| 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 | @@ -403,5 +402,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 the receiver's home package or fundamental method set, then the 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 5589ed7..c879ed1 100644 --- a/spec/glossary.md +++ b/spec/glossary.md @@ -201,7 +201,7 @@ This file gives short, reusable names to concepts that appear across multiple sp ### 3.28 coercion site - **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 or by the compiler-provided literal lowering of a fundamental type — the site says where one may be inserted, not that arbitrary conversion is built in. +- **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:** A user-defined operator implementation may be declared only in the home package of one of its user-defined operand types. Fundamental-only operators are compiler-provided. +- **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 0df160d..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 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 `Void()`, is simply discarded. +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). diff --git a/spec/memory.md b/spec/memory.md index aa41c3d..6d244da 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() } ``` diff --git a/spec/operators.md b/spec/operators.md index 91ca25b..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`. Fundamental types have no home package, so they do not by themselves grant a package permission to declare an operator; their compiler-provided operators are part of the language. See [`functions.md`](functions.md) §6.1 for the corresponding method-resolution rule. +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. @@ -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 | A user implementation must live in the home package of at least one user-defined operand type; fundamental-only operators are compiler-provided. | +| 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 9962b2d..8251f73 100644 --- a/spec/packages.md +++ b/spec/packages.md @@ -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 is imported explicitly and its members are reached by the same `packageName$member` qualification. Fundamental language types are not package members; see [`types.md`](types.md) §2.6. +No source package is available without such an import. There is no ambient or automatically-imported source package: every 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 diff --git a/spec/syntax.md b/spec/syntax.md index 24ee42e..6ca8020 100644 --- a/spec/syntax.md +++ b/spec/syntax.md @@ -129,9 +129,9 @@ EnumName.property FieldType [ ## 2. Types ### 2.1 Fundamental types -`Int`, `Float`, `Bool`, `String`, `Void` +`Int`, `Float`, `Bool`, `String`, `Unit` -These type names are part of the language and are available unqualified in every source file. They are not members of a package. See [`types.md`](types.md) §2.6 for their semantics. +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 @@ -252,7 +252,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 @@ -305,7 +305,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` returns `expr`, including when `expr` has type `Void`. +`=> expr` returns `expr`, including when `expr` has type `Unit`. ### 3.3 Positional constructors @@ -440,12 +440,14 @@ A lambda literal omits only the function name. `this` is legal only in the first 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() }) ``` @@ -461,12 +463,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() } ``` @@ -487,10 +491,9 @@ Operator definitions are package-scope verb declarations whose names are operato ```zane return expr -return ``` -Bare `return` is legal only in a verb whose primary return type is `Void`. See [`functions.md`](functions.md) §3.5. +See [`functions.md`](functions.md) §3.5 for return-path requirements. --- @@ -547,11 +550,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 ``` @@ -665,18 +668,14 @@ ReturnType?AbortType ```zane expr ? binder { ... } -expr ? { ... } ``` Every path inside the handler must end with one of: ```zane resolve expr -resolve return expr -return abort expr -abort ``` ### 6.3 `??` shorthand diff --git a/spec/types.md b/spec/types.md index bf366d4..1faa864 100644 --- a/spec/types.md +++ b/spec/types.md @@ -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. -- **`Fundamental and declared types`.** `Int`, `Float`, `Bool`, `String`, and `Void` belong to the language; `type` introduces a new distinct named type and `alias` an interchangeable name. +- **`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. --- @@ -85,13 +85,13 @@ 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 `Void` are **fundamental language types**. Their names are available unqualified in every source file and do not belong to a package. The compiler defines their canonical identities and lowers their operations to the storage primitives in the `@primitives$` namespace. +`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 compiler provides the canonical constructors and literal lowering for fundamental types. At a coercion site, a matching source literal may therefore become the expected fundamental type: `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 literal lowering does not create general truthiness or numeric narrowing. +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. -`Void` is the canonical unit type. It is a zero-sized value type with exactly one value, constructed as `Void()`. It may appear wherever any other value type may appear, including symbols, fields, arrays, generic arguments, function parameters, and return types. +`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 { @@ -101,13 +101,13 @@ type Player = #struct { Player(name String, extraSettings T Type) => init{name, extraSettings} -player Player = Player("Manuel", Void()) -completed Void = performWork() +player Player = Player("Manuel", Unit()) +completed Unit = performWork() ``` -An implementation may erase `Void` values and fields from runtime storage. Their type-level presence and evaluation order remain observable to the type checker. +An implementation may erase `Unit` values and fields from runtime storage. Their type-level presence and evaluation order remain observable to the type checker. -> **Story:** [`stories/types.md`](../stories/types.md#void-exposes-the-package-that-wasnt-one) — "Void exposes the package that wasn't one". +> **Story:** [`stories/types.md`](../stories/types.md#unit-exposes-the-package-that-wasnt-one) — "Unit exposes the package that wasn't one". --- @@ -369,7 +369,10 @@ 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 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. @@ -425,11 +428,12 @@ Operator operands **are** coercion sites, because operators desugar to ordinary 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. If `U` is a compiler concept type and `T` is a fundamental type with matching canonical literal lowering, rewrite the argument as `T(arg)`. -3. Otherwise, collect all visible applicable `implicit` constructors from `U` to `T`. -4. If exactly one applicable implicit constructor exists, rewrite the argument as `T(arg)`. -5. If multiple applicable implicit constructors exist, the site is an ambiguity error. -6. If none exist, the site is a normal type error. +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. + +The bundled `core` package declares the implicit constructors from compiler concept types to the corresponding fundamental types. Literal coercion therefore 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. @@ -465,7 +469,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 user-defined type. A third-party package **MUST NOT** declare an implicit constructor between two imported types. Fundamental types have no home package, so they do not by themselves grant a package permission to declare a conversion; their compiler-provided constructors are part of the language. +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. @@ -490,7 +494,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 @@ -533,7 +540,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 user-defined 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 user-defined constructible type therefore has a name, and that name is what its constructor is called by (§3.1). The fundamental types are compiler-defined named types rather than source declarations; see §2.6. +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". @@ -555,8 +562,8 @@ 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 `Void`; compiler-defined, package-free, and available unqualified | -| `Void` | Zero-sized singleton value type; `Void()` is its sole value and may be stored or used as a generic argument | +| 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 | diff --git a/stories/dependencies.md b/stories/dependencies.md index 86ecbdf..ebba35a 100644 --- a/stories/dependencies.md +++ b/stories/dependencies.md @@ -74,10 +74,10 @@ The cost we accept is the one the [previous chapter](#the-toolchain-rides-one-ta ## 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 `Void` had compiler-defined 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. +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. -So `core` disappears as a package. `Int`, `Float`, `Bool`, `String`, and `Void` belong directly to the language; reusable functionality belongs in ordinary, specifically named packages and follows the dependency rules without privilege. This reverses the previous chapter rather than erasing it: first we tried coupling `core` to the compiler, then making it fully ordinary, and only the control-flow question showed that both answers assumed a package boundary that was not real. +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. -The cost is that the language has a fixed fundamental vocabulary which no dependency may replace, and implementations cannot evolve those types on an independent library cadence. That is not incidental coupling; it is the coupling the syntax had already committed to. Owning it in the language makes the dependency graph simpler and the semantics honest. +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 803d3ea..31d8e21 100644 --- a/stories/error-handling.md +++ b/stories/error-handling.md @@ -52,10 +52,10 @@ One question sits underneath all of this: is failing a *side effect*? Zane has a 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). -## Payloadless syntax over a real value +## 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 `Void` became the real unit value, that interpretation stopped holding: a successful call returns `Void()`, and a payloadless failure carries that same sole value down the abort path. Forbidding assignment would have recreated the pseudo-type distinction the type change removed. +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 kept the bare forms because writing the only possible payload adds no information. `abort` means `abort Void()` and `resolve` means `resolve Void()` when the relevant door is typed `Void`; a handler may omit its binder or bind the value when uniform code benefits from a name. The explicit forms remain legal, so generic expansions never need a special case that deletes an argument merely because its instantiated type is `Void`. +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 mostly conceptual: "payloadless" no longer means literally no typed value flows through the door, only that the one flowing value contains no information and occupies no storage. That is a more precise model, and it lets error handling keep its concise surface without placing `Void` outside the type system. +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 3ebeb92..96300a8 100644 --- a/stories/functions.md +++ b/stories/functions.md @@ -46,10 +46,10 @@ But callbacks are real, and a language needs a way to pass behavior as data. The 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. -## The one-value return door +## Every return carries a value -Making `Void` a real value raised an apparent choice between orthogonality and convenient procedure syntax. If every function returns a value, the strict reading says a `Void` function must end in `return Void()` just as a `MyUnit` function must end in `return MyUnit()`. We considered that rule seriously because it makes the return checker beautifully uniform, and rejected it because the extra expression carries no choice: `Void` has exactly one value, so spelling it at every early return repeats information the signature already fixes. +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 shorthand is safe only because `Void()` is compiler-known and effect-free. Generalizing fallthrough to every zero-sized or single-valued type would silently call user constructors, which may log, abort, mutate, or do anything else a verb may do. We therefore keep the privilege nominal and narrow: reaching the end of a `Void` verb and bare `return` both mean `return Void()`. An explicit `return Void()` remains legal, and once the value exists, `Void noOperation() => Void()` follows from the ordinary expression-body rewrite rather than requiring another exception. +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. -The cost is that `Void` is not interchangeable with a user-defined unit type in control flow even when their layouts are identical. That difference is intentional: one is the language's canonical completion type, the other is an ordinary nominal type with ordinary construction. Uniform storage wins on both; only the return door distinguishes them. +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 edea655..f19bb13 100644 --- a/stories/types.md +++ b/stories/types.md @@ -104,12 +104,14 @@ Finding the name took a couple of wrong turns worth recording, because each was 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. -## Void exposes the package that wasn't 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 clean application when there are no settings to carry — but only if `Void` is a real type with a real value, rather than a return-position marker the type system pretends is a type until someone tries to store it. Making that use legal means `Void()` exists, occupies no storage, and can sit in a symbol, field, array, or type argument exactly as any other zero-sized value can. The first alternative was to keep the old half-state: let `Void` name the absence of a return while forbidding values of it. That preserved familiar procedure syntax and broke the generic model precisely where a unit type is most useful, so we rejected it. +The question arrived through a generic field. If `Player` carries optional extra settings, `Player` is the clean application when there are no settings to carry — but only if the language has a real type with a real value, rather than a return-position marker the type system pretends is a type until someone tries to store it. Making that use legal means `Unit()` exists, occupies no storage, and can sit in a symbol, field, array, or type argument exactly as any other zero-sized value can. Calling that type `Void` no longer fit once it had a value: a void has none, while a unit type has exactly one. 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 the language's unit type from `MyUnit`? Layout does not. Both may take zero bytes, and both have one logical value. The distinction is construction. `MyUnit()` is an ordinary user-written constructor and may execute arbitrary code before returning `init{ }`; the compiler cannot silently call it when a function falls through without inventing an effect. `Void()` is the compiler-known canonical value and cannot be overloaded. That makes storage behavior general and completion shorthand specific: both types are ordinary values, but only `Void` can be returned by a bare `return` or by reaching the end of a `Void` verb. +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. -Following that distinction led past `Void` 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; `Void` controlled payloadless returns. 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. +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. -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 is to make the five names fundamental language types and keep `@primitives$` below them as compiler machinery. The cost is a deliberate exception to the rule that user-defined types originate in mould declarations: these five have compiler-defined identities and are simply present. That exception is smaller and more honest than a package that can supposedly be removed or replaced even though the language cannot say what an `if` means without it. +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. From 6a5c41d40ac6aec96018ff55265000af515b984a Mon Sep 17 00:00:00 2001 From: Manuel Stieger <149385373+TheLazyCat00@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:31:05 +0200 Subject: [PATCH 3/5] docs: address fundamental type review --- spec/error-handling.md | 2 ++ spec/functions.md | 1 + spec/memory.md | 1 + spec/packages.md | 4 ++-- spec/syntax.md | 1 + spec/types.md | 4 +++- stories/error-handling.md | 2 +- 7 files changed, 11 insertions(+), 4 deletions(-) diff --git a/spec/error-handling.md b/spec/error-handling.md index 131eb9b..4b864b4 100644 --- a/spec/error-handling.md +++ b/spec/error-handling.md @@ -30,6 +30,7 @@ ReturnType?AbortType A declaration with no `?AbortType` cannot abort. ### 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 @@ -107,6 +108,7 @@ count Int = parse("abc") ?? Int(0) ``` ### 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 diff --git a/spec/functions.md b/spec/functions.md index 7685b87..deeb7da 100644 --- a/spec/functions.md +++ b/spec/functions.md @@ -188,6 +188,7 @@ 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} }`. ### 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 diff --git a/spec/memory.md b/spec/memory.md index 6d244da..64281c0 100644 --- a/spec/memory.md +++ b/spec/memory.md @@ -296,6 +296,7 @@ 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 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/packages.md b/spec/packages.md index 8251f73..9acdcdf 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 is imported explicitly and reached by qualification. +- **`No implicit source packages`.** No source package is available without an import, and none injects unqualified names. 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. --- @@ -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 requires an explicit import | +| Implicit source packages | None; every source package requires an explicit import; bundled `core` instead 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 6ca8020..3782e61 100644 --- a/spec/syntax.md +++ b/spec/syntax.md @@ -129,6 +129,7 @@ EnumName.property FieldType [ ## 2. Types ### 2.1 Fundamental types + `Int`, `Float`, `Bool`, `String`, `Unit` 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. diff --git a/spec/types.md b/spec/types.md index 1faa864..1fe5ffc 100644 --- a/spec/types.md +++ b/spec/types.md @@ -85,6 +85,7 @@ 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. @@ -407,11 +408,12 @@ A coercion site is a position that passes a value into a contract whose destinat - 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. A control-flow expression fills a slot fixed by the language instead: `Bool` for a condition and `Int` for a counted-loop bound. +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: diff --git a/stories/error-handling.md b/stories/error-handling.md index 31d8e21..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. From c5e90c40e364c0591970cb932df78a0fa804f55d Mon Sep 17 00:00:00 2001 From: Manuel Stieger <149385373+TheLazyCat00@users.noreply.github.com> Date: Sun, 26 Jul 2026 22:44:03 +0200 Subject: [PATCH 4/5] docs: clarify core visibility and Unit erasure --- spec/packages.md | 6 +++--- spec/types.md | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/packages.md b/spec/packages.md index 9acdcdf..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 source packages`.** No source package is available without an import, and none injects unqualified names. The bundled `core` implementation is outside the source package and manifest system; it provides the predeclared fundamental names. +- **`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 source package is available without such an import. There is no ambient or automatically-imported source package: every 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). +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 source packages | None; every source package requires an explicit import; bundled `core` instead provides predeclared fundamental names outside the source package and manifest system | +| 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/types.md b/spec/types.md index 1fe5ffc..b9c9bf7 100644 --- a/spec/types.md +++ b/spec/types.md @@ -106,7 +106,7 @@ player Player = Player("Manuel", Unit()) completed Unit = performWork() ``` -An implementation may erase `Unit` values and fields from runtime storage. Their type-level presence and evaluation order remain observable to the type checker. +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". @@ -435,7 +435,7 @@ At one coercion site requiring destination type `T`, given an argument with stat 4. If multiple applicable implicit constructors exist, the site is an ambiguity error. 5. If none exist, the site is a normal type error. -The bundled `core` package declares the implicit constructors from compiler concept types to the corresponding fundamental types. Literal coercion therefore follows this same algorithm rather than a separate compiler-only lowering rule. +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. From d09442589e6ae430be28e954f61de4f3e672c330 Mon Sep 17 00:00:00 2001 From: Manuel Stieger <149385373+TheLazyCat00@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:26:43 +0200 Subject: [PATCH 5/5] docs: clarify the path from Void to Unit --- stories/types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stories/types.md b/stories/types.md index f19bb13..573dc57 100644 --- a/stories/types.md +++ b/stories/types.md @@ -106,7 +106,7 @@ The honest note is what the name gives up next to its siblings. "Product" and "s ## Unit exposes the package that wasn't one -The question arrived through a generic field. If `Player` carries optional extra settings, `Player` is the clean application when there are no settings to carry — but only if the language has a real type with a real value, rather than a return-position marker the type system pretends is a type until someone tries to store it. Making that use legal means `Unit()` exists, occupies no storage, and can sit in a symbol, field, array, or type argument exactly as any other zero-sized value can. Calling that type `Void` no longer fit once it had a value: a void has none, while a unit type has exactly one. The rename says what the model became and leaves `Never` as the natural future name for a type with no values. +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.