# Spec 1 — Ownership, containers, and copies Status: **frozen**. Closes plan.org open decisions #6 and #10, and resolves the contradiction between "value structs copy on assignment" and owning containers. Everything else in the design references this vocabulary. ## The four container types | Notation | Layout | Assignment | Owns storage | Allocator | |-----------|-------------------|------------|--------------|-----------| | `[n T]` | n contiguous `T` | copies | no (inline) | — | | `[T]` | ptr + len | copies the *view* | no | — | | `(Vec T)` | ptr + len + cap | **moves** | yes | stored | | `(Map K V)` | open-addressed, flat key/value arrays | **moves** | yes | stored | - `[n T]` is a value. It lives wherever it is declared, copies on assignment and on pass-by-value, and is what `defconst colors [4 u32] ...` and `(defvar grid [rows [cols u32]] ...)` are. - `[T]` is a **non-owning slice**: a borrowed window into a `[n T]`, a `(Vec T)`, or a literal in read-only memory. Copying a slice copies ptr+len, never the elements. A slice may be `const`-qualified; freeing through one is not possible because a slice has no allocator and no `cap`. - `(Vec T)` and `(Map K V)` are **move-only**. Binding, passing, or returning one transfers ownership; the source binding is dead afterwards and using it is a compile error. There is no shallow copy, so there is no double free. ## Maps — first implementation Every map is homogeneous: `(Map K V)` has one key type and one value type. The first implementation accepts only built-in structural key types: integers, enums, strings, fixed arrays, and value structs composed recursively from those types. Tuples and triples join that set when they are introduced. `Ptr`, slices, `Vec`, and `Map` are not map keys yet. Equality and hashing for those keys are compiler-provided structural operations, not type classes and not operations available to an unconstrained type variable. An empty map takes its type from its context: ``` (defvar enemies (Map string Enemy) (map-new)) ``` `(get m k)` returns `(Option V)`: absence is `None`, not an untyped `nil`. `(put m k v)` is the upsert operation and returns `Unit`; it either inserts or replaces. `(set (get m k) v)` is not map syntax. The first Map implementation admits copyable keys and values only, so `get` returns a copy. Move-aware lookup, removal, and owned entries are deferred until `Vec`/`Map` values are supported in maps; the map itself remains an owning, move-only container. ## Copying is always explicit `(clone x)` produces an independent deep copy of a `Vec`/`Map` using the current allocator; `(clone x alloc)` names one. Value types (`[n T]`, structs of value types, primitives) need no `clone` — assignment already copies them. A struct containing a `Vec` field is itself move-only. Ownership is structural, not declared: a type is a value type iff all of its fields are. ## Borrowing - `(as-slice v)` / `(as-slice v lo hi)` view a `Vec` or fixed array as `[T]`. - **The first implementation follows Zig/Odin's explicit model, not Rust's borrow checker.** A slice is invalidated by any operation that may reallocate its owner (`push`, `put`, `reserve`); its user is responsible for respecting that contract. Dev builds carry a generation word on `Vec` and trap on use of a stale slice. `Ptr` is the explicit lower-level escape hatch and has the same lifetime contract. - A future lightweight provenance pass may reject the obvious mistakes (a borrow of a local escaping, use after an owner moves, and reallocation with a live borrow). It must not require Rust-style lifetime annotations or dictate an ECS-shaped object model. Long-lived graph links use `(Handle a)`; temporary graphs may use explicitly managed, stable region storage. - Cross-referencing long-lived objects uses `(Handle a)` into a pool, never a raw pointer or slice. A stale handle is detectable. ## Taking an address `(addr x)` yields `(Ptr T)` for any assignable place `x` — a local, a global, a field, an element. The pointer is non-owning and does not extend anything's lifetime, so `addr` of a local is only valid while that frame lives. This is the same escape question as case 3 below. The first implementation leaves it as an explicit lifetime contract; a future provenance pass can check it. `addr` is how a value struct is shared mutably without an allocator — recursive descent over a cursor, an entity passed down a call chain — and it is why milestone 2 needs no heap at all. In the first implementation its non-escape rule is an explicit programmer contract, aided by dev checks; the future provenance pass above may enforce it. ## Places — what `set` accepts A fixed set of assignable forms, not a `setf`-style extensible place mechanism: ``` (set x v) ; a local or a defvar (set (.field x) v) ; struct field; x may be a struct, (Ptr S) or (Handle S) (set (at a i ...) v) ; fixed array, slice, or Vec element (set (deref p) v) ; whole-object store through a pointer ``` `.field` and `at` auto-deref exactly one pointer or handle level, which is what makes `(set (.hp e) ...)` legal when `e : (Ptr Enemy)` and illegal when `e : Enemy` bound by value. **Mutating something you matched.** Pattern bindings bind *values*, so a matched struct is a copy. To mutate in place, obtain a pointer first — the pointer is visible in the type: ``` (match (resolve w h) ; (Option (Ptr Enemy)) (Some e) (set (.hp e) ...) ; e : (Ptr Enemy), field access derefs None ...) ``` `deref` yields a value; `resolve` yields a pointer. Both are overloaded on `(Ptr a)` and `(Handle a)` and resolve at compile time. ## Generics Parametric polymorphism is monomorphisation, with **no type classes and no constraints**. The consequence is a hard rule: > A type variable `a` supports only what every type supports: move, `clone`, > field-free storage. It does **not** support `=`, `<`, `+`, or `hash`. Anything else is passed in explicitly as a function value: ``` (defn largest [xs [a] gt (Fn [a a] bool)] (Option a) ...) ``` Ordered/arithmetic operators over `a` are therefore rejected, not silently instantiated. The alternatives — compile-time interfaces, or intrinsics restricted to primitives — are deliberately deferred until the base checker is stable (build sequence milestone 4). `println` is the deliberate exception. It is a compiler-provided, type-directed intrinsic: monomorphisation selects or emits a structural printer for each concrete instantiation, so `(println x)` is legal for `x : a` without introducing a `Printable` type class. Structs, fixed arrays, options and, eventually, Vecs and Maps print structurally. `Ptr` and `Handle` print their address or identity rather than recursively dereferencing, and collection printers impose depth and length limits. `any` and `Error` use their runtime type metadata. User generic code still passes an explicit function for every other operation that depends on a type's structure. Type arguments are **inferred at call sites** from the argument types; there is no explicit instantiation syntax in the first implementation. A type variable that appears only in the return type is therefore an error. ## Function values Three cases, split by whether the value escapes the frame that made it. **1. `(Fn [T1 T2] R)` — a plain, stable function pointer.** No captured environment or allocation. In a dev build, a reference to a top-level `defn` is the address of a stable trampoline that loads that function version's indirection cell and calls its current body; it is never the address of a particular body. Thus stored callbacks and ordinary calls observe a later *body* redefinition, as in Common Lisp. Release builds may call the body directly because it cannot be redefined. A signature-changing redefinition makes a new internal function version and a new trampoline ABI. Newly compiled code resolves the source name to that new version. Existing callers and stored `Fn` values keep their old trampoline and therefore safely call the old version. The session immediately warns at every tracked caller source location that still targets the old signature; recompiling one either retargets it successfully or reports an ordinary type error. This is what raylib callbacks, hot-reload cells, and function parameters use. A top-level `defn` is one, so `(largest hps >)` passes `>` at `i32` directly. This is the only function type that may cross an FFI boundary or sit in a reload cell. **2. Non-escaping `fn` — captures by value into a stack environment.** A `fn` whose value provably does not outlive the frame that created it gets an environment allocated in that frame and captures the named locals **by value** at the point of creation. No heap, no allocator, no lifetime question. This covers essentially every lambda in practice: - callbacks to `reduce` / `filter` / `each` / `map`, which consume them and return - comparators passed to a function that does not store them - `handler-bind` handler bodies That last one is not a convenience. A handler must be able to see the enclosing locals — `(fn [c] (push errors c) (invoke-restart 'skip-form))` capturing a local `(Vec ParseError)` *is* the accumulation pattern, and conditions are not worth building without it. Handlers are strictly non-escaping: the `handler-bind` frame outlives every call to them. Captured `Vec`/`Map` are captured **by pointer**, not moved, since the capture does not outlive the owner. A non-escaping `fn` is therefore not itself an owner. **3. Escaping closures — deferred.** A `fn` stored in a struct, pushed into a container, or returned needs a heap environment and an answer to "which allocator owns it, and what happens when the frame arena resets". Do not settle this until a concrete use case requires it; revisit it with the optional lightweight provenance work. **Early exit inside a `fn`.** `try`, `some`, and `return` in a `fn` body exit the `fn`, not the enclosing function — a `fn` is a function. Code that wants to propagate out of a loop uses an imperative loop form, not a callback. ## Allocators The allocator is part of the calling convention (`context/allocator`, `context/temp`). `Vec` and `Map` record the allocator they were created with, so `free` and `clone` never need it named again. Allocation uses the current implicit allocator by default, as in Odin; an operation never falls back to a hidden global allocator, and an explicit allocator can override the context.