Arithmetic with no answer is a condition: the plan

This commit is contained in:
Joseph Ferano 2026-09-13 22:46:26 +07:00
parent 60d4c762e1
commit 69d10bec4e

64
HANDOFF-arith.md Normal file
View File

@ -0,0 +1,64 @@
# 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".
## Status
Stub. Nothing below the line has landed yet; this section is updated as it does.