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:
parent
10b1abbf09
commit
9ce51ba94e
112
FIX.org
112
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.
|
||||
|
||||
8
NEXT.md
8
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.
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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 |
|
||||
|
||||
166
lib/check.ml
166
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 [<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; \
|
||||
|
||||
@ -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 ──────────────────────────
|
||||
;;
|
||||
|
||||
@ -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 ->
|
||||
|
||||
@ -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 _ ->
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))))
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 () =
|
||||
|
||||
16
vendor/edn/provide.flan
vendored
16
vendor/edn/provide.flan
vendored
@ -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
|
||||
|
||||
4
vendor/edn/read.flan
vendored
4
vendor/edn/read.flan
vendored
@ -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))))
|
||||
|
||||
4
vendor/json/json.flan
vendored
4
vendor/json/json.flan
vendored
@ -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 ────────────────────────────────────────────
|
||||
|
||||
|
||||
16
vendor/json/provide.flan
vendored
16
vendor/json/provide.flan
vendored
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user