An i32 min / -1 and an f32 cast out of range. The first is where the two
backends disagreed silently rather than both dying -- x86 divided in 64 bits
and truncated on the store, answering -2147483648, where LLVM emitted poison --
and it is the only case that exercises the widening on the way into the
condition, so a bug there would have left every other row passing. The second
is the one place the two backends reach the same answer by deliberately
different routes, f32 bounds here and widened doubles there, and the survey is
what says the routes agree.
Item 3 of HANDOFF-x86-rt.md, which was blocked on the language decision rather
than on code. check_div and check_cast sit beside check_at and check_slice and
reuse bounds_call unchanged; flan_arith_error takes three extras, so the
channel lands in r9 and the argument registers are exactly full.
Three things differ from the LLVM side because the instruction set does: two
branches rather than one branch and a select, since there is no select and a
second compare on the cold path is free; the cast bounds compared in the
source's own precision rather than widened to a double, which is exact because
every bound is a power of two; and NaN excluded by choosing the direction of
each compare, because ucomis sets CF, ZF and PF together when either operand is
unordered.
Both programs now print byte-identical stdout, stderr and exit status through
either backend.
Three arithmetic situations had no defined behaviour and the two backends
disagreed about all three: a divide or remainder by zero, which was a raw
SIGFPE with no message and no location; (/ min -1), whose quotient is one past
the top of the type; and a float to integer cast whose value does not fit,
which LLVM called undefined and would fold to anything.
They now signal ArithError with `error`, exactly as a bad index signals
BoundsError, and die with a sentence naming the file, the line and the operands
only if nothing answered. The guards ride the same --checks flag as the bounds
check and are elided with it.
No restart is established at the failing operation. The sketch this started
from asked for use-value, and the implementation ruled it out: a restart frame
is allocated by the restart-case that offers it, on its own stack, so the
runtime cannot hold one on a program's behalf and use-value here would mean an
alloca and a restart frame at every division in every checked build. That is
the cost already refused for indexing, buying a silently different answer.
The x86 backend is unchanged and is the next commit.
Item 17 left two correctness items and a loose end. The loose end is the
answer to the first item: flan_vec_as_slice's Flan-level return type is Unit,
so is_void answers before is_agg is ever tested, and that is the convention
rather than one symbol's accident. So there was no sret-for-Rt to build, and
building one would have been untestable and wrong.
The second item is built and tested by the only thing that can test it, a
preloaded dlsym, because a dev build with nothing yet redefined prints what a
release build prints whether or not anything reads a cell.
Survey: 93 MATCH / 0 DIFFER / 2 refused before, 97 / 0 / 0 after; 97 / 0 / 0
again with --dev on both sides.
FnAddr (Fnval n) emitted the symbol, which is right for a whole-program build
and wrong the instant anything is redefined into it. It now reads the cell,
and so does every direct call, which is what emit.ml's body_of does and is the
half that matters: a redefinition is one store, and it has to reach call sites
that already exist.
What is emitted, all of it behind dev:
- one cell per function in .data, .globl, initialised to the body this build
compiled. Spelled exactly as Emit.cellname spells it, because the point of
having one here is that an LLVM-built module binds
@"flan.cell.<n>" = external global ptr against it. nm -D over the two
builds of the same program gives identical sets of 68 cell symbols.
- the cell load placed after the arguments, which emit.ml has as a
load-bearing comment: a redefinition landing between two calls must not
land in the middle of one. CallPtr stays the other way round.
- the flan_dev_reg_enable constructor, which arms the allocation registry.
Not emitted: Emit.cellptr, the deeper spelling for a name the host was never
built with. It cannot arise in a whole-program build and belongs with the
redefinition module that would introduce one.
The --x86 --dev refusal is relaxed, and the argument is that flan dev never
reaches this fork: --x86 is read only by flan build, and the daemon builds
host and modules through Build.executable / Build.shared without it. So the
flag means a host whose call sites are redefinable, and nothing claims the
module that would redefine through them exists.
Two things were needed to believe any of that. First, the corpus with --dev on
both sides: 97 MATCH, 0 DIFFER, same as without it. Before the constructor was
added that read 96/1 — registry.flan asks (live? ...) and got four zeroes,
which is the whole of what a dev host does differently besides the cells.
Second, and the corpus cannot do this one: a dev build starts with every cell
pointing at the body this build compiled, so it prints what a release build
prints whether anything reads the cell or not. spike/x86/cells.sh preloads a
shared object whose constructor dlsyms flan.cell.twice and stores a different
body there -- the one store a redefinition ends in, done from outside, no
compiler involved. Both dev builds then print the new answer for a direct call
and for a function value, and both release builds are unchanged, which is what
says the change came from the indirection and not from symbol interposition.
One thing the later lane inherits, now written in both headers rather than
left to be discovered. x86.ml licenses its own calling convention on the
grounds that a dev build is compiled entirely here and a release build
entirely by LLVM, so the two never meet in one process. A cell an LLVM-built
module can store into is the first thing that could make that false: the
conventions agree on scalars and disagree on every aggregate, so an
Emit.redefinition module dlopened into an --x86 host would be right until the
first redefined function took or returned a struct. The answer is a
redefinition emitter here, not a classifier.
Two things the first pass left on unspecified ground.
Form.to_source is the only field in the protocol carrying arbitrary literal
data -- a macro may build any literal at all -- and Wire.quote escapes only
the quote and the backslash, on the stated ground that both readers take
everything else as itself. test_repl checks that ground now: every escape the
reader knows, both byte-literal spellings to_string would have written raw,
and a whole float, each sent through the printer and the socket and back.
And expand_all read the macro's name out of a tuple beside the walk that
replaces it. OCaml does not promise which half runs first; the failure would
have been the wrong macro named, never an error.
C-c C-m. One step on the bare key, the fixpoint under C-u: a macro may
quasiquote a call to another macro, and Loc.from_macro is outermost-wins, so
by the time a full expansion settles the intermediate name is gone. One step
is the only thing that can say which macro produced what.
The expansion runs against the macros the *session* holds -- the prelude's,
its imports', and every defmacro evaluated since it started -- and writes
nothing back: a defmacro handed to C-c C-m does not join the session by having
been looked at.
Both non-termination refusals stay refusals, and only where they are needed.
One step makes one call and does not look at the answer, so (s/spin) one-
stepped answers with itself; all the way hits the fuel and names the macro,
inside Dev.serve's guard, so the daemon replies rather than hanging. Macro's
module handling is a Fun.protect now -- a build that raised was a process
about to exit, and the daemon is not that process.
No printer for a Form existed. Form.to_string is an error-message renderer and
is what Macro.key digests, so it is untouched; Form.to_source round-trips
floats, strings and bytes through the reader, and Form.pretty decides where
the line breaks go and leaves the columns to flan-mode.
The answer is a read-only flan-mode buffer shaped like the disassembly one,
with cnr's idea in it: m expands the form at point one more step in place.
Three inherited keys refuse by name -- an expansion is in no file. The text is
sent padded onto its own line and its own column, unlike C-x C-e, so the
refusal lands on the call and not at the start of its line.
The head dispatch already names it; test_repl pins it beside the defvar case,
and the editor-typed defmacro moved below the describe assertion that says an
evaluation changes nothing -- that one does, on purpose. BUILT.md gains the
boundary of the shape: the set only grows, a deleted defmacro still expands,
and that is exactly what a deleted defn already does.
Item 17 left this as a loose end: flan_vec_as_slice returns a slice by value
and should hit the "aggregate return" refusal, bounds-condition.flan exercises
it in and out of bounds and matches, and nobody traced why.
It is the first of the two possibilities that report named — the refusal is
narrower than it reads, and nothing is going right by accident.
flan_vec_as_slice's Flan-level return type is Unit. check.ml builds it as
[rt loc Types.Unit "flan_vec_as_slice"] and flan_rt.c writes the two words
through a [void *out] parameter, so [is_void rty] answers first and the
[is_agg rty] test below it is never reached.
That is not one symbol's accident, it is the convention. Every aggregate-valued
runtime result crosses through an out-pointer the checker allocates; every
other [rt] builder in check.ml answers Unit, an Int, a Ptr, an Alloc or a
Handle. And the other user of this path, a [declare]d C function, is covered by
[crossable], which admits String and Slice only as a parameter and refuses an
aggregate return outright.
So there is no sret convention to build for Rt, and building one would be worse
than the refusal: the C boundary wants SysV classification — a 16-byte slice
comes back in rax:rdx — and not the hidden-pointer convention this backend uses
internally. There is no classifier in the file and nothing to test one against.
The line stays as a guard against those two rules changing, and now says which
rules and what the work would actually be.
With it goes the rest of item 16's claim that the container runtime is
unexercised. It is: Vec and Map through vec.flan, vec-of-vec.flan, maps.flan
and map-iter.flan, and Pool through registry.flan, handles.flan,
generics.flan and pool-stale-region.flan. All match.
Two loose ends from NEXT.md.
slice-from-ptr's run-time refusal borrowed @flan_slice_error and reported a
range and a length the caller never wrote. It has flan_slice_promise_error
now: signals BoundsError, walks the handlers, offers the break loop, falls
through to a message and a status like the two beside it. The sentence names
what was promised and what was passed, and a second line says what is not
checked. The condition fields stay (0, n, 0) — the violated condition as a
range, and not (0, n, n), which reads as in bounds.
And a session now holds the buffer's own defmacros: seeded in Session.create
from the same read that produced decls, and added by Session.eval so a
defmacro typed at the editor joins the set the way a defn does. Not a re-read
of the file, which would put unsaved-versus-saved skew inside expansion. The
commit stays below the checker. Macro.program dedupes the ambient set against
the forms being parsed, left-wins, because unqualified names can now collide.
Another lane landed (slice-from-ptr p n) while this backend was not looking,
and it arrived as two refusals rather than one: slice-from-ptr.flan and
bounds.flan both stopped building through --x86. Neither is a new obstacle —
a Slice _ is {ptr, i64} here exactly as it is in emit.ml, so the form is one
store of the pointer and one of the length and no new representation at all.
The half worth writing down is the check. There is nothing to compare the
length against — only the caller knows how many elements live behind that
pointer — so what is checked is that the promise is not absurd, and that test
is *signed*. check_slice's own compares are unsigned, and a negative i32
sign-extended to 64 bits is a huge unsigned value that an unsigned "hi <= len"
waves through; the result would be a slice about 2^64 long that reads as a
pass and faults somewhere else entirely.
Nothing in the corpus walks that path: every length in slice-from-ptr.flan is
a literal, and a negative literal is refused by check.ml before any code is
emitted. So spike/x86/p7-slice-from-ptr.flan takes the length as a parameter
and runs it through a restart-case, which puts the condition's low/high/length
on stdout and compares them against the LLVM build.