From 9ce51ba94eb345b0bd4d627ba80513f0a82bb271 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Mon, 21 Sep 2026 09:41:33 +0700 Subject: [PATCH] One slice over everything with elements, and the warning at the push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- FIX.org | 112 +++++++++++++++++++ NEXT.md | 8 +- docs/BUILT.md | 30 +++-- docs/SPIKE-DYNAMIC.md | 2 +- docs/overview.md | 2 +- lib/check.ml | 166 +++++++++++++++++++++------- lib/prelude.ml | 14 +-- lib/render.ml | 4 +- lib/shim.ml | 2 +- runtime/flan_rt.c | 4 +- spec-memory.md | 16 ++- syntax-sketch.flan | 2 +- test/programs/algorithms.flan | 10 +- test/programs/bounds-condition.flan | 6 +- test/programs/edn-read.flan | 4 +- test/programs/format.flan | 8 +- test/programs/generics.flan | 4 +- test/programs/global-init.flan | 2 +- test/programs/init-conditions.flan | 2 +- test/programs/into.flan | 12 +- test/programs/json.flan | 2 +- test/programs/shim-nul.flan | 2 +- test/programs/slurp.flan | 10 +- test/programs/string-eq.flan | 2 +- test/programs/strings.flan | 6 +- test/programs/vec-global.flan | 2 +- test/programs/vec.flan | 18 ++- test/test_acceptance.ml | 4 +- test/test_flan.ml | 49 +++++++- test/test_valgrind.ml | 6 +- vendor/edn/provide.flan | 16 +-- vendor/edn/read.flan | 4 +- vendor/json/json.flan | 4 +- vendor/json/provide.flan | 16 +-- web/index.html | 2 +- 35 files changed, 408 insertions(+), 145 deletions(-) diff --git a/FIX.org b/FIX.org index ded3757..efd0249 100644 --- a/FIX.org +++ b/FIX.org @@ -4734,3 +4734,115 @@ So it is gone, with the call respelled, at the merge: sand.flan:121 says ~:log-warning~, the ~warning 4~ member and its paragraph are out of raylib.flan, and the ~constant~ line is out of bindings. Every member of every mapped enum now carries its prefix, with no exception. + +* One slice, and the warning moved to the push — 2026-09-21 +The ruling, in the author's words: + +#+begin_quote +merge them into one slice. as-slice goes away. +#+end_quote + +** Why a second name was the wrong shape +[as-slice] existed as a warning. check.ml said so beside it: a view of a Vec +is a borrow from storage a push, a put or a reserve may reallocate out from +under you — the explicit Zig/Odin contract spec-memory.md chose instead of a +borrow checker — and a different word at the call site was how a reader was +meant to be told. + +It does not work, for two reasons and the second is the one that decides it. + +The input type already determines the semantics completely. A Vec can only be +borrowed; a fixed array, a slice or a string can only be viewed. There is no +call site anywhere at which a reader would want to pick between the two +behaviours for one input, so the second name expressed no choice. It was a +label, not an operation. + +And it was a label in the wrong place. It warns at the moment the view is +taken, which is the one moment nothing is wrong: the view is correct when it +is made. The danger arrives later, at the push. A warning stapled to the safe +end of the story is a warning nobody reads at the unsafe end. + +** What landed +One [slice], over a fixed array, a slice, a string and now a Vec, at all three +arities. The Vec's half is [vec_slice] in check.ml; everything the previous +lane built — the backwards-literal refusal, the static out-of-range refusal +where a length is known, the runtime trap, single evaluation of a non-trivial +target, the zero-cost implicit length on a fixed array — is untouched, and a +Vec reaches none of the static ones because it has no static length to reach +them with. + +The merge is entirely in the checker. Neither backend has an arity case, a +type case, or a line about this: the Vec path builds the same +[flan_vec_as_slice] call [as-slice] built, and the C symbol keeps its name. + +*(slice v lo) was free.* The runtime already reads a [hi] of -1 as "to the +end", which is what the one-argument form passes, so the tail form passes the +caller's [lo] and the same -1. No slot, no length read, no second evaluation of +the target, nothing computed that was not computed before. The Vec had no +two-argument spelling only because the name it had was not the name that grew +the arities. + +*A Vec a call returned is refused, at every arity*, and for a different reason +than the array. [(slice (mk))] over an array dangles: the view outlives a +temporary the frame reuses. [(slice (make-vec))] does not dangle — the storage +a returned Vec owns lives until its allocator's free-all or destroy. What is +lost is the *owner*: the header is a temporary, so nothing can ever free that +block, and the program has written a leak it cannot spell the fix for. The +refusal says that rather than borrowing the array's sentence, and names the +[let] that keeps the owner. + +The refusal for the name itself is in [ordinary_call], after every table, so a +program that defines an [as-slice] of its own still reaches its own. It reads +for somebody who has never heard of the old name — "there is no as-slice" — +and writes the call back out with the arguments the reader wrote, spelling any +argument that is a name or a number and standing in for one that is not, so +the suggestion is always a form that compiles. + +** The warning, moved +docs/BUILT.md gains a section next to the Vec surface table, and the [push] +row points at it: a view of a Vec is invalidated by [push], [put] or [reserve], +nothing checks it, and the rule is to take the view again afterwards. +spec-memory.md's "Borrowing" says the same in its own register and drops the +old two-spelling line. + +** Investigated and NOT built: a live view at the push +The brief asked whether a [push] with a live view of the same Vec in scope is +detectable cheaply, and said to build it only if the obvious case is catchable +with no false positives. It is not, and the reason is not analysis cost. + +The decisive case is one line of *correct* code: + +#+begin_src flan + (reserve v 100) + (let [s (slice v)] + (push v 1) + (println (at s 0))) +#+end_src + +The reserve is exactly how a program says "this push will not reallocate", and +under the contract the spec chose that promise is the program's to make. Any +flag on this — error or warning — is a false positive by the language's own +semantics, not by an approximation the check settled for. The bar the brief +set therefore cannot be met by a cheaper check, because the obstacle is not +precision. + +The syntactic sketch is worth writing down so nobody re-derives it. To avoid +flagging the common and harmless [(let [s (slice v)] (println (len s)) +(push v 1))] — where the view is dead by the push — the check must find a use +of the view *after* the push, which is liveness. Textual order is not +execution order across an [if] or a loop; a [set] of the view's binding or a +shadowing of either name breaks it; and narrowing it to one straight-line +statement list to make the order real shrinks it to almost nothing. Meanwhile +static flow tracking was deliberately repealed on 2026-09-18, and this is a +borrow checker's question wearing a smaller hat. Two reasons to stop, and the +first one is sufficient on its own. + +What was built instead is the sentence, in the two places a reader meets the +operation that breaks the view. + +** Swept +Every spelling in the repo: lib/ (check.ml, prelude.ml, render.ml, shim.ml), +runtime/flan_rt.c comments, test/ (test_flan.ml, test_acceptance.ml, +test_valgrind.ml and fifteen programs), vendor/edn and vendor/json, web, +docs/BUILT.md, docs/overview.md, docs/SPIKE-DYNAMIC.md, spec-memory.md, +NEXT.md and syntax-sketch.flan. sand.flan never used it. diff --git a/NEXT.md b/NEXT.md index aeded27..9dadea0 100644 --- a/NEXT.md +++ b/NEXT.md @@ -483,7 +483,7 @@ sign-extended to i64 walks straight through them. It is behind `f.md.checks` lik and -O2 alike and off only when checks as a whole were asked off. **It owns nothing, and needed no analysis to say so.** The result is a `Types.Slice`, which is not move-only and -carries no allocator, so `free` refuses it by the rule it already had — the same rule that refuses `(as-slice v)`. +carries no allocator, so `free` refuses it by the rule it already had — the same rule that refuses `(slice v)`. **Where the promise is written.** `rl/font-recs` and `rl/font-glyphs` in `vendor/raylib/raylib.flan`, because that is the one place raylib's invariant (both arrays hold `glyph-count` entries) is knowable. `examples/text-rectangle-bounds.flan` @@ -1706,7 +1706,7 @@ What landed: `append`/`append-i64`/`append-f64` (the builder), `concat`, `join`, Tests: `programs/strings.flan`, `programs/format.flan`, `programs/algorithms.flan`, `programs/math2.flan`. `string-from-bytes`, refused in that block, turned out to already exist: `string` is a builtin and -`(string (as-slice v))` is the round trip. +`(string (slice v))` is the round trip. **What could not be built, and why each one could not.** All four want a compiler or runtime change, and none of them wants a language decision. @@ -1764,7 +1764,7 @@ to avoid rather than inherit. Four, with what to do instead. **1. One argument-order rule, held everywhere.** Clojure's sequence functions take the collection *last* (`(map f coll)`) and its collection functions take it *first* (`(assoc m k v)`). The split is deliberate there, and it is why Clojure needs **two** threading macros instead of one. **Our rule: the thing being operated on comes first.** -That is already what the language does — `(at a i)`, `(len xs)`, `(push v x)`, `(as-slice v)` — and `into` follows it +That is already what the language does — `(at a i)`, `(len xs)`, `(push v x)`, `(slice v)` — and `into` follows it with the source first. Hold it; do not ship two of anything to paper over a split. **2. A membership test says which thing it tests.** Clojure's `contains?` checks *keys*, so `(contains? [1 2 3] 1)` is @@ -2465,7 +2465,7 @@ expander last, on 6's unions. rather than a literal calling-convention parameter; both are stated as amendments in `docs/BUILT.md`. A user-written allocator is refused by name with milestone 5 as the reason. -~~2. **`(Vec T)`**~~ **Done**, over the type-erased runtime, with `push`, `reserve`, `at`, `len`, `as-slice`, `free` and +~~2. **`(Vec T)`**~~ **Done**, over the type-erased runtime, with `push`, `reserve`, `at`, `len`, `slice`, `free` and `clone`, and with move-only enforced by a dead set that unions at an `if` or a `match` join. `at` and `len` were extended rather than duplicated. The header is six words in *every* build, not four in release — a layout that changes with a build flag can disagree silently across the reload boundary — and that is the third amendment. diff --git a/docs/BUILT.md b/docs/BUILT.md index 38dc87c..283d7c5 100644 --- a/docs/BUILT.md +++ b/docs/BUILT.md @@ -2117,7 +2117,7 @@ hand-off dropping a number, which is what the note left behind said it was. **A `(Vec T)` shows as `` and a `(Ptr T)` as ``**, because that is what `render.ml` already does for them everywhere else: following a pointer a REPL was handed is not a safe thing to do on someone's behalf, and walking a -`Vec` structurally is a walk over storage the frame does not own — `(print (as-slice v))` is how that is asked for, +`Vec` structurally is a walk over storage the frame does not own — `(print (slice v))` is how that is asked for, and it says at the call site that it borrowed. Each slot is rendered **from its address** rather than copied into the thunk first. A copy would be one `alloca` the @@ -2424,14 +2424,30 @@ fires. | `context/allocator` / `context/temp` | the current implicit allocator, and the per-frame arena | | `(with-allocator a body...)` | rebinds for a dynamic extent and releases nothing | | `(vec-new T)` / `(vec-new T a)` / `(vec-new)` | a Vec, against the context or a named allocator | -| `(push v x)` / `(reserve v n)` | `()`, both | +| `(push v x)` / `(reserve v n)` | `()`, both. **Either may reallocate, and any `[T]` view of this `Vec` is stale afterwards** — see below | | `(at v i)` / `(len v)` | the array names, extended — not a parallel pair | -| `(as-slice v)` / `(as-slice v lo hi)` | a non-owning `[T]` view | +| `(slice v)` / `(slice v lo)` / `(slice v lo hi)` | a non-owning `[T]` view — the array names again, extended | | `(clone v)` / `(clone v a)` | the only copy; assignment moves | | `(free v)` | consumes its argument | | `(bytes s)` / `(bytes s a)` | a writable copy of a string's bytes, against the context or a named allocator — an allocating operation like `vec-new`: StorageExhausted with retry, a registry note in dev builds. The answer is a `[u8]` view of the block, so nothing can `free` it through the slice; it lives until its allocator's `free-all` or destroy | | `(bytes-view s)` | the string's own storage as a `[u8]`, costing nothing — the old `(bytes s)` reinterpret, renamed. Read-only by convention: a literal's view points into `.rodata` and a store through it traps | +### A view of a `Vec` goes stale at the `push`, and nothing checks it + +`(slice v)` copies a pointer and a length out of the `Vec` and keeps neither the `Vec` nor its allocator. `push`, +`put` and `reserve` are the three operations that may hand the block back and take a bigger one, and when one of them +does, every view taken before it holds the old address and the old length. Reading through such a view reads freed +memory; writing through it writes to memory the program no longer owns. **The compiler does not check this and will +not tell you.** That is `spec-memory.md`'s explicit Zig/Odin contract, chosen instead of a borrow checker, and it is +one rule: take the view again after the `push`. + +It is stated here, beside `push`, rather than at `slice`, because the `slice` is not the moment anything goes wrong — +the `push` is. There used to be a second name, `as-slice`, for taking a view of a `Vec`, on the theory that a +different word at that call site was the warning. It was not one. The value handed to `slice` already decides what +the result means — a `Vec` can only be borrowed, a fixed array or a string can only be viewed, and no call site ever +chooses between the two — so the second name expressed no choice a reader could make. One name, `slice`, over +everything that has elements; the warning is where it bites. + ### Three amendments to a frozen spec, and one addition **1. `free-all` is retain-capacity, and `arena-destroy` is the operation that hands pages back.** The spec's table has @@ -2512,7 +2528,7 @@ what was built and why. Move-only as a *type* property (what may be copied, what Reading a move-only local is a move unless the site said it was a borrow. That is the conservative direction: passing one to a function, binding it, returning it and `free`ing it are all moves and all reach one place, and the handful of -operations that only look at a container (`at`, `len`, `as-slice`, `push`, `reserve`, `clone`) say so. Only a +operations that only look at a container (`at`, `len`, `slice`, `push`, `reserve`, `clone`) say so. Only a *syntactically simple* target counts as a borrow — in `(len (f v))` the call still moves `v`. At an `if` and at a `match`, every arm is checked from the state before the form and the **union** of what they moved @@ -2539,7 +2555,7 @@ three shapes are refused where they are declared, each naming `drop`: - **a `Vec` of a `Vec`**, because the type-erased runtime copies and releases elements bytewise: `clone` would duplicate inner headers instead of copying what they own and `free` would drop their buffers. -And a `Vec` does not cross to C: handing a header that owns storage to C hands out an owner. `(as-slice v)` as +And a `Vec` does not cross to C: handing a header that owns storage to C hands out an owner. `(slice v)` as `(Ptr T)` plus `(len v)` is the shape that does cross, and the refusal says so. ### `StorageExhausted` went in *with* `Vec`, not after it @@ -3006,7 +3022,7 @@ a slice is invalidated by a `push`. The handle survives that and the pointer doe explicit Zig/Odin borrowing contract one level down, and it is worth naming rather than implying, because it reintroduces the silent-wrong-answer mode the handle just removed for anyone who keeps a resolved pointer across an insert. Chunked never-moving storage is the fix and it costs code; taking the contract is the smaller correct thing, -given `as-slice` already established it. +given `slice` already established it. ### `len` is the slot high-water and `live` is the count, in that direction @@ -3675,7 +3691,7 @@ be wrong in a way worth being able to see: the prelude *was* reaching the expand The list at the foot of `prelude.ml` used to be one sentence — every entry needed to produce bytes that did not exist in its input, and there was no allocator. `join`, `concat`, `split`, `to-lower`, `to-upper`, `repeat` and `replace` have moved up into the code; `string-from-bytes` turned out to be the `string` builtin all along, and -`(string (as-slice v))` is the round trip, free precisely because the layouts are identical. +`(string (slice v))` is the round trip, free precisely because the layouts are identical. What remains is refused for four different reasons, and is now written that way: `pad`/`center` for *nothing at all* except that no caller has asked; `format`/`sprintf` for variadics of mixed type; `map`/`filter`/`reduce`/`sort-by` diff --git a/docs/SPIKE-DYNAMIC.md b/docs/SPIKE-DYNAMIC.md index 015c7ac..8b3a771 100644 --- a/docs/SPIKE-DYNAMIC.md +++ b/docs/SPIKE-DYNAMIC.md @@ -335,7 +335,7 @@ this one too. **A typed `Vec` prints as `` and a dyn vec does not.** That looks like a mismatch and is not. The typed printer's refusal is about walking storage it does not own — `render.ml` says so, and directs you to -`(print (as-slice v))`, which borrows explicitly at the call site. A dyn vec's storage belongs to the +`(print (slice v))`, which borrows explicitly at the call site. A dyn vec's storage belongs to the collector, the printer is inside the runtime that owns it, and there is nobody to ask permission from. So a dyn vec prints the way a *slice* does, which is also the only rendering that carries any information. diff --git a/docs/overview.md b/docs/overview.md index e462c0d..9ab67ba 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -16,7 +16,7 @@ accurate**. Read instead: | "C-like with Roc syntax" | S-expressions, Clojure's brackets; C's value model | | "Start with interpreter, output C later" | Frontend → typed IR → LLVM IR as text → `clang`. An interpreter is acceptable for milestone 2 only | | "lists, slice, fixed-length array, matrices" | Four container types, distinct ownership: `[n T]`, `[T]`, `(Vec T)`, `(Map K V)` — see spec-memory.md | -| `arr[4]`, `arr[1..]` index syntax | `(at a i)`, `(as-slice a lo hi)` — no infix, no bracket indexing | +| `arr[4]`, `arr[1..]` index syntax | `(at a i)`, `(slice a lo hi)` — no infix, no bracket indexing | | `const by default?` | Locals are assignable places; parameters are not; `const` qualifies slices and pointers | | Rust-style iterator chains ending in `.collect()` | `->>` threading over slices; every collecting operation allocates from an explicit allocator, usually the frame arena | | `Ptr a` | Kept, as `(Ptr a)`. Cross-references use `(Handle a)` instead | diff --git a/lib/check.ml b/lib/check.ml index 1dd2449..fb5acd0 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -6005,6 +6005,74 @@ and vec_at ctx loc (target : Tast.expr) (idx : Ast.expr list) = "a Vec takes exactly one index — (at v i) — and its element is indexed \ separately" +(* [(slice v)], [(slice v lo)] and [(slice v lo hi)] over a Vec — the arm for + it is in [slice], and this is the half that differs from an array's. + + The result is a non-owning view: copying it copies ptr+len and never the + elements, and it carries no allocator, so freeing through one is not + expressible. What makes a Vec's view different from an array's is that the + storage it names can move — a [push], a [put] or a [reserve] may reallocate + and leave the view addressing the old block. Nothing checks that, which is + the explicit Zig/Odin contract spec-memory.md chose over a borrow checker, + and it is written down where a reader meets it: beside [push] in BUILT.md + and in spec-memory.md's "Borrowing". + + It used to be spelled [as-slice], on the theory that a second name warns + about that. It does not: the input type already decides which of the two + things happens, so there was no choice at the call site for the name to + express — and the moment it warned about was the moment the view is taken, + while the danger arrives later, at the push. + + No slot and no length read at any arity: -1 is the runtime's "to the end", + so the short forms pass a constant where an array passes its length, and + the target appears exactly once in all three. *) +and vec_slice ctx ~want loc (target : Tast.expr) elem (bounds : Ast.expr list) = + (* The mirror of the array rule one screen down, for a different reason. An + array a call returned is storage the frame reuses, so the view dangles; + a Vec a call returned owns storage that outlives the expression, and what + is lost is the owner — the header is a temporary, so nothing can ever + [free] it and the block lives to the allocator's free-all or destroy. A + [let] keeps the owner, so that is what the refusal names. *) + (match target.Tast.e with + | Tast.Call _ | Tast.CallPtr _ -> + fail loc + "this slices a Vec a call returned, and the header a call answers is a \ + temporary — the view outlives it, and the storage it names can no \ + longer be freed, because there is no Vec left to free it through. \ + Bind it first: (let [v (…)] (slice v …))" + | _ -> ()); + let int k = mk loc index_ty (Tast.Int (k, Types.I32)) in + (* -1 is "to the end". A Vec's length is not static, so unlike an array + there is no constant to fold and the runtime reads the length word it is + carrying anyway. *) + let to_end () = int (-1L) in + let lo, hi = + match bounds with + | [] -> int 0L, to_end () + | [ lo ] -> index_expr ctx lo, to_end () + | [ lo; hi ] -> index_expr ctx lo, index_expr ctx hi + | _ -> assert false + in + (* The same refusal the other targets get. There is no static length to + check a single bound against, but a literal pair that runs backwards is + wrong without one — and [hi] here may be the -1 sentinel, which is not a + bound anybody wrote. *) + (match literal lo, literal hi with + | Some a, Some b when b >= 0L && a > b -> + fail loc "slice [%Ld %Ld) runs backwards — lo must not exceed hi" a b + | _ -> ()); + let out = fresh_slot ctx (Types.Slice elem) in + let fill = + rt loc Types.Unit "flan_vec_as_slice" + [ target; addr_of loc (mk loc (Types.Slice elem) (Tast.Local out)); + lo; hi; size_of loc elem; here loc ] + in + expect ctx loc ~want + (mk loc (Types.Slice elem) + (Tast.Let ([ (out, mk loc (Types.Slice elem) + (Tast.Zero (Types.Slice elem))) ], + [ fill; mk loc (Types.Slice elem) (Tast.Local out) ]))) + (* Every arm below is a name an editor can be asked about and no program ever wrote down, so each one needs a line in [builtins] further down this file. A new arm without an entry fails the build — test_flan reads both. *) @@ -6689,37 +6757,6 @@ and named_call ?(qualified = false) ctx ~want loc name args = (region_check ctx.env loc target (with_note loc (alloc_guard ctx loc attempt) note)) | _ -> assert false) - (* (as-slice v) and (as-slice v lo hi) — spec-memory.md, "Borrowing". The - result is a non-owning view: copying it copies ptr+len and never the - elements, and it carries no allocator, so freeing through one is not - expressible. A push, a put or a reserve may invalidate it; that is the - explicit Zig/Odin contract the spec chose over a borrow checker. *) - | "as-slice" -> - (match args with - | target :: rest when List.length rest = 0 || List.length rest = 2 -> - let target = check ctx target in - let elem = vec_elem loc "as-slice" target.Tast.ty in - let lo, hi = - match rest with - | [] -> - mk loc index_ty (Tast.Int (0L, Types.I32)), - (* -1 is "to the end": (as-slice v) has no static length to pass. *) - mk loc index_ty (Tast.Int (-1L, Types.I32)) - | [ lo; hi ] -> index_expr ctx lo, index_expr ctx hi - | _ -> assert false - in - let out = fresh_slot ctx (Types.Slice elem) in - let fill = - rt loc Types.Unit "flan_vec_as_slice" - [ target; addr_of loc (mk loc (Types.Slice elem) (Tast.Local out)); - lo; hi; size_of loc elem; here loc ] - in - expect ctx loc ~want - (mk loc (Types.Slice elem) - (Tast.Let ([ (out, mk loc (Types.Slice elem) - (Tast.Zero (Types.Slice elem))) ], - [ fill; mk loc (Types.Slice elem) (Tast.Local out) ]))) - | _ -> fail loc "as-slice is (as-slice v) or (as-slice v lo hi)") (* spec-memory.md's first release point. Since the repeal, what it consumes it consumes at run time only: nothing marks the binding dead, so a second [free] or a read after this one type-checks and misbehaves at run time — @@ -7476,7 +7513,13 @@ and named_call ?(qualified = false) ctx ~want loc name args = The target is read twice when [hi] is the implicit length, so a target that is not already a name goes into a slot first: [(slice (f x))] must call [f] once. An array target never needs the slot, because its length - is a constant and the target appears exactly once. *) + is a constant and the target appears exactly once. + + One name over every target that has elements, a Vec included. There used + to be a second, [as-slice], for the Vec alone; the input type already + decides which semantics apply — a Vec can only be borrowed, an array can + only be viewed, and no call site chooses — so the second name expressed + nothing and is gone. [vec_slice] above is the Vec's half. *) | "slice" -> (match args with | [] | _ :: _ :: _ :: _ :: _ -> @@ -7486,6 +7529,11 @@ and named_call ?(qualified = false) ctx ~want loc name args = | target :: bounds -> let target = check ctx target in let ty = target.Tast.ty in + match ty with + (* A Vec leaves here: everything below is written around a length the + compiler can see, and a Vec's is a word the runtime reads. *) + | Types.Vec elem -> vec_slice ctx ~want loc target elem bounds + | _ -> (* A string slices to a string, not to a [u8]: the result views the same bytes and is read-only for the same reason the source is, and calling it a byte slice would hand out a writable-looking view of @@ -7494,7 +7542,8 @@ and named_call ?(qualified = false) ctx ~want loc name args = | Types.Array (_, t) | Types.Slice t -> Types.Slice t | Types.String -> Types.String | other -> - fail loc "slice takes an array, a slice or a string, found %s" + fail loc + "slice takes an array, a slice, a string or a Vec, found %s" (Types.to_string other) in (* An array that came back from a call is a value in a temporary this @@ -7582,7 +7631,7 @@ and named_call ?(qualified = false) ctx ~want loc name args = from. **It owns nothing.** The result is a [Types.Slice], which carries no - allocator and is the same non-owning view (as-slice v) answers — so + allocator and is the same non-owning view (slice v) answers — so [free] refuses it by the rule it already had ("free takes an owning container"). *) | "slice-from-ptr" -> @@ -7690,7 +7739,7 @@ and named_call ?(qualified = false) ctx ~want loc name args = The lowering mirrors [vec-new]: a hidden (Vec u8) temp holds the block so the allocation registry can read its extent, the attempt sits under [alloc_guard] so a failure signals StorageExhausted with retry, and the - answer is the [as-slice] of the whole of it. The slice carries no + answer is the [slice] of the whole of it. The slice carries no allocator, so nothing can [free] this block through it — it lives until its allocator's free-all or destroy, which is the story every borrowed view already has and is written down in BUILT.md's surface table. @@ -8095,6 +8144,41 @@ and ordinary_call ctx ~want loc name args = else if String.contains name '/' then unimplemented loc (Printf.sprintf "the call %s into an imported package" name) 4 + else if name = "as-slice" then + (* A name nothing defines, near enough to [slice] to be worth a + sentence rather than a did-you-mean: whatever [slice] is given + decides what the view means, so there is one word for it and this + says which. Asked here, after every table, so that a program that + defines an [as-slice] of its own still reaches its own. *) + (* The call is written back out rather than described, and every + argument the reader wrote that can be spelled is spelled. One that + cannot — a call, a field, anything with structure — becomes the + name it stands for, so the suggestion is always a form that + compiles rather than a form with a hole in it. *) + let spell stand_for (a : Ast.expr) = + match a.Ast.e with + | Ast.Var v -> v + | Ast.Int n -> Int64.to_string n + | _ -> stand_for + in + let call = + match args with + | [] -> "(slice v)" + | t :: bounds -> + let names = [ "lo"; "hi" ] in + let bounds = + List.mapi + (fun i b -> + " " ^ spell (try List.nth names i with _ -> "n") b) + bounds + in + "(slice " ^ spell "v" t ^ String.concat "" bounds ^ ")" + in + Loc.failk "check/unknown-function" loc + "there is no as-slice. slice takes the view, and what it is given \ + says what the view is: over a Vec it borrows the storage the Vec \ + owns, over an array or a string it looks at the value itself. \ + Write %s" call else (* The did-you-mean comes first, and for a capitalised head it is asked of the *type* tables as well: [(Piont 1 2)] with [Point] declared is @@ -8388,7 +8472,7 @@ and generic_call ctx ~want loc name vars pats pret args = [dyn] is an ordinary case of [Types.t], so it substituted like any other type and a copy was generated at it. The copy then reached whatever the body did with the value, and the dyn answers are not all there — [(Option - dyn)] has no descriptor the collector can find, [as-slice] over a + dyn)] has no descriptor the collector can find, [slice] over a [(Vec dyn)] refuses. So the refusal existed, it just arrived from inside the generic's own source: [(or-else (Some d) e)] over two dyns is reported against [:385], a line the caller did not write and cannot act @@ -8939,10 +9023,6 @@ let builtins : (string * string * string) list = "Makes room for n more. For a map the number is entries rather than \ slots — the block is sized so that n still sits under the load \ factor."); - ("as-slice", "as-slice [(Vec T) i32? i32?] [T]", - "A non-owning view of the whole Vec, or of the half-open range \ - [lo hi). It carries no allocator, and a push, a put or a reserve may \ - invalidate it."); ("free", "free [(Vec T)|(Map K V)] ()", "Releases the container's block. It does not recurse into elements that \ own storage — such a container is refused here, and releasing its \ @@ -9029,11 +9109,13 @@ let builtins : (string * string * string) list = allocator's epoch checked first. On a string it is the byte, a u8. It \ is also a place, so (set (at v i) x) goes through the same check; a \ string is the exception, being a view it does not own."); - ("slice", "slice [[n T]|[T]|string i32? i32?] [T]|string", + ("slice", "slice [[n T]|[T]|string|(Vec T) i32? i32?] [T]|string", "The half-open range [lo hi) as a non-owning view. lo defaults to 0 and \ hi to the length, so (slice a) is the whole of it and (slice a n) is \ the tail from n. A bound may sit one past the end; a literal pair that \ - runs backwards is refused here. A string slices to a string."); + runs backwards is refused here. A string slices to a string. A view of \ + a Vec is a borrow from storage the Vec owns, and a push, a put or a \ + reserve on that Vec may invalidate it."); ("slice-from-ptr", "slice-from-ptr [(Ptr T) i32] [T]", "Puts a length on a pointer that came back from C. The caller promises \ it addresses that many initialised T and that they outlive the result; \ diff --git a/lib/prelude.ml b/lib/prelude.ml index 163fdd3..60968a4 100644 --- a/lib/prelude.ml +++ b/lib/prelude.ml @@ -231,7 +231,7 @@ let source = {flan| ;; to return a new sequence from — and it stays the right shape now that there ;; is one, because sorting a thing you already own should not allocate. The ;; allocating tier is further down, and a caller sorts a Vec by sorting -;; (as-slice v). +;; (slice v). ;; ;; **map, filter, reduce and a sort taking a comparator are here now**, in a ;; section of their own after the f32 family. They were blocked on *function @@ -1816,7 +1816,7 @@ let source = {flan| ;; stopped being true when `Vec` landed, and most of the list has moved up into ;; the building section above: join, concat, split, to-lower, to-upper, repeat ;; and replace are all written now, and `string-from-bytes` turned out to be -;; the `string` builtin all along — (string (as-slice v)) is the round trip, +;; the `string` builtin all along — (string (slice v)) is the round trip, ;; and the layouts being identical is exactly why it is free. ;; ;; What is left is refused for four *different* reasons, which is why they are @@ -2025,14 +2025,14 @@ let source = {flan| ;; change this, and it does not exist. (defn form-nil [] [Form] (let [v (vec-new Form)] - (as-slice v))) + (slice v))) (defn form-cons [x Form rest [Form]] [Form] (let [v (vec-new Form)] (push v x) (dotimes [i (len rest)] (push v (at rest i))) - (as-slice v))) + (slice v))) (defn form-append [a [Form] b [Form]] [Form] (let [v (vec-new Form)] @@ -2040,7 +2040,7 @@ let source = {flan| (push v (at a i))) (dotimes [i (len b)] (push v (at b i))) - (as-slice v))) + (slice v))) ;; The rest of a macro's arguments, which is what a variadic body is: a macro ;; takes one parameter, the slice of the forms at its call site. @@ -2050,7 +2050,7 @@ let source = {flan| (while (< i (len xs)) (push v (at xs i)) (set i (+ i 1))) - (as-slice v))) + (slice v))) ;; The elements of a vector form, which is what a [ ] pattern in a macro's ;; parameter list unwraps. The other arm is unreachable from a generated @@ -2083,7 +2083,7 @@ let source = {flan| (let [d (i64->bytes gensym-n)] (dotimes [i (len d)] (push v (at d i)))) - (Form.Sym {.s (string (as-slice v))}))) + (Form.Sym {.s (string (slice v))}))) ;; ── The first special form to stop being one ────────────────────────── ;; diff --git a/lib/render.ml b/lib/render.ml index ad83dd8..5f92f8b 100644 --- a/lib/render.ml +++ b/lib/render.ml @@ -208,8 +208,8 @@ let rec render c depth (e : Tast.expr) : Tast.expr list = would make an acceptance test's output depend on the heap. *) | Types.Alloc -> [ lit "" ] (* Printing a Vec structurally would be a walk over storage this function - does not own, and the walk is what [as-slice] is for: (print (as-slice - v)) prints the elements and says at the call site that it borrowed. *) + does not own, and the walk is what [slice] is for: (print (slice v)) + prints the elements and says at the call site that it borrowed. *) | Types.Vec _ -> [ lit "" ] | Types.Fn _ as ft -> [ lit ("<" ^ Types.to_string ft ^ ">") ] | Types.Option t -> diff --git a/lib/shim.ml b/lib/shim.ml index 4e65881..ccad00d 100644 --- a/lib/shim.ml +++ b/lib/shim.ml @@ -248,7 +248,7 @@ let rec cty env ~needed ~loc ~what (t : Ast.texpr) : string = | Ast.Tapp ("Vec", _) -> fail loc "%s is a Vec, which owns its storage — handing its header to C hands out \ - an owner. Pass (as-slice v) as (Ptr T) and (len v), the same shape a \ + an owner. Pass (slice v) as (Ptr T) and (len v), the same shape a \ slice crosses in" what | Ast.Tfn _ -> diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index 0e8a99d..9a3aed9 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -1802,7 +1802,7 @@ void *flan_vec_at(flan_vec *v, int32_t i, int64_t size, const uint8_t *loc, return (uint8_t *)v->ptr + (int64_t)i * size; } -/* [hi] of -1 means "to the end": (as-slice v) has no static length to write. */ +/* [hi] of -1 means "to the end": (slice v) has no static length to write. */ void flan_vec_as_slice(flan_vec *v, void *out, int32_t lo, int32_t hi, int64_t size, const uint8_t *loc, int64_t loclen, void *xfer) { @@ -2692,7 +2692,7 @@ int64_t flan_map_len(flan_map *m, const uint8_t *loc, int64_t loclen) { * backward shift moves entries to lower slots, and a cursor already past them * steps over entries it has not answered. The epoch check below catches a * released arena and nothing catches either of these, which is the same - * bargain [as-slice] already makes. + * bargain [slice] already makes. * * The layout is the one the geometry describes and is worth restating because * it is the thing most likely to be got wrong here: [data] is *one* allocation diff --git a/spec-memory.md b/spec-memory.md index d869b2b..9ced367 100644 --- a/spec-memory.md +++ b/spec-memory.md @@ -98,12 +98,18 @@ region" for the rule that stands in its place and for what it costs. ## Borrowing -- `(as-slice v)` / `(as-slice v lo hi)` view a `Vec` or fixed array as `[T]`. +- `(slice x)` / `(slice x lo)` / `(slice x lo hi)` view a `Vec`, a fixed array, + a slice or a string. One name, because the thing handed to it decides what + the view means: a `Vec` can only be borrowed, an array can only be viewed, + and no call site picks between the two. +- **A view of a `Vec` is invalidated by `push`, `put` or `reserve` on that + `Vec`, and nothing checks it.** Those are the operations that may reallocate, + and a view holds the old address and the old length. This is the whole of the + contract: the view goes stale at the `push`, not at the `slice`, and it is + the program's business to take the view again afterwards. - **The first implementation follows Zig/Odin's explicit model, not Rust's - borrow checker.** A slice is invalidated by any operation that may reallocate - its owner (`push`, `put`, `reserve`); its user is responsible for respecting - that contract. Dev builds carry a generation word on `Vec` and trap on use of - a stale slice. `Ptr` is the explicit lower-level escape hatch and has the + borrow checker.** Dev builds carry a generation word on `Vec` and trap on use + of a stale slice. `Ptr` is the explicit lower-level escape hatch and has the same lifetime contract. - A future lightweight provenance pass may reject the obvious mistakes (a borrow of a local escaping, use after an owner moves, and reallocation with a diff --git a/syntax-sketch.flan b/syntax-sketch.flan index 32462cb..bd1eacf 100644 --- a/syntax-sketch.flan +++ b/syntax-sketch.flan @@ -211,7 +211,7 @@ ;; individually. `each` borrows it as a slice. (defn draw-frame [w (Ptr World) dt f32] () (with-allocator context/temp - (->> (as-slice (.enemies w)) + (->> (slice (.enemies w)) (filter (fn [e] (on-screen? (.pos e)))) (each (fn [e] (rl/draw-texture (.spr e) (.pos e)))))) (free-all context/temp)) diff --git a/test/programs/algorithms.flan b/test/programs/algorithms.flan index 23abeb8..d86f288 100644 --- a/test/programs/algorithms.flan +++ b/test/programs/algorithms.flan @@ -106,15 +106,15 @@ ;; the bytes never do, so this sorts a borrowed view of a string literal -- ;; which an in-place byte sort could not, since a literal lives in .rodata. (let [f (split (bytes-view "pear,apple,Fig,apple,banana") \,)] - (sort-bytes (as-slice f)) - (show-fields (as-slice f)) ; Fig apple apple banana pear + (sort-bytes (slice f)) + (show-fields (slice f)) ; Fig apple apple banana pear (free f)) ;; And the round trip the whole second tier is for: split, sort, join. (let [f (split (bytes-view "delta,alpha,charlie,bravo") \,)] - (sort-bytes (as-slice f)) - (let [j (join (as-slice f) (bytes-view " < "))] - (println (string (as-slice j))) ; alpha < bravo < charlie < delta + (sort-bytes (slice f)) + (let [j (join (slice f) (bytes-view " < "))] + (println (string (slice j))) ; alpha < bravo < charlie < delta (free j)) (free f)) 0) diff --git a/test/programs/bounds-condition.flan b/test/programs/bounds-condition.flan index 99a6c1f..42cccff 100644 --- a/test/programs/bounds-condition.flan +++ b/test/programs/bounds-condition.flan @@ -40,7 +40,7 @@ ;;;; is why there is one condition type and not two. ;;;; 4. Every route to a bad index signals: reading a fixed array, writing ;;;; one (a different lowering — place/Pindex, not At), a slice, a Vec -;;;; element, and a Vec's as-slice. A Vec's check lives inside the +;;;; element, and a Vec's slice. A Vec's check lives inside the ;;;; runtime rather than in emitted IR, so those two are plumbed ;;;; separately and are the ones most likely to be left behind. @@ -166,11 +166,11 @@ (show "low" low) (show "length" length) (restart-case - (do (show "vec-slice" (i64 (len (as-slice v 0 2)))) + (do (show "vec-slice" (i64 (len (slice v 0 2)))) (set frames (+ frames 1))) (continue [] (set skipped (+ skipped 1)))) (restart-case - (do (show "vec-slice" (i64 (len (as-slice v 0 9)))) + (do (show "vec-slice" (i64 (len (slice v 0 9)))) (set frames (+ frames 1))) (continue [] (set skipped (+ skipped 1)))) (show "high" high)) diff --git a/test/programs/edn-read.flan b/test/programs/edn-read.flan index 590f555..0e3f9ab 100644 --- a/test/programs/edn-read.flan +++ b/test/programs/edn-read.flan @@ -67,11 +67,11 @@ ;; A document in a buffer this program owns and can write to. (bytes-view "literal") ;; is not that — a literal is constant data behind a writable-looking slice — -;; so the source is built with append and the write goes through as-slice. +;; so the source is built with append and the write goes through slice. (defn survives-its-buffer [] () (let [buf (vec-new u8)] (append (addr buf) (bytes-view "{:name \"level-1\" :xs [1 2]}")) - (let [src (as-slice buf) + (let [src (slice buf) v (edn/read src)] (dotimes [i (len src)] (set (at src i) \x)) diff --git a/test/programs/format.flan b/test/programs/format.flan index 6fdb5e1..dfec99b 100644 --- a/test/programs/format.flan +++ b/test/programs/format.flan @@ -11,7 +11,7 @@ (defn show [x f64 p i32] () (let [v (format-f64 x p)] - (println (string (as-slice v))) + (println (string (slice v))) (free v))) (defn main [] i32 @@ -89,12 +89,12 @@ (let [b (vec-new u8)] (append (addr b) (bytes-view "fps ")) (let [f (format-f64 59.94 1)] - (append (addr b) (as-slice f)) + (append (addr b) (slice f)) (free f)) (append (addr b) (bytes-view " / frame ")) (let [f (format-f64 0.0166667 4)] - (append (addr b) (as-slice f)) + (append (addr b) (slice f)) (free f)) - (println (string (as-slice b))) ; fps 59.9 / frame 0.0167 + (println (string (slice b))) ; fps 59.9 / frame 0.0167 (free b)) 0) diff --git a/test/programs/generics.flan b/test/programs/generics.flan index 83f7aab..853d4cf 100644 --- a/test/programs/generics.flan +++ b/test/programs/generics.flan @@ -246,8 +246,8 @@ (let [a (arena-new 4096) keep (filter (slice ns 0 4) (fn [x] (> x 5))) one (one-of 4.5)] - (println (len (as-slice keep))) - (println (at (as-slice one) 0)) + (println (len (slice keep))) + (println (at (slice one) 0)) (free one) (free keep) (free-all a)))) diff --git a/test/programs/global-init.flan b/test/programs/global-init.flan index b45371c..8b0a121 100644 --- a/test/programs/global-init.flan +++ b/test/programs/global-init.flan @@ -73,7 +73,7 @@ ;; program's when main runs. (push names 11) (push names 22) - (println (len (as-slice names))) + (println (len (slice names))) (println (at names 1)) (free names) ;; And the arena, used the way sand.flan means to use it. diff --git a/test/programs/init-conditions.flan b/test/programs/init-conditions.flan index d89827a..6d80e07 100644 --- a/test/programs/init-conditions.flan +++ b/test/programs/init-conditions.flan @@ -13,7 +13,7 @@ ;;;; (defn read-file [path string] dyn ;;;; (let [src (slurp path (heap-allocator))] ;;;; (defer (free src)) -;;;; (read (as-slice src)))) +;;;; (read (slice src)))) ;;;; ;;;; `slurp` signals FileError and the handler further out unwinds. The ;;;; transfer leaves this frame through its defers — and `src` was never diff --git a/test/programs/into.flan b/test/programs/into.flan index 50a42d2..34aad46 100644 --- a/test/programs/into.flan +++ b/test/programs/into.flan @@ -46,20 +46,20 @@ ;; No transforms: a copy into the destination named in the form. (let [xs [7 8 9] v (into xs (vec-new i32))] - (show (as-slice v)) ; 7 8 9 + (show (slice v)) ; 7 8 9 (free v)) ;; map then filter. (let [xs [1 2 3 4 5 6] v (into xs (vec-new i32) (map double) (filter even?))] - (show (as-slice v)) ; 2 4 6 8 10 12 + (show (slice v)) ; 2 4 6 8 10 12 (free v)) ;; filter then map, over the same source: a different answer, because the ;; stages are in the order they were written. (let [xs [1 2 3 4 5 6] v (into xs (vec-new i32) (filter even?) (map double))] - (show (as-slice v)) ; 4 8 12 + (show (slice v)) ; 4 8 12 (free v)) ;; One pass and no intermediate collection. The two chains above pulled @@ -69,8 +69,8 @@ ;; A name as the source is read, not moved: src is still alive here. (let [src (into [3 1 2] (vec-new i32)) v (into src (vec-new i32) (map double))] - (show (as-slice v)) ; 6 2 4 - (show (as-slice src)) ; 3 1 2 + (show (slice v)) ; 6 2 4 + (show (slice src)) ; 3 1 2 (free v) (free src)) @@ -88,7 +88,7 @@ ;; elements come out of it. (let [xs [1 2 3 4] v (into (source (slice xs 0 4)) (vec-new i32) (filter even?))] - (show (as-slice v)) ; 2 4 + (show (slice v)) ; 2 4 (free v)) (print builds) (println "") ; 1 0) diff --git a/test/programs/json.flan b/test/programs/json.flan index 220dcf5..925a750 100644 --- a/test/programs/json.flan +++ b/test/programs/json.flan @@ -407,7 +407,7 @@ (let [buf (vec-new u8)] (append (addr buf) (bytes-view doc)) (with-allocator frame - (let [v (read-doc (as-slice buf))] + (let [v (read-doc (slice buf))] (println (describe v)) ; object (println (count-leaves v)) ; every leaf in the graph (println (sum-ints v)) ; [1 2 3] diff --git a/test/programs/shim-nul.flan b/test/programs/shim-nul.flan index 64e3d31..e2bd18e 100644 --- a/test/programs/shim-nul.flan +++ b/test/programs/shim-nul.flan @@ -22,7 +22,7 @@ (push v 104) ; h (push v 105) ; i (println "before") - (c-puts (string (as-slice v))) + (c-puts (string (slice v))) (println "unreachable") (free v)) 0) diff --git a/test/programs/slurp.flan b/test/programs/slurp.flan index 1182a6b..f07f1e2 100644 --- a/test/programs/slurp.flan +++ b/test/programs/slurp.flan @@ -29,7 +29,7 @@ ;; ── The happy path ──────────────────────────────────────────────── (let [v (slurp "programs/assets/a.txt")] (println (len v)) ; 13 - (print (string (as-slice v))) ; hello from a + (print (string (slice v))) ; hello from a (free v)) ;; Byte-exact, the same as an embed: nothing here decodes anything. @@ -52,7 +52,7 @@ (invoke-restart 'use-value "programs/assets/b.bin"))] (let [v (slurp "programs/assets/does-not-exist")] (println (len v)) ; 3 - (println (string (as-slice v))) ; BBB + (println (string (slice v))) ; BBB (free v))) (println seen) ; 1 (println (= last-reason file-missing)) ; true @@ -65,7 +65,7 @@ (barf "slurp-out.txt" (bytes-view "round trip\n")) (let [v (slurp "slurp-out.txt")] (println (len v)) ; 11 - (print (string (as-slice v))) ; round trip + (print (string (slice v))) ; round trip (free v)) ;; barf's own failure signals the same condition with op = write. A directory @@ -80,7 +80,7 @@ (println seen) ; 1 (println (= last-op file-op-write)) ; true (let [v (slurp "slurp-out.txt")] - (print (string (as-slice v))) ; second + (print (string (slice v))) ; second (free v)) ;; ── retry: the file was not there, so the handler makes it ──────── @@ -96,7 +96,7 @@ (barf "slurp-made.txt" (bytes-view "made by the handler\n")) (invoke-restart 'retry))] (let [v (slurp "slurp-made.txt")] - (print (string (as-slice v))) ; made by the handler + (print (string (slice v))) ; made by the handler (free v))) (println seen) ; 1 (println (= last-reason file-missing)) ; true diff --git a/test/programs/string-eq.flan b/test/programs/string-eq.flan index 8df7238..383c8e1 100644 --- a/test/programs/string-eq.flan +++ b/test/programs/string-eq.flan @@ -22,7 +22,7 @@ ;; fire -- what answers here is the byte loop, or the length check first ;; ruling nothing out since both are three bytes. (let [heap (to-lower (bytes-view "ABC"))] - (let [h (string (as-slice heap))] + (let [h (string (slice heap))] (println (= "abc" h)) ; true (println (!= "abc" h))) (free heap)) diff --git a/test/programs/strings.flan b/test/programs/strings.flan index df1a057..e15ea35 100644 --- a/test/programs/strings.flan +++ b/test/programs/strings.flan @@ -9,10 +9,10 @@ ;;;; 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. as-slice borrows -- it copies ptr+len and never the elements -- so v +;;; 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 (as-slice (deref v))))) + (println (string (slice (deref v))))) (defn main [] i32 ;; The builder. Three appends and two numbers into one Vec, which is the @@ -130,7 +130,7 @@ ;; 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 (as-slice f) (bytes-view "/"))] + (let [j (join (slice f) (bytes-view "/"))] (show (addr j)) ; a/b/c (free j)) (free f)) diff --git a/test/programs/vec-global.flan b/test/programs/vec-global.flan index 202a28f..2c6c1c4 100644 --- a/test/programs/vec-global.flan +++ b/test/programs/vec-global.flan @@ -48,7 +48,7 @@ ;; The second run. Nothing re-initialises the global between them, so the ;; length keeps climbing and the loaded data is the same block it was. (entry) - (println (len (as-slice the-data))) + (println (len (slice the-data))) ;; A copy is the one thing something else may own, and freeing that copy ;; leaves the global untouched. (let [c (clone the-data)] diff --git a/test/programs/vec.flan b/test/programs/vec.flan index bc7193c..732d8bd 100644 --- a/test/programs/vec.flan +++ b/test/programs/vec.flan @@ -42,12 +42,18 @@ (set (at v 1) 99) (println (at v 1)) ; 99 - ;; as-slice is a non-owning view: it copies ptr+len and never the + ;; slice over a Vec 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 + ;; one. [at] and [len] over it are the array operations, unchanged, and + ;; the three arities are the ones every other target has -- the tail form + ;; included, which the Vec had no spelling for while it had a name of its + ;; own. The view is of storage v owns: a push here would move it, and + ;; nothing would say so. + (println (sum (slice v))) ; 139 + (println (len (slice v 1 3))) ; 2 + (println (at (slice v 1 3) 0)) ; 99 + (println (len (slice v 1))) ; 2 + (println (at (slice v 1) 1)) ; 30 ;; clone is the only copy: assignment moves. The copy is independent, and ;; freeing it leaves the original alone. @@ -65,7 +71,7 @@ (println (len v)) ; 4 ;; The structural printer reaches both new types. Neither is followed: a - ;; Vec's elements are printed through (as-slice v), which says at the call + ;; Vec's elements are printed through (slice v), which says at the call ;; site that it borrowed, and an allocator's contents are the runtime's and ;; its address is not stable across runs. (println v) ; diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 41326a5..56bbc35 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -1344,7 +1344,7 @@ let () = moves are here too — into a call and out of one — because a Vec that cannot be handed to a function is not a container anyone can use. *) let vec_out = - "0\n3\n10\n30\n99\n139\n2\n99\n-1\n10\n3\n4\n\ + "0\n3\n10\n30\n99\n139\n2\n99\n2\n30\n-1\n10\n3\n4\n\ \n\n2\n4\n7\nfalse\n11\n5\n" in outputs "vec" "programs/vec.flan" vec_out; @@ -2392,7 +2392,7 @@ let () = Three claims, and the second is the one that had to be decided rather than inherited. (1) Five frames finish and seven are abandoned, out of five routes to a bad index — fixed-array read, fixed-array write (a - different lowering), slice, Vec element and Vec as-slice, the last two + different lowering), slice, Vec element and Vec slice, the last two checked inside the runtime rather than in emitted IR. (2) `cleaned` is 5, which is every defer on every one of those paths: an answered bounds failure leaves through the same unwind path a `return` uses and runs diff --git a/test/test_flan.ml b/test/test_flan.ml index 2cf9eaa..5fbf903 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -1959,9 +1959,50 @@ let () = ~needle:"(slice a lo hi)"; rejects_check "slice of four" (arr ^ "(defn f [] [i32] (slice a 0 1 2))") ~needle:"given 4 arguments"; - rejects_check "slice of a Vec" - "(defn f [v (Vec i32)] [i32] (slice v))" - ~needle:"slice takes an array, a slice or a string"; + rejects_check "slice of something with no elements" + "(defn f [n i32] [i32] (slice n))" + ~needle:"slice takes an array, a slice, a string or a Vec"; + (* A Vec is the fourth thing slice takes, and it is the one that borrows: + the view is of storage the Vec owns and a push may move it. That is a + rule about the push and is written down beside the push; here what is + pinned is that all three arities reach a Vec, the two-argument one + included — it had no spelling at all while the Vec had a name of its + own. There is no static length to check a bound against, so the only + refusal left is the literal pair that runs backwards. *) + let vec = "(defn f [v (Vec i32)] " in + accepts "the whole of a Vec" (vec ^ "[i32] (slice v))"); + accepts "the tail of a Vec" (vec ^ "[i32] (slice v 2))"); + accepts "a range of a Vec" (vec ^ "[i32] (slice v 1 3))"); + accepts "a Vec bound is not known here" (vec ^ "[i32] (slice v 0 9999))"); + rejects_check "reversed slice of a Vec" (vec ^ "[i32] (slice v 2 1))") + ~needle:"runs backwards"; + rejects_check "slice of a Vec of four" + (vec ^ "[i32] (slice v 0 1 2))") ~needle:"given 4 arguments"; + (* The mirror of the returned-array refusal, for a different loss. The + storage a returned Vec owns outlives the expression, so the view does not + dangle — what is gone is the owner, and with it any way to free the + block. *) + rejects_check "slice of a returned Vec" + "(defn mk [] (Vec i32) (vec-new i32)) (defn f [] [i32] (slice (mk)))" + ~needle:"the header a call answers is a temporary"; + rejects_check "slice of a returned Vec, three arguments" + "(defn mk [] (Vec i32) (vec-new i32)) (defn f [] [i32] (slice (mk) 0 1))" + ~needle:"the header a call answers is a temporary"; + (* There is no [as-slice]: one word takes the view and what it is given says + what the view is. The refusal names [slice] and writes the call back out, + because a reader meeting it has no reason to know there was ever a second + name. *) + rejects_check "as-slice is not a name" + (vec ^ "[i32] (as-slice v))") ~needle:"there is no as-slice"; + rejects_check "as-slice suggests the call that replaces it" + (vec ^ "[i32] (as-slice v 1 3))") ~needle:"Write (slice v 1 3)"; + rejects_check "as-slice over an expression suggests a form that compiles" + "(defn mk [] (Vec i32) (vec-new i32)) \ + (defn f [] [i32] (as-slice (mk) 1 3))" + ~needle:"Write (slice v 1 3)"; + (* A program's own definition of the name still reaches its own. *) + accepts "a program may define as-slice" + "(defn as-slice [n i32] i32 n) (defn f [] i32 (as-slice 3))"; (* An array a call returned is a temporary the slice would outlive. It dangled silently on both backends before, at the three-argument spelling; (slice (mk)) is the spelling that would have made it @@ -2970,7 +3011,7 @@ let () = accepts "a global Vec is borrowed, mutated and cloned" "(defonce g (Vec u8)) \ (defn f [] () (set g (vec-new u8)) (push g 1) (set (at g 0) 2) \ - (println (len (as-slice g))) (let [c (clone g)] (free c)))"; + (println (len (slice g))) (let [c (clone g)] (free c)))"; rejects_check "try is milestone 6" "(defn f [] i32 (try 1))" ~needle:"milestone 6"; (* dotimes and defer are implemented, and a defer in a [let] is now one of diff --git a/test/test_valgrind.ml b/test/test_valgrind.ml index 8631e71..007c79c 100644 --- a/test/test_valgrind.ml +++ b/test/test_valgrind.ml @@ -316,7 +316,7 @@ let heap_oob = \ (let [v (vec-new i32)]\n\ \ (push v 1)\n\ \ (push v 2)\n\ - \ (let [s (as-slice v)] (print (at s 4000)) (println \"\"))\n\ + \ (let [s (slice v)] (print (at s 4000)) (println \"\"))\n\ \ (free v))\n\ \ 0)\n" @@ -334,7 +334,7 @@ let heap_uninit = \ (let [v (vec-new i32)]\n\ \ (push v 1)\n\ \ (push v 2)\n\ - \ (let [s (as-slice v)] (print (at s 3)) (println \"\"))\n\ + \ (let [s (slice v)] (print (at s 3)) (println \"\"))\n\ \ (free v))\n\ \ 0)\n" @@ -397,7 +397,7 @@ let arena_reuse = \ (with-allocator frame\n\ \ (let [w (vec-new i32)]\n\ \ (push w 5) (push w 6)\n\ - \ (let [t (as-slice w)] (print (at t 3)) (println \"\"))))\n\ + \ (let [t (slice w)] (print (at t 3)) (println \"\"))))\n\ \ 0)\n" let () = diff --git a/vendor/edn/provide.flan b/vendor/edn/provide.flan index 8a9b36c..f4d9170 100644 --- a/vendor/edn/provide.flan +++ b/vendor/edn/provide.flan @@ -61,7 +61,7 @@ (let [v (vec-new u8)] (append (addr v) (bytes-view a)) (append (addr v) (bytes-view b)) - (string (as-slice v)))) + (string (slice v)))) (defn joined3 [a string b string c string] string (joined a (joined b c))) @@ -74,7 +74,7 @@ (defn i64->string [n i64] string (let [v (vec-new u8)] (append-i64 (addr v) n) - (string (as-slice v)))) + (string (slice v)))) ;; The tokenizer answers byte offsets, because that is what a slice into the ;; buffer costs nothing to produce. A person reading a refusal wants a line and @@ -150,7 +150,7 @@ (let [t (expect c tok-string) b (vec-new u8 a)] (append (addr b) (.text t)) - (string (as-slice b)))) + (string (slice b)))) ;; Whether the next thing, past trivia, is this byte. Enough of a peek for ;; every loop below — "is the collection over" is the only lookahead a reader @@ -463,7 +463,7 @@ (return out)))) (let [sname (Form.Sym {.s name}) rname (Form.Sym {.s (joined "read-" name)}) - struct `(defstruct ~sname ~(Form.Vec {.xs (as-slice fields)})) + struct `(defstruct ~sname ~(Form.Vec {.xs (slice fields)})) reader `(defn ~rname [c (Ptr Cursor) a Allocator] ~sname (let [out (~sname {}) @@ -472,16 +472,16 @@ (while (ok? c) (let [k (next c)] (when (or (not (ok? c)) (= (.kind k) tok-map-close)) - ~@(as-slice missing) + ~@(slice missing) (return out)) (when (!= (.kind k) tok-keyword) (fail c err-unexpected-token (.pos k)) (return out)) - (cond ~@(as-slice clauses)))) + (cond ~@(slice clauses)))) out))] (push decls struct) (push decls reader) - (ok-derived sname (as-slice decls) `(~rname c a))))) + (ok-derived sname (slice decls) `(~rname c a))))) ;; ── Comparing and rendering a type form ───────────────────────────── @@ -572,7 +572,7 @@ out)) (defn ~fname [p string a Allocator] ~sname (let [b (slurp p a)] - (~bname (as-slice b) a))))))))) + (~bname (slice b) a))))))))) ;; A refusal, as a declaration. `compile-error` is an expression and a ;; top-level position takes a declaration, so it goes in the body of a function diff --git a/vendor/edn/read.flan b/vendor/edn/read.flan index 55c530d..cb973ff 100644 --- a/vendor/edn/read.flan +++ b/vendor/edn/read.flan @@ -68,7 +68,7 @@ (defn copy-text [s [u8]] string (let [b (vec-new u8)] (append (addr b) s) - (string (as-slice b)))) + (string (slice b)))) ;; ── Reading ───────────────────────────────────────────────────────── @@ -158,4 +158,4 @@ (defn read-file [path string] dyn (let [src (slurp path (heap-allocator))] (defer (free src)) - (read (as-slice src)))) + (read (slice src)))) diff --git a/vendor/json/json.flan b/vendor/json/json.flan index a186b08..bdb80c9 100644 --- a/vendor/json/json.flan +++ b/vendor/json/json.flan @@ -661,7 +661,7 @@ ;; free-all and under the heap it is a block nobody has freed. There is no ;; third owner. ;; -;; The builder is a (Vec u8) and the answer is (string (as-slice b)) taken ONCE +;; The builder is a (Vec u8) and the answer is (string (slice b)) taken ONCE ;; at the end. That ordering is load-bearing rather than stylistic: a push ;; after the slice has been taken can grow the Vec into a new block, and in an ;; arena the old block is still mapped, so the already-taken string would go on @@ -718,7 +718,7 @@ ;; because a dropped character is not worth a crash and the ;; tokenizer is the place that already said no. None (do))))))))) - (Some (string (as-slice b))))) + (Some (string (slice b))))) ;; ── Reading past a value ──────────────────────────────────────────── diff --git a/vendor/json/provide.flan b/vendor/json/provide.flan index 23a0dae..2312b5e 100644 --- a/vendor/json/provide.flan +++ b/vendor/json/provide.flan @@ -46,7 +46,7 @@ (let [v (vec-new u8)] (append (addr v) (bytes-view a)) (append (addr v) (bytes-view b)) - (string (as-slice v)))) + (string (slice v)))) (defn joined3 [a string b string c string] string (joined a (joined b c))) @@ -58,7 +58,7 @@ (defn i64->string [n i64] string (let [v (vec-new u8)] (append-i64 (addr v) n) - (string (as-slice v)))) + (string (slice v)))) ;; The tokenizer answers byte offsets. A person reading a refusal wants a line ;; and a column, so the newlines before the offset are counted here — once per @@ -324,7 +324,7 @@ (return out)))) (let [sname (Form.Sym {.s name}) rname (Form.Sym {.s (joined "read-" name)}) - struct `(defstruct ~sname ~(Form.Vec {.xs (as-slice fields)})) + struct `(defstruct ~sname ~(Form.Vec {.xs (slice fields)})) reader `(defn ~rname [c (Ptr Cursor) a Allocator] ~sname (let [out (~sname {}) @@ -333,14 +333,14 @@ (while (and (ok? c) (not (at-byte? c \}))) (let [k (expect c tok-string)] (expect c tok-colon) - (cond ~@(as-slice clauses)) + (cond ~@(slice clauses)) (comma c))) (let [close (expect c tok-object-close)] - ~@(as-slice missing)) + ~@(slice missing)) out))] (push decls struct) (push decls reader) - (ok-derived sname (as-slice decls) `(~rname c a))))) + (ok-derived sname (slice decls) `(~rname c a))))) ;; ── The small predicates the refusals are written against ─────────── @@ -375,7 +375,7 @@ (defn copy-of [s [u8]] string (let [b (vec-new u8)] (append (addr b) s) - (string (as-slice b)))) + (string (slice b)))) ;; ── Comparing and rendering a type form ───────────────────────────── @@ -453,7 +453,7 @@ out)) (defn ~fname [p string a Allocator] ~sname (let [b (slurp p a)] - (~bname (as-slice b) a))))))))) + (~bname (slice b) a))))))))) ;; A refusal, as a declaration. `compile-error` is an expression and a top-level ;; position takes a declaration, so it goes in the body of a function nothing diff --git a/web/index.html b/web/index.html index 995701d..778bf3e 100644 --- a/web/index.html +++ b/web/index.html @@ -911,7 +911,7 @@ type as an argument — (vec-new t), (map-new t i32), (let [ns [5 3 9 1] one (one-of 4.5)] (println (first-or (slice ns 0 4) -1)) - (println (at (as-slice one) 0)) + (println (at (slice one) 0)) (free one)))
3