12 Commits

Author SHA1 Message Date
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
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
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
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
5993875539 A prompt on the running program
flan-repl.el is a comint buffer whose every line goes through the same
eval-expr request C-x C-e uses - no new protocol, no compiler support. Deriving
from comint rather than hand-rolling a prompt is the same call as deriving
flan-mode from lisp-mode: history, the input ring and kill/yank already exist
and are not worth rewriting. There is no subprocess behind it; the "process" is
a stub comint needs in order to have a prompt.

It is program-scoped: a name typed at the prompt resolves against the running
program's top-level namespace, so in sand you write sim/settle. A buffer
visiting a package's file gets the alias applied for it because the file says
which package it belongs to, and a prompt has no file to derive one from. RET
on a half-typed form opens a line instead of sending it, with balance checked
through the Flan syntax table so a paren inside a string does not count.

A value and the program's output are different things and arrive by different
routes: the value is the result of the request and appears at the prompt, while
anything printed rides along on the same reply into *flan-output*. Showing them
in one place would be convenient and wrong, so there is a test for the
separation - and it caught a real bug. The renderer's Unit case emitted () with
no evaluation at all, so (print-line "x"), the most ordinary thing anyone types
at a prompt, answered while nothing happened. A Unit expression is almost
always a call made for its effect; it is evaluated and then reported.
2026-09-11 07:21:37 +07:00
8a94f16acd The program's output goes where someone is looking at it
Its stdout is a pipe into the daemon now, and whatever it printed since the
last reply rides along with the next one into *flan-output*. Arriving with a
reply rather than by a separate request is the point: the output an evaluation
itself caused is the output anyone wants to see.

Draining that pipe is a liveness requirement, not a nicety. A pipe nobody reads
fills at 64K and the next write blocks the program forever, so it is read from
the accept loop's select whether or not an editor is asking, and the buffer is
capped - a program printing every frame must not grow the daemon without limit,
and the newest text is the useful end.

test_dev read the program's transcript off the daemon's stdout, which is no
longer where it goes; it collects :output from replies instead, which is also
what the editor does. The emacs test moved to a fixture that keeps running,
since it now evaluates more times than the old one had reloads to give.
2026-09-11 07:05:50 +07:00
7ce1d09900 C-x C-e: an expression, evaluated inside the running program
A different primitive from redefining a name. There is no name to install a
body into, so the expression is wrapped in a function with nowhere to be called
from; the module exports flan_reload_call to say "run this once", and the agent
calls it after the install - on the game thread, at a frame boundary, so an
expression that reads the program's state sees a point the program agrees is
consistent.

Nothing is marshalled back because nothing could be. A Flan value carries no
header, so no code at run time can say what it is; the compiler knows the type
and renders it there, in the thunk. That is the layout decision's bill, and it
is why the printer set is the scalars rather than everything.

The rendering does not go through stdout. Stdout belongs to the program, it is
in the hot path for anything that prints, and a dev-only feature must not put a
branch in it - so flan_rt.c is untouched and the value goes to flan_dev_result,
read back over the agent's socket. Safe without a handshake because the
generation counter is bumped last: the daemon waits for it to move rather than
assuming the program has reached a frame boundary.

u64 refuses by name, because i64->bytes is signed and anything past 2^63 would
come back negative. Everything without a derived printer refuses the same way.
A number that is quietly wrong is the failure this whole thing exists to
prevent.

An evaluation is not a declaration: the thunk is built against the program and
never spliced into it, so describe does not fill up with an eval/N for every
expression ever typed.

The test that matters is the same expression twice. The fixture increments
ticks every frame, so two evaluations must disagree - a value computed in the
compiler, or read from a copy of the program's state, would not.
2026-09-11 07:00:58 +07:00
56395edd59 The Emacs client, and the loop is closed
C-c C-c recompiles the top-level form at point and installs it in a running
program at that program's next frame boundary. Verified against sand: an
unsaved buffer edit to game-draw, and 240 consecutive frames drew it.

flan-mode.el derives from prog-mode with lisp-mode's syntax table, which is
most of the work - Flan is s-expressions, so sexp motion, paren matching,
beginning-of-defun and indentation are already right. What it adds is Flan's
own brackets ([ and { are brackets and not symbol characters, since every
binding list and every type is written with them), the characters a name may
contain, and its keywords.

flan-dev.el has no parser in it, which is what the protocol choice bought:
prin1 writes a request, read reads a reply. C-c C-k sends a buffer as one
module rather than a form at a time, because a defvar and the function using it
have to arrive in the same load or the first refers to storage that does not
exist yet. An error comes back with a location and point moves there.

Framing is in bytes and Emacs counts characters, so every length goes through
string-bytes and the process is binary. Otherwise one non-ASCII character in a
buffer puts the reply stream out of step by exactly as many bytes as the
payload has of them - a bug that reads as a corrupt protocol and only appears
for some people. test_emacs.ml drives the real client against a real daemon for
that reason: it is not the same claim as the daemon answering correctly, and a
mistake in the framing, in beginning-of-defun over Flan's syntax table, or in
the reply reader passes test_dev.ml and fails here.
2026-09-10 22:16:44 +07:00