A type variable moves unless it says otherwise, and the clause is a map

This commit is contained in:
Joseph Ferano 2026-09-13 14:14:55 +07:00
parent b6bb1e502b
commit 4072dd2089

24
NEXT.md
View File

@ -27,6 +27,13 @@ clause tells the abstract pass what it may assume, so the body checks at the def
- A type variable binds as `$t` in a signature and is used bare. Already decided; see the sigil entry.
- A generic body is checked **abstractly**, once, with nothing substituted.
- The clause is written as a **Clojure-style map at the head of the body**, `{:where (ordered? $t)}`, chosen by
the author over a bare keyword. It disambiguates because a bare `{}` in expression position is already refused
(`parse.ml:110`), so a `{}` there can be nothing else, and Clojure's `{:pre [...] :post [...]}` is the
precedent. It leaves room for further keys without new syntax. **One catch to settle first:** `{K V}` is
currently a legal return type, so `(defn f [...] {string i32} {:where ...} body)` puts two braces in a row
meaning different things. That resolves itself if `{K V}` goes in favour of `(Map K V)`, which is a separate
open question in this file.
- A `where` clause over **compile-time type predicates** admits the operators the body needs. Four are wanted —
`ordered?`, `equal?`, `hashable?`, `numeric?` — against Odin's forty-one. The prelude's nine non-collapsing
functions need only the first two.
@ -60,6 +67,23 @@ The prelude keeps a per-type layer for the numeric ones. That is the honest numb
### Still open, and not blocking
0. ~~**Ownership.**~~ **Decided by the author.** A type variable is **move-only by default**, and a
`copyable?` predicate in the `where` clause is what tells the checker otherwise. Conservative in the safe
direction: move rules are the stricter ones, so assuming them can only refuse programs, never admit a bad one.
It costs nothing at run time — the predicate changes what the checker permits and emits no code.
**Odin is no help here and this is worth knowing before someone goes looking.** Odin has no move semantics and
no ownership tracking at all: `Raw_Dynamic_Array` (`base/runtime/core.odin:523`) is a plain struct of pointer,
length, capacity and allocator, freely copyable, and an alias is the programmer's problem to notice. Nothing in
its compiler tracks it, so `$T` never has to answer this. **The prior art is Rust**, where a parameter defaults
to move and `T: Copy` is the opt-in. One difference to keep: Rust's `Copy` is a trait a user may implement,
while `copyable?` is a predicate the compiler answers, because it already knows which types own heap storage.
Same ergonomics, none of the trait machinery, consistent with the `where` decision above.
What this means in the body: a generic may not use a parameter twice unless it declares `copyable?`.
`(defn twice [x $t] $t (+ x x))` is refused without it — correct at `i32`, wrong at `(Vec i32)`, and the
checker cannot tell which until it substitutes.
1. **The runaway refusal.** `(defn grow [x $t] () (grow [x x]))` asks for a copy at `[2 t]`, then `[2 [2 t]]`,
forever. Before the spike's cap it did not fail, it **hung** — and `Session.eval` runs the same code, so what
hangs is `C-c C-c` with the daemon wedged behind it and nothing to show. The cap is a depth counter refusing