Arithmetic with no answer is a condition: the plan
This commit is contained in:
parent
60d4c762e1
commit
69d10bec4e
64
HANDOFF-arith.md
Normal file
64
HANDOFF-arith.md
Normal 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.
|
||||
Loading…
x
Reference in New Issue
Block a user