# 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. ## Order of work 1. `lib/prelude.ml`, `runtime/flan_rt.c`, `lib/emit.ml`, tests. Committed first and separately. 2. `lib/x86.ml`'s `prim` `Div`/`Rem`/`Cast` arms last, rebased onto the dev-loop tip, kept mechanical — this is item 3 of `HANDOFF-x86-rt.md`'s "What remains". ## 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.