flan/test/programs/vec.flan
Joseph Ferano af8d291154 (Vec T) over a type-erased runtime, with StorageExhausted going in beside it
Two element types, one runtime, and the element type appears nowhere below
the call site: size_of and align_of are produced where the concrete type is
known, which without generics is simply the concrete call site. That is
Odin's arrangement and it is what spec-memory.md specifies. `at` and `len`
were already the names for a fixed array and a slice, so a Vec extends them
rather than adding a parallel pair — the asymmetry `nth` was removed for —
and the value form and the place form go through one helper so they cannot
drift apart.

StorageExhausted lands with step 2 rather than after it, because the
signatures depend on it: `push` and `reserve` are Unit, `clone` is the
container, and nothing grows a Result. It is built out of nodes that already
existed — a while, a restart-case and an error — so the backend learned
nothing about allocation. The restart is established at the failing
allocation, which spec-memory.md names as the exception to "restarts go at
the resync point, once", and the element a push was given is bound to a slot
before the loop so a retry re-attempts the allocation and not the expression.

Move-only is a dead set on the checker context, and it is flow-sensitive at
an `if`: both arms start from the same set and the union survives the join,
so `(if c (free v) (free v))` is legal and a one-armed free still kills the
binding. The case a dead set cannot answer is a move inside a loop — merged
once at the end of the body it counts one move, not two — so that is a rule,
refused with its reason.

Four decisions the spec did not settle:

The Vec header is six words in every build, not four in release. A layout
that changes with a build flag can disagree across the reload boundary
silently: a redefinition module is built by llc and ld against a host built
separately, and nothing makes the two agree on a struct size. The 32-byte
release layout is deferred on that.

A zeroed Vec has a null allocator, and the first operation needing storage
adopts the context allocator. Odin's behaviour. The alternative was refusing a
Vec-typed struct field until drop lands; shipping the null was a null deref on
the first push.

A Vec's length and index are i32, like every other length here. Widening
indices is one change across all the containers, not a Vec question.

`let` has no type annotation, so a local Vec has nowhere to say what it holds
and the element type is written at the call: `(vec-new i32)`. This is not the
explicit instantiation syntax the generics section rules out — nothing here is
generic and the name resolves as an ordinary type. Where the context says, it
may be left out.

The allocator grew a budget: a ceiling on live bytes, 0 for none. The retry
restart is only answerable by a handler that can make the *same* request
succeed, and for a fixed backing store the handler that works is the one that
raises the ceiling — releasing the region a container lives in invalidates
the container, which is what the epoch check catches. The spec's "grows the
arena and then invokes retry" needed something to grow.

The generation word is bumped on every reallocation and read by nothing. The
stale-slice trap it is for needs a slice that can carry the Vec's identity,
and a slice is ptr+len. Said plainly rather than implied by the word's
presence.
2026-09-12 11:07:57 +07:00

103 lines
3.6 KiB
Plaintext

;;;; (Vec T) — spec-memory.md, "The four container types" and "Allocators".
;;;;
;;;; ptr + len + cap + allocator, owning and move-only, over one type-erased
;;;; runtime. The element type appears nowhere in that runtime: size_of and
;;;; align_of are produced at the call site, which without generics is simply
;;;; the concrete call site. So this file being two element types with one
;;;; runtime behind them is the whole claim.
(defstruct Point [x i32 y i32])
;;; Ownership transfers on the call. The caller's binding is dead after this,
;;; which is what the refusal cases in test_acceptance assert.
(defn consume [v (Vec i32)] i32
(let [n (len v)]
(free v)
n))
;;; A Vec is returned by moving it out, so the callee's binding is the
;;; caller's. Nothing is released at function exit — there is no scope-end
;;; anything in this language.
(defn make [n i32] (Vec i32)
(let [v (vec-new i32)]
(dotimes [i n] (push v (* i i)))
v))
(defn sum [xs [i32]] i32
(let [total 0]
(dotimes [i (len xs)] (set total (+ total (at xs i))))
total))
(defn main [] i32
(let [v (vec-new i32)]
(println (len v)) ; 0
(push v 10)
(push v 20)
(push v 30)
(println (len v)) ; 3
(println (at v 0)) ; 10
(println (at v 2)) ; 30
;; A Vec element is a place, and the same bounds and epoch check stands
;; behind the value form and the place form.
(set (at v 1) 99)
(println (at v 1)) ; 99
;; as-slice is a non-owning view: it copies ptr+len and never the
;; elements, and it carries no allocator, so nothing can be freed through
;; one. [at] and [len] over it are the array operations, unchanged.
(println (sum (as-slice v))) ; 139
(println (len (as-slice v 1 3))) ; 2
(println (at (as-slice v 1 3) 0)) ; 99
;; clone is the only copy: assignment moves. The copy is independent, and
;; freeing it leaves the original alone.
(let [w (clone v)]
(set (at w 0) -1)
(println (at w 0)) ; -1
(println (at v 0)) ; 10
(free w))
;; reserve does not change the length, only the capacity, so a reserve
;; that succeeds is invisible except that the pushes after it do not grow.
(reserve v 64)
(println (len v)) ; 3
(push v 40)
(println (len v)) ; 4
(free v))
;; A second element type over the same runtime, and a struct element, so
;; that size_of and align_of are doing work rather than both being 4.
(let [ps (vec-new Point)]
(push ps (Point {:x 1 :y 2}))
(push ps (Point {:x 3 :y 4}))
(println (len ps)) ; 2
(println (.y (at ps 1))) ; 4
(free ps))
;; A Vec made against an explicit allocator records it, so free and clone
;; never need it named again. An arena cannot free one block, so this free
;; keeps the block — releasing it is free-all's job, and that is the
;; difference the capability set exists to state.
(let [a (arena-new 4096)]
(let [v (vec-new i32 a)]
(push v 7)
(println (at v 0)) ; 7
(free v))
(println (can-free? a)) ; false
(free-all a)
(arena-destroy a))
;; The pushes go into whatever the context names, with nothing passed.
(let [a (arena-new 4096)]
(with-allocator a
(let [v (vec-new i32)]
(push v 5)
(push v 6)
(println (+ (at v 0) (at v 1))) ; 11
(free v)))
(arena-destroy a))
(println (consume (make 5))) ; 5
0)