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.
103 lines
3.6 KiB
Plaintext
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)
|