Merge branch 'worktree-agent-ab376a9e12b4af136' into dev-loop

# Conflicts:
#	FIX.org
This commit is contained in:
Joseph Ferano 2026-09-21 07:05:41 +07:00
commit 450728c9f2
8 changed files with 424 additions and 41 deletions

71
FIX.org
View File

@ -4181,3 +4181,74 @@ took compiler, socket and session down together.
arena-destroy — the slice carries no allocator, so (free) cannot take it. arena-destroy — the slice carries no allocator, so (free) cannot take it.
Fine against an arena or the frame allocator; a heap-tier copy is a block Fine against an arena or the frame allocator; a heap-tier copy is a block
that lives until exit. Documented in BUILT.md's surface table. that lives until exit. Documented in BUILT.md's surface table.
* slice's arities, and at/slice over a string — 2026-09-20
The two rulings, in the author's words:
#+begin_quote
slice should take multiple arities, none just pass the whole slice, 1 start
from n, 2 n to m
#+end_quote
#+begin_quote
at/slice should work on strings.
#+end_quote
Both came out of the same wall: a fixed array does not decay to a slice at a
call, so handing [6 2 4 9 1 9 4 5] to a generic sort meant writing
(slice a 0 (len a)) every time; and a string could be neither indexed nor
sliced at all, so the only route to a byte was (bytes s) — which the lane
changing bytes into a copying operation would have turned into an allocation
per index.
What landed. (slice a) is the whole of it and (slice a n) is the tail from n,
written out in check.ml into the three-argument form — same node, same static
bound checks, same runtime trap, and on a fixed array the implicit length is
the constant (len a) already folds to. A target that is not already a name
goes through a slot first, so (slice (f x)) calls f once. Neither backend
needed arity work.
Strings: (at s i) is the byte, bounds-checked, and (slice s ...) at all three
arities answers a *string* viewing the same bytes — not a [u8], because a
byte slice is writable-looking and these bytes are not the program's to
write. (set (at s i) x) is refused in check_place and says so. The backends
needed one case each: emit.ml's element_addr grew the String arm, and
x86.ml's index_len grew the length it checks a string index against — it had
been returning None, so x86 would have indexed a string with no check at all
once the checker allowed it.
Open, and not invented here: (slice s) cannot be passed to a [u8] parameter.
Crossing wants bytes-view, which is the other lane's to land.
** Review follow-ups on the same lane
Found by the independent review of this branch against dev-loop, and all of
it fixed here rather than queued.
The blocker was an interaction and not a bug in either half. dev-loop's
single-index fast arm (ab94c69) checks its own target and calls [indexed]
directly, on the stated grounds that [indexed] refuses a string by name —
which was true until this branch made [indexed] accept one. A refusal in
[check_place] therefore covered the spellings that go through it —
(set (at g 0 0) x) and (addr (at s 0)) — and missed the one a person
writes: (set (at s 0) 90) compiled, LLVM dropped the store and x86 exited
255. The question now lives in [indexed] itself, behind a
~place location, and is asked at every dimension — (at g 0 0) over a
[[2 string]] reaches a string only at the last step. [refuse_string_place]
is the one message, and [addr] gets it too, so it reads as value-versus-place
rather than as an assignment rule.
Slicing an array a call returned is refused outright now, at every arity.
It dangles — the view outlives the temporary, both backends print reused
bytes, nothing traps — and it dangled the same way at (slice (mk) 0 3) long
before this branch. It was cheap to refuse and nothing in the tree did it.
An array literal is untouched: the frame holds one for as long as the form
it is written in.
Noted, not fixed:
- A sliced string loses the trailing NUL both backends emit after a string
constant. The contract is ptr+len and nothing promised otherwise, but a
declare-c wrapper that leaned on the courtesy is now leaning on a slice's
end.
- (at d i) over a dyn string works and (slice d 1) is refused. Pre-existing,
and semantics never fork, so dyn slice should exist.
- (slice "abc" 0 99) is not refused at compile time, because Types.String
carries no length. Consistent with a slice of a slice; a missed nicety.

View File

