sort-i32! was the only sort in the language. sort-f32! and sort-bytes! are
the other two, and they are copies rather than an abstraction for a reason
worth naming precisely: map, filter, reduce and a sort taking a comparator
are not blocked on generics, they are blocked on *function values*. Types.Fn
exists and check.ml refuses it with "a function type is not implemented yet --
milestone 5", and there is nothing else in the language to pass. Generics on
top of that is what would make them one copy instead of one per element type.
The f32 family carries one caveat the i32 family cannot have: a NaN makes the
order undefined, because every comparison against one is false, so the
insertion loop never moves it and never moves anything past it. sum-f32
accumulates in f64 for a stronger version of sum-i32's argument -- an f32
total does not wrap, it absorbs, and the answer comes out silently short.
The test prints the difference rather than the total, because %g hides it.
sort-bytes! is the one a caller of split actually wants, and its ordering is
memcmp's: bytewise, unsigned, prefix first. Not alphabetical -- "Zebra" sorts
before "apple" -- and the note says so, for the same reason the ASCII-case
note refuses a locale. The slices move and the bytes never do, so it sorts
fields borrowed out of a string literal, which an in-place byte sort could
not.
f64->bytes is snprintf "%g": six significant digits, exponent notation of its
own accord, and no precision to pass it. A frame time of 1/60 comes back as
0.0166667 and a score past a million as 1.23457e+06. format-f64 returns a Vec
instead, so it inherits neither that nor the shared static scratch buffer --
and it is the reason append-i64! exists, because it renders the integer part
and the fraction through that one buffer in strict sequence.
Half away from zero at the last digit kept, which is round-f32's rule and not
printf's. 0.125 at two places is 0.13 here and 0.12 there; matching printf
would mean pinning a particular libc's nearest-even on the binary value, and
that answer is not the same on every target anyway.
The three cases that ship broken are each one line and each tested: the
carry, where the rounded fraction equals the scale and is the next integer
(0.999995 at five places prints "0.100000" without it); the zero padding,
without which 1.005 at three places prints "1.5"; and the sign, which belongs
to the number rather than to its integer part, since -0.5 has an integer part
of 0 and 0 carries no sign.
The clamp on the precision is spelled (min 9 (max 0 prec)) and not with the
clamp macro, and the reason is a finding: the prelude is never
macro-expanded. macro.ml's pass runs over the file being compiled, and the
prelude arrives at the checker through Check.program's own prepend, so a
prelude function calling a prelude macro resolves the macro's underlying
defn -- the one that takes a [Form] -- and reports an arity error.
Eight functions that the file used to refuse by name, and the refusal was
always one sentence -- there is no allocator -- which stopped being true when
Vec landed. Three rules hold across all of them and are written at the head
of the section: the result is owned and the caller frees it, the allocator is
the context's, and no signature carries a Result because no allocating
operation returns an error.
The builder is not a type. Odin's strings.Builder wraps a [dynamic]u8; here
the (Vec u8) already is that and already has push, so a wrapper would be a
move-only struct whose only method is the one it wraps. What was missing is
appending a run of bytes, and append! is that -- taking a (Ptr (Vec u8)),
because a Vec parameter moves and a by-value builder would be consumed by its
first append.
append-i64! and append-f64! are the argument for the whole shape. The
runtime renders numbers into one shared static buffer, so two of its results
cannot be held at once; these copy out before returning, so a builder holds as
many numbers as it likes. strings.flan puts two integers and a float on one
line to show it.
split returns a (Vec [u8]) and not a (Vec (Vec u8)): the fields borrow the
input, and the owning shape is refused outright because a Vec copies and
releases its elements bytewise. Constructing it needed a one-line slices-new,
because (vec-new) takes its element type as a bare symbol and [u8] is not
one -- a compiler gap, noted rather than worked around in silence.
replace-bytes guards its empty needle with an if and not an early return: a
returned Vec is a move, the dead set spans the function, and a return on one
branch would kill the binding on the other.
The merged exit closes stdout so the compiler reads EOF and learns the program
has finished. POSIX then hands descriptor 1 to the next thing that asks — a
socket, a module's object file — and the llc after that inherits it as its
stdout. /dev/null takes the slot back, and the EOF is unaffected because the
pipe's write end is genuinely gone.
Verified beyond the headless suite: sand.flan builds and runs merged under
Xvfb, one process with no children, and a game-draw typed in at the socket is
drawing frames a second later.
The two declares inherit the sin/cos caveat in full and not the sqrt one:
IEEE-754 requires nothing of atan2f or powf either, so they are the third and
fourth places in the prelude where native and wasm32 may differ in the last
bit. Every case in math2.flan is therefore a value that is exact in binary --
a quadrant boundary, a power of two, a perfect square -- rather than one that
would pin a particular libm and then fail on wasi.
clamp is the interesting one. The prelude already argued against wrapping
(min hi (max lo x)) in a function, and that argument gets stronger rather
than weaker: min and max are builtins at every numeric type and there are no
generics, so a clamp *function* is one copy per type. A macro is
type-agnostic for free and emits nothing at all. The test calls the same
three words at i32, i64, u8 and f32 to show it, and counts evaluations to
show that each argument appears once -- the shape that names x twice reads
identically and calls it twice.
lo above hi answers hi and is not checked. A macro has no error facility, so
the only diagnostic available would be a run-time one, in the construct whose
whole point is that it costs nothing at run time.
[4 T] is the type syntax and is unchanged; it already works in a defvar, a
parameter, a field and a return. A let binding is the one position with no
type slot, and there the brackets are an array literal of two elements whose
second is a type name — which came back as "unknown name rl/Vector2" and cost
32 hand-written Vector2s in one raylib example.
(array COUNT TYPE) is a parser form rather than a builtin call, because the
second argument is a type and the parser's callers have none. Parse assembles
the Tarray itself, so the count takes a constant's name for free and a value
in the type position is refused by the type reader's own message. The checker
resolves it to Tast.Zero — no new backend node and no new type.
(zeroed [4 T]) was proposed first and rejected: the parser can tell, a person
cannot. zeroed keeps its job of being inferred; array is the one that is told.
A reply over the socket looks the same either way, which is the point of the
merge and also why nothing in the suite noticed it. So the daemon's pid is
checked directly: /proc/<pid>/exe is the merged program, at a path named for
the pid that built it, and there is no child at all.
The same check runs the other way round over --two-process, which gets one
round trip of its own — describe, an eval, and the output coming back — because
it is the escape hatch for a machine that cannot build the compiler object and
an escape hatch nobody exercises is not one.
Linux only, by /proc, and skipped rather than faked elsewhere: what is being
asked about is the process table.
`flan dev` now builds one binary that is the compiled Flan program and holds
the whole OCaml compiler, and execs it. The program keeps main() — macOS needs
the window there — and caml_startup happens on a pthread beside it, next to the
listener flan_agent.c already starts. The editor's socket and the wire protocol
are untouched: Emacs cannot tell the difference.
Two rules are written into lib/dev.ml rather than discovered later. The game
thread must never call into OCaml, because a native thread has no safe points
and so can never be stopped by the collector — which is exactly why a frame is
never paused, and exactly what one convenient direct call would undo. And no
OCaml value may be stored in Flan memory without caml_register_global_root,
which is the way the spike's "the GC does not touch the arenas" measurement
stops being true.
The link is spelled in dev.ml out of Build's existing public pieces rather than
as a mode of Build.executable: lib/build.ml belongs to another lane this week.
It should collapse into Build once that lands.
A Flan main does not return — Emit ends it with flan_exit and an unreachable —
so in one process that call would take the compiler down with a program that
merely finished. flan_rt.c grows a hook, null in every other build, that the
merged entry point uses to flush, close stdout and park. The compiler then
learns the program is done the same way the daemon did: the pipe reads EOF.
--two-process keeps the old shape for a machine that cannot build the compiler
object, and nothing has been deleted.
BUILT.md gains "Macros: the compiler dlopens the program": the image format and
why nothing aggregate crosses to C, quasiquote before the walk and why that is
load-bearing, the distinction between a quasiquoted call and a real one, the
two different non-termination failures, -linkall and the session-path argument
that forced it, the three cost numbers, and what unless proves and what it does
not.
"Why there is no interpreter" gains its consequence: compile-and-dlopen is a
mechanism now rather than an absence.
NEXT.md loses the whole expander design and its handoff, which are done, and
keeps six things that are not: four special forms left, with why when and
dotimes are the hard two -- the prelude uses them 29 and 12 times, so moving
either makes the prelude depend on the macro the macro module has to compile
the prelude to get; that a macro has no way to say why something is wrong,
which is the biggest gap and the reason unless went before cond; macros not
imported; a prelude macro not being able to call a macro; nested quasiquote;
and gensym's counter per module.
One claim corrected rather than left standing: the packages section said the
topological package order exists for the expander. The expander does not read
it, because macros are not imported. The order is right and correct and
nothing uses it yet.
The expander collects defmacros from the prelude and from the file being
compiled. Not from an imported package, and the reason is an ordering one:
Load learns a package's imports by parsing it, so reaching a package's macros
would mean resolving that package's own imports over Forms, before Load runs.
That is a second import resolver, and it is a bigger thing than this lane.
Refused by name, which is the rule that caught the two misparse bugs. Left
alone the call arrives at the checker as an unknown name -- true, and no help.
Refused where the defmacro is written rather than where it is called, because
that is where the fix goes.
The check has to sit in Load's read, because that is the only place that can
see one: by the time Parse is finished a defmacro is an ordinary Ast.Defn and
the word is gone.
Measured while here, since a prelude that grows a defmacro is a cost every
program pays or does not:
- A build of a program that names no macro: 50ms, the same as before. The
pass scans the top level, finds nothing, and no compiler runs.
- A program that calls one: 310ms the first time, 70ms after. The 240ms is
the clang driver building the macro module; it is cached under the object
cache, keyed by the prelude's source and the file's defmacros, so it is
paid once per change rather than once per build.
- A hello-world's binary carries exactly one symbol out of all of this:
flan.gensym-n, eight bytes. Reach.link drops unless, form-cons, form-nil,
form-append, form-rest and gensym, because nothing reachable calls them.
plan.org milestone 5 says when, unless, until, cond and dotimes are special
forms only until macros land. This is the first one to stop being one, and
running test/programs/macro-unless.flan means the compiler built a shared
object, dlopened it into itself and called a Flan function to find out what
(unless c a b) means.
unless is the one that moved because it is the one nothing else needs: zero
uses in the prelude, so moving it cannot make the prelude depend on the
expander that compiles it. Its coverage is sand.flan, seven calls, compiled
through Session in test_session -- which is the in-process path and the reason
lib/dune now passes -linkall. Say plainly what that coverage is not: nothing
in test/programs used unless before today, so macro-unless.flan is a test
written after the feature. The corpus that was written before it is sand.flan
and web/examples/control.flan, and both compile unchanged.
lib/macro.ml is the half of expansion that has to compile something. Expand is
the image format and the quasiquote desugaring and depends on nothing above
Form; this needs Check, Build and Emit, so it sits above the parser it feeds
and arrives through Parse.expander.
What it does, in order:
- Collects every defmacro from the prelude and from the file. Not from an
imported package: Load learns a package's imports by parsing it, so
collecting from one means a second import resolver over Forms, and that is a
bigger thing than this.
- Builds them in rounds, because a macro's body may call a macro and a body
with an unexpanded call in it will not compile at all -- the call is a name
nothing defines. Round 0 takes every macro that names no macro still
waiting; round 1 expands the rest against round 0's module. A round that
takes nothing while macros remain is a ring and is named. macros.flan has
the round-1 case and macro-cycle.flan has the ring, and the distinction
between them is the one thing here that is easy to get wrong: a call inside
a quasiquote is *not* a compile-order dependency. It is part of what the
macro answers, and the answer is expanded again after it returns. The first
macro-cycle.flan written for this commit quasiquoted, and it was not a cycle
at all -- it hit the fuel instead, correctly.
- Walks bottom up, so a macro never sees a call to another macro in what it is
handed, and re-expands what comes back, so a macro that expands into a call
to itself keeps going. That loop is bounded at 200 and says which macro ran
out: macro-spin.flan.
- Skips all of it when the file names no macro, which is nearly every file.
Otherwise every build in the suite would pay a clang driver to answer a
question nobody asked. When it does build, the module is cached under the
object cache and keyed by the prelude's source plus the file's defmacros, so
a second process pays a dlopen.
lib/dune passes -linkall, which is the one line in another lane's file. The
module installs itself into Parse.expander at initialisation and nothing
references it, so without -linkall the linker drops it from every executable
that does not name the module -- bin/main.exe among them -- and a program
calling a macro fails with an unknown name. The alternative was an install
call at every entry point, including ones in files this lane must not touch.
The one thing a macro cannot do that parse.ml could is give a reason. A macro
runs inside the compiler and anything it signals aborts the compile with no
location, so a malformed (unless) answers a name nothing defines and the
report is "unknown name unless-takes-a-test-and-a-body" at the call site --
right place, wrong sentence. NEXT.md says so.
test_flan.ml's "unless -> if(not)" assertion is gone, because it asserted a
desugaring in a file that no longer does one. Nothing else in the suite
changed.
Two things that look like plumbing and are the frontend half of expansion.
A quasiquote becomes calls to the prelude's three form-building functions and
nothing else: form-nil, form-cons for an item, form-append for a splice. It is
pure, it needs nothing loaded, and it runs over every form on the way into
Parse.program and Parse.decl, which is what lets the prelude's own macros parse
in a process that has not built a macro module yet.
Running it *before* the expander's walk is not an ordering preference. A cond
macro's body contains a quasiquoted (cond ...) for its own tail; with the
quasiquote still standing, the walk would see that head and expand it then and
there, against the wrong arguments. Desugared first, that subform is a
(Form.Sym {.s "cond"}) and there is no head left to mistake. So the walk needs
no idea that quoting exists, which is the whole reason this runs first.
Nesting levels are not counted -- not by the reader, which was written that way
deliberately, and not here. A quasiquote inside a quasiquote is refused by
name. Only a macro that writes a macro wants one, nothing in the corpus does,
and CL's level arithmetic costs more than the use case is worth so far.
A defmacro is now an Ast.Defn: (defmacro m [args] body) is
(defn m [args [Form]] Form body). There is no Ast.Defmacro and there is not
going to be one -- a macro is [Form] -> Form, compiled by the same backend as
everything else, and the only thing that makes it a macro is that the expander
calls it at compile time. One parameter, the slice of forms at the call site,
so variadics come free in a language with no &rest; two parameters is a
misunderstanding rather than an arity error and says so.
Parse.expander is the hook the walk arrives through, because expanding a macro
means compiling and dlopening it, so the expander sits above Check and Build
and Parse sits below them. Nothing fills it in yet.
The quasiquote refusal stays as a backstop: it now means a form reached the
parser without coming through program or decl. gensym's refusal is gone -- it
is an ordinary prelude function returning a Form, and a macro body calls it
like any other.
The last commit put the prelude's types into the set the parser uses to tell a
return type from the first form of a body, and put them in plainly. That set is
read by two arms: a bare symbol, and a list head. The list-head arm is why
(defn f [] (Some 1) (bar)) does not lose its body, and the comment above it has
warned about this since it was written -- so adding Rune plainly made
(defn f [] (Rune {.code 65}) (bar)) a function returning a Rune with a
one-form body, silently, in every file in the language. Confirmed before
fixing: it failed with "a map type is {K V}", which is the misparse arriving a
step later wearing someone else's error.
The enums already solve this one comment up, under their own key, for the same
reason. The prelude's types go in the same way. No prelude type takes
arguments, so a bare symbol is the only type position any of them can occupy.
Both halves are pinned in test_flan.ml's return-type section: Form is a return
type, and a prelude struct literal opening a body is not.
The capability lists were written before the code held the line they claim.
Under an expression root, RET on a field of a union built `(.at s)' and sent
it, and the checker refused it — "a union's fields belong to a case ... they
are reached by (match ...)". A refusal from the far end of a socket is exactly
what this buffer's own comment says not to do: every refusal is by name, here,
with the reason, because RET working on some lines and erroring on others
teaches nothing about the language.
It is a refusal of the *parent* and not of the value at point, which is why it
is not in `flan-inspect-refusal': a struct field that merely holds a union is
an ordinary accessor and has to stay enterable. It is a field of the union
itself that cannot be written. The two cases are one test each.
The slot root steps into it by offset and is unaffected, which is the
difference the manual now claims and the tests now show.
`lib/dev.ml' cited DISCUSS.md item 1 as a hole; item 1 is the answer now, so
it cites BUILT.md instead. And the item 1 stub is two sentences and a pointer
— everything else in it is in BUILT.md verbatim, and DISCUSS.md's own header
says nothing in it is a decision.
The daemon side and the Emacs side both landed with nothing written down. Four
files owed something.
`BUILT.md` gets the whole of it: why rooting at an address alone was rejected
and why that rejection was half wrong, what a path step is and how a union's
case travels with it, why the slot goes by index and not by name, and the two
capability lists side by side — the expression root works on a running program
and cannot name a frame; the slot root names one frame and one slot and reaches
an option's payload and a union case's fields, and needs the program stopped.
Neither contains the other, which is the reason there are two.
`emacs/MANUAL.md` says the same thing in the register that file uses, under the
inspector, because the person pressing `i` is the one who needs to know which
root they got and what it cannot do. The globals section's claim that `i` works
on a global "exactly as it does on a local" was true and is now the interesting
difference, so it says what the difference is.
`NEXT.md`'s decided item is struck with what actually shipped: a frame and a
slot index rather than an address and a type, and `l` crossing between the modes
was predicted as a cost and turned out not to be one.
`DISCUSS.md` item 1 is no longer an open question. The number stays — cimport.ml
and NEXT.md cite these by number — and what stays with it is the one correction
worth keeping: an address is not an expression, but a step does not have to be
one either.
And BUILT.md's last paragraph still said `render.ml` prints `(V {:x 1.5})` and
that the printer would move when its reader did. They moved together some time
ago.
The boundary was verified by compilation and had never executed. Now it does:
three Flan functions compiled into a .so, dlopened into the test process, and
called with Forms this side laid out in raw memory.
lib/expand.ml is the image format and nothing else yet. A Form is 24 bytes,
align 8, payload at offset 8, and every case holds one member at the payload's
start -- a string and a slice are both { ptr, i64 }, so there is no third
offset anywhere in it. The tag is the case's position in the prelude's
defunion, which is why that list says it is a layout contract; a tag this file
and the prelude disagree about is named rather than read as some other case.
The case sends one Form of every one of the nine shapes through an identity
macro, so a tag nobody thought about is a failure and not a gap. Then two
arguments through a macro that reads the second, because a slice whose length
did not cross reads past its arguments and an identity macro would not notice.
Then a Form the *macro* allocated, through the prelude's form-cons, on the
loaded module's own heap: that is the direction nothing had ever tested, and
it is the one the expander spends all its time in.
Checked by breaking the last expectation before restoring it.
A parser bug this turned up, and it is the reason the previous lane's Form
work could not have been finished as written: is_type_form decides whether a
leading form is a return type or the first form of the body by asking whether
its name is a declared type, and the set it asks was collected from the file's
own declarations only. Check.program prepends the prelude to every program, so
the prelude's types are every file's types -- but nothing told the parser that.
It never mattered while the prelude's structs were only taken as parameters.
A macro is (defn m [args [Form]] Form ...), and bare Form in return position
was parsed as a body expression and reported as an unknown name, while [[Form]]
worked, because a Vec in that position is a type whatever is inside it. The
prelude's types are now part of the base set, read once.
Item 12 asked five questions. All five come back clean, and the answer is
feasible with no obstacle that argues for porting the compiler.
The one that mattered most was signals, and it turned out to be a non-question:
OCaml 5.2 on Linux/amd64 installs no signal handlers at all -- fifteen swept at
four moments, every one SIG_DFL, and a plain ocamlopt executable behaves the
same, so embedding changes nothing. OCaml 5 checks the stack limit explicitly
instead of arming a guard page, so the SIGSEGV the break loop wants was never
taken. The break loop can have it outright and Stack_overflow still works. That
is measured on one platform only, and the entry says so: macOS/arm64 is the one
claim here that must be re-run rather than trusted.
The rest: -output-complete-obj links with dune uninvolved and the existing C
stubs intact, no symbol collides across the four .c files and libasmrun, the
game keeps the main thread while a C-created listener calls into OCaml after
caml_c_thread_register, and a compaction left an 8 MiB arena byte-for-byte
untouched. Startup is 0.6ms and the compiler adds 4.14MB -- which makes the
merged dev build smaller than today's daemon alone, and there is one of it
rather than two.
Written as item 14 with the numbers, the three ways the GC assumption would
break, what the spike does not cover, and the order the real work goes in. The
backend is left exactly where item 13 put it.
The `inspect' verb had no coverage. The discriminating case is not a path
step, it is the frame: dev-inspect.flan gives `mark' to a global holding 99
and to a local of the OUTER frame holding a Point, so evaluating the name and
rooting at the frame answer differently and not even with the same type. One
`eval-expr' and one `inspect' of that name is the bug and the fix in a pair.
The slot index comes off the locals listing's fourth element rather than being
written as a literal, which exercises the field the editor depends on and
keeps the test from passing for the wrong reason if slot allocation shifts.
The rest is what a path can and cannot do: a struct field, an array element,
an option's payload and a union case's field — the last two having offsets but
no accessor form in the language — and four refusals, each checked for naming
the step and saying why. A `:path' of `nil' is read as the slot itself,
because Emacs has no other spelling for an empty list.
Two claims about the frame, since `stopped_frame' being shared is an assertion
about code rather than about behaviour until something proves it: the frame
whose body was redefined under it is refused, and so is the whole stack once
the program resumes.
The first six probes each proved a piece. merged.sh puts them together: the
program's @main is renamed out of the way, a C main takes the main thread and
runs it there, caml_startup happens on a thread beside it, and clang links the
lot -- the emitted program object, flan_rt.c, flan_dev.c, flan_agent.c and the
whole compiler as one -output-complete-obj. It runs, and the compiler inside it
compiles the very source the program was built from.
Nothing is wired up. The two halves share an address space and do not speak.
That is the point: the question was whether they can, not what they would say.
sig.sh and symbols.sh answer the two questions the first pass got wrong or
skipped. The SIGSEGV reading in harness5.c was taken at the wrong moment --
OCaml 5 starts domains after caml_startup returns, so the disposition had to be
read from inside the runtime, and against a plain ocamlopt executable as a
control. symbols.sh is the hazard nobody looks for until the link fails: four
.c files that are compiled into two different processes today, and the OCaml
runtime, all landing in one link.
`i' on a local sent the local's *name* to be evaluated, and an expression is
evaluated where the evaluator stands. That is the right frame only when the
frame is the innermost one; on any other it may resolve to a global, to
another binding of the same name, or to nothing, with the locals listing right
above it showing the frame's own storage and nothing saying the two disagree.
The daemon verb for the fix landed already. What was missing was the state
layer under it: `flan-inspect--expr' held a bare expression, so there was
nowhere to put a frame. It is `flan-inspect--root' and `flan-inspect--path'
now — `(:expr E)' or `(:slot FRAME SLOT NAME)', plus the steps walked from it
— and a stack entry is `(ROOT PATH . POINT)'. RET appends a step, `l' restores
a pair it pushed. Every step is still a fresh request, so the view is never
stale.
`l' cannot cross between the two roots, and that is structural rather than a
rule someone has to keep: RET only ever extends the path under the root the
buffer already has, and `flan-inspect' and `flan-inspect-slot' both start with
an empty stack, so a mixed stack cannot be built at all. It stays true if a
third rooting mode is added.
The break buffer hands over the frame and the slot *index*, which is the
fourth element `locals' now puts on each line. A name does not identify a
slot: two slots of one frame can share one, and a refused slot is not in the
listing, so its position is not an identifier either. A global still goes in
by name, because a global's name really is an expression that means the same
thing wherever it is evaluated — the loaded thunk binds to the program's own
storage through the dynamic linker.
Two smaller things the wire needed. A field step carries the type it was read
out of, because a union's payload is at an offset that depends on the case and
only the renderer knows which case the value is in — so `Union.case.field',
which is the head the renderer wrote with the field appended. And an empty
path is sent by omission: Emacs prints an empty list as `nil', which is a
symbol on the wire, so the daemon now reads that as no path rather than
refusing it as a step.
24 bytes, align 8, payload at offset 8. Those three numbers are the whole
agreement between the compiler and a dlopened macro -- the compiler writes a
Form into raw memory a field at a time and reads one back the same way -- and
they were written down in a handoff note and asserted nowhere. Nothing at run
time would notice a disagreement of one byte; the macro would simply return a
different form than it built.
So they go through the oracle the DWARF cases already use: ptrtoint of a
getelementptr through null, constant-folded by llc and read back out of the
.quad. The offsets and the size that oracle already answered. Alignment it did
not, and reading [2 x i64] out of the emitted type and concluding 8 would be
asserting the layout against itself -- the circularity BUILT.md rejected when
it turned down a _Static_assert. It is asked instead: the offset of field 1 in
{ i8, Form } is alignof(Form), because a member sits at the first offset its
own alignment allows.
Checked by breaking it both ways before restoring: 25 for the size and 16 for
the alignment each fail, and name which number moved.
The llc plumbing is now one run_oracle over a module of folded constants, with
llvm_members and llvm_align as the two questions asked through it.
Item 12 asks five questions and says to answer them with a spike rather than a
rewrite. spike/embed/ is that spike: one script, six binaries, each one built to
fail loudly at the thing it is asking about. It is deliberately not a dune
target -- the root dune only excludes old-ocaml/, so a dune file here would land
in @default and make the spike part of the build. It drives ocamlfind and clang
by hand against the flan.cmxa dune already produces.
The probes, in the order they would kill the idea: the smallest possible link, a
C main() reaching one OCaml function; the whole compiler linked in and doing
real work; the same again with lib/dynload_stubs.c from the unmerged dlopen
branch, because that is the only C the compiler itself is built from; the game
keeping the main thread while caml_startup happens on a pthread beside it; the
SIGSEGV disposition read on both sides of caml_startup; and an 8 MiB arena
checked byte for byte across a compaction.
No result is written down yet. This is the apparatus.
The prelude's own (vec-new Form) was refused with "nothing here says what
(vec-new) is a Vec of" -- a message about a missing annotation, to a program
that had written one. The build went red the moment the Form declaration was
checked against anything, which is why the front half landed unmeasured.
The test a leading bare symbol has to pass was spelled out twice, once in
vec_new_elem and once in map_new_types, and both lists were written before
unions existed: primitives, structs, enums, aliases. resolve_name has known
about unions since they landed, so the two halves disagreed about what a type
name is. Now there is one list, read by both, so the next kind of type cannot
be added to one of them.
The case is in unions.flan rather than in a file of its own, because what it
asserts is that a union is an element type like any other -- (vec-new Shape),
(map-new string Shape) -- and that is a sentence about unions.
Also drops forms.so, a build artefact the last lane committed.
OCaml side is done and builds. Emacs side is mid-edit and INCOMPLETE — see
the handoff below. `dune build --root . @check` is green. `dune test --root .`
was NOT run. The .el files were not byte-compiled and flan-inspect.el will not
work as it stands: the state layer still speaks the old single-expression
shape while the helpers above it have been rewritten for roots and paths.
WHAT WORKS (daemon, lib/, in the parent commit and unchanged here)
- `(:op "inspect" :frame N :slot I :path (...))` renders one value rooted at a
stopped frame's slot address. `Session.render_slot` is `render_locals` with a
path applied to the root before the walk and one line out instead of one per
slot; no second walk was written and no backend change was needed.
- A path step is a string for a struct field, an integer for an array or slice
element, and the symbol `some` for an option's payload. A union case field is
spelled `Union.case.field`, because the payload's offset depends on the case
and only the renderer knows which case the value is in.
- Every step that does not fit the type in hand is refused by name with the
reason: a field the type does not have, an index past a fixed array's end,
`some` on something that is not an option, a union field without its case.
- The frame's identity IS checked, and not by a second copy: `Dev.stopped_frame`
is one function now and `locals` and `inspect` both go through it — alive,
stopped, frame exists, the frame is the program's and not a thunk's, the body
is one this session holds, the slot count matches, and `Emit.slot_fingerprint`
matches. `inspect` additionally refuses an unbound slot, for the listing's
reason: a null address would fault on the stopped game thread.
- The slot travels by INDEX, not by name. Two slots can share a name
(`fresh_slot` only allocates) and a refused slot is not in the listing, so
neither the name nor the position identifies one. `locals` now puts the slot
index as a fourth element on each `:locals` entry.
- `Dev.run_render_thunk` is one function; `locals`, `globals` and `inspect`
share the build/deliver/wait/read tail.
- `layout`'s "union values are milestone 6" is corrected.
WHAT IS HALF-BUILT, AND EXACTLY WHERE IT STOPS
`emacs/flan-inspect.el`. Done: the header comment explaining the two roots;
`flan-inspect-step-expr` taking a 3-element `:field` step; `flan-inspect-wire-step`;
`flan-inspect--root-label`; `flan-inspect-refusal` taking an optional ROOT and
allowing an option's payload under a `:slot` root.
NOT done, and this is the whole of what is left:
1. `flan-inspect--expr` / `flan-inspect--stack` still hold a bare expression.
They must become `flan-inspect--root` (`(:expr EXPR)` or
`(:slot FRAME SLOT NAME)`) plus `flan-inspect--path`, with stack entries of
`(ROOT PATH . POINT)`.
2. `flan-inspect--value` must branch on the root: `eval-expr` with
`(flan-inspect--root-label root path)` for `:expr`; for `:slot`, send
`(:op "inspect" :frame F :slot S :path P)` with P built by
`flan-inspect-wire-step` over the path, and take `:value` from the reply.
3. `flan-inspect--show`, `-into`, `-pop`, `-refresh` rewired to (ROOT PATH).
`-into` must build a `:some` step when the node's kind is `option`, and put
the parent node's `:type` as the third element of a `:field` step.
4. New entry point `flan-inspect-slot (frame slot name)`, kept separate from
`flan-inspect (expr)` — `emacs/flan-mode.el` autoloads and binds the latter
and that file is out of this lane.
5. `emacs/flan-cnr.el`: the `flan-cnr-inspect` text property must carry
`(:slot FRAME SLOT NAME)` on a local line — the slot index is `(nth 3 l)`
now — and `(:expr NAME)` on a global line, with `flan-cnr-inspect`
dispatching to the right entry point.
6. `emacs/test-flan-cider.el`: the fixture at "`i' on a local inspects it by
name" asserts the old behaviour and must be rewritten; the locals fixtures
need a fourth element.
7. `test/test_dev.ml`: no coverage of the new op yet. The discriminating test
to write first is a stack whose OUTER frame has a local whose name is also a
global with a different value, asserting `inspect` answers the frame's value.
A new `test/programs/dev-inspect.flan` is picked up by the existing glob.
8. `BUILT.md`, `emacs/MANUAL.md`, and striking the item from `NEXT.md`'s
"Decided in discussion" and `DISCUSS.md` item 1 — none done.
THE THREE ANSWERS THE TASK ASKED FOR
- Navigation in the new mode: the daemon supports it fully — RET extends the
path, `l` shortens it, and both are a fresh request, so the view is never
stale. The Emacs half of that is item 3 above and is not wired.
- `l` does not cross between the modes, and that is structural rather than a
rule: a stack entry carries its own root, RET only ever extends the path
under the root it already has, and every new root starts with an empty
stack. A mixed stack cannot be constructed, so the question does not arise —
and it stays answered if a third rooting mode is added.
- What each mode cannot do that the other can. The expression root works on a
RUNNING program and roots at anything you can write, a call included; it
cannot name a frame, so it is the bug. The slot root names one frame and one
slot and is exact; it reaches an option's payload and a union case's fields,
which have offsets but no accessor in the surface language; it needs a
stopped program, it is refused when the frame's body was redefined since it
was entered, and it cannot root at an expression at all.
`i' in the break buffer sent a local's *name* to be evaluated, and an
expression is evaluated where the evaluator stands. On the innermost frame
that is the right frame; on any other it may resolve to a global, to another
binding of the same name, or to nothing, with the listing above it showing the
frame's own storage and nothing saying the two disagree.
The shadow stack is what makes the second rooting mode cheap: a frame's
address and every slot's type are both here, so a step into a field is an
address plus an offset with that field's type — the arithmetic
`Render.render' already does for the listing. `Session.render_slot' is
`render_locals' with a path applied to the root and one line out.
The slot travels by *index*, because a name is not unique: two `v's is two
slots and both are in the listing, and a refused slot is not, so the position
in the list is not an identifier either. So `locals' now puts the index on
each line.
The frame checks are `locals'' by construction — `stopped_frame' is one
function now, and an inspector with its own copy would be free to read a frame
whose body was redefined since it was entered. The build-and-read tail is one
function too, for the reason this file already records about the fingerprint.
And `layout' said union values were milestone 6, which they have not been
since today.
Running a macro means compiling it and loading it into the compiler, and the
step that reads as small in NEXT.md is not: OCaml has no dlopen for ELF, and
lib/dune had no foreign_stubs. So the boundary is built first and the expander
not at all. lib/dynload_stubs.c is the whole of it — dlopen, dlsym, a
four-argument call into a macro thunk, and a peek/poke family, because OCaml
cannot address the raw memory a Form image has to be laid out in.
Nothing aggregate crosses to C. The unions lane verified a union's memory
layout against clang, which is a different claim from LLVM's convention for an
aggregate passed or returned by value in hand-written IR, so Emit.macro_thunk
wraps every macro in void(ptr,i64,ptr,ptr): the slice is built and the result
stored on the LLVM side, and the compiler's side is four pointers.
Build.macro_module links the runtime in rather than declaring it external, so
the module has no undefined symbols and the compiler's own link needs no
-rdynamic. That is the difference from Build.shared, whose host is a running
Flan program.
defunion Form and the list-building surface quasiquote will desugar into are in
the prelude. Form mirrors Form.value and not Form.t: no loc field, so the
compiler stamps the call site's location onto everything a macro returns.
The compiler builds. dune test was not run, and Form's layout is asserted
nowhere — NEXT.md's new handoff section says what the three numbers are, what
the next two commits should be, and the four decisions this made that the
design did not settle.
check_finite already walked a union's cases, so a union containing itself by
value was refused before the emitter could try to lay it out -- which it would
have done forever, since payload_lay calls lay calls payload_lay. Asserted
both ways round: directly, and two unions through each other.
Through a pointer it works, and that is the shape a Form has, so it is in the
program rather than only in the prose: a Tree with a (Ptr Tree) field, matched
through a deref, summed recursively.
BUILT.md also records why match's fall-through is still unreachable rather
than a trap. It is only sound because no reachable program can hold a tag no
case names: Zero is tag 0, every construction writes a tag the checker
resolved, and uninit -- the one way to get bytes nobody wrote -- is refused on
a union for exactly this reason. The refusal is what pays for the unreachable.