flan/test/programs/strings.flan
Joseph Ferano 9ce51ba94e One slice over everything with elements, and the warning at the push
as-slice was a warning, not an operation. The input type already decides
which of the two things happens — a Vec can only be borrowed, an array or a
string can only be viewed, and no call site picks between them — so the second
name expressed no choice a reader could make. And it warned at the moment the
view is taken, which is the one moment nothing is wrong; the danger arrives
later, at the push. slice now takes a Vec at all three arities and as-slice
is gone.

(slice v lo) was free, and is the arity the Vec never had: the runtime already
reads a hi of -1 as "to the end", so the tail form passes the caller's lo and
the same -1 — no slot, no length read, no second evaluation. The merge is
entirely in the checker; the Vec path builds the flan_vec_as_slice call it
always built and neither backend has a line about any of it.

A Vec a call returned is refused at every arity, and not for the array's
reason. (slice (mk)) over an array dangles. (slice (make-vec)) does not — the
storage outlives the expression — but the header is a temporary, so nothing
can ever free the block. The refusal says that and names the let.

The name's own refusal sits in ordinary_call after every table, so a program
that defines an as-slice still reaches its own. It reads for somebody who has
never heard of the old name and writes the call back out, spelling each
argument that is a name or a number.

The warning moved to where it bites: BUILT.md gains a section beside the Vec
table and the push row points at it, spec-memory.md's Borrowing says the same.
Investigated and deliberately not built — a diagnostic for a live view at the
push. (reserve v 100) then a slice, a push and a read is correct code under
the contract the spec chose, so any flag on it is a false positive by the
language's own semantics rather than by an approximation. FIX.org has the
finding and the syntactic sketch that does not work.
2026-09-21 09:51:35 +07:00

155 lines
6.7 KiB
Plaintext

