From 4072dd20891dfd71fa230bd33e7b7f4dc22ee3d6 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 13 Sep 2026 14:14:55 +0700 Subject: [PATCH] A type variable moves unless it says otherwise, and the clause is a map --- NEXT.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/NEXT.md b/NEXT.md index faca132..2b09ff3 100644 --- a/NEXT.md +++ b/NEXT.md @@ -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