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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, n>` primitive |
Expand All @@ -62,17 +62,17 @@ The spec states *what* the language does; the **why** lives in a parallel set of
|---|---|
| [`stories/foundations.md`](stories/foundations.md) | [`spec/foundations.md`](spec/foundations.md) — the bets behind captured intent, staged compilation, casing-determines-kind, strictness-as-performance, and the collapse of value/reference into one `#` axis |
| [`stories/lexical.md`](stories/lexical.md) | [`spec/lexical.md`](spec/lexical.md) — casing as a hard-error kind signal and the `<>` disambiguation it buys, why field privacy lives in the name and reaches across packages, and a delimiter for each kind of separated thing |
| [`stories/types.md`](stories/types.md) | [`spec/types.md`](spec/types.md) — the two-axis value/reference model, why `#` marks a distinct type rather than a box, constructing a type by what it is, confining `#` to the body forms, why every type must be named, dropping the `tuple` mould once a named `struct` does the job better, adding named (dart-style) constructors on the syntax variants introduced, and naming the `enum`'s shape the peer mould |
| [`stories/types.md`](stories/types.md) | [`spec/types.md`](spec/types.md) — the two-axis value/reference model, why `#` marks a distinct type rather than a box, constructing a type by what it is, confining `#` to the body forms, why every user-defined type must be named, dropping the `tuple` mould once a named `struct` does the job better, adding named constructors, naming the `enum`'s shape the peer mould, and making `Unit` a real fundamental unit type |
| [`stories/adt.md`](stories/adt.md) | [`spec/adt.md`](spec/adt.md) — splitting `enum` from `variant` against the hype, the shared struct body, escaping the matcher machine with case overloads and the turn to a central `match` block, matching variants rather than patterns, keeping enum data outside the members, reducing a match group to sugar for one arm per case, and building a variant by naming a case rather than calling a constructor |
| [`stories/generics.md`](stories/generics.md) | [`spec/generics.md`](spec/generics.md) — the parameter model, the `<>`/`()` split, size-in-the-type, and the deferred features |
| [`stories/dependencies.md`](stories/dependencies.md) | [`spec/dependencies.md`](spec/dependencies.md) — URL identity, the manifest/resolution split, prebuilt distribution, symbol-rewriting, the browsable global cache, the package-graph acyclicity rule, and the opt-in remapping model |
| [`stories/dependencies.md`](stories/dependencies.md) | [`spec/dependencies.md`](spec/dependencies.md) — URL identity, the manifest/resolution split, prebuilt distribution, symbol-rewriting, the browsable global cache, the package-graph acyclicity rule, opt-in remapping, and why `core` became a bundled implementation package |
| [`stories/memory.md`](stories/memory.md) | [`spec/memory.md`](spec/memory.md) — the no-GC-no-lifetimes goal, the move problem and the anchor, lazy backpointer creation, the indexed heap table, the rooted-guest rules and the host/guest terminology split, the collapse to one value/reference axis with a borrowed receiver, and the shift to segmented chunked bump arenas |
| [`stories/lifetimes.md`](stories/lifetimes.md) | [`spec/lifetimes.md`](spec/lifetimes.md) — lexical scope in place of a borrow checker, what may be moved, the declaration-block rule that kills flow analysis, downgrade instead of use-after-move, parameter-rooted returned guests, and why each strict rule is the minimal guard against one specific memory corruption |
| [`stories/effects.md`](stories/effects.md) | [`spec/effects.md`](spec/effects.md) — inferring effects instead of annotating them, receiver-scoped `mut`, capabilities in place of ambient I/O, the four-level ladder and the Total-Pure/Pure split, what deliberately is not an effect, and mutation through a borrowed receiver |
| [`stories/concurrency.md`](stories/concurrency.md) | [`spec/concurrency.md`](spec/concurrency.md) — the parallelism/concurrency split and the refusal of `async` coloring, why `spawn` marks only a call, water-tower lifetimes, signature-based safety without locks, and value-typed mutation closing the aliased-write gap |
| [`stories/error-handling.md`](stories/error-handling.md) | [`spec/error-handling.md`](spec/error-handling.md) — the two-doors model and why failure is control flow rather than a `Result` value, `resolve` as expression-substitution rather than assignment, typed abort paths and the deliberately-absent propagate operator, and keeping abortability orthogonal to effects |
| [`stories/control-flow.md`](stories/control-flow.md) | [`spec/control-flow.md`](spec/control-flow.md) — `guard` as an active exit that opens no scope of its own, doing without `while` behind a written loop bound, and one-based counting after the loop that forced the question |
| [`stories/functions.md`](stories/functions.md) | [`spec/functions.md`](spec/functions.md) — pulling methods out of the type body and the verb model that revealed, mutation made visible with `:`/`!`, overloading on parameter shape alone, and why callables are call-only while self-typed lambdas are values |
| [`stories/error-handling.md`](stories/error-handling.md) | [`spec/error-handling.md`](spec/error-handling.md) — the two-doors model and why failure is control flow rather than a `Result` value, `resolve` as expression-substitution rather than assignment, typed abort paths and the deliberately-absent propagate operator, keeping abortability orthogonal to effects, and explicit path values through `Unit` |
| [`stories/control-flow.md`](stories/control-flow.md) | [`spec/control-flow.md`](spec/control-flow.md) — `guard` as an active exit that opens no scope of its own, doing without `while` behind a written loop bound, one-based counting after the loop that forced the question, and why control-flow contracts use fundamental semantic types |
| [`stories/functions.md`](stories/functions.md) | [`spec/functions.md`](spec/functions.md) — pulling methods out of the type body and the verb model that revealed, mutation made visible with `:`/`!`, overloading on parameter shape alone, why callables are call-only while self-typed lambdas are values, and why every return carries an explicit value |
| [`stories/operators.md`](stories/operators.md) | [`spec/operators.md`](spec/operators.md) — the fixed vocabulary worth overloading, `~` as the universal flip, laws enforced through derived operators, grammar-only grouping, and home-package coherence |
| [`stories/packages.md`](stories/packages.md) | [`spec/packages.md`](spec/packages.md) — the directory as namespace and compilation unit, declarations as move checks, explicit qualified access through `$`, and keeping mutable state inside values so the effect model can see it |

