The first is CLHS 9.1.4.1: a clause that signalled the condition it handles used to re-enter itself until the stack ran out. The stale list now names a lifted clause by the function it is written in, and BUILT.md's cost line is measured against master.
A dev cell carries its body's signature word beside the body, every call through a cell (and every function value taken from one) compares it with the word the site was compiled for, and the session lists the stale callers by file and line on the reply. Both backends, both installers; release builds have neither the word nor the compare.
flan--special was three kinds of name in one list: the forms the parser
dispatches on, the functions the compiler provides, and the words that
stand for themselves. Drawing push like let said they were the same kind
of thing. They are three lists now, each read off the file that decides
it, and the two names in the old one that are not in the language at all
-- cast and none -- are gone with it.
What else had been missed: dyn, Allocator, Vec, Map, Fn, int and float
were not types, $t was not anything, handler-case was not a keyword, a
comma was not whitespace, & was not a name character -- which a macro's
parameter list needs, now that it destructures -- imenu had no heading
for a macro and none for the four dispatch forms, and a labelled loop
indented its body under its own binding vector, which loops.flan has
four of and would have said.
Then the second half. The defs op already told the editor what every
name is, and only completion and eldoc were listening, so a macro you
defined looked exactly like a function you defined. It carried no macro
kind to listen for and could not have: a macro is a defn by the time
there is a program. So the op answers with one -- keeping the location
off the defn it drops, so M-. still goes there -- and with the type
names, read off the checker's environment, since an enum is an i32 and
an alias is gone by then.
The editor draws them by kind, after the static rules and never over
them: a program defining its own length does not get to repaint the
builtin. A hash table and a matcher rather than a regexp of every name,
rebuilt when the cache is and not when a key is pressed. With no session
the rules come off and the buffer is what it always was.
The author: "I think I prefer length over len, because then I'll use len as
the variable name". One arm in check.ml, one row in the table beside it, and
every (len x) in lib, test, examples, vendor, spike, docs, web, emacs,
plan.org and NEXT.md rewritten.
Shadowing and builtin/ had already taken most of the sting out: a (defn len
...) was legal and won in its own file, and builtin/len reached past it. What
was left is that len was still a builtin — the defn earned a warning, and a
wrapper had to say builtin/ at every inner call. Now there is nothing under
the short name: len is an ordinary identifier in every position, which is
what (let [len (length xs)] ...) wants.
length takes over as shadowing's worked example rather than the feature
losing one. shadow-builtin.flan, builtin-qualified.flan, pkgs/shadowed and the
builtin/ rows in test_flan move to it and go on testing shadowing.
A call to a len nothing defines is answered where an unknown function is,
after every table and after the shadowing guard, so a program with its own len
never reaches it. The sentence is said rather than guessed at — len and length
are three edits apart and the did-you-mean's net is one — and the call is
written back out through spell_arg, as-slice's spelling lifted out of it and
now shared, so what is printed compiles.
sand.flan:33 still calls the old name and is the author's to change; until it
does, test_acceptance and test_session abort there. Both were run green
against a copy with that one line changed. FIX.org says so.
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.
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.
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.
`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.
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.
`-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 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.