The reference page: a stale recorded output, a refusal that no longer happens, and the condition it never mentioned

web/examples/check.sh was failing, and had been since 2026-09-12. printing.out
records the structural printer's output with colon field labels; the printer emits
dots. The language is right and the recording is stale -- the colon-to-dot sweep
worked on Flan source forms and never touched a .out file, and check.sh is not
part of dune test, so nothing has run it since the day both landed. The acceptance
suite settles which side is correct: it expects the dot spelling everywhere and is
green. The page quoted the same stale line.

The x86 paragraph told a reader that conditions are the visible gap and quoted a
build failing by name. That exact command now succeeds and produces a binary:
conditions, guards, cells, redefinition modules and DWARF all landed, the survey
is 103 MATCH / 0 DIFFER / 0 refused, and what is still refused is narrow enough to
name -- an aggregate crossing the C boundary, which is the classifier this backend
exists not to have.

ArithError was absent from the page entirely. It sits beside the bounds section
because it is the same decision twice, including the part where no restart is
established at the failing operation. Its two programs are in web/examples/ and
checked, which is the page's own rule for anything it quotes.
This commit is contained in:
Joseph Ferano 2026-09-14 07:43:52 +07:00
parent 1a6ea7ae69
commit acda9edd7c
6 changed files with 89 additions and 12 deletions

9
web/examples/arith.flan Normal file
View File

@ -0,0 +1,9 @@
;; Three integer operations have no right answer. Each used to be a bare SIGFPE
;; or an undefined value; each signals ArithError now. The divisor goes through
;; a global so that constant folding cannot answer it before the backend does.
(defvar zero i32 0)
(defn main [] ()
(println "before")
(println (/ 10 zero))
(println "unreachable"))

3
web/examples/arith.out Normal file
View File

@ -0,0 +1,3 @@
before
arith.flan:8:12: divide by zero: (/ 10 0)
exit 134

7
web/examples/cast.flan Normal file
View File

@ -0,0 +1,7 @@
;; A float-to-integer cast whose value does not fit. The condition it violated
;; is reported as the range the destination type can hold, which is the same
;; shape BoundsError uses for a slice: the violated condition, written out.
(defvar big f64 1e30)
(defn main [] ()
(println (i32 big)))

2
web/examples/cast.out Normal file
View File

@ -0,0 +1,2 @@
cast.flan:7:17: this value does not fit the integer type it is cast to, which holds [-2147483648 2147483647]
exit 134

View File

@ -1,6 +1,6 @@
42
1.5
(Enemy {:hp 3 :name "wisp" :key :left})
(Enemy {.hp 3 .name "wisp" .key :left})
(some 32)
none
no newline: true

View File

