56 Commits

Author SHA1 Message Date
306fc88094 A union's fields have no accessor, so RET refuses them where none exists
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.
2026-09-12 20:40:02 +07:00
0389c2282c What the second root can reach, what it needs, and why both are kept
The daemon side and the Emacs side both landed with nothing written down. Four
files owed something.

`BUILT.md` gets the whole of it: why rooting at an address alone was rejected
and why that rejection was half wrong, what a path step is and how a union's
case travels with it, why the slot goes by index and not by name, and the two
capability lists side by side — the expression root works on a running program
and cannot name a frame; the slot root names one frame and one slot and reaches
an option's payload and a union case's fields, and needs the program stopped.
Neither contains the other, which is the reason there are two.

`emacs/MANUAL.md` says the same thing in the register that file uses, under the
inspector, because the person pressing `i` is the one who needs to know which
root they got and what it cannot do. The globals section's claim that `i` works
on a global "exactly as it does on a local" was true and is now the interesting
difference, so it says what the difference is.

`NEXT.md`'s decided item is struck with what actually shipped: a frame and a
slot index rather than an address and a type, and `l` crossing between the modes
was predicted as a cost and turned out not to be one.

`DISCUSS.md` item 1 is no longer an open question. The number stays — cimport.ml
and NEXT.md cite these by number — and what stays with it is the one correction
worth keeping: an address is not an expression, but a step does not have to be
one either.

And BUILT.md's last paragraph still said `render.ml` prints `(V {:x 1.5})` and
that the printer would move when its reader did. They moved together some time
ago.
2026-09-12 20:36:48 +07:00
4d29e52dbe The inspector's stack carries a root, so `i' names the frame it is looking at
`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.
2026-09-12 20:31:18 +07:00
03d4460d72 WIP: the inspector's address root, half wired on the Emacs side
OCaml side is done and builds. Emacs side is mid-edit and INCOMPLETE — see
the handoff below. `dune build --root . @check` is green. `dune test --root .`
was NOT run. The .el files were not byte-compiled and flan-inspect.el will not
work as it stands: the state layer still speaks the old single-expression
shape while the helpers above it have been rewritten for roots and paths.

WHAT WORKS (daemon, lib/, in the parent commit and unchanged here)

- `(:op "inspect" :frame N :slot I :path (...))` renders one value rooted at a
  stopped frame's slot address. `Session.render_slot` is `render_locals` with a
  path applied to the root before the walk and one line out instead of one per
  slot; no second walk was written and no backend change was needed.
- A path step is a string for a struct field, an integer for an array or slice
  element, and the symbol `some` for an option's payload. A union case field is
  spelled `Union.case.field`, because the payload's offset depends on the case
  and only the renderer knows which case the value is in.
- Every step that does not fit the type in hand is refused by name with the
  reason: a field the type does not have, an index past a fixed array's end,
  `some` on something that is not an option, a union field without its case.
- The frame's identity IS checked, and not by a second copy: `Dev.stopped_frame`
  is one function now and `locals` and `inspect` both go through it — alive,
  stopped, frame exists, the frame is the program's and not a thunk's, the body
  is one this session holds, the slot count matches, and `Emit.slot_fingerprint`
  matches. `inspect` additionally refuses an unbound slot, for the listing's
  reason: a null address would fault on the stopped game thread.
- The slot travels by INDEX, not by name. Two slots can share a name
  (`fresh_slot` only allocates) and a refused slot is not in the listing, so
  neither the name nor the position identifies one. `locals` now puts the slot
  index as a fourth element on each `:locals` entry.
- `Dev.run_render_thunk` is one function; `locals`, `globals` and `inspect`
  share the build/deliver/wait/read tail.
- `layout`'s "union values are milestone 6" is corrected.

WHAT IS HALF-BUILT, AND EXACTLY WHERE IT STOPS

`emacs/flan-inspect.el`. Done: the header comment explaining the two roots;
`flan-inspect-step-expr` taking a 3-element `:field` step; `flan-inspect-wire-step`;
`flan-inspect--root-label`; `flan-inspect-refusal` taking an optional ROOT and
allowing an option's payload under a `:slot` root.

NOT done, and this is the whole of what is left:

1. `flan-inspect--expr` / `flan-inspect--stack` still hold a bare expression.
   They must become `flan-inspect--root` (`(:expr EXPR)` or
   `(:slot FRAME SLOT NAME)`) plus `flan-inspect--path`, with stack entries of
   `(ROOT PATH . POINT)`.
2. `flan-inspect--value` must branch on the root: `eval-expr` with
   `(flan-inspect--root-label root path)` for `:expr`; for `:slot`, send
   `(:op "inspect" :frame F :slot S :path P)` with P built by
   `flan-inspect-wire-step` over the path, and take `:value` from the reply.
3. `flan-inspect--show`, `-into`, `-pop`, `-refresh` rewired to (ROOT PATH).
   `-into` must build a `:some` step when the node's kind is `option`, and put
   the parent node's `:type` as the third element of a `:field` step.
4. New entry point `flan-inspect-slot (frame slot name)`, kept separate from
   `flan-inspect (expr)` — `emacs/flan-mode.el` autoloads and binds the latter
   and that file is out of this lane.
5. `emacs/flan-cnr.el`: the `flan-cnr-inspect` text property must carry
   `(:slot FRAME SLOT NAME)` on a local line — the slot index is `(nth 3 l)`
   now — and `(:expr NAME)` on a global line, with `flan-cnr-inspect`
   dispatching to the right entry point.
6. `emacs/test-flan-cider.el`: the fixture at "`i' on a local inspects it by
   name" asserts the old behaviour and must be rewritten; the locals fixtures
   need a fourth element.
