slice takes one, two or three arguments, and reaches a string

A fixed array does not decay to a slice at a call, so passing one to a
function over [$t] meant writing (slice a 0 (len a)) at every call site.
(slice a) is the whole of it now and (slice a n) is the tail from n, filled
in by 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. Neither backend grew an arity case. A
target that is not already a name goes through a slot first, so (slice (f x))
calls f once.

at and slice also reach a string, because (bytes s) was the only route to a
byte and it is about to start copying. (at s i) is the byte, bounds-checked;
(slice s ...) at all three arities answers a string viewing the same bytes,
not a [u8], which would be a writable-looking view of storage the program
does not own.

Neither is a place, and the refusal lives in [indexed] rather than in
check_place, which is the part that matters. There are three routes to a
Pindex and they share no code: check_place, the single-index set arm that
checks its own target, and addr. Asked in check_place, the question is
answered for two of them and missed for the one a person writes — a store
into a string literal compiled, and the backends disagreed about it. So
[indexed] takes a ~place location and asks at every dimension, because
(at g 0 0) over a [[2 string]] reaches the string only at the last step.
One message, and addr gets it too, so it reads as value-versus-place rather
than as a rule about assignment.

Slicing an array a call returned is refused at every arity. The view
outlives the temporary, both backends print whatever the frame reused, and
nothing traps — which was already true of (slice (mk) 0 3) and only
survivable while nobody wrote it. (slice (mk)) is short enough to become a
habit. An array literal is not this case and stays legal.

Two backend cases. emit.ml's element_addr grew the String arm beside the
Slice one. x86.ml's index_len had answered None for a string — correct while
nothing could index one, and a skipped bounds check the moment something
could — and now reads the length word, so both check the same thing.
This commit is contained in:
Joseph Ferano 2026-09-20 23:28:11 +07:00
parent 4a2c04bcc7
commit 700e762b27
8 changed files with 424 additions and 41 deletions

71
FIX.org
View File

@ -4110,3 +4110,74 @@ full pass and reword things."
- A site for user =error= calls. flan_error has no loc parameter; threading - A site for user =error= calls. flan_error has no loc parameter; threading
one through means both backends' call emission. Same lane as above if the one through means both backends' call emission. Same lane as above if the
frame is being touched anyway. frame is being touched anyway.
* 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

@ -6105,3 +6105,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
@ -8790,11 +8883,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

@ -952,6 +952,14 @@ let () =
infers "bytes of a string" "(bytes \"hi\")" "[u8]"; infers "bytes of a string" "(bytes \"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
@ -1839,6 +1847,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