@ -6107,3 +6107,84 @@ loop: it asserts both that the data file resolved against the buffer's directory
what comes back is code a person can read. The generated readers survey under `@x86` — the what comes back is code a person can read. The generated readers survey under `@x86` — the
fixed-array map key is a hash and equality pair nothing generated had asked the backend fixed-array map key is a hash and equality pair nothing generated had asked the backend
for before — and `@sanitize` is clean over both programs. for before — and `@sanitize` is clean over both programs.
## `slice` has three arities, and `at`/`slice` reach a string
`(slice a)` is the whole of `a`, `(slice a n)` is the tail from `n`, and `(slice a n m)` is the half-open range it
has always been. The two short forms exist because a fixed array does not decay to a slice at a call: passing
`[6 2 4 9 1 9 4 5]` to the prelude's `sort`, which takes `[$t]`, used to require `(slice a 0 (len a))` written out
at every call site.
```flan
(sort (slice [6 2 4 9 1 9 4 5]))
(let [a [6 2 4 9 1 9 4 5]] (sort (slice a)) (println (slice a)))
```
**They are one node, not three.** `check.ml` fills the missing arguments in and hands the backends the
three-argument form. The implicit `lo` is the literal 0. The implicit `hi` is the literal length when the target is
a fixed array — the same constant `(len a)` folds to — and the `Len` primitive otherwise, which reads the length
word a slice or a string is already carrying. So the short spelling costs exactly what the long one costs, the
static refusals apply to it unchanged (a literal `lo` past the end of a fixed array is an error at compile time,
not a trap), and the runtime bounds check is the same check on the same numbers. Neither backend grew an arity
case.
**The target is read twice when `hi` is implicit,** so a target that is not already a local or a global goes into a
slot first and both reads come from the slot. `(slice (f x))` calls `f` once. An array target never needs the slot:
its length is a constant, so the target appears once.
`(slice)` and four or more arguments are refused by name — the message lists the three forms rather than claiming
an argument count that is now wrong.
**A string indexes and slices.** `(at s i)` is the `i`th byte, a `u8`, bounds-checked the way an array or slice
index is. `(slice s)`, `(slice s n)` and `(slice s n m)` answer a **string**, not a `[u8]`: the result views the
same bytes, and a byte slice is writable-looking while these bytes are not the program's to write. No copy, one
`sub` and two stores, the same code a slice of a slice emits.
```flan
(let [s "insertion"]
(println (at s 0)) ; 105
(println (slice s 6)) ; ion
(println (slice s 0 6))) ; insert
```
Before this, the only route to a string's bytes was `(bytes s)`. That was free while `bytes` was a reinterpret, and
`(at (bytes s) i)` stops being free the moment `bytes` copies — which is why these reach the string directly rather
than through it.
**Neither is a place**, and the refusal lives in `indexed` rather than in `check_place`, which is the part worth
writing down. `check_place` is not the only way to a `Pindex`: the single-index `set` arm checks its own target
and calls `indexed` directly, on the stated grounds that `indexed` refuses a string by name — true until this
made `indexed` accept one. So a refusal written in `check_place` covered `(set (at g 0 0) x)` and `(addr (at s
0))`, which do go through it, and missed one index, which is the only spelling a person writes. A store into a
string literal compiled, and the two backends disagreed about it: LLVM dropped the store, x86 exited 255. The
question is now `indexed`'s, behind a `~place` location, and it is asked at *every* dimension, because `(at g 0
0)` over a `[[2 string]]` reaches the string at the last step and nowhere before it. One function,
`refuse_string_place`, holds the message, and a third caller of `indexed` cannot reopen this.
It reads as a value-versus-place refusal and not an assignment one, because `addr` asks the same question: a
string is a read-only view of bytes it does not own — a literal's live in constant storage — so there is nothing
to assign into *or take the address of*. Copy them into a buffer you own. A slice of a string is a string, so it
inherits all of this rather than needing its own.
**A sliced string loses the trailing NUL.** `x86.ml` emits one after every string constant and `emit.ml`'s
constants carry one too, so a whole string handed to C has happened to be NUL-terminated. A slice points into the
middle of the buffer and its end is a length, not a byte. This changes nothing that was promised — the contract is
ptr+len and `declare-c` takes a string apart into exactly that — but a hand-written wrapper that leaned on the
courtesy would be leaning on a slice's end now.
**Two backend cases, and one of them was a hole.** `emit.ml`'s `element_addr` grew a `String` arm beside the
`Slice` one; they are the same two words and the same check. `x86.ml`'s `elements` already handled a string, but
its `index_len` answered `None` for one — correct while nothing could index a string, and a silent skipped bounds
check the moment something could. It now reads the length word, so both backends check the same thing.
**An array a call returned is refused as a slice target**, at every arity. `(slice (mk))` where `mk` answers a
`[3 i32]` slices a temporary: the view outlives the storage, both backends print whatever the frame reused those
bytes for, and nothing traps. That was already true of `(slice (mk) 0 3)` and had never been written down, which
was survivable while nobody wrote it — `(slice (mk))` is short enough to become a habit, so it is an error now and
the message names the fix: bind it with a `let` first. An array *literal* is not the same case and stays legal:
the frame holds one for as long as the form it is written in, which is what `(sort (slice [6 2 4 9 1 9 4 5]))`
depends on.
A string still does not cross to a `[u8]` parameter, and `(slice s)` does not make it one. That crossing is
`bytes-view`'s. And `slice` over a `dyn` is still refused while `(at d i)` over one works — the asymmetry is
pre-existing and wants closing, since the two spaces are meant to compute the same thing.

View File

@ -3073,7 +3073,9 @@ let rec check ctx ?want (e : Ast.expr) : Tast.expr =
let pp, ty = vec_at ctx loc target [ idx ] in let pp, ty = vec_at ctx loc target [ idx ] in
Tast.Pderef pp, ty Tast.Pderef pp, ty
| _ -> | _ ->
let iidx, ty = indexed ctx target [ idx ] in (* [~place], for the reason [check_place] passes it: [indexed]
accepts a string, and this arm never reaches [check_place]. *)
let iidx, ty = indexed ~place:loc ctx target [ idx ] in
Tast.Pindex (target, iidx), ty Tast.Pindex (target, iidx), ty
in in
let v = check ctx ~want:pty v in let v = check ctx ~want:pty v in
@ -5346,6 +5348,24 @@ and struct_target ctx (target : Ast.expr) : Tast.expr * string =
fail target.Ast.loc "%s is not a struct, so it has no fields" fail target.Ast.loc "%s is not a struct, so it has no fields"
(Types.to_string other) (Types.to_string other)
(* (at s i) reads a string's byte, and reading is the whole of what a string
does here: it is a view of bytes the program does not own a literal's
are in constant storage, where a store is dropped on one backend and
faults on the other so there is no address of one to hand out either.
[addr] asks the same question and gets the same answer, so the refusal
names taking the address rather than only assigning.
It is a function and not a case inside [check_place] because [check_place]
is no longer the only way to a [Pindex]: the single-index [set] arm checks
its target itself and calls [indexed] directly, and [indexed] accepts a
string. Both call this, so neither can drift away from the other. *)
and refuse_string_place loc (ty : Types.t) =
if Types.equal ty Types.String then
fail loc
"a string is a read-only view of bytes it does not own, so (at s i) is \
a value and not a place there is nothing to assign into or take the \
address of. Copy the bytes into a buffer you own and use that"
and check_place ctx loc (p : Ast.place) : Tast.place * Types.t = and check_place ctx loc (p : Ast.place) : Tast.place * Types.t =
match p with match p with
| Ast.Pvar name -> | Ast.Pvar name ->
@ -5394,7 +5414,7 @@ and check_place ctx loc (p : Ast.place) : Tast.place * Types.t =
let p, ty = vec_at ctx loc target idx in let p, ty = vec_at ctx loc target idx in
Tast.Pderef p, ty Tast.Pderef p, ty
| _ -> | _ ->
let idx, ty = indexed ctx target idx in let idx, ty = indexed ~place:loc ctx target idx in
Tast.Pindex (target, idx), ty) Tast.Pindex (target, idx), ty)
| Ast.Pderef target -> | Ast.Pderef target ->
let target = check ctx target in let target = check ctx target in
@ -5448,14 +5468,24 @@ and index_expr ctx (e : Ast.expr) =
| other -> | other ->
fail e.Ast.loc "an index is an integer, found %s" (Types.to_string other) fail e.Ast.loc "an index is an integer, found %s" (Types.to_string other)
(* [(at a i)] and [(at grid row col)]: one index per dimension. *) (* [(at a i)] and [(at grid row col)]: one index per dimension.
and indexed ctx (target : Tast.expr) (idx : Ast.expr list) =
[~place] carries the location of the form being assigned into, and is
given by the two callers that are building somewhere to store. It is asked
at *every* dimension rather than once about the target: [(at g 0 0)] over a
[[2 string]] reaches a string at the last step and nowhere before it, so a
question asked only of [g] would miss it. *)
and indexed ?place ctx (target : Tast.expr) (idx : Ast.expr list) =
let rec go ty = function let rec go ty = function
| [] -> [], ty | [] -> [], ty
| i :: rest -> | i :: rest ->
let elem = let elem =
match ty with match ty with
| Types.Array (_, t) | Types.Slice t -> t | Types.Array (_, t) | Types.Slice t -> t
(* A string indexes to its bytes, and only to read them. *)
| Types.String ->
Option.iter (fun l -> refuse_string_place l ty) place;
Types.Int Types.U8
| other -> | other ->
fail i.Ast.loc "%s cannot be indexed" (Types.to_string other) fail i.Ast.loc "%s cannot be indexed" (Types.to_string other)
in in
@ -7356,35 +7386,98 @@ and named_call ?(qualified = false) ctx ~want loc name args =
let idx, ty = indexed ctx target idx in let idx, ty = indexed ctx target idx in
prim Tast.At ty (target :: idx)) prim Tast.At ty (target :: idx))
| _ -> fail loc "%s is (%s collection index ...)" name name) | _ -> fail loc "%s is (%s collection index ...)" name name)
(* (slice a), (slice a lo) and (slice a lo hi). The two short forms are
written out here into the three-argument one and are that form after
this line: same node, same checks, same code. Nothing is added at run
time, because neither missing argument needs anything computed
[lo] is 0, and [hi] is the length, which on a fixed array is the
constant [len] already folds to and on a slice or a string is the
length word the value is carrying anyway.
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. *)
| "slice" -> | "slice" ->
arity ctx loc name 3 args;
(match args with (match args with
| [ target; lo; hi ] -> | [] | _ :: _ :: _ :: _ :: _ ->
fail loc
"slice is (slice a), (slice a lo) or (slice a lo hi) — given %d \
arguments" (List.length args)
| target :: bounds ->
let target = check ctx target in let target = check ctx target in
let elem = match target.Tast.ty with let ty = target.Tast.ty in
| Types.Array (_, t) | Types.Slice t -> t (* A string slices to a string, not to a [u8]: the result views the
| other -> fail loc "slice takes an array or a slice, found %s" same bytes and is read-only for the same reason the source is, and
(Types.to_string other) calling it a byte slice would hand out a writable-looking view of
storage the program does not own. *)
let result = match ty with
| 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"
(Types.to_string other)
in in
prim Tast.Slice (Types.Slice elem) (* An array that came back from a call is a value in a temporary this
(let lo_loc = lo.Ast.loc and hi_loc = hi.Ast.loc in expression does not own: the slice would outlive it and view
let lo = check ctx ~want:index_ty lo in whatever the frame reused those bytes for, with nothing to trap on.
let hi = check ctx ~want:index_ty hi in A [let] gives it a name and a lifetime, so that is what the refusal
let ty = target.Tast.ty in names. An array *literal* is not this case it is written here and
(* A bound may sit one past the end, so the length is checked against the frame holds it for as long as the form it is written in. *)
lo and hi both, not against the last valid index. *) (match ty, target.Tast.e with
(match literal lo with | Types.Array _, (Tast.Call _ | Tast.CallPtr _) ->
| Some k -> static_index lo_loc ty ~past_end:true "slice bound" k fail loc
| None -> ()); "this slices an array a call returned, and a returned array is a \
(match literal hi with temporary the slice would outlive it and view storage the \
| Some k -> static_index hi_loc ty ~past_end:true "slice bound" k frame has reused. Bind it first: (let [a ()] (slice a ))"
| None -> ()); | _ -> ());
(match literal lo, literal hi with let int k = mk loc index_ty (Tast.Int (k, Types.I32)) in
| Some a, Some b when a > b -> (* [hi] is wanted twice only when it is the implicit length of
fail loc "slice [%Ld %Ld) runs backwards — lo must not exceed hi" a b something whose length is not static. *)
| _ -> ()); let needs_slot =
[ target; lo; hi ]) List.length bounds < 2
| _ -> assert false) && (match ty with Types.Array _ -> false | _ -> true)
&& (match target.Tast.e with
| Tast.Local _ | Tast.Global _ -> false
| _ -> true)
in
let slot = if needs_slot then Some (fresh_slot ctx ty) else None in
let src () = match slot with
| Some s -> mk loc ty (Tast.Local s)
| None -> target
in
let whole_len () = match ty with
| Types.Array (n, _) -> int n
| _ -> mk loc index_ty (Tast.Prim (Tast.Len, [ src () ]))
in
let lo_loc, lo, hi_loc, hi =
match bounds with
| [] -> loc, int 0L, loc, whole_len ()
| [ lo ] ->
lo.Ast.loc, check ctx ~want:index_ty lo, loc, whole_len ()
| [ lo; hi ] ->
lo.Ast.loc, check ctx ~want:index_ty lo,
hi.Ast.loc, check ctx ~want:index_ty hi
| _ -> assert false
in
(* A bound may sit one past the end, so the length is checked against
lo and hi both, not against the last valid index. *)
(match literal lo with
| Some k -> static_index lo_loc ty ~past_end:true "slice bound" k
| None -> ());
(match literal hi with
| Some k -> static_index hi_loc ty ~past_end:true "slice bound" k
| None -> ());
(match literal lo, literal hi with
| Some a, Some b when a > b ->
fail loc "slice [%Ld %Ld) runs backwards — lo must not exceed hi" a b
| _ -> ());
let body = mk loc result (Tast.Prim (Tast.Slice, [ src (); lo; hi ])) in
(match slot with
| None -> expect ctx loc ~want body
| Some s ->
expect ctx loc ~want
(mk loc result (Tast.Let ([ (s, target) ], [ body ])))))
(* (slice-from-ptr p n) — NEXT.md, "a pointer from C needs a length before (* (slice-from-ptr p n) — NEXT.md, "a pointer from C needs a length before
it can be indexed". A (Ptr T) that came back from C is readable at it can be indexed". A (Ptr T) that came back from C is readable at
@ -8854,11 +8947,14 @@ let builtins : (string * string * string) list =
a string, a Vec and a Map."); a string, a Vec and a Map.");
("at", "at [collection i32 ...] T", ("at", "at [collection i32 ...] T",
"The element at an index, bounds-checked — and for a Vec with the \ "The element at an index, bounds-checked — and for a Vec with the \
allocator's epoch checked first. It is also a place, so \ allocator's epoch checked first. On a string it is the byte, a u8. It \
(set (at v i) x) goes through the same check."); is also a place, so (set (at v i) x) goes through the same check; a \
("slice", "slice [[n T]|[T] i32 i32] [T]", string is the exception, being a view it does not own.");
"The half-open range [lo hi) as a non-owning view. A bound may sit one \ ("slice", "slice [[n T]|[T]|string i32? i32?] [T]|string",
past the end; a literal pair that runs backwards is refused here."); "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.");
("slice-from-ptr", "slice-from-ptr [(Ptr T) i32] [T]", ("slice-from-ptr", "slice-from-ptr [(Ptr T) i32] [T]",
"Puts a length on a pointer that came back from C. The caller promises \ "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; \ it addresses that many initialised T and that they outlive the result; \

View File

@ -2029,7 +2029,11 @@ and element_addr f (target : Tast.expr) idx =
ins f "%s = getelementptr inbounds %s, ptr %s, i64 0, i64 %s" ins f "%s = getelementptr inbounds %s, ptr %s, i64 0, i64 %s"
p (ll ty) ptr i64; p (ll ty) ptr i64;
go p elem rest go p elem rest
| Types.Slice elem -> (* A string is the same two words as a slice of u8 and indexes the
same way, bounds check included. *)
| Types.Slice _ | Types.String ->
let elem =
match ty with Types.Slice e -> e | _ -> Types.Int Types.U8 in
(* A slice is ptr+len, so step through the pointer it holds. *) (* A slice is ptr+len, so step through the pointer it holds. *)
let s = load f ptr ty in let s = load f ptr ty in
let base = fresh f in let base = fresh f in

View File

@ -2417,13 +2417,14 @@ and bounds_call f sym (loc : Loc.t) (extra : int list) =
already died inside that call and nothing falls through to here."; already died inside that call and nothing falls through to here.";
ud2 f.b ud2 f.b
(* The length an index is checked against, or [None] for the forms [emit.ml] (* The length an index is checked against, or [None] for the one form
does not check either: a raw pointer, which has no length, and a string, [emit.ml] does not check either: a raw pointer, which has no length. A
which its [element_addr] does not index at all. *) string carries its length in the second word exactly as a slice does, so
(at s i) is checked against it here and in [emit.ml] both. *)
and index_len _f (base : loc) (ty : Types.t) = and index_len _f (base : loc) (ty : Types.t) =
match ty with match ty with
| Types.Array (n, _) -> Some (`Const n) | Types.Array (n, _) -> Some (`Const n)
| Types.Slice _ -> Some (`At (shift base 8)) | Types.Slice _ | Types.String -> Some (`At (shift base 8))
| _ -> None | _ -> None
and load_len f = function and load_len f = function

View File

@ -79,4 +79,39 @@
(sort (slice zs 2 3)) (sort (slice zs 2 3))
(reverse (slice zs 2 3)) (reverse (slice zs 2 3))
(show (slice zs 0 (len zs))) ; 100 -1 0 4 9 9 200 300 (show (slice zs 0 (len zs))) ; 100 -1 0 4 9 9 200 300
;; ── The short arities ──────────────────────────────────────────────
;; (slice a) and (slice a n) are (slice a 0 (len a)) and (slice a n (len a))
;; written out in the checker, so each line here is read against the
;; spelling above it that it stands for. A fixed array does not decay to a
;; slice at a call, so passing one to a function over [$t] is what these
;; exist for.
(load-xs)
(show (slice xs)) ; 5 -3 5 0 12 -3 7
(show (slice xs 4)) ; 12 -3 7
;; Of a slice rather than of an array: the length is the slice's own, so
;; these index from where the previous one started, not from the array's 0.
(show (slice (slice xs))) ; 5 -3 5 0 12 -3 7
(show (slice (slice xs 2) 1)) ; 0 12 -3 7
;; A literal array is nobody's named place, and it still slices: the two
;; lines the author hit are these, one sorting through the prelude's
;; generic [$t] sort and one over character literals.
(sort (slice [6 2 4 9 1 9 4 5]))
(let [a [6 2 4 9 1 9 4 5]]
(sort (slice a))
(show (slice a))) ; 1 2 4 4 5 6 9 9
(let [cs [\I \N \S \E \R \T \I \O \N \S \O \R \T]]
(sort (slice cs))
(println (string (slice cs)))) ; EIINNOORRSSTT
;; ── Strings ────────────────────────────────────────────────────────
;; A string is ptr+len over bytes, so it indexes to a byte and slices to a
;; string viewing the same bytes. No copy, and no route through [u8].
(let [s "insertion"]
(print (at s 0)) (println "") ; 105
(println (slice s)) ; insertion
(println (slice s 6)) ; ion
(println (slice s 0 6))) ; insert
(println (slice "sorted" 2 4)) ; rt
0) 0)

View File

@ -433,12 +433,21 @@ let () =
odd length; a reverse-sorted slice; and a sort of a subslice whose odd length; a reverse-sorted slice; and a sort of a subslice whose
neighbours must be untouched, which is the in-place, ptr+len claim neighbours must be untouched, which is the in-place, ptr+len claim
itself. At -O0 as well a slice parameter is an alloca of a two-word itself. At -O0 as well a slice parameter is an alloca of a two-word
struct, and mem2reg is exactly what would hide it being copied. *) struct, and mem2reg is exactly what would hide it being copied.
The tail is the short arities and strings. (slice a) and (slice a n)
are the three-argument form written out in the checker, so the rows
that matter are the ones where the implicit length is not the array's:
a slice of a slice counts from where that slice starts. The string rows
are a byte read and three views, none of which copies. *)
let slices_out = let slices_out =
"5 -3 5 0 12 -3 7\n23\n-3\n12\n0\n-1\n99\n\ "5 -3 5 0 12 -3 7\n23\n-3\n12\n0\n-1\n99\n\
7 -3 12 0 5 -3 5\n-3 7 12 0 5 -3 5\n\ 7 -3 12 0 5 -3 5\n-3 7 12 0 5 -3 5\n\
-3 -3 0 5 5 7 12\n1 2 3 4 5\n\ -3 -3 0 5 5 7 12\n1 2 3 4 5\n\
100 -1 0 4 9 9 200 300\n100 -1 0 4 9 9 200 300\n" 100 -1 0 4 9 9 200 300\n100 -1 0 4 9 9 200 300\n\
5 -3 5 0 12 -3 7\n12 -3 7\n5 -3 5 0 12 -3 7\n0 12 -3 7\n\
1 2 4 4 5 6 9 9\nEIINNOORRSSTT\n\
105\ninsertion\nion\ninsert\nrt\n"
in in
(* (slice-from-ptr p n) — NEXT.md, "a pointer from C needs a length before (* (slice-from-ptr p n) — NEXT.md, "a pointer from C needs a length before
it can be indexed". The pointers that motivated it come from C, but the it can be indexed". The pointers that motivated it come from C, but the
@ -454,6 +463,13 @@ let () =
sfp_out; sfp_out;
outputs "slice algorithms" "programs/slices.flan" slices_out; outputs "slice algorithms" "programs/slices.flan" slices_out;
outputs ~opt:"-O0" "slice algorithms, -O0" "programs/slices.flan" slices_out; outputs ~opt:"-O0" "slice algorithms, -O0" "programs/slices.flan" slices_out;
(* And on the hand-written backend, because indexing a string is the one
part of this that is not the checker alone: emit.ml's element_addr grew
a String case and x86.ml's index_len grew the length it checks against,
and a backend that skipped the check would print the same numbers here
while reading off the end one line later. *)
outputs ~x86:true "slice algorithms, --x86" "programs/slices.flan"
slices_out;
(* println, one row per arm of render.ml's walk. The walk is shared with (* println, one row per arm of render.ml's walk. The walk is shared with
the REPL, but its only coverage was the REPL tests -- a dev build, the REPL, but its only coverage was the REPL tests -- a dev build,
emitting to flan_dev_emit. This is the same walk with the other emitter emitting to flan_dev_emit. This is the same walk with the other emitter

View File

@ -953,6 +953,14 @@ let () =
infers "bytes-view of a string" "(bytes-view \"hi\")" "[u8]"; infers "bytes-view of a string" "(bytes-view \"hi\")" "[u8]";
infers "len is i32" "(len (bytes \"hi\"))" "i32"; infers "len is i32" "(len (bytes \"hi\"))" "i32";
infers "slice of a slice" "(slice (bytes \"hi\") 0 1)" "[u8]"; infers "slice of a slice" "(slice (bytes \"hi\") 0 1)" "[u8]";
infers "slice of the whole" "(slice (bytes \"hi\"))" "[u8]";
infers "slice from n" "(slice (bytes \"hi\") 1)" "[u8]";
(* A string slices to a string and indexes to a byte. Not to a [u8]: the
result views bytes the program does not own, and a byte slice is
writable-looking. *)
infers "slice of a string" "(slice \"hi\" 0 1)" "string";
infers "whole of a string" "(slice \"hi\")" "string";
infers "string index is a u8" "(at \"hi\" 0)" "u8";
infers "parse text to f64" "(bytes->f64 (bytes \"1.5\"))" "f64"; infers "parse text to f64" "(bytes->f64 (bytes \"1.5\"))" "f64";
(* An untyped integer constant is usable where a float is wanted, as in (* An untyped integer constant is usable where a float is wanted, as in
@ -1840,6 +1848,77 @@ let () =
accepts "a slice's length is not known here" accepts "a slice's length is not known here"
"(defn f [s [u8]] [u8] (slice s 0 99))"; "(defn f [s [u8]] [u8] (slice s 0 99))";
(* The short arities are the three-argument form written out, so they are
held to the same standard: the implicit hi on a fixed array is the same
literal (len a) folds to, and a lo past it is refused here and not later.
A 1-argument slice cannot fail either check 0 and the length are both
in range by construction so what is pinned about it is that it is
accepted on each of the three things slice takes. *)
accepts "the whole of an array" (arr ^ "(defn f [] [i32] (slice a))");
accepts "the tail of an array" (arr ^ "(defn f [] [i32] (slice a 3))");
rejects_check "tail past len" (arr ^ "(defn f [] [i32] (slice a 4))")
~needle:"out of bounds for length 3";
rejects_check "negative tail" (arr ^ "(defn f [] [i32] (slice a -1))")
~needle:"is negative";
accepts "the whole of a slice" "(defn f [s [u8]] [u8] (slice s))";
accepts "the tail of a slice" "(defn f [s [u8]] [u8] (slice s 2))";
rejects_check "slice with no target" "(defn f [] i32 (slice))"
~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";
(* 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
idiomatic, so it is refused rather than written more often. A literal
is not this case the frame holds one for as long as the form it is
written in, which is what the acceptance program sorts. *)
rejects_check "slice of a returned array"
"(defn mk [] [3 i32] [7 8 9]) (defn f [] [i32] (slice (mk)))"
~needle:"a returned array is a temporary";
rejects_check "slice of a returned array, three arguments"
"(defn mk [] [3 i32] [7 8 9]) (defn f [] [i32] (slice (mk) 0 3))"
~needle:"a returned array is a temporary";
accepts "slice of an array literal"
"(defn f [] [i32] (slice [7 8 9]))";
(* A string indexes and slices, and neither is a place: it is a view of
bytes the program does not own a literal's are in constant storage
so there is no store through one to allow. *)
accepts "a string slices" "(defn f [s string] string (slice s 1))";
accepts "a string indexes" "(defn f [s string] u8 (at s 1))";
(* Both routes to a Pindex, because there are two and they do not share a
line of code: one index goes through the [set] arm that checks its own
target, two through [check_place]. The one-index spelling is the one a
person writes, and it is the one that would silently store into
constant data. *)
rejects_check "set through a string"
"(defn f [s string] () (set (at s 0) 65))" ~needle:"not a place";
rejects_check "set through a string, two indices"
"(defn f [s [2 string]] () (set (at s 0 0) 65))" ~needle:"not a place";
(* The mirror, and the one the walk could break by moving the question a
level up: the refusal is about the type being *indexed*, not about the
element that comes out. An array of strings has a string element and
indexes nothing but the array, so assigning a whole one stays legal. *)
accepts "set an array's string element"
"(defn f [s [2 string]] () (set (at s 0) \"world\"))";
rejects_check "set through a string's slice"
"(defn f [s string] () (set (at (slice s 1) 0) 65))"
~needle:"not a place";
(* The address of one is the same question and gets the same answer, so
the message has to fit a reader who asked for a pointer and not a
store. *)
rejects_check "the address of a string's byte"
"(defn f [s string] (Ptr u8) (addr (at s 0)))"
~needle:"take the address of";
(* And a string is still not a [u8]: slicing one does not smuggle a byte
slice out of it. *)
rejects_check "a string slice is not a byte slice"
"(defn g [b [u8]] i32 (len b)) (defn f [s string] i32 (g (slice s)))"
~needle:"expected [u8], found string";
(* (slice-from-ptr p n). The one form in the language whose central claim the (* (slice-from-ptr p n). The one form in the language whose central claim the
compiler cannot check whether n is the truth about what p addresses so compiler cannot check whether n is the truth about what p addresses so
what it does check is worth pinning: the argument really is a pointer, the what it does check is worth pinning: the argument really is a pointer, the