7. `test/test_dev.ml`: no coverage of the new op yet. The discriminating test
   to write first is a stack whose OUTER frame has a local whose name is also a
   global with a different value, asserting `inspect` answers the frame's value.
   A new `test/programs/dev-inspect.flan` is picked up by the existing glob.
8. `BUILT.md`, `emacs/MANUAL.md`, and striking the item from `NEXT.md`'s
   "Decided in discussion" and `DISCUSS.md` item 1 — none done.

THE THREE ANSWERS THE TASK ASKED FOR

- Navigation in the new mode: the daemon supports it fully — RET extends the
  path, `l` shortens it, and both are a fresh request, so the view is never
  stale. The Emacs half of that is item 3 above and is not wired.
- `l` does not cross between the modes, and that is structural rather than a
  rule: a stack entry carries its own root, RET only ever extends the path
  under the root it already has, and every new root starts with an empty
  stack. A mixed stack cannot be constructed, so the question does not arise —
  and it stays answered if a third rooting mode is added.
- What each mode cannot do that the other can. The expression root works on a
  RUNNING program and roots at anything you can write, a call included; it
  cannot name a frame, so it is the bug. The slot root names one frame and one
  slot and is exact; it reaches an option's payload and a union case's fields,
  which have offsets but no accessor in the surface language; it needs a
  stopped program, it is refused when the frame's body was redefined since it
  was entered, and it cannot root at an expression at all.