Expand Down
11 changes: 8 additions & 3 deletions spec/control-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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 {
Expand All @@ -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.

Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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`
Expand Down
8 changes: 4 additions & 4 deletions spec/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,13 +268,13 @@ At a high level, dependency resolution proceeds in this order:

## 14. Toolchain Version

The `zane-version` field in `zane.coda` pins the toolchain tag used to build the project. It selects the compiler alone; the reserved `zane` key in `zane-versions.coda` records the commit that tag must resolve to.
The `zane-version` field in `zane.coda` pins the toolchain tag used to build the project. It selects the compiler and its bundled `core` implementation; the reserved `zane` key in `zane-versions.coda` records the commit that tag must resolve to.

- The compiler is released under its own tag, so a project always builds with a known compiler. This frees the toolchain to evolve without preserving backward compatibility across versions: each project states the compiler version it builds with.
- The standard library is **not** special, and no part of it is coupled to the toolchain tag. `core`, `std`, and every other library are ordinary packages, each fetched, versioned, pinned, and remapped like any other dependency, with its own `deps` row in `zane.coda` and entry in `zane-versions.coda`. `core` is installed like any package; a project that never adds it cannot name the core surface types.
- The compiler and `core` implementation are released under one toolchain tag, so a project always builds with matching definitions of the fundamental language types. This frees the toolchain to evolve without preserving backward compatibility across versions: each project states the toolchain version it builds with.
- The standard library is **not** special, and no ordinary library is coupled to the toolchain tag. `std` and every other source library are ordinary packages, each fetched, versioned, pinned, and remapped like any other dependency, with its own `deps` row in `zane.coda` and entry in `zane-versions.coda`. The bundled `core` implementation is compiler infrastructure rather than a manifest dependency; see [`types.md`](types.md) §2.6.
- The reserved `zane` key is subject to the same tag/commit verification as every other entry (§4): a moved toolchain tag is detected, not silently trusted.

> **Story:** [`stories/dependencies.md`](../stories/dependencies.md#cutting-core-loose-from-the-toolchain) — "Cutting core loose from the toolchain" explains why `core` is an ordinary package rather than a toolchain-coupled one.
> **Story:** [`stories/dependencies.md`](../stories/dependencies.md#the-package-that-was-the-language) — "The package that was the language".

---

Expand Down
31 changes: 23 additions & 8 deletions spec/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ ReturnType?AbortType

A declaration with no `?AbortType` cannot abort.

### 2.2 `Void` abort type
If failure carries no payload, the abort type is `Void`. In that case `abort` is written without an argument.
### 2.2 `Unit` abort type

If failure carries no meaningful payload, the abort type is `Unit`. The abort path still carries a value explicitly:

```zane
abort Unit()
```

### 2.3 Abortability is orthogonal to `mut`
Abortability and mutation are independent. A method may be:
Expand Down Expand Up @@ -66,10 +71,10 @@ value Int = parse("42") ? err {
}
```

When the abort type is `Void`, the binder is omitted:
The binder is required even when the abort type is `Unit`:

```zane
done Bool = tryFinish() ? {
done Bool = tryFinish() ? ignored {
resolve false
}
```
Expand Down Expand Up @@ -102,8 +107,17 @@ Falling through a handler block is a compile-time error.
count Int = parse("abc") ?? Int(0)
```

### 3.4 `Void` primary returns are not assignable
Calls whose primary return type is `Void` may not be assigned to variables. When such calls are abortable, the handler still attaches to the call expression itself.
### 3.4 `Unit` primary returns are values

A call whose primary return type is `Unit` produces the `Unit` value returned by the callee. The result may be assigned or passed like any other value.

```zane
completed Unit = performWork()
```

When such a call is abortable, the handler attaches to the call expression exactly as for any other primary return type.

> **Story:** [`stories/error-handling.md`](../stories/error-handling.md#the-empty-door-still-carries-a-value) — "The empty door still carries a value".

### 3.5 `match` is abort-transparent
A `match` expression passes the output of its selected arm straight up. If the arms are abortable, the whole `match` is abortable and takes a `?` (or `??`) handler exactly like any other abortable expression. Abortability is not introduced or swallowed by `match`; it simply flows through.
Expand Down Expand Up @@ -134,7 +148,7 @@ result Int = match token {
| `return` | parent function | leave via the primary return path |
| `abort` | parent function | leave via the abort path |

When the primary return type is `Void`, `resolve` takes no value.
When the primary return type is `Unit`, the handler writes `resolve Unit()`. `resolve` always carries an explicit value.

> **Story:** [`stories/error-handling.md`](../stories/error-handling.md#handling-a-fork-resolve-and-why-it-isnt-assignment) — "Handling a fork: `resolve`, and why it isn't assignment".

Expand Down Expand Up @@ -276,7 +290,7 @@ Zig also keeps failure explicit and avoids stack unwinding, but the surface mode
|---|---|---|
| Signature order | `Error!Value` | `Value?Abort` |
| Recovery syntax | `catch` with labeled-block patterns | `?` with `resolve`/`return`/`abort` |
| Payload-free failure | inferred error sets and union mechanics | explicit `Void` abort type |
| Payload-free failure | inferred error sets and union mechanics | explicit `Unit` abort type |
| Integration with effects | no corresponding `mut`-based effect layer | abortability and effects are analyzed separately; both must be satisfied at call sites |

---
Expand All @@ -290,4 +304,5 @@ Zig also keeps failure explicit and avoids stack unwinding, but the surface mode
| Handler paths | Must end with `resolve`, `return`, or `abort` |
| `??` | Shorthand for resolve-with-default |
| `resolve` | Exits only the handler block |
| `Unit` path value | `Unit()` is a real primary or abort-path value and is written explicitly with `return`, `resolve`, or `abort` |
| Abort-free function | Statically guaranteed not to abort |
Loading