flan/HANDOFF-arith.md

4.2 KiB

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

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