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:
parent
1a6ea7ae69
commit
acda9edd7c
9
web/examples/arith.flan
Normal file
9
web/examples/arith.flan
Normal 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
3
web/examples/arith.out
Normal file
@ -0,0 +1,3 @@
|
||||
before
|
||||
arith.flan:8:12: divide by zero: (/ 10 0)
|
||||
exit 134
|
||||
7
web/examples/cast.flan
Normal file
7
web/examples/cast.flan
Normal 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
2
web/examples/cast.out
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -434,6 +434,48 @@ Measured cost on a
|
||||
50-million-iteration dependency chain over a 1024-element array: 0.11–0.12s checked
|
||||
against 0.12–0.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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user