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

This commit is contained in:
Joseph Ferano 2026-09-13 23:10:00 +07:00
commit 385445faa7
8 changed files with 1046 additions and 3 deletions

130
HANDOFF-arith.md Normal file
View File

@ -0,0 +1,130 @@
# Arithmetic that has no answer is a condition
Three arithmetic situations had no defined behaviour in Flan, and the two backends disagreed on all three. A divide by
zero was a raw `SIGFPE` — the process died with no message, no location, and nothing to handle. `INT64_MIN / -1` raised
`SIGFPE` under `idiv` and was undefined under LLVM. A float-to-integer cast whose value does not fit produced x86's
fixed "integer indefinite" under one backend and whatever the optimiser liked under the other.
All three now signal `ArithError` with `error`, exactly as an out-of-range index signals `BoundsError` and a failed
allocation signals `StorageExhausted`. A Lisp that dies naming the file and the line beats one that dies with `SIGFPE`,
and a program that genuinely does not care installs a handler once at startup and never thinks about it again.
## The shape
```lisp
(defstruct ArithError [op i32 lhs i64 rhs i64])
```
`op` is a small integer code and not a keyword, because `FileError`'s `op` is already a small integer code
(`prelude.ml:1380`, built at `check.ml:3096`) and the field has to be filled in from C, where a keyword is not a thing
that exists. The codes and what `lhs`/`rhs` hold under each are documented on the `defstruct` itself; the short version
is that a division carries its two operands and a cast carries the destination type's representable range, which is
the violated condition written as a range. That reuse of two fields for two meanings is `BoundsError`'s precedent
exactly — `low` and `high` are one index for an `at` and two ends for a `slice`, so that a handler writes one clause
and not two — and `flan_slice_promise_error` already packs a violated condition into them as `(0, n, 0)`.
There is no location field, because `BoundsError` has none either: the location is an argument to the runtime helper
and is used only in the message printed when nothing answered.
## No restart is established at the failing operation
The brief that started this work sketched `use-value` everywhere and `saturate` where clamping is meaningful. Reading
the existing implementation changed that, and the reason is mechanical rather than a matter of taste.
`runtime/flan_rt.c:70-86` says it: a restart frame is allocated **by the `restart-case` that offers it, on its own
stack**, and a transfer carries that frame's address. The runtime therefore cannot host a restart on a program's
behalf; a `use-value` on division would have to be an `alloca` plus a `flan_restart_push`/`pop` pair emitted at every
division site in every checked build.
That is the identical cost `prelude.ml:70-76` and `flan_rt.c:483-490` already refuse for indexing, in prose, on the
record: a site restart on every operation, buying a silently different answer. Division is if anything the weaker
case — an `at` at least has an element to hand back. So `ArithError` follows `BoundsError`: it signals, a handler may
inspect it and transfer out through a restart the program already established (a frame loop's `continue`), and with
nothing answering it falls through to a message naming the file and the line.
`saturate` on the **cast** arm alone is the one place a site restart might still earn its frame, because casts are rare
and clamping is a canonical answer rather than an arbitrary one. It is not built here and is follow-up shaped.
## The guards ride `--checks`
The same flag as the bounds check, elided with it. The divide guard is a branch *before* the instruction and not a
handler after it, because `SIGFPE` cannot be caught and resumed. Once the zero test is being paid for, the
`INT64_MIN / -1` test is nearly free on the same path.
Unsigned division gets the zero test only; there is no overflow case. Float division is **not** guarded at all:
IEEE `x / 0.0` is `inf`, which is defined and wanted — `rand-f32` in the prelude divides by a float constant.
## The x86 backend, item 3 of `HANDOFF-x86-rt.md`
Landed too, in its own commit and after a rebase onto the dev-loop tip. `check_div` and `check_cast` sit next to
`check_at` and `check_slice` and reuse `bounds_call` unchanged — it already spells the whole shape, the location string
into rdi/rsi, the extras out of frame temporaries, the channel, the guard and the `ud2`. `flan_arith_error` takes three
extras, so the channel lands in r9 and the argument registers are exactly full.
Three things differ from the LLVM side, all of them because the instruction set does:
- **Two branches rather than one branch and a `select`.** There is no `select` here, and a second compare on the cold
path costs nothing. The ordinary path still pays one compare and one not-taken branch, which is what `emit.ml` pays.
- **The cast bounds are compared in the source's own precision** instead of widening the value to a double first. Every
bound is a power of two and therefore exact in an `f32` as well as an `f64`, so the two routes answer identically —
and the survey is there to say so, which is why `arith.flan` has an `f32` case.
- **NaN is excluded by choosing the direction of each compare.** `ucomis` sets CF, ZF and PF together when either
operand is unordered, so the low test jumps to the failure on "below" (which a NaN takes) and the high test swaps its
operands to ask "hi > v" (which a NaN answers false).
The `INT_MIN / -1` test compares against the *narrow* type's most negative value in a 64-bit register, which is sound
because `load_loc` has already widened both operands according to their own signedness. That case is where the two
backends used to diverge silently rather than both dying: x86 divided in 64 bits and truncated on the store, producing
`-2147483648` for an `i32`, where LLVM emitted poison. `arith.flan` has an `i32` case for exactly that reason.
`spike/x86/survey.sh` is 101 MATCH / 0 DIFFER / 0 REFUSED, with the two programs this change adds among them.
`arith.flan` carries an `i32` overflow case and an `f32` cast case on purpose, and neither is padding. The `i32`
overflow is where the two backends disagreed *silently* rather than both dying, and it is the only thing that
exercises the widening on the way into the condition — if that were wrong, the number in the message would be garbage
and every other case would still pass. The `f32` cast is the one place the two backends reach the same answer by
deliberately different routes, and the survey is what says the routes agree rather than the comment above them.
## What landed
**`lib/prelude.ml`** — `(defstruct ArithError [op i32 lhs i64 rhs i64])`, immediately after `BoundsError`, with the
codes and the per-code meaning of `lhs`/`rhs` written on it.
**`runtime/flan_rt.c`** — `flan_arith_error`, built out of the same three pieces `flan_bounds_error` is: fill a
`flan_arith_cond` on this frame, `flan_signal`, try `flan_break_hook`, and fall through to a sentence and `rt_die()` if
neither answered. `flan_arith_fail` is the sentence, and there is one per code rather than one shared "overflow",
because the reader who reaches `(/ min -1)` has probably never had to think about that case.
**`lib/emit.ml`** — `check_div` and `check_cast`, next to `check_at` and `check_slice` and built on the same
`signal_block`, so an answered failure leaves through the innermost pad and runs the defers. `check_div` is called from
`prim`'s `Div`/`Rem` arm and `check_cast` from `cast`'s `Float -> Int` arm.
Three things in those two functions are worth knowing:
- **Integer division only.** IEEE `x / 0.0` is an infinity and is a defined answer somebody may want — the prelude's
own `rand-f32` divides by a float constant — so guarding a float division would be refusing a result the language
already promises.
- **One branch, not two.** The zero test and the `INT_MIN / -1` test are `or`-ed into a single compare-and-branch, and
*which* of them fired is decided by a `select` that is dead on the fall-through path. The guard is also dropped
outright when the divisor is a literal that cannot trigger it, which is nearly every division anyone writes.
- **The cast test is exact and catches NaN.** Both bounds are powers of two and therefore exact in a double, an `f32`
source is `fpext`-ed first so there is one set of bounds rather than two, and the comparisons are *ordered*, which is
what makes a NaN fail both halves instead of passing both.
**`test/programs/arith.flan`** — the unhandled half, one case per argument in `bounds.flan`'s shape. Case 0 is the one
that must not die and is four shapes rather than one: a dynamic divisor, a literal one the guard drops, unsigned
division, and a float division by zero.
**`test/programs/arith-condition.flan`** — the answered half, in `bounds-condition.flan`'s shape: a `handler-bind`
clause that reads the condition and takes a frame loop's `continue`. Five codes, four frames finishing, eight
abandoned, and twelve defers run.
**`test/test_acceptance.ml`** — both programs at `-O0` and `-O2`, the answered one also as a dev build, and the
`--checks`-off case asserted on the IR rather than by running an unchecked program, because an unchecked divide by zero
has no defined behaviour to assert on — it is the SIGFPE this change exists to replace.
## The tests the brief asked for, and the one substitution
Unhandled-with-a-location, a `handler-bind` that inspects the condition, and `--checks` off are all there. The
`use-value` restart case is **not**, because no `use-value` restart is established — see above. What stands in its
place is the same thing that stands in for it in `bounds-condition.flan`: a handler that transfers out through a
restart the program already had.

View File

@ -769,6 +769,166 @@ let signal_block f (loc : Loc.t) ~guard ok emit_call =
term f "unreachable";
label f good
(* ── Arithmetic with no answer ─────────────────────────────────────────
Three situations that had no defined behaviour until now, and they share a
helper for the same reason the two bounds checks share one: they are one
condition, ArithError, and a handler should write one clause and not five.
The shape is [signal_block]'s and not [fail_block]'s, so an answered
failure leaves through the innermost pad and runs the defers on its way
out, exactly as an answered bad index does.
The thing worth knowing about the divide guard is that it is a branch
*before* the instruction rather than anything after it. A SIGFPE cannot be
caught and resumed, so there is no version of this that tests afterwards;
the branch is the price of the operation having a defined behaviour at all,
not the price of that behaviour being a condition. Which is also why the
overflow test rides along for nearly nothing: the zero test has already
put a compare and a branch on the path, and (/ min -1) is two more compares
folded into the same one. *)
(* The codes flan_arith_fail switches on, and the ones the prelude's
ArithError documents. They are named here rather than written as bare
numbers at the call sites, because a bare number at a call site is exactly
the kind of agreement that drifts. *)
let arith_div_zero = 0
let arith_rem_zero = 1
let arith_div_overflow = 2
let arith_rem_overflow = 3
let arith_cast_range = 4
(* ArithError's fields are i64 and an operand may be narrower, so every
operand is widened on the way into the condition signed or not according
to its own type, so that (-1 : i8) reads back as -1 and (255 : u8) reads
back as 255. A u64 above 2^63 still reinterprets as negative, which the
prelude says out loud and which a fourth field is not worth fixing. *)
let widen f (k : Types.ikind) v =
if Types.bits k = 64 then v
else begin
let t = fresh f in
ins f "%s = %s %s %s to i64" t
(if Types.signed k then "sext" else "zext") (ll (Types.Int k)) v;
t
end
(* The most negative value of a signed kind, as the decimal LLVM wants. *)
let int_min k = Int64.neg (Int64.shift_left 1L (Types.bits k - 1))
(* A divide or a remainder. [is_rem] only picks which pair of codes is used;
the tests are identical, because `srem` overflows on exactly the operands
`sdiv` does the intermediate quotient is the thing that does not fit.
Both tests are elided when the divisor is a literal that cannot trigger
them, which matters more than it looks: (/ x 2) is the common case, and
without this every one of them would carry a branch forever. *)
let check_div f ~guard loc ~is_rem (k : Types.ikind) ~lit a b =
if f.md.checks then begin
let ty = ll (Types.Int k) in
let need_zero = match lit with Some n -> Int64.equal n 0L | None -> true in
let need_ovf =
Types.signed k
&& (match lit with Some n -> Int64.equal n (-1L) | None -> true)
in
if need_zero || need_ovf then begin
(* [false] rather than an emitted instruction when a test is elided: an
LLVM operand may be a constant, and the [or] and the [select] below
then fold to nothing without a special case for either shape. *)
let bad_zero =
if need_zero then begin
let t = fresh f in
ins f "%s = icmp eq %s %s, 0" t ty b;
t
end
else "false"
in
let bad =
if need_ovf then begin
let lo = fresh f in
ins f "%s = icmp eq %s %s, %Ld" lo ty a (int_min k);
let neg1 = fresh f in
ins f "%s = icmp eq %s %s, -1" neg1 ty b;
let ovf = fresh f in
ins f "%s = and i1 %s, %s" ovf lo neg1;
let t = fresh f in
ins f "%s = or i1 %s, %s" t bad_zero ovf;
t
end
else bad_zero
in
let ok = fresh f in
ins f "%s = xor i1 %s, true" ok bad;
(* Which of the two it was is decided with a [select] rather than with a
second branch, so the hot path keeps the single compare-and-branch the
zero test already cost. The select is dead on the fall-through and any
optimiser sinks it into the cold block; at -O0 it is one instruction
nobody is going to notice next to a division. *)
let code = fresh f in
ins f "%s = select i1 %s, i32 %d, i32 %d" code bad_zero
(if is_rem then arith_rem_zero else arith_div_zero)
(if is_rem then arith_rem_overflow else arith_div_overflow);
let aw = widen f k a and bw = widen f k b in
signal_block f loc ~guard ok (fun id n ->
ins f
"call void @flan_arith_error(ptr %s, i64 %d, i32 %s, i64 %s, i64 %s, \
ptr %s)"
id n code aw bw xfer_param)
end
end
(* A float to integer cast whose value does not fit. The two bounds are exact
in a double for every integer kind up to 64 bits both are powers of two
so the test is exact rather than approximate, and it is written as
[lo <= v < hi] with an *open* top because the top bound is 2^(n-1) or 2^n
itself, which is the first value that does not fit rather than the last one
that does.
Ordered comparisons, which is what makes NaN fail both of them. That is
wanted: a NaN cast to an integer is as undefined as a value out of range
and would otherwise walk straight through the guard.
An f32 source is extended to a double first. The extension is exact and
costs an instruction, and it buys writing one set of bounds instead of two
and never having to ask whether 2^63 is representable in the narrower
type. *)
let check_cast f ~guard loc (src : Types.fkind) (k : Types.ikind) v =
if f.md.checks then begin
let v =
match src with
| Types.F64 -> v
| Types.F32 ->
let t = fresh f in
ins f "%s = fpext float %s to double" t v;
t
in
let n = Types.bits k in
let signed = Types.signed k in
(* The first value below the range and the first value above it, and then
the range the condition reports, which is the last value *in* it. *)
let lo_f = if signed then ldexp (-1.0) (n - 1) else 0.0 in
let hi_f = if signed then ldexp 1.0 (n - 1) else ldexp 1.0 n in
let lo_i = if signed then int_min k else 0L in
let hi_i =
if signed then Int64.sub (Int64.shift_left 1L (n - 1)) 1L
else if n = 64 then -1L
else Int64.sub (Int64.shift_left 1L n) 1L
in
(* LLVM takes a double constant as the hex of its bits, which is the only
spelling that cannot lose anything on the way through. *)
let dbl x = Printf.sprintf "0x%016Lx" (Int64.bits_of_float x) in
let a = fresh f in
ins f "%s = fcmp oge double %s, %s" a v (dbl lo_f);
let b = fresh f in
ins f "%s = fcmp olt double %s, %s" b v (dbl hi_f);
let ok = fresh f in
ins f "%s = and i1 %s, %s" ok a b;
signal_block f loc ~guard ok (fun id nn ->
ins f
"call void @flan_arith_error(ptr %s, i64 %d, i32 %d, i64 %Ld, i64 %Ld, \
ptr %s)"
id nn arith_cast_range lo_i hi_i xfer_param)
end
(* [at] is strict: the last valid index is len - 1. *)
let check_at f ~guard loc idx len =
if f.md.checks then begin
@ -1718,6 +1878,20 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
| Types.Int k, _ -> if Types.signed k then "srem" else "urem"
| t, _ -> failwith ("arithmetic on " ^ Types.to_string t)
in
(* A divide or a remainder by zero, and the one division that overflows,
signal ArithError. Integers only: IEEE says x / 0.0 is an infinity and
that is a defined answer somebody may want the prelude's own
[rand-f32] divides by a float constant so guarding a float division
would be refusing a result the language already promises.
A literal divisor is handed through so that the guard can be dropped
when it cannot fire, which is nearly every division anyone writes. *)
(match x.Tast.ty, p with
| Types.Int k, (Tast.Div | Tast.Rem) ->
let lit = match y.Tast.e with Tast.Int (n, _) -> Some n | _ -> None in
check_div f ~guard:(fun () -> guard f) e.Tast.loc
~is_rem:(p = Tast.Rem) k ~lit a b
| _ -> ());
let t = fresh f in
(* No nsw/nuw: arithmetic wraps (plan.org, Types). *)
ins f "%s = %s %s %s, %s" t op (ll x.Tast.ty) a b;
@ -1967,7 +2141,7 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) =
| Tast.SizeOf t, [] -> Printf.sprintf "%d" (fst (lay f.md t))
| Tast.AlignOf t, [] -> Printf.sprintf "%d" (snd (lay f.md t))
| Tast.AddrOf, [ x ] -> addr f x
| Tast.Cast target, [ x ] -> cast f x target
| Tast.Cast target, [ x ] -> cast f ~guard:(fun () -> guard f) x target
| _ -> failwith "malformed primitive"
(* A slice argument crosses to C as ptr+len, never as a struct by value. *)
@ -1999,7 +2173,7 @@ and shim_in_out f name (x : Tast.expr) =
ins f "call void %s(ptr %s, i64 %s, ptr %s)" name p n tmp;
load f tmp (Types.Slice (Types.Int Types.U8))
and cast f (x : Tast.expr) target =
and cast f ~guard (x : Tast.expr) target =
let v = value f x in
(* An enum is an i32 at run time and its own type only in the checker, so a
cast involving one is a cast on that i32. Nothing in the surface language
@ -2017,6 +2191,7 @@ and cast f (x : Tast.expr) target =
| t -> t
in
let src = concrete x.Tast.ty and target = concrete target in
let src_loc = x.Tast.loc in
if Types.equal src target then v
else
let op =
@ -2026,7 +2201,14 @@ and cast f (x : Tast.expr) target =
else if Types.bits b = Types.bits a then "bitcast"
else if Types.signed a then "sext" else "zext"
| Types.Int a, Types.Float _ -> if Types.signed a then "sitofp" else "uitofp"
| Types.Float _, Types.Int b -> if Types.signed b then "fptosi" else "fptoui"
(* The one cast that can have no answer. LLVM calls an out-of-range
fptosi undefined and will fold it to anything; x86 produces a fixed
"integer indefinite". Neither is a result, so the value is tested
against the destination's range first and signals ArithError when it
misses. See [check_cast], which is also where NaN is dealt with. *)
| Types.Float a, Types.Int b ->
check_cast f ~guard src_loc a b v;
if Types.signed b then "fptosi" else "fptoui"
| Types.Float a, Types.Float b ->
if Types.bits_f b > Types.bits_f a then "fpext" else "fptrunc"
(* Nothing in the surface language writes this: [check.ml] has no cast
@ -2435,6 +2617,10 @@ declare void @flan_transfer_fail(ptr, i64) noreturn cold
declare void @flan_bounds_error(ptr, i64, i64, i64, ptr) cold
declare void @flan_slice_error(ptr, i64, i64, i64, i64, ptr) cold
declare void @flan_slice_promise_error(ptr, i64, i64, ptr) cold
; The same shape and the same reason: it signals ArithError and returns when
; something answered it. The i32 is the op code and the two i64s are the
; operands, or the destination's range for a cast.
declare void @flan_arith_error(ptr, i64, i32, i64, i64, ptr) cold
declare ptr @flan_context_allocator()
declare ptr @flan_context_temp()
declare ptr @flan_heap_allocator()

View File

@ -82,6 +82,64 @@ let source = {flan|
;; the default side of the rule.
(defstruct BoundsError [low i64 high i64 length i64])
;; What an arithmetic operation with no answer signals. Three situations, and
;; until now none of them had a defined behaviour: a divide or remainder by
;; zero, which was a raw SIGFPE with no message and no location; the one
;; division that overflows, (/ most-negative -1), whose true quotient is one
;; past the top of the type and which `idiv` also makes a SIGFPE; and a float
;; to integer cast whose value does not fit, where x86 produces a fixed
;; "integer indefinite" and LLVM calls the whole thing undefined and may fold
;; it to anything.
;;
;; They signal, for the reason spelled out over BoundsError and for one more
;; that is specific to these: a SIGFPE cannot be caught and resumed, so the
;; only way to get a message naming the file and the line is to test *before*
;; the instruction. Once that branch is being paid for, making it a condition
;; rather than a die costs nothing further, and a program that genuinely does
;; not care installs a handler once at startup and never thinks about it again.
;;
;; Same shape as the two above, and for the same reasons: fixed numeric
;; fields, no rendered message, nothing that allocates. It is signalled from
;; the runtime flan_arith_error in runtime/flan_rt.c so **these three
;; fields are a C struct that has to agree with this one field for field**,
;; the same hand-kept agreement flan_bounds_cond keeps with BoundsError.
;;
;; `op` is a small integer and not a keyword, exactly as FileError's `op` is,
;; because the field is filled in from C and a keyword is not a thing that
;; exists there. The codes:
;;
;; 0 (/ a 0) 1 (% a 0)
;; 2 (/ min -1) 3 (% min -1)
;; 4 a float to integer cast whose value does not fit
;;
;; `lhs` and `rhs` are the two operands for codes 0 through 3 and the
;; destination type's representable range for code 4 the violated condition
;; written as a range, which is what flan_slice_promise_error already does
;; with BoundsError's fields. Two meanings over two fields rather than two
;; condition types, so that a handler writes one clause and not five. The
;; value that did not fit is not carried, because it is a float and these
;; fields are not; what the handler needs in order to say something useful is
;; the range it missed.
;;
;; The fields are the low 64 bits of whatever they hold. A u64 operand above
;; 2^63 therefore reads back negative, which is the same reinterpretation
;; every i64 field in every condition here makes and is not worth a fourth
;; field to fix.
;;
;; **No restart is established at the failing operation**, which is
;; BoundsError's decision and not StorageExhausted's. The sketch this started
;; from asked for `use-value`, and the implementation is what ruled it out: a
;; restart frame is allocated by the restart-case that offers it, on its own
;; stack, and a transfer carries that frame's address (runtime/flan_rt.c, the
;; restart stack). The runtime cannot hold one on a program's behalf, so
;; `use-value` here would mean an alloca and a restart frame emitted at every
;; division in every checked build the identical cost refused for indexing
;; a few lines above, buying a silently different answer. What answers a
;; division by zero is the restart the program already established, a frame
;; loop's `continue`, which is reachable from a handler without anything being
;; pushed here.
(defstruct ArithError [op i32 lhs i64 rhs i64])
;; A breakpoint. (pause) stops the program where it stands and hands it to the
;; break loop, with the whole stack under it readable C-c C-b lists the
;; frames, TAB opens one, and taking `continue` resumes at the call.

View File

@ -1752,6 +1752,140 @@ and check_slice f (base : loc) (ty : Types.t) (loc : Loc.t) (lo : Tast.expr)
bounds_call f "flan_slice_error" loc [ a; b; c ];
lbl f.b ok)
(* [emit.ml]'s [check_div] and [check_cast], item 3 of HANDOFF-x86-rt.md's
list, and the one item on it that was blocked on a language decision rather
than on code. That decision is in HANDOFF-arith.md: a divide or remainder by
zero, the one division that overflows, and a float to integer cast whose
value does not fit all signal ArithError, exactly as a bad index signals
BoundsError.
Why it could not be left to the hardware, which is the temptation here and
is what this backend did until now. `idiv` raises SIGFPE on both the zero
and the overflow case, and a SIGFPE cannot be caught and resumed so there
is no version of this that tests afterwards, and nothing that dies with a
location. The other backend calls the same three situations undefined and
folds them to whatever it likes. Neither is a behaviour a program can be
written against, and the two disagreed, which is what a survey diff would
eventually have found the hard way.
These reuse [bounds_call] unchanged: it spells the whole shape the
location string into rdi/rsi, the extra arguments out of frame temporaries
into rdx/rcx/r8/r9, the channel after them, the guard, and the [ud2] that
stands where [emit.ml] writes [unreachable]. flan_arith_error takes three
extras, so the channel lands in r9 and the register file is exactly full. *)
(* The codes are [emit.ml]'s, read from there rather than copied: they are an
agreement with flan_arith_fail in the runtime, and an agreement kept in two
places is an agreement that drifts. *)
(* rax holds the dividend and rcx the divisor, both already widened to 64 bits
by [load_loc] according to their own signedness which is what lets the
overflow test compare against the *narrow* type's most negative value in a
64-bit register and mean it.
Two tests and two ways out rather than [emit.ml]'s single branch with a
[select], because there is no [select] here and a second compare on the cold
path is free. The ordinary path still pays one compare and one
not-taken branch, which is the same as over there.
Both tests are dropped when a literal divisor cannot trigger them. That is
not a micro-optimisation: (/ x 2) is the common case and would otherwise
carry a compare and a branch forever. *)
and check_div f (loc : Loc.t) ~is_rem (k : Types.ikind) ~lit =
if f.md.Emit.checks then begin
let need_zero = match lit with Some n -> Int64.equal n 0L | None -> true in
let need_ovf =
Types.signed k
&& (match lit with Some n -> Int64.equal n (-1L) | None -> true)
in
if need_zero || need_ovf then
scoped f (fun () ->
let so = ptmp f and sa = ptmp f and sb = ptmp f in
store_int f.b ~src:rax ~mm:(Frame sa) ~size:8;
store_int f.b ~src:rcx ~mm:(Frame sb) ~size:8;
let ok = new_label f "arith" and bad = new_label f "arithbad" in
let zcode = if is_rem then Emit.arith_rem_zero else Emit.arith_div_zero in
let ocode = if is_rem then Emit.arith_rem_overflow else Emit.arith_div_overflow in
if need_zero then begin
let nz = new_label f "arithnz" in
cmp_imm f.b ~dst:rcx 0;
jcc_lbl f.b ~cc:cc_ne nz;
imm_into f ~reg:rdx (Int64.of_int zcode);
store_int f.b ~src:rdx ~mm:(Frame so) ~size:8;
jmp_lbl f.b bad;
lbl f.b nz
end;
if need_ovf then begin
cmp_imm f.b ~dst:rcx (-1);
jcc_lbl f.b ~cc:cc_ne ok;
(* Through r11 rather than as an immediate: the most negative i64
does not fit the imm32 [cmp_imm] encodes, and one spelling for
every width beats a special case for the one that does not. *)
imm_into f ~reg:r11 (Int64.neg (Int64.shift_left 1L (Types.bits k - 1)));
cmp_rr f.b ~a:rax ~c:r11;
jcc_lbl f.b ~cc:cc_ne ok;
imm_into f ~reg:rdx (Int64.of_int ocode);
store_int f.b ~src:rdx ~mm:(Frame so) ~size:8
end
else jmp_lbl f.b ok;
lbl f.b bad;
bounds_call f "flan_arith_error" loc [ so; sa; sb ];
lbl f.b ok;
(* rdx is the high half of the dividend and [cqo] is what fills it, so
whatever the overflow test left there does not survive; rax and rcx
are untouched on this path and do not need reloading. *)
())
end
(* A float to integer cast whose value does not fit. xmm0 holds the value, in
the *source's* precision, and the bounds are compared in that same precision
rather than widened to a double first the way [emit.ml] does it: both bounds
are powers of two, so both are exact in an f32 as well as in an f64, and the
two tests therefore answer identically. Doing it here saves a conversion and
a second live xmm register.
The direction of each compare is chosen so that a NaN fails both. [ucomis]
sets CF, ZF and PF together when either operand is unordered, so the test
for the low end is written as "jump to the failure when below", which a NaN
takes, and the test for the high end swaps its operands and asks the same
question the other way round. A NaN cast to an integer is exactly as
undefined as 1e300 is and has no business walking through the guard. *)
and check_cast f (loc : Loc.t) (src : Types.fkind) (k : Types.ikind) =
if f.md.Emit.checks then begin
let f64 = (src = Types.F64) in
let n = Types.bits k in
let signed = Types.signed k in
let lo_f = if signed then ldexp (-1.0) (n - 1) else 0.0 in
let hi_f = if signed then ldexp 1.0 (n - 1) else ldexp 1.0 n in
let lo_i = if signed then Int64.neg (Int64.shift_left 1L (n - 1)) else 0L in
let hi_i =
if signed then Int64.sub (Int64.shift_left 1L (n - 1)) 1L
else if n = 64 then -1L
else Int64.sub (Int64.shift_left 1L n) 1L
in
let klo = float_const f lo_f ~f64 and khi = float_const f hi_f ~f64 in
scoped f (fun () ->
let so = ptmp f and sa = ptmp f and sb = ptmp f in
let ok = new_label f "fits" and bad = new_label f "nofit" in
fload f.b ~dst:1 ~mm:(Sym (klo, 0)) ~f64;
ucomis f.b ~f64 ~a:xmm0 ~c:1;
jcc_lbl f.b ~cc:cc_b bad;
fload f.b ~dst:1 ~mm:(Sym (khi, 0)) ~f64;
(* The operands the other way round, so that the code asked for is one a
NaN answers false to: this is "hi > v" and not "v < hi". *)
ucomis f.b ~f64 ~a:1 ~c:xmm0;
jcc_lbl f.b ~cc:cc_a ok;
lbl f.b bad;
imm_into f ~reg:rax (Int64.of_int Emit.arith_cast_range);
store_int f.b ~src:rax ~mm:(Frame so) ~size:8;
imm_into f ~reg:rax lo_i;
store_int f.b ~src:rax ~mm:(Frame sa) ~size:8;
imm_into f ~reg:rax hi_i;
store_int f.b ~src:rax ~mm:(Frame sb) ~size:8;
bounds_call f "flan_arith_error" loc [ so; sa; sb ];
lbl f.b ok)
end
and element f (base : loc) (ty : Types.t) (i : Tast.expr) : loc =
let elem =
match ty with
@ -1949,6 +2083,18 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) dst =
| Tast.Shl -> shl_cl f.b ~dst:rax
| Tast.Shr -> if signed then sar_cl f.b ~dst:rax else shr_cl f.b ~dst:rax
| Tast.Div | Tast.Rem ->
(* The guard goes *before* the instruction, which is the whole of why
it has to be emitted at all: `idiv` raises SIGFPE on both the zero
and the overflow case and a SIGFPE cannot be caught and resumed.
Integers only this arm is already inside the non-float half
because IEEE x / 0.0 is an infinity and is a defined answer. *)
(match t with
| Types.Int k ->
let lit =
match b.Tast.e with Tast.Int (n, _) -> Some n | _ -> None
in
check_div f e.Tast.loc ~is_rem:(p = Tast.Rem) k ~lit
| _ -> ());
if signed then (cqo f.b; idiv_r f.b ~src:rcx)
else (xor_rr f.b ~dst:rdx ~src:rdx; div_r f.b ~src:rcx);
if p = Tast.Rem then mov_rr f.b ~dst:rax ~src:rdx
@ -2153,6 +2299,13 @@ and cast f (a : Tast.expr) (target : Types.t) dst =
fstore f.b ~src:xmm0 ~mm:(lmem f dst ~scratch:r11) ~f64:(f64_of dst_t)
| true, false ->
fload f.b ~dst:xmm0 ~mm:(lmem f l ~scratch:r11) ~f64:(f64_of src_t);
(* [cvttsd2si] answers a fixed "integer indefinite" for a value out of
range, which is a number rather than an answer and the other backend
calls the same cast undefined and will fold it to anything. So the
value is tested against the destination's range first. *)
(match src_t, dst_t with
| Types.Float sk, Types.Int k -> check_cast f a.Tast.loc sk k
| _ -> ());
cvttf2si f.b ~f64:(f64_of src_t) ~dst:rax ~src:xmm0;
store_loc f ~reg:rax dst dst_t

View File

@ -585,6 +585,112 @@ void flan_slice_promise_error(const uint8_t *loc, int64_t loclen, int64_t n,
flan_slice_promise_fail(loc, loclen, n);
}
/* ── Arithmetic with no answer is a condition ──────────────────
*
* Three situations, and until now none of them had a defined behaviour.
* A divide or remainder by zero was a raw SIGFPE: the process died with no
* message, no location, and nothing to handle. (/ INT64_MIN -1) is the one
* division that overflows its true quotient is one past the top of the
* type -- and `idiv` makes that a SIGFPE too, where LLVM calls it undefined.
* And a float to integer cast whose value does not fit produces x86's fixed
* "integer indefinite" under one backend and whatever the optimiser feels
* like under the other.
*
* They now signal ArithError, and the argument is the one made for
* BoundsError above with one addition that is specific to these: a SIGFPE
* cannot be caught and resumed, so the only way to get a message naming the
* file and the line at all is a test *before* the instruction. The emitted
* branch is therefore not the price of making this a condition it is the
* price of it having any defined behaviour at all and once it is being
* paid, signalling rather than dying costs nothing further.
*
* **No restart is established here**, which is BoundsError's decision rather
* than StorageExhausted's, and here it is forced rather than chosen. A
* restart frame is allocated by the restart-case that offers it, on its own
* stack, and a transfer carries that frame's address see the restart stack
* at the top of this file. Nothing in C can push one on a program's behalf,
* so a `use-value` at the failing division would have to be an alloca and a
* push/pop emitted at every division in every checked build. That is the same
* cost refused for indexing, buying a silently different answer, and division
* is the weaker case of the two: an `at` at least has an element to hand
* back. What answers this is the restart the program already established.
*
* The condition is three fields on this frame and must agree field for field
* with the prelude's (defstruct ArithError [op i32 lhs i64 rhs i64]) the
* same hand-kept agreement flan_bounds_cond has with BoundsError. `op` is one
* of the codes below; `lhs` and `rhs` are the two operands for a division and
* the destination type's representable range for a cast, which is the
* violated condition written as a range, exactly as flan_slice_promise_error
* writes one into BoundsError's fields. */
enum {
FLAN_ARITH_DIV_ZERO = 0,
FLAN_ARITH_REM_ZERO = 1,
FLAN_ARITH_DIV_OVERFLOW = 2,
FLAN_ARITH_REM_OVERFLOW = 3,
FLAN_ARITH_CAST_RANGE = 4
};
typedef struct { int32_t op; int64_t lhs, rhs; } flan_arith_cond;
static const uint8_t flan_arith_name[] = "ArithError";
#define FLAN_ARITH_NAMELEN 10
/* The sentence each code gets when nothing answered. It is separate from the
* struct because the condition deliberately carries no rendered message:
* formatting is the unhandled path's job, and this is the unhandled path. */
static void flan_arith_fail(const uint8_t *loc, int64_t loclen, int32_t op,
int64_t lhs, int64_t rhs) {
fflush(stdout);
switch (op) {
case FLAN_ARITH_DIV_ZERO:
fprintf(stderr, "%.*s: divide by zero: (/ %lld 0)\n", (int)loclen,
(const char *)loc, (long long)lhs);
break;
case FLAN_ARITH_REM_ZERO:
fprintf(stderr, "%.*s: remainder by zero: (%% %lld 0)\n", (int)loclen,
(const char *)loc, (long long)lhs);
break;
/* Worth its own sentence rather than sharing the word "overflow", because
* the reader who hits it has probably never had to think about this case:
* it is the single pair of operands in the whole type for which a division
* overflows, and it overshoots by exactly one. */
case FLAN_ARITH_DIV_OVERFLOW:
case FLAN_ARITH_REM_OVERFLOW:
fprintf(stderr,
"%.*s: (%s %lld %lld) overflows: the quotient is one past the "
"largest value the type holds, and this is the only pair of "
"operands for which that is true\n",
(int)loclen, (const char *)loc,
op == FLAN_ARITH_DIV_OVERFLOW ? "/" : "%", (long long)lhs,
(long long)rhs);
break;
default:
fprintf(stderr,
"%.*s: this value does not fit the integer type it is cast to, "
"which holds [%lld %lld]\n",
(int)loclen, (const char *)loc, (long long)lhs, (long long)rhs);
break;
}
rt_die();
}
void flan_arith_error(const uint8_t *loc, int64_t loclen, int32_t op,
int64_t lhs, int64_t rhs, void *xfer) {
flan_arith_cond c;
uint32_t id = flan_name_id(flan_arith_name, FLAN_ARITH_NAMELEN);
c.op = op;
c.lhs = lhs;
c.rhs = rhs;
flan_signal(id, &c, xfer);
if (*(void **)xfer != NULL) return;
if (flan_break_hook != NULL) {
flan_break_hook(flan_arith_name, FLAN_ARITH_NAMELEN, &c, xfer);
if (*(void **)xfer != NULL) return;
}
flan_arith_fail(loc, loclen, op, lhs, rhs);
}
/* ── Allocators, spec-memory.md ────────────────────────────────────────
*
* One type-erased procedure plus an opaque data pointer, which is Odin's

View File

@ -0,0 +1,199 @@
;;;; Arithmetic with no answer is a condition, not a SIGFPE.
;;;;
;;;; Three situations had no defined behaviour in this language until now, and
;;;; the two backends disagreed on all three. A divide or remainder by zero was
;;;; a raw SIGFPE: the process died with no message, no location, and nothing
;;;; to handle — which is the worst failure in the whole system, because it
;;;; tells the programmer less than a segfault does. (/ min -1) is the one
;;;; division whose true quotient is one past the top of the type, and `idiv`
;;;; makes that a SIGFPE too where LLVM calls it undefined. And a float to
;;;; integer cast whose value does not fit produces x86's fixed "integer
;;;; indefinite" under one backend and whatever the optimiser feels like under
;;;; the other.
;;;;
;;;; All three now signal ArithError with `error`, the same way a bad index
;;;; signals BoundsError. This program is the "something answered" half; the
;;;; unhandled half is arith.flan, which dies with a sentence naming the file,
;;;; the line and the operands.
;;;;
;;;; The decision worth reading this file for is the same one BoundsError
;;;; made, and here it was forced rather than chosen. **No restart is
;;;; established at the failing operation.** A restart frame is allocated by
;;;; the restart-case that offers it, on its own stack, and a transfer carries
;;;; that frame's address — so nothing in the runtime can push one on a
;;;; program's behalf, and a `use-value` at the failing division would mean an
;;;; alloca and a restart frame emitted at every division in every checked
;;;; build. That is the cost already refused for indexing, buying a silently
;;;; different answer, and division is the weaker case of the two: an `at` at
;;;; least has an element to hand back. What answers a division by zero here is
;;;; the restart the program already had — a frame loop's `continue`, which is
;;;; sand.flan's shape and is what a game wants: abandon this frame, keep the
;;;; window open.
;;;;
;;;; Four things are asserted:
;;;;
;;;; 1. The frame is abandoned and the program carries on, over every one of
;;;; the five codes.
;;;; 2. Defers run. An answered arithmetic failure leaves through the same
;;;; unwind path a `return` uses, so the defers run innermost first; a
;;;; SIGFPE ran none, and could not have.
;;;; 3. The condition carries the numbers: `op` says which of the five, and
;;;; `lhs`/`rhs` are the two operands for a division and the destination
;;;; type's representable range for a cast.
;;;; 4. Division that is fine stays fine, including the two shapes the guard
;;;; is allowed to elide — a literal divisor that is neither 0 nor -1, and
;;;; unsigned division, which has no overflow case at all.
(defvar frames i64)
(defvar skipped i64)
(defvar cleaned i64)
(defvar op i32)
(defvar lhs i64)
(defvar rhs i64)
;;; Globals rather than locals because a handler cannot see the locals of the
;;; function that established it — check.ml refuses a capture by name and says
;;; to use a global.
(defvar zero i64)
(defvar neg1 i64 -1)
(defvar big i64 9223372036854775807)
(defvar huge f64 1e300)
(defvar small f64 -1e300)
(defvar uz u32)
(defn show [name string n i64] ()
(print name) (print " ") (print n) (println ""))
;;; Two frames deep with a defer on the way, so the transfer has something to
;;; cross and something to run on its way out.
(defn divide [a i64 b i64] i64
(defer (set cleaned (+ cleaned 1)))
(/ a b))
(defn remainder [a i64 b i64] i64
(defer (set cleaned (+ cleaned 1)))
(% a b))
(defn narrow [x f64] i64
(defer (set cleaned (+ cleaned 1)))
(i64 x))
(defn narrow-8 [x f64] i8
(defer (set cleaned (+ cleaned 1)))
(i8 x))
;;; The frame loop's shape: one restart-case around the work, offering
;;; `continue`, which abandons this frame and nothing else.
(defn div-frame [a i64 b i64] ()
(restart-case
(do (show "div" (divide a b))
(set frames (+ frames 1)))
(continue [] (set skipped (+ skipped 1)))))
(defn rem-frame [a i64 b i64] ()
(restart-case
(do (show "rem" (remainder a b))
(set frames (+ frames 1)))
(continue [] (set skipped (+ skipped 1)))))
(defn cast-frame [x f64] ()
(restart-case
(do (show "cast" (narrow x))
(set frames (+ frames 1)))
(continue [] (set skipped (+ skipped 1)))))
(defn cast8-frame [x f64] ()
(restart-case
(do (show "cast8" (i64 (narrow-8 x)))
(set frames (+ frames 1)))
(continue [] (set skipped (+ skipped 1)))))
(defn main [] i32
;; The most negative i64, which no literal in this language can spell: the
;; reader parses the digits and then negates, and the positive half of the
;; pair does not fit.
(let [min (- (- (i64 0) big) 1)]
(handler-bind
[(ArithError [c]
;; The numbers rather than a message, for the reason StorageExhausted
;; has none: formatting allocates, and a condition has to be buildable
;; on a frame where allocation may be the thing that failed.
(set op (.op c))
(set lhs (.lhs c))
(set rhs (.rhs c))
;; Abandon the frame. The transfer crosses `divide` — running its
;; defer — and lands in the clause of the restart-case two frames out.
(invoke-restart 'continue))]
;; Fine, so the handler never runs and the defer runs on the ordinary
;; return path.
(div-frame 10 3)
;; Code 0: a divide by zero. `lhs` and `rhs` are the operands as written.
(div-frame 10 zero)
(show "op" (i64 op))
(show "lhs" lhs)
(show "rhs" rhs)
;; Code 1: the same for a remainder, which is a different instruction and
;; its own arm in both backends.
(rem-frame 7 zero)
(show "op" (i64 op))
;; Code 2: the one division that overflows. Nothing about the divisor is
;; wrong and nothing about the dividend is wrong; it is the pair.
(div-frame min neg1)
(show "op" (i64 op))
(show "lhs" lhs)
(show "rhs" rhs)
;; Code 3: and its remainder, which overflows on exactly the same pair —
;; the intermediate quotient is the thing that does not fit, and `srem`
;; computes one too.
(rem-frame min neg1)
(show "op" (i64 op))
;; In range, so no signal: the truncation toward zero is the ordinary
;; result and the guard is not in the way of it.
(cast-frame 3.9)
(cast-frame -3.9)
;; Code 4, both ends. `lhs` and `rhs` are the destination's range rather
;; than the value, because the value is a float and these fields are not;
;; what a handler needs in order to say anything useful is the range it
;; missed.
(cast-frame huge)
(show "op" (i64 op))
(show "lhs" lhs)
(show "rhs" rhs)
(cast-frame small)
(show "op" (i64 op))
;; A narrower destination reports its own range, which is the whole point
;; of carrying one: 300 is a perfectly ordinary number and is out of
;; range only for this type.
(cast8-frame 12.0)
(cast8-frame 300.0)
(show "lhs" lhs)
(show "rhs" rhs)
;; NaN fails both halves of the range test, which is deliberate: a NaN
;; cast to an integer is exactly as undefined as a value out of range,
;; and an unordered comparison would have waved it through.
(cast-frame (/ (f64 0.0) (f64 0.0)))
(show "op" (i64 op))
;; And the shapes the guard is allowed to drop, which have to keep
;; working. A literal divisor that is neither 0 nor -1 needs no test at
;; all; unsigned division needs the zero test and has no overflow case,
;; because there is no most-negative value to overflow from.
(show "lit" (/ (+ big 0) 2))
(show "u" (i64 (/ (u32 100) (+ uz 7))))))
;; Four frames finished, eight were abandoned, and all twelve ran their
;; defer — the claim a SIGFPE could not make.
(show "frames" frames)
(show "skipped" skipped)
(show "cleaned" cleaned)
0)

81
test/programs/arith.flan Normal file
View File

@ -0,0 +1,81 @@
;;;; Arithmetic with no answer, unhandled. One program, one case per argument,
;;;; the same shape bounds.flan has and for the same reason: a death is
;;;; observable only as an exit status and a sentence on stderr, so each case
;;;; needs its own run.
;;;;
;;;; What is asserted is the *reason* — the location, and which operation
;;;; against which operands. Before this change none of these cases had a
;;;; reason to assert on: the first four were a raw SIGFPE, which prints
;;;; nothing at all, and the last was undefined and would have printed whatever
;;;; the optimiser decided the answer was.
;;;;
;;;; Everything comes through a global rather than a literal, which keeps the
;;;; operands dynamic. A literal divisor is exactly the case the guard is
;;;; allowed to elide, and folding these away would leave the test asserting on
;;;; a program that does not contain the check.
(defvar zero i64)
(defvar neg1 i64 -1)
(defvar big i64 9223372036854775807)
(defvar ten i64 10)
(defvar uz u32)
(defvar huge f64 1e300)
;; The narrow versions of the same two failures. They are here because they are
;; the ones the two backends reach by different routes: the overflow test
;; compares against the *narrow* type's most negative value inside a 64-bit
;; register, and the f32 range test is compared in f32 on one backend and in a
;; double on the other. Both routes are supposed to give the same answer and
;; the survey is what says so.
(defvar i32big i32 2147483647)
(defvar m1-32 i32 -1)
(defvar wide f32 1e30)
(defn main [args [string]] i32
(let [n (i32 (bytes->i64 (bytes (at args 1))))
;; The most negative i64. No literal spells it — the reader parses the
;; digits and then negates, and the positive half does not fit — so it
;; is built, which also keeps it out of the constant folder's reach.
min (- (- (i64 0) big) 1)]
(cond
;; None of these may die. Division that is fine has to stay fine, and
;; these are the four shapes the guard has an opinion about: an ordinary
;; dynamic divisor, a literal one the guard drops entirely, unsigned
;; division, which has no overflow case because it has no most-negative
;; value, and a float division by zero, which is an infinity and is a
;; defined answer this language is not in the business of refusing.
(= n 0) (do (print (/ ten (+ zero 3))) (print " ")
(print (/ ten 2)) (print " ")
(print (/ (u32 100) (+ uz 7))) (print " ")
(print (/ (f64 1.0) (f64 0.0)))
(println ""))
(= n 1) (print (/ ten zero)) ; divide by zero
(= n 2) (print (% ten zero)) ; remainder by zero
;; The one division that overflows. Nothing is wrong with either
;; operand on its own; it is the pair, and it is the only pair.
(= n 3) (print (/ min neg1))
;; `srem` overflows on exactly the operands `sdiv` does, because the
;; quotient is what does not fit and a remainder computes one too.
(= n 4) (print (% min neg1))
;; A float too large for the destination, and the same value against a
;; narrower destination, which reports its own range.
(= n 5) (print (i64 huge))
(= n 6) (print (i8 (/ huge 1e290)))
;; NaN, which fails the range test at both ends rather than passing it at
;; neither: the comparisons are ordered, deliberately.
(= n 7) (print (i64 (/ (f64 0.0) (f64 0.0))))
;; The same overflow one width down, which is the case the two backends
;; used to disagree about *silently* rather than both dying: this one
;; loaded sign-extended into a 64-bit register, divided there and
;; truncated on the store, answering -2147483648, where the other backend
;; emitted poison. Neither was wrong about anything; they just were not
;; the same program.
(= n 8) (print (/ (- (- (i32 0) i32big) 1) m1-32))
;; And an f32 source, whose range test the two backends reach by
;; different routes on purpose — compared in f32 here and in a widened
;; double there, which agree because every bound is a power of two and is
;; exact in both.
(= n 9) (print (i32 wide))
:else (println "?"))
0))

View File

@ -1275,6 +1275,136 @@ let () =
outputs ~dev:true "a bad index is a condition, dev"
"programs/bounds-condition.flan" bounds_cond_out;
(* ── Arithmetic with no answer ─────────────────────────────────────
The same pair of programs and the same pair of shapes, for the three
situations that had no defined behaviour at all until now: a divide or
remainder by zero, which was a raw SIGFPE no message, no location,
nothing to handle the one division that overflows, and a float to
integer cast whose value does not fit, which LLVM called undefined and
would fold to anything.
arith.flan is the unhandled half. It is asserted the way bounds.flan is,
on the reason rather than on a result: the location, and which operation
against which operands. The first case is the one that must *not* die,
and it is four shapes rather than one an ordinary dynamic divisor, a
literal one the guard is allowed to drop, unsigned division, which has
no overflow case because it has no most-negative value, and a float
division by zero, which is an infinity and is a defined answer this
language has no business refusing. *)
let arith ?opt () =
let exe = compile ?opt "programs/arith.flan" in
let traps name arg reason =
let code, text = run exe (Some arg) in
if code <> 134
|| not (contains text "programs/arith.flan:")
|| not (contains text reason)
then begin
incr failures;
Printf.printf
"FAIL %s\n got: %S (exit %d)\n wanted: %S (exit 134)\n"
name text code reason
end
in
let code, text = run exe (Some "0") in
if text <> "3 5 14 inf\n" || code <> 0 then begin
incr failures;
Printf.printf "FAIL arithmetic that is fine\n got: %S (exit %d)\n"
text code
end;
traps "divide by zero" "1" "divide by zero: (/ 10 0)";
traps "remainder by zero" "2" "remainder by zero: (% 10 0)";
(* The only pair of operands in the whole type for which a division
overflows, and the reason this cannot be left to the hardware: `idiv`
raises SIGFPE here and LLVM calls it undefined, so the two backends
disagreed about a case that is one comparison away from being
answerable. *)
traps "the division that overflows" "3"
"(/ -9223372036854775808 -1) overflows";
(* `srem` overflows on exactly the operands `sdiv` does: the quotient is
what does not fit, and a remainder computes one on the way. *)
traps "the remainder that overflows" "4"
"(% -9223372036854775808 -1) overflows";
(* The range is the destination's, and it is in the message because it is
the only thing that explains the failure the value is a float and
the number that matters about it is which end it fell off. *)
traps "a float too large for an i64" "5"
"which holds [-9223372036854775808 9223372036854775807]";
traps "a float too large for an i8" "6" "which holds [-128 127]";
(* NaN fails both halves of the range test rather than passing both,
which is what the comparisons being *ordered* buys. An unordered pair
would have waved it through into an fptosi that is as undefined for a
NaN as it is for 1e300. *)
traps "NaN cast to an integer" "7"
"does not fit the integer type it is cast to";
(* One width down, and this is the case the two backends disagreed about
*silently* rather than both dying: x86 loaded the operands
sign-extended into 64-bit registers, divided there and truncated on
the store, answering -2147483648, where LLVM emitted poison. It is
also the only thing that exercises the widening on the way into the
condition if that is wrong, the number in this sentence is
garbage. *)
traps "the i32 division that overflows" "8"
"(/ -2147483648 -1) overflows";
(* An f32 source, whose range test the two backends reach by deliberately
different routes: emit.ml widens the value to a double and compares
against double bounds, x86.ml compares in f32 against an f32 constant.
They agree because every bound is a power of two and is exact in both,
and this is the row that says so rather than the comment. *)
traps "an f32 too large for an i32" "9"
"which holds [-2147483648 2147483647]";
(try Sys.remove exe with Sys_error _ -> ())
in
arith ();
arith ~opt:"-O0" ();
(* The release build drops the guards. Asserted on the IR and not by
running an unchecked program, for the reason the bounds case gives: an
unchecked divide by zero has no defined behaviour to assert on it is
the SIGFPE this whole change exists to replace. *)
let ap =
Reader.read_file "programs/arith.flan" |> Parse.program |> Check.program
in
if not (contains (Emit.program ap) "call void @flan_arith_error(") then begin
incr failures;
print_endline "FAIL checks on: no arithmetic guard emitted"
end;
if contains (Emit.program ~checks:false ap) "call void @flan_arith_error("
then begin
incr failures;
print_endline "FAIL --no-bounds-checks: an arithmetic guard survived"
end;
(* And the answered half. Five codes, five routes, one condition type, and
the same three claims bounds-condition.flan makes. (1) The frame is
abandoned and the program carries on: four frames finish and eight are
abandoned. (2) `cleaned` is 12, every defer on every one of those paths
an answered arithmetic failure leaves through the unwind path a
`return` uses, where a SIGFPE ran nothing and could not have. (3) The
condition carries the numbers: `op` is which of the five, and
`lhs`/`rhs` are the operands for a division and the destination's range
for a cast, which is two meanings over two fields rather than five
condition types and is BoundsError's precedent exactly.
The last two rows are the elisions, which are here because a dropped
guard is indistinguishable from a broken one unless the result is
asserted: a literal divisor, and unsigned division, which gets the zero
test and no overflow test at all. *)
let arith_cond_out =
"div 3\nop 0\nlhs 10\nrhs 0\nop 1\n\
op 2\nlhs -9223372036854775808\nrhs -1\nop 3\n\
cast 3\ncast -3\n\
op 4\nlhs -9223372036854775808\nrhs 9223372036854775807\nop 4\n\
cast8 12\nlhs -128\nrhs 127\nop 4\n\
lit 4611686018427387903\nu 14\n\
frames 4\nskipped 8\ncleaned 12\n"
in
outputs "arithmetic with no answer is a condition"
"programs/arith-condition.flan" arith_cond_out;
outputs ~opt:"-O0" "arithmetic with no answer is a condition, -O0"
"programs/arith-condition.flan" arith_cond_out;
outputs ~dev:true "arithmetic with no answer is a condition, dev"
"programs/arith-condition.flan" arith_cond_out;
(* And the half that finishes that thought. bounds-condition.flan's last
line is `10 99 12 13` an abandoned frame's leftovers and a restart
undoes none of it, because a restart is not a transaction