2026-09-12 20:21:14 +07:00
635d12782d The globals a stopped stack reaches, in one section and not under a frame
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.
2026-09-12 16:22:58 +07:00
2179627593 The break buffer opens itself, and a breakpoint is not called a failure
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.
2026-09-12 16:19:55 +07:00
0f3b633449 The printed struct moves to the dot, with the reader that parses it
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.
2026-09-12 16:19:55 +07:00
a0610cecd5 A number in the inspector reads in the two bases it was written in
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.
2026-09-12 16:19:39 +07:00
64342c406e The manual says how a form is indented, and NEXT loses what landed
The indentation rules were written and tested but never described anywhere a
user would look. MANUAL.md had no section on editing at all — it starts at
`C-c C-c' and assumes the file is already written — so the rule that cost the
friction, a binding vector lining up name under name, was only visible by
trying it.

What is written down is what was checked, not what the port was aimed at: the
call fallback, the `handler-bind' clause vector, `defn' parameter alignment
with a return type after it, and `restart-case' clause bodies were each
reindented from scratch and the manual quotes the result.

NEXT.md keeps the half of the field-label handover that is still open. The
printer in render.ml has to move in the same commit as the inspector that
parses it, and that is the inspector lane's; the font-lock half is done here,
so only that half is struck.
2026-09-12 16:17:04 +07:00
0d549a6b6e Indentation is ported from clojure-mode, which has the shapes Flan uses
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.
2026-09-12 16:05:12 +07:00
6d54a4390e A field label is a dot, and the colon belongs to keys 2026-09-12 15:04:07 +07:00
cb757868b4 Keep the printed struct a colon; it is a wire format Emacs reads back
render.ml's output is parsed by emacs/flan-inspect.el, which hard-codes
the colon when it reads a field out of a rendered struct. Moving the
printer on its own would break inspection in the dev loop without
breaking a test that says so, so the printer waits and moves with its
reader, in the Emacs lane.

The sweep could not tell a rendered *expectation* from a Flan *source*
snippet -- both are strings in a test -- so it converted both. The suite
named every one it got wrong, and those are back.

emacs/test-flan-dev.el:415 is the one edit inside emacs/: Flan source sent
to the daemon for eval, which the parser now refuses in the old spelling.
One label, in a fixture.
2026-09-12 14:55:47 +07:00
c598169155 Folding a frame is a display operation when there is nothing to ask 2026-09-12 14:54:53 +07:00
8d56bd0ced TAB folds a frame, which is what the header always said it did 2026-09-12 12:31:16 +07:00
3439dec5b1 Being on the frame line is not the same as having no locals 2026-09-12 12:30:51 +07:00
4857db1fc4 A breakpoint, which is only error under a restart-case 2026-09-12 12:25:08 +07:00
81d46c342e The stack section draws the frames it was written for
The web build's three artifacts are ignored alongside the native executables:
a page, its loader and the module are output, not source.
2026-09-12 12:16:30 +07:00
404b8559c5 The conditions buffer names and types the fields it used to refuse
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.
2026-09-12 10:39:22 +07:00
3afce2aeac Never restarting is the thesis; say where the holes in it are
The manual described the signature-change refusal as though it were the
design. It is not, and session.ml already said so at the refusal: a
signature change should make a new version, leave old callers on the old
one, and warn at the stale sites. plan.org calls it signature generations
and stale-caller warnings, and it is milestone 7's unfinished half.

The struct-layout rejection is the decided one and stays. Conflating them
made a placeholder look like a rule.
2026-09-12 09:38:26 +07:00
64ce9ec05b A manual for the Emacs side, in plain language
The site's Emacs section is a key table and four paragraphs -- a
reference for someone who already knows the shape. This is the other
document: how to set it up, what the loop actually is, what each buffer's
own keys do, and what to do when something is refused.

The three buffer keymaps were never written down anywhere a user would
look; they were only in the define-key calls.
2026-09-12 09:31:27 +07:00
96ab4c9cf0 Retire the per-type printers, since print says all of it
print-str, print-i64, print-f64, print-bytes, print-line and newline leave
the prelude. print and println are the whole printing surface now, and print
is the better call at every one of the sites that used them: it is the same
structural walk without the newline, so the no-newline case the family was
kept for is covered, and it takes the value as it is. The old print-i64
forced an explicit (i64 x) at every call site, because this language widens
nothing implicitly; that cast is gone from 127 places.

Dropping it moves one answer. hash-grid returns u64, and the cast through
the signed printer showed sand-headless's hash as -2851001042534928384.
print routes a u64 through flan_u64_to_bytes, so it now prints
15595743031174623232 — the same 64 bits, read as the unsigned number they
are. The pinned expectation follows the correction.

test-flan-dev.el and test_session.ml both reached for print-line as "a name
the prelude has"; they reach for rand-seed instead.
2026-09-12 05:32:25 +07:00
4001c3246c Merge branch 'dwarf-names' into dev-loop
A let-bound local is its own name under lldb now, and a redefinition
module carries DWARF when the daemon was asked for it.

Resolved against the println track in session.ml: the thunk keeps the
render walk's appended slots and gains the names beside them, the walk's
own scratch having none to keep.
2026-09-12 05:19:38 +07:00
d0a8339bb5 DWARF in a redefinition, and one flag that means it everywhere
Emit.redefinition has taken ~debug since it was written and was tested
with it; Session.eval never passed it, so every body installed by C-c C-c
lost its debug info in the running process.

Passing it alone would have been half a fix. Build.shared is what forces
-O0, and dev.ml built modules at -O2, so the llvm.dbg.declares would have
been emitted and then deleted by mem2reg: a line table, and no locals.
And a module with DWARF loaded into a host without it lines up against
nothing. So it is one flag — flan dev --debug and flan reload --debug —
and it sets the host build, the module builds and the emitted metadata
together. Off by default: a debug build is an -O0 build, and quietly
making every reloaded body -O0 changes the frame time of the one function
you are iterating on, in the loop whose point is watching that number.

What a dlopen'd module does to a breakpoint, measured against the reload
fixture rather than reasoned about:

  - lldb reads the new module's DWARF on the dlopen and says so: "1
    location added to breakpoint 3".
  - A breakpoint set by NAME gains a second location either way, so
    dlopen was never the difficulty. What the line table buys is that it
    stops with source instead of disassembly.
  - A FILE AND LINE breakpoint on the new body resolves only with it;
    without, it sits at locations = 0 (pending) forever.
  - A FILE AND LINE breakpoint on the HOST's copy stays pinned at
    locations = 1. That is correct, not stale: the old body is still
    mapped and every call site that has not gone through its cell again
    still reaches it.
  - The stack crosses intact — a frame in the reloaded .so and the one
    below it in the host each name their own .flan file.

    (lldb) frame variable
    (long) step = 10
    (long) prior = 11

The transcripts are in flan-dape.el, replacing the note that said the
module carries no DWARF yet.

flan-cnr.el's stack pane was refusing for the wrong reason. DWARF was
never its gap; nothing is attached to the stopped program, and a socket
cannot read another process's frames. Reworded to say that.

Source interleaving in the disassembly buffer is unblocked and not done:
objdump -dS interleaves a --debug module's Flan source correctly, so
Dev.asm_of needs the -S and a parse_listing that tolerates source lines.
2026-09-12 05:14:03 +07:00
2f8436018c Merge branch 'restart-at' into dev-loop
A restart the innermost frame shadows could be seen and not taken;
it is taken by position now, off a snapshot that stopped moving under
the break loop. The editor half this was briefed as building already
existed — the stale line that said otherwise is fixed.
2026-09-12 05:04:30 +07:00
0c9f043bb1 Stamp a choice with the break it was chosen from
The snapshot made the listing stand still; it did not make the handoff safe.
A choice is validated against the snapshot on top when the request lands and
resolved against the snapshot on top when the game thread next looks, and
between those the loop runs evaluations — one that errors pushes a break of
its own, whose loop reaches the flag first and takes its own index 2 for the
one somebody picked off the outer list. That is the failure this change exists
to end, arriving by a different door.

So each snapshot carries a generation, a choice carries the generation it was
validated against, and a loop claims only what is addressed to it. A mismatch
is left set rather than dropped: the listener already answered ok, so the break
it was meant for must still be able to take it. Depth could not do this — an
outer break resuming and a new one starting reuses the number. The snapshot is
also popped before the depth comes down now, so the two never describe
different breaks.

The client's own new path gets tested too: the candidate table is pure, so the
shapes a real daemon will not easily produce are checked directly, and the
break-and-resume test now goes through restart-at rather than by name.
flan-dev-unreachable-restarts was dead on arrival — flan-break reads
:unreachable off its own reply — and is gone.
2026-09-12 05:02:14 +07:00
ac7d4a0e95 The prompt numbers its choices, because a name could not say which
now lists the restarts by position and sends the position, with the
name alongside as the receipt the program checks. A restart below the
evaluation the break is inside is shown marked rather than hidden: someone who
can see a restart in their own source and not on this list has been told
nothing, and the refusal carries the reason.
2026-09-12 04:56:25 +07:00
93231e8c9e println, the structural printer, shared with the REPL
session.ml already had this: a compile-time walk over a Tast type that
emits the calls to print a value of it, handling every concrete type the
language has. It was dev-build-only and went to flan_dev_emit, and
prelude.ml justified the per-type print-* functions by saying a real
println had to wait for milestone 5 and generics. It did not. plan.org
specifies println as compiler-provided and per concrete type, which is
not overloading: there is nothing to dispatch on at run time and no
user-supplied printer to choose between, so no type variables appear.

The walk moves to render.ml, parameterised on an emitter and a slot
allocator. The emitter is five functions rather than five extern names
because the two sides are not both extern calls -- the REPL's are, and
stdout's compose a conversion with a write. The slot allocator differs
too: the REPL builds a thunk's frame, println takes slots from the
enclosing function being checked, once per call site.

Two runtime shims, both only reachable from the walk. flan_u64_to_bytes,
because routing u64 through the signed printer makes 0xFFFF...F read as
-1, which is the one way println could disagree with the REPL about a
value both can hold. flan_escape_bytes, so a string nested in a printed
structure is quoted and escaped -- same table as flan_dev_emit_str, noted
in both, because the REPL and println must not disagree about what a
struct looks like.

A string at top level prints raw and nested prints quoted. Not a conflict:
(println "hello") has to print hello, and a struct's string field has to
be distinguishable from the punctuation around it. The split is top-level
vs nested, so it lives in check.ml and not in the walk.

Found on the way: a field of an Option had no gep in emit.ml, so the
walk's Option arm had never run -- the REPL would have failed on one too.
Option is { i8, T } with no declared name, so its layout is now spelled
out. Nothing in the surface language reaches a field of an Option; the
printer does, to read the tag without unwrapping a None.

The print-* functions stay. They print without a newline, which println
cannot express -- slices.flan's show prints elements separated by spaces
-- and they are raw where print is structural.

println.flan covers every arm at -O0 and -O2: the u64, the raw/quoted
split, both Option arms, the depth and span caps, and the slice arm's
loop twice over plus once inside a dotimes, which is where per-call-site
slot allocation would show if it were per-iteration.
2026-09-12 04:55:42 +07:00
6ed413a232 An inspector and a conditions buffer, and a restart nobody could take
Two CIDER-shaped buffers: C-c C-i navigates a value, C-c C-b shows the
condition, the restarts and the stack. C-c C-M-b keeps the old one-key prompt.

The inspector needs no protocol change at all. eval-expr already answers a
rendered string, and Session.render writes exactly seven shapes, so that string
is a grammar. Navigation is a stack of *expressions* rather than of handles:
going into :pos sends (.pos b), into element 2 sends (at (.tags b) 2) - both
ordinary Flan a person could type. CIDER keeps its stack on the server because
a JVM value can be retained; nothing here can, since a Flan value has no header
and the render thunk is dlclosed the moment it returns. The view is therefore
never stale, where CIDER's shows the object as it was when you pushed. What it
buys over C-x C-e is the depth-4 span-8 bound: a field past it comes back as
... and nothing recovers it from the echo area, and re-rooting renders it from
depth 0.

SBCL decided the order - condition, restarts, then stack - because invoke-debugger
prints the condition and show-restarts and then stops; the backtrace is a command
you type. The restarts are the decision and the stack is the explanation for it.

And SBCL found a live bug. show-restarts omits the bracket on a name already
used further in, which is not decoration: §4 takes the first frame offering a
name, so a second frame offering retry is real, is on the list, and cannot be
chosen. The old prompt showed retry twice and sent the string either way, and
the inner frame took it silently. restarts.flan's own nested function has been
that counterexample since the transfer landed. The buffer draws the shadowed row
unbracketed and refuses RET on it by name, with a test asserting nothing was
sent - which stops the lie without restoring the choice. Taking a restart by
index is the fix and is recorded as such.

Sections that cannot be filled are drawn saying why rather than omitted: a
missing section cannot be told from an empty one, and only one of those is a
fact about the program.
2026-09-12 04:17:41 +07:00
13e8002d39 Merge branch 'cider-inspect' into dev-loop 2026-09-12 04:16:14 +07:00
b661cd788b The shape of a condition is not the same refusal as its contents
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.
2026-09-12 04:14:47 +07:00
d336da65e5 Merge branch 'disasm-overlay' into dev-loop 2026-09-12 04:10:54 +07:00
a9a411709c A stopped program should show its choices, not spell them
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.
2026-09-12 04:08:39 +07:00
94b78a1a83 A value you can walk into, because the walk has a bound
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.
2026-09-12 04:08:13 +07:00
38004eec3e Drive both new things through the real client
The overlay half is checked as far as --batch allows and no further:
execute-kbd-macro runs no pre-command-hook there, so the hook is proved
installed in the right buffer and in nobody else's, and proved to clear and
uninstall when run — which is what the command loop does with it. That Emacs
runs it is Emacs' contract, and a test pretending to check it would be
checking nothing.
2026-09-12 04:03:19 +07:00
71bc492cad Merge branch 'dwarf-debug' into dev-loop
# Conflicts:
#	test/test_acceptance.ml
2026-09-12 04:03:06 +07:00
5f5cc8bee9 lldb already speaks DAP; Emacs only needs to be told how to build
No DAP implementation here, and there should not be one. `flan build --debug'
puts DWARF in the executable, lldb reads it, lldb-dap speaks the protocol — so
what was actually missing was a dape-configs entry that knows to build a .flan
file first and where the binary lands.

The build goes through dape's own `compile' key rather than a shell-out, so a
rejected program lands in a compilation buffer and next-error walks it. Flan's
diagnostics are already file:line:col.

`flan-debug' goes through `dape--config-eval' and not `alist-get'. `dape'
takes a config whose forms are already evaluated — that is what M-x dape does
after reading one — and handing it the stored entry would pass the list
(flan-dape--binary (flan-dape--source)) to lldb as a program name. Driven
headlessly to prove it: a breakpoint set by line in the .flan buffer, hit,
reported as flan.tick at debug.flan:19 with c and n in scope.

The keybinding is registered from here rather than in flan-mode.el, so this
file is the only thing anyone has to load to get it and flan-mode keeps
working for someone who never installs dape.

The two frictions are written down at the bottom of flan-dape.el from lldb
transcripts, not from reasoning about what ought to happen, because the guess
I started from was wrong. Across a reload a breakpoint set by *name* gains a
second location and both stay live — the old body is still mapped and still
what old call sites reach. One set by *file and line* stops firing, and not
because dape pinned it to an address: the redefinition module has no line
table to resolve against. Given one, lldb does re-resolve on dlopen.

Which names the gap: Emit.redefinition takes ~debug and Session.eval does not
pass it, so `flan reload' and the `flan dev' daemon build modules without
DWARF. lib/session.ml is the dev loop's file, not this lane's.

test-flan-dape.el is not in dune test. It wants Emacs, dape, lldb-dap and a
built flan at once, and wiring four optional things into the acceptance table
would make that table's failures mean less, not more.
2026-09-12 03:58:26 +07:00
c1a612abfb The rejection goes away when you do anything else
An overlay that lasted until the next accepted evaluation was a durable
annotation on the source, which is not what it is: it is feedback about the
action that just failed, and the moment you move, type or evaluate it is
describing a program state nobody is in any more.

pre-command-hook rather than post-command-hook, which fires at the end of
the failing command and would take the overlay down before redisplay ever
drew it. Buffer-local and installed only while an overlay exists, so a
session of twenty buffers does not end up running this on every keystroke
in all of them.
2026-09-12 03:56:41 +07:00
8eb6de15c5 Ask from the editor, and put the caveat above the code
C-c C-a on a name, C-u for the IR. The header is SBCL's habit of saying
which function and out of what before a line of code, with one line it does
not have: what the answer claims. A reader looking at a listing will assume
it is what the program is running, and for a delivered-but-not-yet-installed
body that is exactly the thing that is not known — so the daemon's own
account of it sits above the first instruction rather than nowhere.
2026-09-12 03:55:51 +07:00
5170746de5 Reflow the note, autoload the client, and three todos
NEXT.md rewrapped to a wider column - a reflow, not a rewrite. The three TODO
entries in it are the substance: live disassembly of what is actually installed
in a cell, error overlays that vanish on the next thing you do rather than
surviving until an evaluation is accepted, and CL-style interactive recovery
where a stopped program offers a typed restart and the editor asks for the
value before invoking it.

