The break buffer captioned every refused restart "below this evaluation", which
is the wrong sentence at a trap: there is no transfer channel, nothing on the
list can be taken, and there is no evaluation to abandon. The terminal listing
had always said the two apart. The wire now does too — a bare ! line ahead of
the entries, because a trap with no restarts at all still has to be able to say
so, and because :abandon being nil means three different things.
The boundary was also the first entry truncation dropped: snap_push walks
innermost first, and the boundary is the outermost frame of the evaluation. A
slot and a name's worth of bytes are kept back for it.
flan_break_resume goes; nothing has called it since choices became positions,
and NEXT.md already said there was no such function. eval_boundary is cleared
between runs beside the two stacks that already were. And the note on a taken
restart stops costing a second round trip: the agent answers ok abandon for the
boundary, which is the end that knows.
Nested boundaries are tested rather than argued: two evaluations, six restarts,
and abandoning the inner leaves the outer with its own still on offer.
A C-x C-e that blew up took the session with it. The expression's break offered
either nothing at all — a bad index establishes no restart, and the program's
own are below the thunk boundary where a transfer has nowhere to land — or a
list on which every entry was refused. That left abort, and abort is _exit(134)
over a mistyped index.
So the boundary offers a restart of its own. The agent pushes a real frame
around every evaluation, after the floor is read so that it lands above it;
taking it unwinds to the thunk, flan_reload_call drops the channel it holds,
and the poll returns to the game loop. It abandons and does not undo, which is
said in the agent's line, the daemon's note, the buffer's row and the manual.
The other half was a silence. The break buffer drew every restart as takeable
and ignored the :unreachable the wire already carried, so a digit on one went
out to be refused and nothing came back. Those rows now carry the reason and
are refused where they are read, and :abandon names the position that drops the
evaluation — a position, because a program may establish a restart of that name
itself.
Not the threading, which is what the report suspected. The thunk does run on
the game thread; a thunk on a thread of its own would have had the same empty
list and the same abort.
The trio the author decided on 2026-09-20 is now all built: def is CL's
defparameter — its initialiser runs on every daemon re-run, unguarded, so
an edited initialiser repaints the same storage on C-c C-c plus re-run —
defonce (Clojure's name for CL's defvar, per the author) initialises once
behind the .init~once. flag, and defconst stays the image.
One parse arm reads both forms; the difference is Ast.reinit, carried to
Tast.global's grerun. Emit.startup_plan gives a def no guard flag, and
Check.check_global lifts every def initialiser — zero and literal
included — into global/<n>, so the host's startup reaches it through the
function cell and a re-evaluated def swaps it (Session's def_inits;
Emit.redefinition declares the cell for a non-sibling target). The old
defvar spelling is refused with the rename and both compiling spellings,
and every program, test, doc and editor list is swept — except sand.flan,
the author's live WIP, whose seven defvar lines are flagged in FIX.org
and keep its three dependent tests red on this branch.
Five fixes off the independent review, plus the author's u8 ruling.
x86 parity: the bad-index block always ran x86 (it is flan dev's
default) and now says so with an explicit --x86; the condition render
gets an assertion under the x86 backend too, beside the LLVM one, and
a user error is pinned as carrying no site on both.
ArithError's layout is now pinned: {i32 op; i64 lhs, rhs} in C against
the prelude's defstruct, read field by field through the break loop's
render, driven from the editor through a divide under a restart-case.
That also covers condition and site on LLVM.
Three refusals that were wrong: trap_site tested the prefix "err"
and so ate any site whose path began with those letters; source_line
let Sys_error from input_line escape and take the whole break reply
with it, leaking the handle; and a condition with no fields was
reported as a name no struct has. The daemon now sends its own field
count and the buffer tells the two empties apart.
Nits taken: an over-long site is dropped rather than silently
truncated into a plausible one; the caret pads with the source line's
own tabs; the headline says when it has cut the field list;
flan-cnr-layout is live again as the single spelling of that request
rather than dead beside an inlined copy.
And the ruling: a u8 renders as 97 (\a) where a person is inspecting
and stays 97 where the program is printing.
A break nested inside a trap's break — a fix candidate evaluated at a
bounds stop raising its own error — copied the outer trap's site into
its snapshot, which is a caret pointing at an unrelated line under the
inner condition's name. snap_push now consumes the global: each
snapshot owns its copy, a nested entry that set no fresh site gets
none, and the outer break keeps its own. Pinned end to end.
The fields section says why the values are missing once, above the
rows, instead of repeating the sentence per field; a row keeps its own
reason only when it has one (no printer for its type).
strip_rebind and shown_names get direct coverage, including the clean
strip a real frame almost never reaches.
A full pass on the break-loop display, off a dogfooding session that
met a BoundsError and read design notes instead of values.
The headline now carries the condition's own fields — BoundsError
reads as 'low 648, high 648, length 100' with nothing special-casing
it — over one short status line, and under it the trap's own site with
the source line and a caret at the column, Elm-style. Fields render
with values through the new condition op, falling back to the layout
with one sentence per missing value.
Every choice goes out by restart-at now, so a shadowed restart is
takeable rather than refused, and its line says only whose name it
repeats. The abort line says what abort does. The bracketed
implementation notes are gone from every string that could print, and
a fixture pins that as a property of the rendering.
Key members are :key-r, :key-space, :key-left-shift; MouseButton members
are :mouse-left through :mouse-back — mouse- over button- because gamepads
have buttons too. Bare members collided across enums and with user code.
The bindings enum directive grew an optional third column declaring the
Flan-side member prefix, stripped before the C prefix is applied, so key-r
checks against KEY_R rather than KEY_KEY_R; a member that does not carry
the declared prefix is reported, not checked under a guessed name. The
enum-member error grew a did-you-mean: one edit away, or the bare name of
a prefixed member, so :r suggests :key-r.
sand.flan is the author's live WIP and is deliberately not touched; its
three keywords (lines 161-166) leave test_session and sand-headless red
until he moves them. Everything else that calls the two enums moved.
MANUAL.md: *flan* and *flan-repl* throughout, the REPL's output and
error-summary behaviour, the two clears on C-c C-o / C-c M-o, the
diagnostics buffer as the one list with the memory section below the
errors, and the settings table without flan-output-buffer. BUILT.md's
three mentions of *flan-output* updated to the routing that exists.
FIX.org records the decision, dated.
*flan-output* is gone. The program's output lands in the daemon's buffer
always — renamed *flan-dev* to *flan* — and at the REPL when one is open,
inserted above the prompt, output first and the value after it. A
rejection puts its message in *flan-diagnostics*, which now pops up, and
leaves one line at the prompt pointing there; the diagnostics buffer got
a major mode of its own, read-only with n/p/RET, and the memory sites
from flan-check-memory render into it as one section below the errors,
replaced whole on every ask. Two clears at the REPL, on CIDER's keys:
C-c C-o for the last send's output, C-c M-o for the transcript.
No daemon changes: output already rides every reply's :output, so both
destinations are editor-side routing.
flan--declaration-heads was topped up a name at a time, so it had lost
defdata and all four of the object and dispatch heads. A head missing
from it is not a quiet fallback: the form goes to the expression
evaluator, and the daemon answers "defclass is a top-level declaration,
not an expression" at the line you pressed the key on. Confirmed both
ways against a real daemon on test/programs/dev-class.flan -- the old
list gets that refusal, the new one installs, and defgeneric and
defmethod install through the same path (area@point, area).
Re-derived whole from Parse.decl, which is the honest authority. The
comment said the authority was the declaration arm of Parse.expr, and
that was never quite true: declare and declare-c have been in this list
from the start and Parse.expr has no arm for either. So the rule is the
plain one -- is it a declaration -- and package is a single named
exception, kept because the two keys have always differed there and
nothing about a line written once at the top of a file asks for that to
change. The rationale in test-flan.el's package check said the same
stale thing and says the surviving reason now.
The new check reads the list off Parse.decl and asserts both keys on
every head, so the derivation is the test rather than a claim beside it.
(defmacro do-grid [[r rows c cols] & body] ...) — positional names, a [ ]
pattern wherever an argument is a vector, and & for the tail. The reading of
the list lives in Expand, below both sides that need it: Parse turns it into
the bindings a macro body opens with, and Macro checks a call against the same
reading before expanding it, so arity and shape are refused with the call's own
location rather than with the Loc.from_macro stamp every node of an expansion
carries.
The breaking half: [args] used to bind the whole argument list and now binds
the first argument. The whole list is [& args], and every defmacro in the tree
— prelude, vendor, tests, the elisp fixtures — was migrated to it. One grammar,
not a legacy mode.
Each was decided in DISCUSS.org; this is the elisp side of all six.
The eval value goes to the end of the line now, where flan-watch has
always put one, and it is echoed as well as drawn. Both of those
overturn a documented decision, so both docstrings and the MANUAL say
what the behaviour is rather than still arguing the old case: a value
wedged mid-line reflows the code after it, and a value that is gone at
the next keystroke with nothing left anywhere is worse than a number
said twice.
C-c C-i inspects what is at point with no prompt, and C-u opens the
minibuffer. It takes the form point is inside, not the one behind
point, because the middle of a name is where a key pressed without
looking lands.
Compiler messages are kept: *flan-diagnostics* accumulates one entry
per message, timestamped, never cleared by anything but
flan-clear-diagnostics. The overlay is unchanged. Each message line
keeps the compiler's own file:line:col: shape, which is both what makes
it jumpable and what makes it paste like a terminal line.
*flan-dev* gets compilation-minor-mode, so a main that does not compile
is a buffer you can M-g M-n through. Verified against a real diagnostic
rather than assumed: the default gnu entry matches the message line and
nothing else in the transcript, so there is no custom regexp here.
#_ is drawn as a comment, via a syntax-propertize-function that fences
the discarded span -- clojure-mode's approach, and the nesting rule is
the reader's, so #_#_ a b greys both with nothing counting.
And the two keyword lists are re-read off lib/parse.ml whole, rather
than topped up by name: defmacro, the four object/dispatch definers and
declare-c on one side; when, cond, and, or, break, continue, recur, fn,
quote, array, signal, error and the four condition forms on the other.
flan.abi.require was spelled by hand in both backends, which is the one
job Mangle has. Moved; emit and x86 produce byte-identical output on the
reload path either way.
Four comments in the dyn-cast code asserted things that are not true.
The warning's location prefix now reads like every other loc-bearing
runtime diagnostic instead of inventing a shape. widen's contract says
what cast_dyn actually does with it. The thread-safety note names the
torn {ptr,len} overread rather than a duplicated line, and says why no
lock. The site table's borrowed loc pointer names what keeps it valid.
The memory op's note claimed a completeness it does not have: dyn push
and put may allocate and are deliberately silent. Said so, in the note,
in the classifier, and in FIX.org where the decision belongs.
The documented flycheck form only matched warnings, so a real error
made it say the checker returned non-zero and found nothing.
flan-clear-memory cleared one buffer where the toggle clears all.
Two comments claimed test/dyn_ops.c calls every function flan_dyn.h
declares; six are declared and never called there.
flan_dyn_stub.c's deadness is written into FIX.org for the author to
decide on. Not deleted here.
flan-check-memory painted every buffer visiting a file the answer named
but decided whether it was already on by looking at the current one.
Asking again from a third buffer therefore stacked a second copy on the
two that were marked, with a second help-echo, and the toggle in the
third buffer took nothing down. Both the toggle and the repaint now go
through the whole buffer list.
The comment beside the faces claimed a dotted underline. The gc face
draws a wave, the same style the error face does; what actually makes
these fainter is that neither inherits error and neither draws its
message into the line. Say that instead, in flan.el and in MANUAL.md.
[Check.memory_sites] is the pass [Check.no_gc]'s shape: it runs over the
finished program, answers a diagnostic list, and tells nothing downstream
that it ran. Two classes on the diagnostic's kind — the collector's heap
and an allocator the program named — so the CLI, the daemon and the editor
dispatch on one field and none of them reads a message to find the class.
[--warn-memory] on check and build prints them where errors go, in the
shape flycheck parses, without moving the exit status. [(:op "memory")]
answers the same list over a session's last checked program, needing no
program on the far end. [M-x flan-check-memory] paints it two faces fainter
than an error's, cleared by an edit or by asking again.
Two of the spec's own examples turned out not to allocate, and the
precision rule outranks the enumeration: (vec-new T) passes a capacity of
literal zero to flan_vec_init, which returns before the grow, and
flan_map_init takes no block at all and says so in its own comment. The
block arrives at the first push, which is the line that is marked. The
classifier reads the capacity argument rather than the symbol, which is
what lets slurp be marked through the same entry point vec-new is silent
through. FIX.org has the rest of the evidence.
The unwinding handler, which spec-conditions.md named and left unwritten while
it asked whether the thing should be a macro over the two operators that were
already here. It should. (handler-case B [(T [c] A)]) is checked as
(restart-case (handler-bind [(T [c] (invoke-restart 'R c))] B) (R [c T] A))
with R a name the form makes up for itself, which is Common Lisp's own
definition of the operator and means neither backend needed a line.
What that buys is not economy, it is the correctness of the parts nobody can
see. The defers between the signal and the form run, and the allocator a
with-allocator rebound is put back, because a transfer already does both for
every frame it leaves. The body and every clause agree on one type, because a
restart-case's body and clauses already do, and a clause that disagrees is
refused with the same message an if with disagreeing arms gets. A condition no
clause lists installs no frame that matches it and goes on outward untouched.
A clause sees the establishing function's locals, which a handler-bind clause
cannot, because a restart clause runs where it was written.
The body comes first and the clauses after it, the opposite of handler-bind's
order: one reads as something put around a body and the other as a body with
answers hung off the end of it. The restart the two halves meet over is named
after the function and numbered within it, and it has to be unique per form,
because two nested handler-cases sharing a name would have the inner frame
shadow the outer one and land a condition at the wrong place.
The refusals name handler-case rather than the machinery underneath it, which
is why check_handler_bind and the restart clauses now take the word the reader
wrote. A break loop entered under a handler-case still lists the made-up
restart, and taking it there is refused loudly rather than answered wrongly;
hiding it would mean a field in a frame layout spelled out in three files.
The survey program runs the same under LLVM, at -O0 and under --x86: normal
completion, a caught condition, one nobody listed passing through with the body
carrying on, both defers on the way out, the two nestings against handler-bind,
a clause that signals and is caught outside the form it belongs to, and a
with-allocator whose restore is on the transfer path.
`flan-inspect-edit' turns off read-only and line truncation, and `--show' is
the one funnel every drawing goes through — but the mode body that set them in
the first place does not run a second time on a buffer already in the mode. So
a listing drawn after a commit or an abandon presented as a listing and was
plain editable text: `C-k' on one of its lines did what `C-k' does. Put back
beside the flag that says which of the two the buffer is.
And that flag is now the marker it should have been. The header above the
value is two lines or three depending on whether the daemon named a type, and
the count that skipped it was written once in the writer and once in the
reader that parses the buffer back — two places to keep in step for nothing.
The MANUAL's key table lists the two keys that commit and abandon, which were
in the prose and not in the table anybody scans.
`e` on a line sets that field: prompted with what is there, sending a Flan
expression that the program evaluates and the checker measures against the type
of the place it is going into. `C-c C-e` turns the buffer into the value — the
Flan literal the program wrote, which is the value's own spelling and not a
second notation invented for editing it — and `C-c C-c` commits, `C-c C-k`
abandons.
A commit is a diff. The buffer is read back as a value, compared leaf by leaf
with what was drawn, and one write goes out per leaf that changed, so editing
one field does not rewrite the others with whatever was on the screen. A shape
that changed is a refusal and it voids the edits collected before it: the walk
visits fields in order, so a struct with one good change and one impossible one
has already collected the good one, and handing that back beside the refusal
would make it possible to send half of what was asked for.
The truncation guard is the one that is easy to miss. `...` is what the
renderer writes where it stopped, and a commit read off a buffer holding one
could not tell a field that was never written from one somebody deleted — so
such a value refuses to be opened for editing at all.
Writing is refused on the expression and address roots, by name. An expression
is evaluated wherever the evaluator stands and whenever it next reaches a frame
boundary, which for a write means possibly into a running program; globals stay
unwritable from here until they have a root that names a stop.
test-flan.el runs the whole chain against a real daemon — a frame, a slot index
off the listing, a render, a set, a re-read, a typed refusal, an edited buffer,
a commit, and a commit against a stop the program has left.
The !-means-mutates convention distinguished nothing — there is no
immutable counterpart to contrast with — so every mutating name drops
the mark: sort, sort-by, sort-bytes, swap, reverse, append, append-i64,
append-f64, encode-rune, split-next, map-remove, map-next, and the test
helpers beside them. Two could not simply shed it: map! is map-in-place,
because map is the into transform's word and means the non-mutating
thing; put! is put-at, because put is the Map builtin. The ?-means-asks
convention stays. Dated records keep the old spellings; watch.clj's
reset-spies! and the other Clojure names are not ours to rename.
flan dev takes the hand-written backend unasked now: it is what that
backend was written for, it halves the C-c C-c round trip, and nothing it
builds outlives the session. Every other command is LLVM by default and
unmoved, which is what keeps lib/x86.ml's calling convention licensed.
--debug picks LLVM on its own rather than landing on the refusal: a
redefinition module from this backend carries no line table, so the one
flag someone types to get a debugger must not choose the backend without
one. --x86 --debug together is still refused, and now says which to drop.
What the flip costs is more than the forms this backend refuses. It
pushes no shadow stack, so a session built by it stops on an error and
cannot say where -- and the agent answered that with "this program was
not built with --dev", which is false of an x86 dev host. Dev.ask
rewrites it; the IR view refuses by name instead of failing to find a
define in a .s file; and a refusal at the host build or at C-c C-c names
--llvm rather than stopping at "unsupported".
flan-daemon-args carries the flag from Emacs, spliced into the one
argument list the daemon buffer's first line is now printed from.
lisp.el finishes end-of-defun by skipping blanks and stepping over the
newline after the closing paren, so the bounds C-c C-c works from end at the
start of the next line. The declaration path never cared -- a newline more or
less in the text sent changes nothing -- but a value drawn at that position
sits in column 0 below the form, in the gap before the next one.
The fixture that missed it inserted the expression at point-max with nothing
after it, which is the one shape where end-of-defun has no newline to step
over. It has one now, and the check is the closing paren rather than a line
number.
The error-overlay check was the same kind of weak: it asserted the line was
not 1, which flan--text alone would have given. It asserts the token now,
which is what flan--text-at is for -- and eval-expr's replies do respect the
column padding, so the switch stands.
The routing C-x C-e already had, asked of the form C-c C-c already picks:
the top-level form point is in, not the one behind it, so reloading the defn
you are standing in is untouched. Both keys are one command and both route --
a person who writes (+ 1 1) and presses C-c C-c has the complaint that started
this, and splitting them would buy a rule to remember and nothing else.
A prefix on the expression path is a flag, the way C-u C-x C-e's already is:
there is no inside for a position to point at, so C-u and C-u C-u say the one
thing, and it does not stick because the thunk is thrown away.
package is where the two keys disagree, which is the one real finding here.
Parse.expr has no arm refusing it, so C-x C-e's question answers no; Parse.decl
has one, so C-c C-c must keep installing it. flan--defun-heads is that list
plus that head.
The value now goes beside the form, in flan-watch's ghost shape and with the
error overlay's lifetime, because eros and CIDER put it there and the echo
area cannot say which of two forms returned 2. flan-inline-result turns it
off; where the two would overlap the overlay wins and the echo is not also
written. A refusal clears it rather than sitting beside it.
The expression path sends flan--text-at now: an expression does not start at
column 1, and an unpadded snippet put its refusal on line 1 of the file.
`arena-new` is a builtin, so it is in no program's symbol table and `defs`
never mentioned it — which made the editor answer "the running program
defines no arena-new" about a name that works. The fix is not a better
refusal: it is that the seventy-eight names the checker answers without
being told are now in the reply, under a kind of their own.
check.ml carries the table, beside the arms it describes, because a table
in another file drifts from them with nothing said. test_flan reads both
the arms and the table and fails on either having a name the other does
not, in both directions — there is no reflecting over a match, so it reads
the source.
Each entry is a signature in `signature_of_fn`'s shape and one line. The
arms that do not have one shape say what is true instead of pretending:
`?` for an argument that may be left out, `|` for the types an arm really
takes, and the checker's own predicate names for the type-directed ones.
The three user-allocator names carry a bare name and no bracket list,
because they are refused wherever they are written.
`defs` grows a fifth string for the prose, and builtins are appended last
so a completion table does not bury the names being worked on. A defn has
no docstring to put there and does not get one here: the Tast keeps none,
and that is a different piece of work.
The editor reads the kind, not a special case. `flan-doc--where` and the
xref backend both answer before their empty-location branch, because a
global's missing location and a builtin's absent one are different facts
and only one of them is about the daemon.
Emacs binds it in emacs-lisp-mode-map and nowhere else, so a mode derived
from lisp-mode inherits nothing and the key answers "undefined" — which
reads as the client being broken rather than as the key being free.
comint hands the input sender its buffer text with font-lock's properties
still on it, and prin1 writes such a string as #(...) — which the daemon
reads as a bare symbol followed by a stray list, so the field stops being
a string without anything saying so. The wire layer strips rather than
asking every caller to remember: it is the one place that knows the text
is about to become bytes.
`-dev-` was in every Emacs symbol this client owns and meant nothing to anyone
typing one: the daemon is `flan dev` at a shell, but from inside Emacs there is
no other kind of connection to distinguish it from. `M-x flan-dev` is now
`M-x flan`, `flan-dev-quit` is `flan-quit`, the private prefix `flan-dev--` is
`flan--`, and every defcustom follows — ninety-odd symbols, with the two files
renamed to emacs/flan.el and emacs/test-flan.el so the file names say the same
thing as the symbols in them.
No aliases. Renaming a defcustom breaks a config that names it and there is no
way around that; the repo has no precedent for softening one, and an alias left
behind is what keeps a rename from finishing. MANUAL.md says the old names are
gone and how to fix a config, which is the whole of the migration path.
Three strings are not symbols and keep their spelling: `.flan-dev.sock`, which
bin/main.ml writes and which a renamed variable searching for a renamed file
would simply never find; and the two buffer names `*flan-dev*` and ` *flan-dev*`,
which name the `flan dev` subcommand's own output rather than anything in elisp.
`flan dev` with a space is the CLI and is untouched everywhere.
The entry point also stops asking a question it already has the answer to. From
a buffer visiting a .flan file it starts that file; from anywhere else it reads
one from the minibuffer as before; `C-u` reads one either way, which is how you
start a second program without leaving the first. The current buffer is still
the only source of the default — the bug where a previous project won over the
buffer you were in was fixed by removing `flan--file` from that position, and
nothing here puts it back.
Four checks on the `interactive' form, evaluated on its own rather than by
calling the command, because calling it would build and launch a program and
the question is only which file the form arrives at and whether it had to ask.
A fifth asserts that nothing answers to the old names. test/test_emacs.ml loads
the test file by path and test/test_session.ml names the client file in a
comment, so the rename reaches those two lines; nothing else outside emacs/ and
the docs moved. Verified by byte-compiling every file
clean and by `dune test` and `@page`.
Two things were missing from the REPL's history rather than broken in it. The
ring worked; nothing reached it by the key almost everyone presses, and nothing
survived quitting Emacs.
`<up>' and `<down>' are history on the line being typed and line motion
everywhere else. The conditional is the whole point: the text above the prompt
is a transcript people scroll back through, and a blunt binding would take that
away to buy something `M-p' already does. Inside a form typed over several
lines the keys still move between them, and reach history from the first. The
search is prefix-filtered, which is comint's `...-from-input' pair and what a
shell does. Each wrapper hands `this-command' over to the comint command it
delegates to, because comint tells a continued search from a fresh one by
looking for its own name in `last-command' -- without it the first press looks
perfect and the second never moves.
History is kept in `flan-repl-history' under `user-emacs-directory', through
`locate-user-emacs-file' so that no-littering and a moved state directory take
it along; the file and the size are both defcustoms. It is read when the mode
starts and written on kill and on exit, but not when the ring is empty and not
when the ring is shorter than the file -- a write replaces the file whole, so
either would throw away a session somebody else's Emacs saved. The exit sweep
is not installed in batch, where every prompt belongs to a test and the file it
would land in belongs to whoever ran it.
Entries are separated by a form feed on its own line rather than by comint's
newline, which is also what is inside a multi-line form: with the default the
file shreds one such form into fragments. The separator is let-bound around
each call instead of set in the buffer, because `comint-write-input-ring' reads
it inside a temporary buffer where a buffer-local value is invisible.
And the multi-line case the recall made reachable: `comint-send-input' sends
the process mark to *point*, and `comint-eol-on-send' carries it no further
than one line, while `flan-repl-return' judged completeness over the whole
input. Recall parks point where the typing stopped -- for an empty prompt, at
the start of what was just recalled -- so RET on a recalled form sent nothing
at all. It now sends what it measured.
emacs/test-flan-repl.el covers both halves from a stub prompt, loaded from
test-flan-cider.el for the reason test-flan-mode.el gives: `emacs/*.el' is
already a dependency of that stanza, so a file here needs no build change. No
daemon: history is a function from a ring and a position to what the buffer
says. Seventeen checks, which cost that suite nothing measurable -- the whole
of it still runs in 0.15s.
The unreadable-frame case wrote both frames as one string, where `(:a14' reads
as a typo rather than as the end of one frame and the start of the next; they
are two inserts now, with a line saying which is which. And the check that a
request after a daemon restart does not wait out a reply the old connection
owed was asserting against the same number it had bound the timeout to -- the
one arrangement that cannot tell "fast" from "timed out a moment ago". The
timeout is ten seconds and the assertion is three, which is the gap the claim
is actually about.
`flan-dev-quit' promises to stop the daemon this Emacs started. It sent `close'
down the current connection as well, on the assumption that the two are the
same program -- and `flan-connect' is the command for attaching to one that is
not, so the assumption was one keystroke from being false. Connect to a daemon
running in a terminal, quit, and two programs ended: the one Emacs owned, and
somebody else's.
The daemon's socket is now kept apart from the connection's, which is what lets
one be told from the other. `flan-connect' names both programs before it
abandons a connection belonging to a daemon this Emacs started, and refuses
outright for a Lisp caller, as `flan-dev' already did for a running program.
`flan-dev-quit' on a connection that is not the daemon's closes that connection
and says what it left running. `flan-dev-restart-program' restarts on the
daemon's own socket, and refuses while the connection is elsewhere: there is no
reading of "build it again" that leaves one session where there was one.
`backward-sexp' does not signal when there is no form before point; it goes to
the beginning of the buffer and stays there. At point-min that is no movement,
so the form the dispatch then looked at was the one *after* point -- whose head
is very often a declaration -- and the region between start and end was empty.
The key installed the file's first declaration by name with no body, which the
daemon accepts, and the echo area reported it as a change. Both paths check for
movement now: the predicate refuses, and the expression arm refuses in words
rather than asking the daemon to evaluate the empty string.
The same predicate asked only for the depth at the open delimiter, and a form
written at column 1 inside a comment or a string is at depth 0 like any other.
A `defvar' in a comment is prose; it is not offered as a declaration any more.
Depth in a narrowed buffer still means depth within the narrowing, which is
noted where it is read rather than changed.
`flan-dev--poll' guarded on `flan-dev--busy' and nothing else, and the watch
timer is precisely the sender that does not bind it: it sends without waiting,
so it has nothing to hold. A poll landing in that gap sent `describe' down a
connection that already owed a reply, read the watch's answer as its own, and
left its own for the watch to read a tick later -- after which the two
consumers stayed swapped for the session, each of them answering the other's
question. It runs `flan-dev-settle-hook' now, which is the invariant the hook's
docstring already claimed.
And the hook runs before `flan-dev--live-connection' rather than after. An
outstanding reply belongs to the connection it was asked on; when a daemon has
been restarted the old connection is gone and no reply is coming on the new
one, but the hook was being asked about the new one and blocked a full
`flan-dev-reply-timeout' -- a frozen Emacs on the first key pressed after a
restart, with the watch armed. `flan-dev--busy' now covers the reconnect too,
which asks the new daemon what it defines and so must not be interrupted by a
poll either.
Two orderings in the reply reader, and both of them were permanent. A frame
whose header arrived and whose body did not fell through the wait loop into
`flan-dev--extract-reply', where `byte-to-position' signalled a wrong-type
error on a position past the end of the buffer -- so the timeout message the
function goes to some trouble to word was never the one anybody read, and the
header stayed at the front of the buffer, where the next request took it as
its own and every request after that was answered by the one before it. The
body deadline is tested again now rather than trusted, and the dead frame is
erased: the timeout is said in the words meant for it, and the connection is
back in step. The header deadline still erases nothing, because a partial
header is a valid prefix of a reply that is merely slow.
The other is `flan-dev--extract-reply' reading the payload before deleting it,
so a payload that would not read was never consumed and the same bytes
signalled again on every later request. It is deleted first now. That makes
the frame gone whether or not the read succeeded, which `flan-watch--tick' has
to know: it cleared its pending flag only on a reply it got back, and would
otherwise wait for ever for one no longer in the buffer.
The 30s deadline in the reply reader was a literal and its message said only
that nothing had arrived. It is flan-dev-reply-timeout now, and the message
names the daemon buffer to look in -- a first compile on a cold cache is the
case that legitimately runs long, and the build log is what says so -- and the
setting to raise. Still never resent: a request the daemon took and died on
may already have run.
Package headers on all eight client files, so package-install-file on the
directory works and the client is not reachable only by load-path. The
daemon-buffer defcustom moves up beside the other buffer names, because the
reply reader now names it and the byte-compiler reads a file in order.
The name freed up by the rename now means what C means by it: the members
overlay one storage, the size is the largest of them, the alignment the
strictest, and nothing anywhere records which one was written. It serves
two things that wanted it. Binding a C header means holding the union the
library holds and reading whichever member the library's own tag says is
live -- a tag Flan cannot see, because the rule relating them is prose in
a manual. Overlaying an f32 on a u32 to look at its bits is the other,
and it is the same read.
So that read is defined rather than refused. This is the one place in the
checker where bytes win over safety on purpose, and the alternative was
not a safer language, it was no feature: type punning *is* reading the
member that was not written. The promise is the one C's implementations
make and C's standard does not -- the layout is the target's, the bytes
are the bytes, a read is a reinterpretation of them -- and what is not
promised is anything about bytes nobody wrote, where a member wider than
the one last stored reads a tail that is indeterminate exactly as a
struct's padding is. ZII narrows that to almost nothing: a union starts
all-bytes-zero unless uninit says otherwise.
uninit on one is allowed, unlike on a defdata. The refusal there was
never about garbage; it is that a tag steers, and a tag no case names
falls past every comparison in a match into a block LLVM may treat as
unreachable. An untagged union steers nothing.
Which is also why three things are refused, each for a reason that does
not expire with a milestone. No move-only member: nothing knows which
member is live, so nothing can tear one down, and unlike the struct and
defdata refusals this is not waiting on recursive teardown -- there is no
fact for teardown to read. No bool at any depth: an i1 loaded from a byte
that is neither 0 nor 1 is a value the optimiser may assume cannot exist,
and a union is the only type that can produce one. No defdata at any
depth, for the reason uninit gives, arriving the other way round. An
Option member is fine and the walk says why: its match is a tag test and
a branch, not a chain with an unreachable tail.
Two members in one literal, a match on a union, a union map key and a
member written into a global initialiser are each refused by name.
A union is a field list whose every offset is zero, so it travels as a
Tast.structure and the checker, the emitter and the x86 backend each grow
one table rather than one shape. A value is a zeroed temporary and a
store -- Set over Pfield, which every backend already has -- so there is
no new IR node and no layout rule spelled out a second time per backend.
The LLVM type is the blob clang gives a union, the DWARF is
DW_TAG_union_type with every member at zero, and the printer names the
type and does not walk it: it cannot know which member is live, and one
of them may be a pointer.
cimport can now check what it could not. A C record holding a union
member was not recorded at all, so the defstruct beside it went unchecked
rather than checked wrongly; a named union member resolves to a defunion
now and the whole record is compared field by field. The defunion itself
is compared against the header's union as a set and not in order --
every member is at offset zero, so a permuted one is the same type and
reporting it would be a finding that is not one -- while a member the
header has and Flan lacks is reported, because that is what changes the
size. A defunion against a C struct, or a defstruct against a C union,
is reported in both directions. An anonymous union member is still
skipped, and the comment now says that the gap is on the Flan side:
there is nothing to declare.
Flan's tagged sum has been spelled defunion since it landed, which was
accurate right up until the language wanted C's untagged union as well.
Both cannot be called the same thing, and the tagged one is the one with
an alternative name that says what it is: a case, its fields, and a tag
that steers which case is live is a data type, not a union.
So the form is defdata everywhere -- the parser, the AST, the checker,
both backends, the prelude's Form, the editor's font-locking and imenu,
the docs and every .flan file in the tree. The internal vocabulary moves
with it: Tast.union is Tast.data, uname is dname, the tables the checker
and the emitter keep are datas. Leaving them would have inverted the
words permanently, with surface defunion meaning one thing and
env.unions meaning the other, which is exactly the kind of drift the
comments in those files exist to prevent. What did not move is case,
variant and vfields: a tagged sum still has cases, and it still has one
live at a time.
defunion is not kept as an alias. An alias would compile the day the
untagged form lands and mean the opposite of what it used to -- the same
silent misparse that made defn's return type mandatory, and worse,
because the reader would have no reason to look. The old spelling is a
named refusal instead, parse/defunion-renamed, which says what it is now
called and that the name is reserved for something else. It fires on the
head alone, so (defunion U [A B]) -- which would otherwise have parsed
cleanly as one field A of type B -- is refused with the rest.
basis had the same false promise the eval note did. It branches on the break
state, which answers "running" while the program is parked — the agent's
listener is alive and nothing has stopped — so disassemble reported that a body
delivered while parked installs at the next frame boundary, which is not a
boundary the program will reach until somebody runs it again.
And flan-rerun had no autoload. The keymap binds it, and flan-mode.el says in
as many words why every command there needs a real autoload rather than a
declare-function: a user who loaded only flan-mode would otherwise find the key
bound to nothing at the exact moment they most want it.
You run a program under flan dev, it opens a raylib window, you close the
window, main returns — and there is no way to get another window short of
flan-dev-restart-program, which throws away the build, the session and every
global with it. In Common Lisp or Clojure the image outlives main, so you call
it again. The process here already outlived main: the exit hook flushed, closed
stdout and sat in for (;;) pause(). Nothing could wake it.
So main() is a loop. The hook records the status and longjmps back into a
setjmp in main() — there is no return available, since flan_exit is reached
from wherever the program happened to be — and the thread waits on a condition
variable until the new rerun op signals it. The main thread is the one that
runs main again: a window belongs to the thread that opened it, and on macOS to
the first thread of the process. A longjmp pops no frame, so the park first
empties the handler stack, the restart stack and the shadow frame chain, each
of which was a chain of allocas in stack the next run is about to write over.
Nothing else is reset; the second run reads whatever the first left in the
globals, which is the semantics that was asked for.
Closing stdout had to go with it. That was how the compiler learned the program
was done, but a pipe delivers EOF once, so the signal and the program's output
were the same resource and spending it left the second run with nowhere to
print. The descriptor hazard the old code reopened /dev/null for goes away with
the close that caused it. Liveness is asked for instead, through a weak symbol
in the same style as the agent's, and is now three states rather than two: Live,
Parked and Gone. Every guard branches on that before consulting the break
state, because the agent's listener answers "running" while the program is
parked and telling somebody whose program has finished that it is running is
worse than saying nothing. Only eval accepts a parked program — it queues and
waits for nothing, and the queued module installs at the first frame boundary
of the next run, so a body can be fixed while parked and the re-run executes
it. Everything else needs a frame boundary or a stopped stack, has neither, and
says which, naming the command that gets the program back.
A re-run while the program is running is refused rather than queued: the test
and the signal happen under one mutex, so two mains writing the same globals at
once never starts.
:parked rides on every reply beside :stopped, for the reason :stopped does —
finishing is as unannounced as stopping, more so when the way it happens is a
mouse click on a title bar. Emacs shows flan:parked in the modeline and binds
flan-rerun to C-c C-M-x.
A defvar typed at the top of a file could not be evaluated with C-x C-e. The
key was wired straight to eval-expr, so the parser met a declaration where an
expression was required and said so: "defvar is a top-level declaration, not
an expression". The only way to get the var installed was C-c C-c. That split
was never a property of the compiler, which has had both evaluators side by
side in lib/dev.ml all along — one installs a body under a name, the other
wraps an expression in a thunk because there is no name to install into. It
was a property of the keymap.
So the key now dispatches on the form it would actually send. Two questions,
both of which have to answer yes before the declaration path is taken: the
depth at the form's open delimiter says whether anything encloses it, and the
head says whether it is a declaration. Requiring the first is what keeps an
inner expression inside a defn body evaluating as an expression — the
enclosing form being a declaration is not a reason to reinstall it, and the
cursor's position in the file was never the question. Requiring it also
leaves a defn written inside a let to the parser, which refuses it correctly,
rather than quietly installing it as a top-level definition.
The head list is spelled out in flan-dev.el and points at the arm of
Parse.expr that is its authority; package is deliberately left out of it,
because this is the set of heads that fail when sent to the expression
evaluator and not the set that introduces a name.
The echo area is now the only thing that says which evaluator ran, so it has
to say it. An expression still reports "=> 42". A declaration reports what
landed, and reports it by name: a defvar installs no bodies, so leading the
sentence with the generic label left it reading "form installed in 4 ms (also
ticks)" — the one name that changed, parenthesised as an afterthought. Where
there are no functions the names are what changed, so they are what the
sentence is about, and the aside is then empty rather than a repeat of it.
C-c C-c is untouched and stays the explicit "reload the definition I am
standing in", which is still the command to use from inside a body.
flan-dev's start command proposed the last program it had started, so
invoking it from a fresh project's buffer offered the previous project's
file. It now proposes the buffer it was called from; restarting the
previous program is what flan-dev-restart-program is for.
Zeroing a fixed array wrote one typed store per element. Above 64 bytes
that becomes a memset, which LLVM can lower as a bulk clear; below it the
inline stores are still cheaper than a call.
Outline's minor-mode map owned TAB in the lowering buffer, so the folding
keys that buffer defines never ran. A buffer-local overriding map gives
them back without touching Outline anywhere else.
FIX.org collects the rough edges found while using the dev loop.