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.

+

So is arithmetic that has no answer

+ +

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

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.

+ that a bad index signals 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.

  • An unknown restart name is a hard stop — a located runtime error. There is no find-restart to test with yet.
  • No supertype, so nothing can say "any condition".
  • @@ -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.

    -

    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.

    +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 .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: