From 242f8c2047322385f06d9c398d20b7aed080a0b4 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 22:03:34 +0700 Subject: [PATCH] Three section headers still said there was no allocator The refusal list was rewritten and the prologues that pointed at it were not, so prelude.ml claimed in three places that what it now contains is impossible: the splitting header said `split` is refused at the foot of the file, forty lines above `split`; the ASCII-case header said Odin's allocating to_lower is not available here, next to the one that was written; and the UTF-8 header said the rest of core/strings is refused rather than ported. Each keeps its point rather than losing it. The iterator is still the shape that owns nothing and still the right call when there is no result to own; lower-ascii and bytes-ci=? are still the right calls when a copy is not wanted, since folding a comparison over two inputs beats lowering both. What changed is the reason, which used to be the absence of an allocator and is now a choice between two shapes that both exist. And strings.flan told the reader the opposite of what it did -- "not freed", on the line above the free. vec.flan already had the right framing: the free is written, it keeps the block because an arena cannot release one, and that is the difference the capability set exists to state. --- lib/prelude.ml | 29 ++++++++++++++++++----------- test/programs/strings.flan | 10 ++++++---- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/lib/prelude.ml b/lib/prelude.ml index 760586f..d2b50bf 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -607,8 +607,11 @@ let source = {flan| ;; Ported from Odin's core/unicode/utf8/utf8.odin, which is the one corner of ;; a string library that is allocation-free by construction: decoding is ;; classification, and every answer it gives is a number. Everything else in -;; Odin's core/strings and all of core/fmt takes `allocator := -;; context.allocator`, and is therefore refused below rather than ported. +;; Odin's core/strings takes `allocator := context.allocator`, which is why +;; this corner came first and the rest waited; most of that rest is ported now +;; and lives in the building section below. core/fmt is still absent, and the +;; reason it stays absent is not allocation — see the refusal list at the foot +;; of this file. ;; ;; Odin's 256-entry accept_sizes table becomes a cond over the lead byte here. ;; The table is the cache-friendly form and the cond is the one you can check @@ -791,12 +794,13 @@ let source = {flan| ;; ── Splitting ───────────────────────────────────────────────────────── ;; -;; `split` returning a sequence of fields must allocate the sequence, and -;; there is no allocator — so it is refused by name at the bottom of this -;; file, and this is the shape that survives. It is Odin's -;; split_by_byte_iterator (strings.odin): a cursor holding the rest of the -;; input, handing back one field at a time. Every field is a slice *of the -;; caller's bytes*; nothing is copied and nothing is owned. +;; The iterator, which owns nothing. `split` returning a sequence of fields has +;; to allocate that sequence, and it does — it is in the building section below +;; — but this stays the right call whenever you do not want to own the result: +;; it is Odin's split_by_byte_iterator (strings.odin), a cursor holding the +;; rest of the input and handing back one field at a time. Every field is a +;; slice *of the caller's bytes*; nothing is copied, nothing is owned, and +;; there is no free to remember. `split` is built on exactly this. ;; ;; One divergence, and it is a wart of Odin's rather than a decision. Odin's ;; iterator stops on an empty final field, so "a,b," iterates a and b and the @@ -828,9 +832,12 @@ let source = {flan| ;; ── ASCII case ──────────────────────────────────────────────────────── ;; ;; Byte in, byte out, and *not* a function over a slice. Odin's to_lower and -;; to_upper both allocate a new string (core/strings/conversion.odin), which -;; is not available here; the obvious substitute — lowering a [u8] in place — -;; is a trap, and it is worth saying why rather than shipping it. A string +;; to_upper both allocate a new string (core/strings/conversion.odin) and so do +;; the ones in the building section below; these are the forms that allocate +;; nothing, and they stay the right call when a copy is not wanted — folding a +;; comparison over two inputs beats lowering both and comparing. What is *not* +;; on offer is the third shape, lowering a [u8] in place, and it is worth +;; saying why rather than shipping it. A string ;; literal is emitted `private unnamed_addr constant` (emit.ml), so (bytes ;; "Hello") is a [u8] pointing straight into read-only memory. An in-place ;; lower-ascii! type checks against that slice, and what happens next depends diff --git a/test/programs/strings.flan b/test/programs/strings.flan index d1e441a..17d8929 100644 --- a/test/programs/strings.flan +++ b/test/programs/strings.flan @@ -137,15 +137,17 @@ ;; The allocator is the context's, so with-allocator moves the whole tier ;; into an arena -- which is the answer to the fixed arity of a defn, and the - ;; reason none of these takes an allocator argument. free-all releases every - ;; one of them at once, including the Vec still bound below it. + ;; reason none of these takes an allocator argument. free-all is what + ;; releases the region, and arena-destroy hands it back. (let [a (arena-new 4096)] (with-allocator a (let [parts [(bytes "in") (bytes "arena")]] (let [j (join (slice parts 0 2) (bytes "-"))] (show (addr j)) ; in-arena - ;; Not freed: an arena cannot release one block, and free-all is - ;; what releases this. + ;; The free is written because the binding is dead after it either + ;; way, and it keeps the block: an arena cannot release one, which + ;; is the difference the capability set exists to state. free-all + ;; below is what actually releases this. (free j)))) (free-all a) (arena-destroy a))