;;;; The prelude's second tier: the functions that return new storage.
;;;;
;;;; Every one of these was refused by name in prelude.ml until there was an
;;;; allocator to return a Vec from, and this file is the corpus that says the
;;;; refusals are lifted. The cases are chosen the way the slice-algorithm
;;;; tests were: each is an input a plausible wrong version gets wrong.
;;;;
;;;; Everything allocated here is freed, even though leaking is defined
;;;; behaviour (spec-memory.md), because this file is the example people copy.
;;; A (Vec u8) printed as text, without the caller writing the two-step every
;;; time. slice borrows -- it copies ptr+len and never the elements -- so v
;;; is still the owner afterwards and is still free-able.
(defn show [v (Ptr (Vec u8))] ()
(println (string (slice (deref v)))))
(defn main [] i32
;; The builder. Three appends and two numbers into one Vec, which is the
;; case the shared static scratch buffer in the runtime makes impossible for
;; i64->bytes on its own: two of its results cannot be held at once, and
;; these two numbers are both in the answer.
(let [b (vec-new u8)]
(append (addr b) (bytes-view "x="))
(append-i64 (addr b) 42)
(append (addr b) (bytes-view " y="))
(append-i64 (addr b) -7)
(append (addr b) (bytes-view " r="))
(append-f64 (addr b) 1.5)
(show (addr b)) ; x=42 y=-7 r=1.5
(free b))
;; concat over three parts, and over none -- the empty result rather than a
;; trap.
(let [parts [(bytes-view "one") (bytes-view "") (bytes-view "two")]]
(let [c (concat (slice parts 0 3))]
(show (addr c)) ; onetwo
(free c)))
(let [parts [(bytes-view "unused")]]
(let [c (concat (slice parts 0 0))]
(println (len c)) ; 0
(free c)))
;; join: n parts, n-1 separators. The one-part case is the one that must not
;; emit a separator at all, and the zero-part case is the one a "append then
;; chop the tail" join gets wrong because there is no tail.
(let [parts [(bytes-view "a") (bytes-view "b") (bytes-view "c")]]
(let [j (join (slice parts 0 3) (bytes-view ", "))]
(show (addr j)) ; a, b, c
(free j))
(let [j (join (slice parts 0 1) (bytes-view ", "))]
(show (addr j)) ; a
(free j))
(let [j (join (slice parts 0 0) (bytes-view ", "))]
(println (len j)) ; 0
(free j))
;; An empty separator is concat.
(let [j (join (slice parts 0 3) (bytes-view ""))]
(show (addr j)) ; abc
(free j)))
;; repeat, including zero times.
(let [r (repeat-bytes (bytes-view "ab") 3)]
(show (addr r)) ; ababab
(free r))
(let [r (repeat-bytes (bytes-view "ab") 0)]
(println (len r)) ; 0
(free r))
;; The allocating case pair. The input is a string literal, which lives in
;; .rodata -- an in-place lower would either segfault at -O0 or be deleted at
;; -O2, and that is exactly why these exist. Digits and punctuation pass
;; through untouched, which is the range check a table-free version gets
;; wrong by shifting every byte.
(let [l (to-lower (bytes-view "Hello, World 42!"))]
(show (addr l)) ; hello, world 42!
(free l))
(let [u (to-upper (bytes-view "Hello, World 42!"))]
(show (addr u)) ; HELLO, WORLD 42!
(free u))
;; replace. "aaa" with "aa" -> "b" is the non-overlapping rule: the answer is
;; "ba", because the match consumes both a's and the scan resumes after them.
(let [r (replace-bytes (bytes-view "aaa") (bytes-view "aa") (bytes-view "b"))]
(show (addr r)) ; ba
(free r))
;; A replacement longer than what it replaces, and one that is empty.
(let [r (replace-bytes (bytes-view "a,b,c") (bytes-view ",") (bytes-view " -- "))]
(show (addr r)) ; a -- b -- c
(free r))
(let [r (replace-bytes (bytes-view "a,b,c") (bytes-view ",") (bytes-view ""))]
(show (addr r)) ; abc
(free r))
;; No occurrence is a copy, and an empty `from` is a copy -- the reading
;; where it matches everywhere is an infinite loop.
(let [r (replace-bytes (bytes-view "abc") (bytes-view "z") (bytes-view "!"))]
(show (addr r)) ; abc
(free r))
(let [r (replace-bytes (bytes-view "abc") (bytes-view "") (bytes-view "!"))]
(show (addr r)) ; abc
(free r))
;; split. n separators, n+1 fields, always -- so the trailing empty field is
;; present, which is where Odin's own iterator and its allocating split
;; disagree with each other.
(let [f (split (bytes-view "a,b,c") \,)]
(println (len f)) ; 3
(println (string (at f 0))) ; a
(println (string (at f 2))) ; c
(free f))
(let [f (split (bytes-view "a,b,") \,)]
(println (len f)) ; 3
(println (len (at f 2))) ; 0
(free f))
(let [f (split (bytes-view ",a") \,)]
(println (len f)) ; 2
(println (len (at f 0))) ; 0
(free f))
;; No separator at all is one field, and the empty input is one empty field.
(let [f (split (bytes-view "abc") \,)]
(println (len f)) ; 1
(println (string (at f 0))) ; abc
(free f))
(let [f (split (bytes-view "") \,)]
(println (len f)) ; 1
(println (len (at f 0))) ; 0
(free f))
;; The fields are slices of the input and nothing was copied: this one
;; round-trips through join, and the separator it rebuilds with is a
;; different one, so an implementation that handed back the original slice
;; would print the original string.
(let [f (split (bytes-view "a,b,c") \,)]
(let [j (join (slice f) (bytes-view "/"))]
(show (addr j)) ; a/b/c
(free j))
(free f))
;; 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 is what
;; releases the region, and arena-destroy hands it back.
(let [a (arena-new 4096)]
(with-allocator a
(let [parts [(bytes-view "in") (bytes-view "arena")]]
(let [j (join (slice parts 0 2) (bytes-view "-"))]
(show (addr j)) ; in-arena
;; 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))
0)