flan-mode's declare-functions become real autoloads. A declare-function only
quiets the byte compiler; it does not load anything, so a user who had loaded
only flan-mode could not invoke M-x flan-dev at all.
2026-09-12 03:41:54 +07:00
61ca469a7c A restart that stopped at the quit has not restarted anything 2026-09-11 20:29:48 +07:00
f925a79475 Say at the top that the terminal is optional now 2026-09-11 20:28:13 +07:00
fc3cd2361a Point at the reason rather than naming the buffer it is in
A program that does not compile kills the daemon before it binds, which is
the failure anyone starting one from Emacs will actually hit. Showing that
buffer is the difference between a message and an answer.

The prompt also offers the program last started: a restart after a quit is
the common case, and it is rarely the buffer you happen to be reading when
you decide on it. C-c C-x does the restart without the prompt at all.
2026-09-11 20:27:50 +07:00
aee8a032b1 Some changes are not a reload, and saying so is the feature
A struct whose layout moved cannot be installed into a program built with the
old one, and the daemon says so. There is no smaller answer than a rebuild: a
session's layouts and global types describe a process only if that session
compiled it, so the program and everything in its memory go too. That is the
cost, and it is why this is its own command and not something C-c C-c falls
back to.

Emacs owns the daemon now, so this is stop-and-start rather than a new op.
The old one is waited out first: it unlinks the socket as it leaves and would
otherwise take its successor's with it.

