flan/test/programs/arena-region.flan
Joseph Ferano d6fc15474b The count is length, so len is a name a program can have
The author: "I think I prefer length over len, because then I'll use len as
the variable name". One arm in check.ml, one row in the table beside it, and
every (len x) in lib, test, examples, vendor, spike, docs, web, emacs,
plan.org and NEXT.md rewritten.

Shadowing and builtin/ had already taken most of the sting out: a (defn len
...) was legal and won in its own file, and builtin/len reached past it. What
was left is that len was still a builtin — the defn earned a warning, and a
wrapper had to say builtin/ at every inner call. Now there is nothing under
the short name: len is an ordinary identifier in every position, which is
what (let [len (length xs)] ...) wants.

length takes over as shadowing's worked example rather than the feature
losing one. shadow-builtin.flan, builtin-qualified.flan, pkgs/shadowed and the
builtin/ rows in test_flan move to it and go on testing shadowing.

A call to a len nothing defines is answered where an unknown function is,
after every table and after the shadowing guard, so a program with its own len
never reaches it. The sentence is said rather than guessed at — len and length
are three edits apart and the did-you-mean's net is one — and the call is
written back out through spell_arg, as-slice's spelling lifted out of it and
now shared, so what is printed compiles.

sand.flan:33 still calls the old name and is the author's to change; until it
does, test_acceptance and test_session abort there. Both were run green
against a copy with that one line changed. FIX.org says so.
2026-09-21 11:58:56 +07:00

110 lines
5.0 KiB
Plaintext

;;;; spec-memory.md's arena rule, which is the run-time half of what replaced
;;;; the three type-level refusals a container of owning elements used to meet.
;;;;
;;;; "Constructing a container whose element type owns storage, against an
;;;; allocator that lacks can-free, is refused at the point of construction:
;;;; one branch per container, not per element." One branch and not a walk,
;;;; because the alternative is something that inspects the graph at release,
;;;; and that is the registry of destructors the frame tier's reset exists to
;;;; not have.
;;;;
;;;; It is a run-time branch and not a compile-time refusal because there is
;;;; nothing static to refuse against: with-allocator rebinds a dynamic
;;;; variable, so which tier a (vec-new) meets is not knowable where it is
;;;; written. The compiler decides only whether to *ask*.
;;;;
;;;; Argument 0 is everything that must work, argument 1 and argument 2 are the
;;;; two ways this dies. Each death is the whole test of its case, so they are
;;;; separate runs rather than one program that could pass by dying early.
(defonce frame Allocator)
(defalias Row (Vec i32))
(defdata Value [Nil (Int [n i64]) (List [items (Vec Value)])])
(defn main [args [string]] i32
(set frame (arena-new 4096))
(let [which (if (> (length args) 1) (i32 (bytes->i64 (bytes-view (at args 1)))) 0)]
(cond
(= which 1)
;; The refusal. The context here is the heap, which can free one
;; block, and a (Vec Value) against it is a free that would release
;; the slots and strand every inner Vec — so the construction dies
;; rather than the free three hundred lines later.
(let [bad (vec-new Value)]
(println (length bad)))
(= which 2)
;; Use after free-all, which is a different mechanism and worth
;; pinning separately: the allocator's epoch moves on every free-all
;; and every container records the epoch it was made at. The header
;; below was copied *out* of the arena container into a local before
;; the release, which is the case the check has to cover and the
;; reason spec-memory.md makes an Allocator a pointer rather than a
;; copied value — a copied allocator would carry its own epoch and
;; the copy would never notice.
(let [outer (vec-new Value frame)]
(let [inner (vec-new Value frame)]
(push inner (Value.Int {.n (i64 7)}))
(push outer (Value.List {.items inner})))
(match (at outer 0)
(List items)
(do (println (length items))
(free-all frame)
(println (length items)))
_ (println 0)))
(= which 3)
;; ZII, which is the hole a guard only at the construction would have
;; left. The items field is omitted from the literal, so it is a zeroed
;; Vec with no allocator at all — it never went near (vec-new) — and
;; the first push is what adopts the context. So the branch is emitted
;; at every growth too, and there it asks the container, which answers
;; from the allocator it will adopt when it has none of its own.
(let [v (Value.List {})]
(match v
(List items)
(do (push items (Value.Int {.n (i64 1)}))
(println (length items)))
_ (println 0)))
:else
(do
;; The control, and it is the case the frame tier exists for: a
;; (Vec (Vec i32)) owns storage at two levels and is perfectly happy
;; in a region, because free-all releases every block the region
;; handed out and the inner ones are among them. The rule asks about
;; the *allocator*, never "does this element own anything", so this
;; must be built without complaint.
(with-allocator frame
(let [rows (vec-new Row)]
(let [row (vec-new i32)]
(push row 1)
(push row 2)
(push rows row))
(println (length rows))
(println (length (at rows 0)))))
(free-all frame)
;; And the same container against the heap dies — asserted from the
;; other side in run 1 above; here the point is only that the region
;; run above got no complaint.
(with-allocator frame
(let [vs (vec-new Value)]
(push vs (Value.Int {.n (i64 41)}))
(println (length vs))))
(free-all frame)
;; And the zeroed field of run 3, this time in the region: the growth
;; guard has to pass here as surely as it has to fail there, or every
;; ZII container in an arena would be unusable.
(with-allocator frame
(let [v (Value.List {})]
(match v
(List items)
(do (push items (Value.Int {.n (i64 1)}))
(println (length items)))
_ (println 0))))
(free-all frame))))
(arena-destroy frame)
0)