# 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. ## 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]`. - A slice is invalidated by any operation that may reallocate the owner (`push`, `put`, `reserve`). This is **not checked** in the first implementation; dev builds carry a generation word on `Vec` and trap on use of a stale slice. - 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 and is checked by the same analysis; until that analysis exists, `addr` of a local may not be stored or returned. `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. ## 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 (get m k) v) ; map entry (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 `=`, `<`, `+`, `hash`, or `print`. 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). 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 function pointer.** No captured environment, no allocation, C calling convention plus the implicit allocator argument. This is what raylib callbacks, hot-reload indirection 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 — still open.** 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". Not settled; see plan.org open decisions. Escape analysis (open decision #4) is the same analysis that classifies cases 2 and 3, so they are decided together. **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. No core operation allocates implicitly.