Also: quitting a daemon that would not close now says its program may have
outlived it, because killing the daemon skips the cleanup that signals the
child — and C-c C-v rather than C-c C-h for the doc buffer, which was
shadowing the way anyone discovers what is under C-c.
2026-09-11 20:26:36 +07:00
76f84071df A signature in the echo area is gone the moment you type
C-c C-h puts what the daemon knows about a name in a buffer instead: kind,
signature, and a button on the place it is written. No new protocol — defs
has carried all four facts since it existed.

Where there is no location it says so in M-.'s own words rather than leaving
the line out, because a missing line reads as "this name has no home" and
the truth is that Tast.global carries no Loc.

imenu and which-function come with it, and neither needs a program running:
they read the buffer, so they work on a file nobody has built yet and keep
working while it is stopped. Anchored at column 0, so a defn inside a let is
not offered as a definition of anything.
2026-09-11 20:23:00 +07:00
ab2a31d002 A dev loop should not need a terminal window
The daemon owns the program's lifetime, so the terminal it was started in
was also the only place that program could be stopped from. M-x flan-dev
builds, launches and connects; M-x flan-dev-quit ends it.

It waits for a connection rather than for the socket file to appear: the
daemon unlinks a stale socket before binding, so waiting on the file either
succeeds instantly against nothing or races the unlink. And when the daemon
dies before binding — which for a program that does not compile is the
ordinary failure — the refusal names its buffer, because that is where the
compiler's reason is and nothing this end sees says it.
2026-09-11 20:17:08 +07:00
5f4005e61d A stopped program, driven from Emacs
The break loop was reachable from a raw socket. This is the half that makes
it reachable from an editor, and it all follows from one fact: a program
stops at a moment nobody asked about.

So the state is learned twice, on purpose. It rides on every reply, beside
the program's output and for the same reason -- the likeliest instant for a
program to stop is the one just after an evaluation, which is a reply the
client is already reading, and learning it a second later from a poll would
mean learning it after the echo area had said the evaluation was fine. And a
timer asks anyway, once a second with `describe', because a program that
stops in a frame of its own game loop produces no reply at all and folding
state into replies that never come says nothing. The timer never reconnects
-- that would quietly erase the `lost' state that exists to be seen -- and
skips while a request is in flight, since accept-process-output runs timers
and a poll firing inside a read would eat that read's reply.

Three ops: `break' for the restart names, `restart' and `abort'. The
annotation owns :stopped and :condition rather than the ops, so one place in
the daemon decides whether the program is stopped and the poll and the prompt
cannot disagree. "ok" from `restart' means accepted, not resumed: the choice
is validated against the stopped stack and taken when that thread next comes
round, so it says so and the client clears its own flag rather than polling
once, finding it stopped, and re-opening the prompt it just answered.

The agent grew one verb, `status', answered in both states. Everything else
the break loop offers is refused while running, rightly; but the question an
editor asks without already knowing had to have an answer either way or there
would be nothing to poll.

And flan_agent_poll had to become re-entrant, which was a bug rather than an
addition. A C-x C-e thunk may itself error, and the break loop that catches
it polls again from inside that call. The old loop cached both indices and
stored tail at the end, rewinding over everything the nested poll consumed --
re-running the thunk that had just stopped the program, which is an unbounded
recursion of breaks. Each job is now claimed before it is run. test_dev.ml
evaluates an expression that errors and resumes it, which fails against the
old shape.
2026-09-11 19:39:29 +07:00
Joseph Ferano
6f3ec2bb91 Keep the indicator, eldoc and the cache to buffers that asked for them
Three things the first cut got wrong by being global when it had no business
being.

