slice-from-ptr on the x86 backend, and the check that has to be signed

Another lane landed (slice-from-ptr p n) while this backend was not looking,
and it arrived as two refusals rather than one: slice-from-ptr.flan and
bounds.flan both stopped building through --x86. Neither is a new obstacle —
a Slice _ is {ptr, i64} here exactly as it is in emit.ml, so the form is one
store of the pointer and one of the length and no new representation at all.

The half worth writing down is the check. There is nothing to compare the
length against — only the caller knows how many elements live behind that
pointer — so what is checked is that the promise is not absurd, and that test
is *signed*. check_slice's own compares are unsigned, and a negative i32
sign-extended to 64 bits is a huge unsigned value that an unsigned "hi <= len"
waves through; the result would be a slice about 2^64 long that reads as a
pass and faults somewhere else entirely.

Nothing in the corpus walks that path: every length in slice-from-ptr.flan is
a literal, and a negative literal is refused by check.ml before any code is
emitted. So spike/x86/p7-slice-from-ptr.flan takes the length as a parameter
and runs it through a restart-case, which puts the condition's low/high/length
on stdout and compares them against the LLVM build.
This commit is contained in:
Joseph Ferano 2026-09-13 20:27:44 +07:00
parent af3daaabe2
commit bdecd2b8f2
2 changed files with 81 additions and 0 deletions

View File

@ -1900,6 +1900,41 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) dst =
load_loc f ~reg:rcx llo lo.Tast.ty;
sub_rr f.b ~dst:rax ~src:rcx;
store_int f.b ~src:rax ~mm:(lmem f (shift dst 8) ~scratch:r11) ~size:8
(* (slice-from-ptr p n): the two words a slice already is, with the pointer
the caller handed over and the length the caller promised. A [Slice _] is
{ptr, i64} here exactly as it is in [emit.ml], so there is no new
representation to build one store of the pointer and one of the length.
The check is the *length itself* and not a range, because nothing here
knows how many elements live behind that pointer; only the caller does.
So what is checked is the half that can be that the promise is not
absurd and it is a *signed* test, which matters: [check_slice]'s
compares are unsigned, and a negative i32 sign-extended to 64 bits is a
huge unsigned value that [jbe] waves straight through.
It reuses [flan_slice_error] for [emit.ml]'s reason: the violated
condition is 0 <= n, which has the shape of a reversed slice, so the range
is reported as [0 n) against a length of 0. *)
| Tast.SliceFromPtr, [ p; n ] ->
let lp = eval f p in
let ln = eval f n in
if f.md.Emit.checks then
scoped f (fun () ->
let a = ptmp f and b = ptmp f and c = ptmp f in
xor_rr f.b ~dst:rax ~src:rax;
store_int f.b ~src:rax ~mm:(Frame a) ~size:8;
store_int f.b ~src:rax ~mm:(Frame c) ~size:8;
load_loc f ~reg:rax ln n.Tast.ty;
store_int f.b ~src:rax ~mm:(Frame b) ~size:8;
cmp_imm f.b ~dst:rax 0;
let ok = new_label f "inb" in
jcc_lbl f.b ~cc:cc_ge ok;
bounds_call f "flan_slice_error" e.Tast.loc [ a; b; c ];
lbl f.b ok);
load_loc f ~reg:rax lp p.Tast.ty;
store_int f.b ~src:rax ~mm:(lmem f dst ~scratch:r11) ~size:8;
load_loc f ~reg:rax ln n.Tast.ty;
store_int f.b ~src:rax ~mm:(lmem f (shift dst 8) ~scratch:r11) ~size:8
(* string and [u8] are the same two words, so both directions are views and
not copies the same non-instruction [emit.ml] emits. *)
| (Tast.Bytes | Tast.StrOfBytes), [ a ] -> lower f a dst

View File

@ -0,0 +1,46 @@
;;;; (slice-from-ptr p n) with a length the checker cannot see.
;;;;
;;;; test/programs/slice-from-ptr.flan covers the form itself, but every length
;;;; in it is a literal, and a negative literal is refused by check.ml before
;;;; any code is emitted. So the run-time half of the check -- the one both
;;;; backends plant beside the form -- is walked by nothing in the corpus.
;;;;
;;;; The half that matters here is that the test is *signed*. check_slice's own
;;;; compares are unsigned, and a negative i32 sign-extended to 64 bits is a
;;;; huge unsigned value that an unsigned "hi <= len" waves straight through.
;;;; Getting that wrong yields a slice whose length is about 2^64, which reads
;;;; as a pass and segfaults somewhere else entirely.
;;;;
;;;; Both cases go through a restart-case, so what is compared is the message
;;;; on stderr as well as the fact that something was signalled.
(defvar a [4 i32])
(defn promised [n i32] i32
;; n is a parameter, so the checker has no literal to look at.
(restart-case
(let [s (slice-from-ptr (addr (at a 0)) n)]
(len s))
(give-up [] -1)))
(defn show [name string n i64] ()
(print name)
(print " ")
(println n))
(defn main [] ()
(dotimes [i 4]
(set (at a i) (* (+ i 1) 10)))
(handler-bind
[(BoundsError [c]
(show "low" (.low c))
(show "high" (.high c))
(show "length" (.length c))
(invoke-restart 'give-up))]
(println (promised 4)) ; 4 -- the truth
(println (promised 0)) ; 0 -- empty is not an error
(println (promised 2)) ; 2 -- shorter than the truth is legal
(println (promised -1)) ; -1 -- signalled, and the restart answered
(println (promised -1000000))))