@ -434,6 +434,48 @@ Measured cost on a
50-million-iteration dependency chain over a 1024-element array: 0.110.12s checked
against 0.120.13s unchecked.</p>
<h3>So is arithmetic that has no answer</h3>
<p>Three integer operations have no right result, and each of them used to be a bare
<code>SIGFPE</code> or an undefined value: a divide or remainder by zero, the one division
that overflows (<code>INT64_MIN / -1</code>, whose true quotient is one past the top of
the type), and a float-to-integer cast whose value does not fit. All three now signal
<code>ArithError</code>, the way a bad index signals <code>BoundsError</code>.</p>
<pre><code>;; The divisor goes through a global so that constant folding cannot
;; answer it before the backend does.
(defvar zero i32 0)
(defn main [] ()
(println "before")
(println (/ 10 zero))
(println "unreachable"))</code></pre>
<pre><code class="sh">$ flan run arith.flan
before
arith.flan:8:12: divide by zero: (/ 10 0)
$ echo $?
134
$ flan run cast.flan
cast.flan:7:17: this value does not fit the integer type it is cast to, which
holds [-2147483648 2147483647]</code></pre>
<p>A Lisp that stops naming the file and the line beats one that dies with
<code>SIGFPE</code>, and a program that genuinely does not care installs a handler once at
startup and never thinks about it again. Float division is deliberately left alone: IEEE
already answers it, with an infinity or a NaN.</p>
<p><strong>No restart is established at the failing operation</strong>, which is the same
decision <code>BoundsError</code> made and for the same reason. A restart frame is
allocated by the <code>restart-case</code> that offers it, on that frame's own stack, so
nothing below the program can push one on its behalf; a <code>use-value</code> at a
division would mean an <code>alloca</code> and a push-and-pop emitted at every division in
every checked build, and what it would buy is a silently different answer. What answers a
division by zero is the restart the program already had — a frame loop's
<code>continue</code> — which is on the stack and reachable from a handler or from the
break loop without anything being pushed at the failure.</p>
<h2 id="types">Types</h2>
<p>Types are annotated at function boundaries and inferred everywhere else. Every type
@ -946,7 +988,7 @@ user-supplied printer to choose between.</p>
<pre><code class="sh">42
1.5
(Enemy {:hp 3 :name "wisp" :key :left})
(Enemy {.hp 3 .name "wisp" .key :left})
(some 32)
none
no newline: true</code></pre>
@ -1248,9 +1290,10 @@ not in a <code>defer</code>, because a defer runs on the ordinary return path to
that version silently rolls back the frames that succeeded.</p>
<p>This matters more here than in most Lisps because the intended use is a
<em>game loop</em>, where the plan is to skip a frame and carry on rather than die. Now
that a bad index signals <code>BoundsError</code> instead of ending the process,
abandoning a frame and retrying it is a real thing to do — and that is exactly the case
a non-idempotent mutation spoils.</p></li>
that a bad index signals <code>BoundsError</code> and a bad division signals
<code>ArithError</code> instead of ending the process, abandoning a frame and retrying
it is a real thing to do — and that is exactly the case a non-idempotent mutation
spoils.</p></li>
<li><strong>An unknown restart name is a hard stop</strong> — a located runtime error.
There is no <code>find-restart</code> to test with yet.</li>
<li><strong>No supertype</strong>, so nothing can say "any condition".</li>
@ -1833,19 +1876,32 @@ own internal calling convention (every aggregate by pointer, no eightbyte rule,
classifier) and match SysV only at the C boundary, where the shim has already flattened
every struct.</p>
<p>It covers a subset of the IR and <strong>refuses the rest by name</strong>, so a
build that succeeds is one it really compiled rather than one it half-compiled.
Conditions are the visible gap — anything reaching the transfer channel is refused:</p>
<p>It <strong>refuses by name</strong> anything it does not lower, so a build that
succeeds is one it really compiled rather than one it half-compiled. Conditions were the
visible gap once and are not any more: the transfer channel, the guard after every call,
bounds and arithmetic failures, indirection cells, redefinition modules and DWARF line
tables all landed, and what is left refused is narrow — an aggregate crossing the C
boundary is the one worth naming, because closing it would mean the eightbyte classifier
this backend is built on not having.</p>
<pre><code class="sh">$ flan build test/programs/algorithms.flan --x86
Fatal error: exception Flan.X86.Unsupported("restart-case needs the transfer
channel, which this backend does not emit a guard for")</code></pre>
<p>What holds it honest is that every program in the corpus is built both ways and the
two are compared byte for byte on stdout, stderr and exit status — not on a disassembly,
which has read perfectly beside a wrong answer more than once. <code>spike/x86/survey.sh</code>
is the script, and it currently reports <strong>103 MATCH, 0 DIFFER, 0 refused by
name</strong>, with 38 programs skipped because they do not compile on either side, have
no <code>main</code>, or run forever. <code>dune build @x86</code> runs it as part of the
build, so a refusal cannot sit unnoticed.</p>
<p><code>--debug</code> is a third flag beside <code>--dev</code> and the optimisation
level. <code>--dev</code> asks whether you can redefine the program while it runs;
<code>--debug</code> asks whether you can stop it and read it. It emits DWARF, sets
<code>-O0</code>, and is refused by name for wasm32. lldb needs no plugin to read a
Flan struct: the struct is its C struct.</p>
Flan struct: the struct is its C struct. Both backends emit it, though not the same
amount: the hand-written one writes a compile unit, a subprogram per function and a line
table out as bytes, because <code>.loc</code> cannot work against a file whose
instructions are <code>.byte</code> blobs, so <code>--x86 --debug</code> gives a
backtrace naming Flan files, functions and lines while <code>print x</code> says the name
is not in the current context.</p>
<p>Some things are refused by name rather than half-supported, and both cross-target
refusals say why:</p>