# 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.