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.
This commit is contained in:
Joseph Ferano 2026-09-21 09:41:33 +07:00
parent 10b1abbf09
commit 9ce51ba94e
35 changed files with 408 additions and 145 deletions

112
FIX.org
View File

@ -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.

View File

@ -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.

View File

@ -2117,7 +2117,7 @@ hand-off dropping a number, which is what the note left behind said it was.
**A `(Vec T)` shows as `<vec>` and a `(Ptr T)` as `<ptr>`**, 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`

View File

@ -335,7 +335,7 @@ this one too.
**A typed `Vec` prints as `<vec>` 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.

View File

@ -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 |

View File

@ -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 [<prelude>: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; \

View File

@ -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
;;

View File

@ -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 "<allocator>" ]
(* 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 "<vec>" ]
| Types.Fn _ as ft -> [ lit ("<" ^ Types.to_string ft ^ ">") ]
| Types.Option t ->

View File

@ -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 _ ->

View File

@ -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

View File

@ -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

View File

@ -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))

View File

@ -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)

View File

@ -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))

View File

@ -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))

View File

@ -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)

View File

@ -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))))

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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]

View File

@ -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)

View File

@ -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

View File

@ -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))

View File

@ -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))

View File

@ -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)]

View File

@ -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) ; <vec>

View File

@ -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\
<vec>\n<allocator>\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

View File

@ -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

View File

@ -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 () =

View File

@ -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

View File

@ -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))))

View File

@ -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 ────────────────────────────────────────────

View File

@ -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

View File

@ -911,7 +911,7 @@ type as an argument — <code>(vec-new t)</code>, <code>(map-new t i32)</code>,
(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)))</code></pre>
<pre><code class="sh">3