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.
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.
C-c C-m. One step on the bare key, the fixpoint under C-u: a macro may
quasiquote a call to another macro, and Loc.from_macro is outermost-wins, so
by the time a full expansion settles the intermediate name is gone. One step
is the only thing that can say which macro produced what.
The expansion runs against the macros the *session* holds -- the prelude's,
its imports', and every defmacro evaluated since it started -- and writes
nothing back: a defmacro handed to C-c C-m does not join the session by having
been looked at.
Both non-termination refusals stay refusals, and only where they are needed.
One step makes one call and does not look at the answer, so (s/spin) one-
stepped answers with itself; all the way hits the fuel and names the macro,
inside Dev.serve's guard, so the daemon replies rather than hanging. Macro's
module handling is a Fun.protect now -- a build that raised was a process
about to exit, and the daemon is not that process.
No printer for a Form existed. Form.to_string is an error-message renderer and
is what Macro.key digests, so it is untouched; Form.to_source round-trips
floats, strings and bytes through the reader, and Form.pretty decides where
the line breaks go and leaves the columns to flan-mode.
The answer is a read-only flan-mode buffer shaped like the disassembly one,
with cnr's idea in it: m expands the form at point one more step in place.
Three inherited keys refuse by name -- an expansion is in no file. The text is
sent padded onto its own line and its own column, unlike C-x C-e, so the
refusal lands on the call and not at the start of its line.
Three follow-ups to the registry reader.
The agent's "reg types" handler held its four row buffers as statics. The lock
argument for that was right — request_lock is held across the whole of
handle_line from both entry points — but the BSS was not: 8KB in every build
this package is linked into, including a release build of a game that imports
the agent and never writes a row. That is flan_dev.c's own argument against a
fixed table at a thirty-second of the size. Malloc'd and freed per request,
like the watch handler beside it.
A followed pointer parses as an opaque node, so the inspector refused it with
"no structure for this type" — false and unhelpful at once. It has structure,
it is drawn, and the reason you cannot step in is that the step would start
from the pointee. Said that way instead.
And the three new commands have elisp tests: the address root's wire, that a
path is refused rather than dropped, the listing's rows and totals, the
overflow warning, and a build with no registry refusing rather than showing an
empty table that reads like a program holding nothing.
flan-watch-ghost-mode paints each watched value inline, after the line holding
the call that wrote it. An addition to the watch buffer and not a replacement:
both can be on at once, and turning either off leaves the other running.
The earlier note said ghost text was gated on a (watch ...) form in check.ml,
because nothing in the table carries a source location. That is true of the
table and the conclusion did not follow. The call site is in the buffer, and
the name in the table is the string literal in it, so the anchor is searched
for rather than reported. Nothing new is asked of the daemon. The head of the
call is a defcustom regexp, because watch-i64 is a name the program's author
chose in their own declare-c and only the C symbol behind it is fixed.
Both pictures are painted from one reply in flan-watch--absorb, so they cannot
disagree and there is no second watch request in flight. That meant the watch
buffer could no longer be the subscription: arming and the timer now hang off
flan-watch--consumers, and only the last consumer out disarms the table.
Overlays are replaced wholesale on every repaint rather than followed through
edits, which is the whole answer to invalidating one whose line moved. Only
buffers shown in a window are scanned.
Settled and written down: two sites of one name both show it and say so,
because the table has one slot and the last writer wins; a watch in a loop
shows the last value written, as the buffer does, because every better answer
is the query UI this design exists to avoid; a stopped program's values say
"last frame" and change face, since inline they sit in code that looks live;
a site with no row is annotated only when the table reports overflow.
syntax-ppss moves point and clobbers the match data, so calling it inside a
re-search-forward loop and then reading match-string restarts the scan and the
loop never ends. Everything is read out before the check now.
emacs/test-flan-watch.el covers it, loaded from test-flan-cider.el the way
test-flan-mode.el is, so no build change is needed. 203 checks, 0 failures.
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.
`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.
A global is program state a frame happened to touch, not part of it, so
nesting it under one implies an ownership that is not there and repeats the
name once per frame that reads it. One section instead, holding the union of
the globals every frame on the stack references — the compiler does the
choosing, since Reach.expr_refs already answers a body's reference set, and
listing every global a program has would bury the one that matters under the
prelude's PRNG state.
Each entry says which frames touch it, by the index the stack section already
numbers them with, which recovers what per-frame nesting would have told you
at no cost in duplication. Ordered by the innermost frame that touches it:
a deep stack makes the union large and proximity to the error is what puts
the likely culprit on top.
Simpler than locals, because a global is reached by name rather than by
address. Emit.redefinition writes a global the host has as external, so the
thunk binds to the program's own storage and nothing is asked of the stopped
thread — no dev-slot round trip and no not-yet-bound case to refuse.
A frame that cannot be attributed contributes nothing and is named in
:skipped; the union being incomplete and the union being complete are
different answers. The hole in that is stated rather than papered over:
slot_fingerprint hashes a body's slots, which is the right cut for locals and
not for this, so a body that names different globals while binding the same
locals is not caught. The test drives the case that is.
MANUAL.md also loses a stale paragraph claiming the fingerprint check never
fires with a failing test pinned to it. It fires, and test_dev covers it.
The client already knew the moment: flan-dev--absorb reads :stopped off every
reply and the poll covers the case where no reply is coming. This is a hook at
that point, not new plumbing.
Deferred through a zero-delay timer, which is the part that is not optional.
absorb notices the stop in the middle of reading a reply on the socket, with
flan-dev--busy bound, and showing the buffer asks the daemon three more
questions — break, layout, backtrace. Issuing those from inside the read they
were triggered by would interleave two conversations on one connection. The
deferred call re-checks the state rather than trusting the edge that scheduled
it, because by then the edge has been consumed and the program may have been
resumed.
Three decisions, settled and written down beside the code.
It displays and does not select. A program stops on its own clock, not the
editor's, and the likeliest moment is a frame of its own game loop while
someone is typing somewhere else. Taking the window would send the next
keystrokes where they were not aimed. `focus' is there for anyone who
disagrees, and nil goes back to the mode line alone.
(pause) is not a special case, though it was worth asking: it is deliberate at
the moment it is *written*, and the frame it fires on still arrives whenever the
program gets there, which is no more expected than an error. What it does get is
an honest headline. (pause) is `error' under a `restart-case', so nothing in the
compiler knows a breakpoint from a failure and this buffer is the first place
that can — calling it unhandled is a small lie at the top of the one buffer that
exists to say what happened.
A stop mid-edit disturbs nothing, which falls out of displaying rather than
selecting. Two guards go past that: nothing happens under an active minibuffer,
because a prompt is modal and rearranging windows under one is hostile; and
nothing happens inside a keyboard macro, because a macro that behaves
differently depending on whether the program stopped cannot be trusted. In both
cases the mode line still says stopped and C-c C-b still works.
render.ml's output and emacs/flan-inspect.el's parser are the two ends of one
wire format, which is why the printer was left on the colon when the rest of
the corpus moved: shifting it alone would have broken inspection in the dev
loop without breaking a test that said so. They move together here.
The field list in the inspector is labelled with the dot too, which is the
spelling flan-inspect-step-expr already used to build `(.x b)' — the label and
the expression it stands for now read the same.
One case needed a guard the colon never did: `...' also begins with a dot and
is the renderer saying it stopped, not a field called `..'. A field name never
starts with a second dot, so one character of lookahead separates them.
The colon is not gone from the rendered grammar. An enum member is `:green' and
is a *value*, so the two are now told apart by the character alone, which is
the only thing that distinguishes them.
Also font lock, handed over with the same change: `:name' was the rule that
drew field labels, and with the colon belonging to keywords every label in the
corpus was left unfontified. `.name' is drawn as a constant, in both the places
it appears — the label in `{.x 1.0}' and the accessor in `(.x v)', which are
the same name.
Decimal is what the value is and stays first; hex and binary go beside it. It
is the wrong base for about half the numbers anyone opens this buffer for — a
colour is 0x303030FF, a gesture is an OR of flags, a mask is read a bit at a
time — and reading those out of a decimal is arithmetic done by hand.
In two places: under the header of a value opened on its own, and on each
numeric row of a field list. The second is the one that matters, because a leaf
cannot be stepped into, so the field list is the only place most numbers are
ever seen.
Nothing is asked of the program. It is arithmetic on text the renderer already
wrote, so it works on a stopped program and costs no round trip. Binary is
grouped in nibbles because a mask is read in nibbles. A negative is shown as
the 64-bit two's complement it is in memory and says the width out loud: the
rendered value carries none, and every Flan integer comes back through i64.
A float is left alone rather than answered wrongly — its bits are an IEEE
layout, reinterpreting them is a different question, and the rendered text does
not carry the width to answer it.
The pointer half of this is not done and the refusal now says why. Render.render
writes the bare word <ptr> for every pointer on purpose: it is the same renderer
print uses, an address is not stable across runs, and test_acceptance pins the
current text for that reason. Showing one is a decision about the language's
printer, not about this buffer.
The reported bug — the second and later bindings of a let one column too far —
was never one missing rule. `flan-indent-function' checked the head of the
enclosing form, and inside a binding vector the enclosing open is `[' and the
symbol after it is the first binding's name, so it fell through to Emacs's
`lisp-indent-function', which treats the vector as a call and aligns under the
first argument instead of the first binding.
Emacs Lisp is the wrong reference. It has no vectors-as-bindings, no maps and
no bracket variety, so every rule Flan needs has to be added by hand and the
binding vector is simply the first one hit. The indenter is rewritten from
clojure-mode's source instead: `clojure-mode' is neither an ancestor nor a
dependency — flan-mode still needs nothing beyond stock Emacs — it is the file
whose rules were read and written out again.
A bracket aligns under its first element, and that one rule fixes the binding
vector, `defn' parameter lists, `restart-case' and `handler-bind' clause
parameters and both spellings of a struct literal at once. `{:x 1}' and
`{.x 1}' indent identically because nothing here looks at the key, which is
what the colon-to-dot lane needs of it.
Where Flan diverges it is handled on purpose. `defn' is `:defn' rather than a
count because the return type between the parameters and the body is optional.
A clause — `(Name [params] body)' — is recognised by its shape, since its head
is a condition class or a restart name and can never be in a table; clojure-mode
reaches the same clauses by backtracking out to the enclosing form, which buys
generality this language has no other use for. Special arguments indent by one
body rather than Clojure's two, and a call whose head is alone on its line
indents its arguments by a body rather than aligning them under the head,
because that is how the whole corpus is written.
Checked by reindenting every .flan file in the tree: the only lines that move
are sand.flan's reported bug, raylib.flan's hand-wrapped parameter vectors —
which is the fix — and lone-`;' comment continuations, which stock
`lisp-indent-line' has always moved.
test-flan-mode.el is loaded from test-flan-cider.el rather than given a stanza
of its own, because emacs/*.el is already a dependency of that test.
Two failures in test-flan-cider.el that predate this: fixture frames lacked
`:fetched', so folding one open went looking for a daemon, and `layout' was
identified by being the last request when `flan-cnr-show' now makes three.
C-c C-b asks layout with the condition's own name and draws the fields under
it. The values stay refused, by name, because the shape of a condition is a
fact about the build and its contents are a fact about the stopped frame — and
only one of those is knowable today. A layout the daemon refuses is nil rather
than an error: the buffer already draws a section saying why one is empty, and
failing the whole command would take away the restarts over an annotation.
Saying "the fields are not available" was under-claiming. A condition is a
struct, and Tast.structs holds every struct's field names and types in the
daemon, which owns the build — no running program is involved in answering
what a Missing is made of. Only the values need the pointer the break loop
was handed, and break_loop currently discards that pointer, so they are two
different gaps with two different fixes on two different sides of the socket.
The buffer now draws a field named and typed with its value refused, which is
what tells you whether the field you were about to blame is even a field of
this condition.
Navigation backwards was not the same list walked the other way. Forward
wrapped and backward stopped, and from the middle of a line the two disagreed
about where a field begins — a field line carries the property on all of it,
so a property-change walk from mid-line finds the end of the field you are
already in. Both now go through one list of field starts. The mid-line case
went red on the first try and the expectation was the thing that was wrong:
landing on the current field's start is what CIDER does and is the less
surprising of the two.
C-c C-b is a completing-read over restart names, which is the whole UI for
the one moment the dev loop exists to make survivable. It shows the names and
nothing else, and it will let you pick one that cannot be taken.
That last part is a bug, not a gap. §4 says restart lookup takes the first
frame offering a name, and flan_find_restart does exactly that; so a second
frame offering "retry" is real, is on the list, and is unreachable — picking
it sends the string "retry" and the inner frame runs, silently. SBCL has
shown this since forever by numbering the restarts and omitting the bracket
on a name already used. Taken as is, and the shadowed row now refuses by name
and says what would fix it: an index verb, which does not exist.
SBCL also decides the order. invoke-debugger prints the condition, then
show-restarts, and stops; the backtrace is a command you type. The restarts
are the decision and the stack is the explanation for it, and a debugger that
opens with forty frames has buried one under the other.
What CIDER's stacktrace buffer gives is the behaviour — frames that fold in
place, everything on the keyboard. Not its cause chain: a JVM exception wraps
another one and a Flan condition wraps nothing.
The fields, the stack and the locals are drawn as sections that say why they
are empty and what each would take. A section left out cannot be told from
one that happened to have nothing in it, and only one of those is a fact
about the program.
C-x C-e renders once and stops at depth 4 and span 8. A field past either
comes back as "..." and nothing recovers it from the echo area. Re-rooting
the walk at that field renders it from depth 0, so the bound moves with you
— that, and not tidiness, is why an inspector is worth having beside the
expression evaluator.
CIDER keeps its inspector stack on the server because a JVM value can be
retained. Nothing here can: a Flan value has no header and the thunk that
rendered it is dlclosed the moment it returns. So the stack is a stack of
expressions on this side, and going into a field means sending a different
one — (.pos b) where the last one was b. It costs a re-evaluation per step,
which buys a view that is never stale and is why refresh is a key someone
presses rather than a timer.
Driven from fixtures, which is also the only way the cases a live program
will not hold still for get tested at all.