The modeline entry was added to `mode-line-misc-info' at load. It returns nil
outside a Flan buffer, so it was invisible — but it was still evaluated on
every redisplay of every buffer in the session, for someone who loads the
client and then spends the afternoon in dired. It is installed buffer-locally
by `flan-dev-setup' now, which already runs in exactly the buffers that want
it. The `derived-mode-p' guard stays: cheap, and it keeps the function honest
wherever it is called from.

`flan-dev-setup' switched eldoc on. Contributing a documentation source is
this file's business; whether eldoc runs at all is the user's, and turning it
on overrules someone who has `global-eldoc-mode' off deliberately. It is on by
default, so nearly everyone gets the same behaviour either way.

And a reconnect forgot the name cache without asking for it again. An empty
cache is honest but silent — eldoc goes quiet, M-. falls through to whatever
else is registered, and nothing says why — until the next install happens to
refill it. It refreshes straight after reconnecting, which is safe from there
because the connection is live by that point and the request does not come
back round through the same function.
2026-09-11 17:58:34 +07:00
Joseph Ferano
7118d6106d eldoc, completion and M-. off one cached reply
All three want the same three facts about a name — what it is, what it looks
like, and where it was written — so the daemon answers all three in one
`defs` reply and the client keeps the last one.

`defs` is its own op rather than more fields on `describe`. `describe` is what
an editor *polls*: it is how the program's output gets drained, and the
existing tests ask it in loops. Signatures riding on that would be paid for
every time anyone glanced at the output buffer. This is asked once on connect
and again after each accepted install, which is exactly when the answer can
have changed — so a `defn` typed a second ago completes.

It is a cache rather than a request per keystroke because of where these are
called from: eldoc fires on an idle timer and completion inside redisplay, and
neither may block on a socket or signal.

Three refusals rather than three guesses. A global has no location because
`Tast.global` carries no `Loc`, and searching the buffer for "(defvar ticks"
instead would find the wrong one in a program of several files. The prelude is
a string inside the compiler, so its location names a file nobody can visit. A
short name that could be several of the program's package-qualified ones is
ambiguous, and picking would be a guess about which function you meant — a
name that is the tail of exactly *one* is not a guess, and resolves.

Functions the checker invented — a lifted handler-bind clause, which carries
an `fparent` — are left out entirely: nobody wrote that name, so completing it
is noise and jumping to it is meaningless.

And the daemon now makes its own source path absolute before building, because
every location it reports derives from it. `flan dev src/game.flan` from a
project root answered `src/game.flan:12:7`, which an editor can only resolve by
guessing what it was relative to.

lib/dev.ml is the only compiler file touched: a `defs` op, its three list
builders, and the one `realpath` in `start`. Nothing existing changed shape —
`describe`, `eval` and `eval-expr` answer byte for byte what they did.
2026-09-11 17:56:37 +07:00
Joseph Ferano
9e7eba1479 Say what landed and what it cost, and flash the form it came from
An install that reports nothing is indistinguishable from one that failed
silently, which is the one thing this loop cannot afford: the whole promise is
that the running program now has the body you just wrote.

The names come from the reply rather than from what was typed, because the
daemon is the one that knows which of them it installed — a `defvar' the
program already had is not among them, and the reply already says so with
`:note'. That case now reads "nothing to install" instead of quoting a
build time for a build that did not happen. `:fns` and `:names' are reported
separately for the same reason: a buffer of five functions and two vars
should not report as five of anything.

A long list is counted and then sampled rather than truncated, since an echo
area cut off in the middle of the tenth name tells you neither how many there
were nor which.

And the region that was sent is flashed, which answers a question the echo
area cannot: `beginning-of-defun' may well have found a different form from
the one you thought point was in.
2026-09-11 17:50:00 +07:00
Joseph Ferano
12f99702b4 Say in the modeline whether there is a program, and reconnect to one
Whether a program is on the other end is the one fact worth a permanent place
on screen, because every command in the client is a lie without it. Until now
it was discovered by something failing, which is the worst moment to learn it.

Three states, not two. `off' is never connected; `lost' is a daemon that has
gone away, which is the ordinary case rather than an error — `flan dev' ends
when its program does, and a program under development exits all the time. So
`lost' is reconnected from, on the socket it was on, the next time anything is
sent.

The reconnect is strictly *before* a send and never after one. A connection
that dies mid-request might have died after the daemon took the request and
ran it; resending would install a definition twice, or evaluate a
side-effecting expression twice. That case now reports what happened and says
it was not resent, rather than silently doing it again.

A socket that is not there is refused by name with the path, and a deliberate
`flan-disconnect' forgets the socket, so the next command says "not connected"
instead of quietly reopening what was just closed.
2026-09-11 17:48:06 +07:00