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:
parent
4a2c04bcc7
commit
700e762b27
71
FIX.org
71
FIX.org
@ -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.
|
||||||
|
|||||||
@ -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.
|
||||||
|
|||||||
164
lib/check.ml
164
lib/check.ml
@ -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; \
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user