From 700e762b27e1b56f458941f4cf172e23f481e6de Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sun, 20 Sep 2026 23:28:11 +0700 Subject: [PATCH] slice takes one, two or three arguments, and reaches a string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- FIX.org | 71 +++++++++++++++++ docs/BUILT.md | 81 +++++++++++++++++++ lib/check.ml | 164 ++++++++++++++++++++++++++++++-------- lib/emit.ml | 6 +- lib/x86.ml | 9 ++- test/programs/slices.flan | 35 ++++++++ test/test_acceptance.ml | 20 ++++- test/test_flan.ml | 79 ++++++++++++++++++ 8 files changed, 424 insertions(+), 41 deletions(-) diff --git a/FIX.org b/FIX.org index 1dacf00..9361c4d 100644 --- a/FIX.org +++ b/FIX.org @@ -4110,3 +4110,74 @@ full pass and reword things." - 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 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. diff --git a/docs/BUILT.md b/docs/BUILT.md index 010816d..ca6d105 100644 --- a/docs/BUILT.md +++ b/docs/BUILT.md @@ -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 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. + +## `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. diff --git a/lib/check.ml b/lib/check.ml index f4f8b47..1756e0d 100644 --- a/lib/check.ml +++ b/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 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 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" (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 = match p with | 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 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) | Ast.Pderef target -> let target = check ctx target in @@ -5448,14 +5468,24 @@ and index_expr ctx (e : Ast.expr) = | 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. *) -and indexed ctx (target : Tast.expr) (idx : Ast.expr list) = +(* [(at a i)] and [(at grid row col)]: one index per dimension. + + [~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 | [] -> [], ty | i :: rest -> let elem = match ty with | 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 -> fail i.Ast.loc "%s cannot be indexed" (Types.to_string other) in @@ -7356,35 +7386,98 @@ and named_call ?(qualified = false) ctx ~want loc name args = let idx, ty = indexed ctx target idx in prim Tast.At ty (target :: idx)) | _ -> 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" -> - arity ctx loc name 3 args; (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 elem = match target.Tast.ty with - | Types.Array (_, t) | Types.Slice t -> t - | other -> fail loc "slice takes an array or a slice, found %s" - (Types.to_string other) + let ty = target.Tast.ty in + (* A string slices to a string, not to a [u8]: the result views the + same bytes and is read-only for the same reason the source is, and + calling it a byte slice would hand out a writable-looking view of + 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 - prim Tast.Slice (Types.Slice elem) - (let lo_loc = lo.Ast.loc and hi_loc = hi.Ast.loc in - let lo = check ctx ~want:index_ty lo in - let hi = check ctx ~want:index_ty hi in - let ty = target.Tast.ty 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 - | _ -> ()); - [ target; lo; hi ]) - | _ -> assert false) + (* An array that came back from a call is a value in a temporary this + expression does not own: the slice would outlive it and view + whatever the frame reused those bytes for, with nothing to trap on. + A [let] gives it a name and a lifetime, so that is what the refusal + names. An array *literal* is not this case — it is written here and + the frame holds it for as long as the form it is written in. *) + (match ty, target.Tast.e with + | Types.Array _, (Tast.Call _ | Tast.CallPtr _) -> + fail loc + "this slices an array a call returned, and a returned array is a \ + temporary — the slice would outlive it and view storage the \ + frame has reused. Bind it first: (let [a (…)] (slice a …))" + | _ -> ()); + let int k = mk loc index_ty (Tast.Int (k, Types.I32)) in + (* [hi] is wanted twice only when it is the implicit length of + something whose length is not static. *) + let needs_slot = + List.length bounds < 2 + && (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 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."); ("at", "at [collection i32 ...] T", "The element at an index, bounds-checked — and for a Vec with the \ - allocator's epoch checked first. It is also a place, so \ - (set (at v i) x) goes through the same check."); - ("slice", "slice [[n T]|[T] i32 i32] [T]", - "The half-open range [lo hi) as a non-owning view. A bound may sit one \ - past the end; a literal pair that runs backwards is refused here."); + allocator's epoch checked first. On a string it is the byte, a u8. It \ + is also a place, so (set (at v i) x) goes through the same check; a \ + string is the exception, being a view it does not own."); + ("slice", "slice [[n T]|[T]|string i32? i32?] [T]|string", + "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]", "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; \ diff --git a/lib/emit.ml b/lib/emit.ml index a85c1ef..933f3f4 100644 --- a/lib/emit.ml +++ b/lib/emit.ml @@ -2029,7 +2029,11 @@ and element_addr f (target : Tast.expr) idx = ins f "%s = getelementptr inbounds %s, ptr %s, i64 0, i64 %s" p (ll ty) ptr i64; 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. *) let s = load f ptr ty in let base = fresh f in diff --git a/lib/x86.ml b/lib/x86.ml index b3419cf..b97bf9d 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -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."; ud2 f.b -(* The length an index is checked against, or [None] for the forms [emit.ml] - does not check either: a raw pointer, which has no length, and a string, - which its [element_addr] does not index at all. *) +(* The length an index is checked against, or [None] for the one form + [emit.ml] does not check either: a raw pointer, which has no length. A + 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) = match ty with | Types.Array (n, _) -> Some (`Const n) - | Types.Slice _ -> Some (`At (shift base 8)) + | Types.Slice _ | Types.String -> Some (`At (shift base 8)) | _ -> None and load_len f = function diff --git a/test/programs/slices.flan b/test/programs/slices.flan index c2a5f57..c2ce561 100644 --- a/test/programs/slices.flan +++ b/test/programs/slices.flan @@ -79,4 +79,39 @@ (sort (slice zs 2 3)) (reverse (slice zs 2 3)) (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) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 71d33af..95d2ad2 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -433,12 +433,21 @@ let () = 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 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 = "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\ -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 (* (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 @@ -454,6 +463,13 @@ let () = sfp_out; outputs "slice algorithms" "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 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 diff --git a/test/test_flan.ml b/test/test_flan.ml index 1d9d57f..2e1ba55 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -952,6 +952,14 @@ let () = infers "bytes of a string" "(bytes \"hi\")" "[u8]"; infers "len is i32" "(len (bytes \"hi\"))" "i32"; 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"; (* 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" "(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 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