diff --git a/web/examples/arith.flan b/web/examples/arith.flan new file mode 100644 index 0000000..401e5d8 --- /dev/null +++ b/web/examples/arith.flan @@ -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")) diff --git a/web/examples/arith.out b/web/examples/arith.out new file mode 100644 index 0000000..a65955d --- /dev/null +++ b/web/examples/arith.out @@ -0,0 +1,3 @@ +before +arith.flan:8:12: divide by zero: (/ 10 0) +exit 134 diff --git a/web/examples/cast.flan b/web/examples/cast.flan new file mode 100644 index 0000000..61901c5 --- /dev/null +++ b/web/examples/cast.flan @@ -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))) diff --git a/web/examples/cast.out b/web/examples/cast.out new file mode 100644 index 0000000..afeaf21 --- /dev/null +++ b/web/examples/cast.out @@ -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 diff --git a/web/examples/printing.out b/web/examples/printing.out index b3ac8a2..4e0ce7a 100644 --- a/web/examples/printing.out +++ b/web/examples/printing.out @@ -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 diff --git a/web/index.html b/web/index.html index a8233c2..7616785 100644 --- a/web/index.html +++ b/web/index.html @@ -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.
+Three integer operations have no right result, and each of them used to be a bare
+SIGFPE or an undefined value: a divide or remainder by zero, the one division
+that overflows (INT64_MIN / -1, 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
+ArithError, the way a bad index signals BoundsError.
;; 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"))
+
+$ 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]
+
+A Lisp that stops 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. Float division is deliberately left alone: IEEE
+already answers it, with an infinity or a NaN.
No restart is established at the failing operation, which is the same
+decision BoundsError made and for the same reason. A restart frame is
+allocated by the restart-case that offers it, on that frame's own stack, so
+nothing below the program can push one on its behalf; a use-value at a
+division would mean an alloca 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
+continue — which is on the stack and reachable from a handler or from the
+break loop without anything being pushed at the failure.
Types are annotated at function boundaries and inferred everywhere else. Every type @@ -946,7 +988,7 @@ user-supplied printer to choose between.
42
1.5
-(Enemy {:hp 3 :name "wisp" :key :left})
+(Enemy {.hp 3 .name "wisp" .key :left})
(some 32)
none
no newline: true
@@ -1248,9 +1290,10 @@ not in a defer, because a defer runs on the ordinary return path to
that version silently rolls back the frames that succeeded.
This matters more here than in most Lisps because the intended use is a
game loop, where the plan is to skip a frame and carry on rather than die. Now
- that a bad index signals BoundsError 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.
BoundsError and a bad division signals
+ ArithError 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.
find-restart to test with yet.It covers a subset of the IR and refuses the rest by name, 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:
+It refuses by name 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.
-$ 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")
+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. spike/x86/survey.sh
+is the script, and it currently reports 103 MATCH, 0 DIFFER, 0 refused by
+name, with 38 programs skipped because they do not compile on either side, have
+no main, or run forever. dune build @x86 runs it as part of the
+build, so a refusal cannot sit unnoticed.
--debug is a third flag beside --dev and the optimisation
level. --dev asks whether you can redefine the program while it runs;
--debug asks whether you can stop it and read it. It emits DWARF, sets
-O0, and is refused by name for wasm32. lldb needs no plugin to read a
-Flan struct: the struct is its C struct.
.loc cannot work against a file whose
+instructions are .byte blobs, so --x86 --debug gives a
+backtrace naming Flan files, functions and lines while print x says the name
+is not in the current context.
Some things are refused by name rather than half-supported, and both cross-target refusals say why: