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.
20 KiB
Flan in Emacs
A manual for the editor side. It assumes you know Emacs and nothing about how
Flan's dev loop is built — if you want that, BUILT.md has it.
The short version: you start a program, you keep it running, and you change it while it runs. Everything below is a variation on that.
Setting up
Put emacs/ on your load path and require the mode. Nothing else is needed —
flan-mode pulls in the rest as you use it.
(add-to-list 'load-path "~/Development/flan/emacs")
(require 'flan-mode)
.flan files open in flan-mode after that. The client, the REPL, the
inspector and the conditions buffer all load on first use, so requiring the mode
does not drag them in.
One optional extra: flan-dape.el gives you lldb through
dape. It is separate on purpose — flan-mode
works without dape installed, and C-c C-g only exists once you load it.
(require 'flan-dape) ; only if you have dape
You also need the flan binary on your PATH. If it is somewhere else, set
flan-dev-command.
Starting a program
Two ways in, and they are different.
M-x flan-dev starts the program for you. It runs flan dev on a file,
waits for it to come up, and connects. This is the normal way.
C-c C-z (flan-connect) attaches to a program that is already running —
one you started in a terminal, say. It looks for a .flan-dev.sock file in the
current directory and upward, so from anywhere in the project it finds the one
program you have running.
Either way, when you are connected the modeline says so and Emacs tells you what it found:
flan dev: connected to ~/Development/flan/.flan-dev.sock (47 functions, 2 globals)
C-c C-q disconnects without stopping the program. M-x flan-dev-quit
stops the program too — but only one this Emacs started. A daemon you launched
in a terminal is not Emacs' to kill, and it will say so rather than do something
surprising.
The loop
This is the part the whole project exists for.
C-c C-c — change one function
Put point anywhere in a top-level form and press it. The form is recompiled and installed into the running program, which does not stop, restart, or lose anything. The next time that function is called, the new one runs.
It works on the buffer text, not the saved file, so you do not have to save first.
C-x C-e — evaluate an expression
The expression before point is compiled, run inside the running program, and its value printed in the echo area. Not a copy of the program, not a simulation — the actual process, with its actual state.
So in a game you can type (len enemies) and get the real number.
C-c C-k — the whole buffer
The whole buffer, sent as one module rather than as a form at a time. That
matters: a defvar and the function that uses it have to arrive together, or the
function refers to storage that does not exist yet.
Use this when you have changed several things at once, or when you have added a new global.
When it lands
Changes install at a frame boundary — the program finishes what it is doing and picks up the new code at a clean point. You do not have to think about this except to know that a change is not necessarily live the same millisecond you press the key.
The REPL
C-c C-r opens *flan-repl*. It is a comint buffer; every line goes through
the same machinery C-x C-e uses, so anything you can evaluate there you can
evaluate here.
One thing to know: it is program-scoped, not buffer-scoped. Names are the
running program's names. In sand you write sim/settle, not settle, because
that is what the program calls it.
C-c C-o shows *flan-output* — whatever the program itself has printed.
That is separate from the REPL, because the program's stdout belongs to the
program.
When the program stops
If the program hits an error nobody handled, it does not die. It stops, on the frame where the error happened, and waits.
The modeline says stopped. Everything else in Emacs behaves normally — a
stopped program looks like a running one from anywhere else.
C-c C-b — the conditions buffer
This is where you decide what to do. It shows three things, in this order:
- the condition — what went wrong
- the restarts — your choices
- the stack — the explanation
That order is deliberate. The decision in front of you is which restart to take; the stack is why. A debugger that opens with forty frames has buried the decision under the explanation.
Keys in that buffer:
| Key | Does |
|---|---|
RET |
take the restart at point |
0–9 |
take that restart by number |
TAB / n |
next restart |
S-TAB / p |
previous |
f |
fold a stack frame open or closed |
i |
inspect a local variable |
a |
abort |
g |
read the program again |
q |
close the buffer |
Why restarts are numbered. A restart is taken by position, not by name.
Two frames can offer a restart with the same name — retry is common — and a
name resolves to the innermost one. So an outer retry is real, is on the list,
and cannot be reached by name. The numbers are how you reach it. Restarts that
genuinely cannot be taken are shown and refused with a reason rather than
silently omitted.
C-c C-M-b is the same choice as a quick one-key prompt, when you already
know which restart you want and do not need the buffer.
The condition's fields are named and typed, and have no values. Under the
condition you get the struct it is — :path string, :tried i32 — because the
daemon compiled the program and knows what that type looks like without asking
the program anything. What is beside each field is a note saying the value is
not available, not a blank: a value lives in the stopped frame, and nothing yet
hands the break loop's condition pointer back. Knowing the shape is still worth
having — it tells you whether the field you were about to blame is a field of
this condition at all.
If that section says it could not resolve the name, read it: a package
qualifies what it declares, so two packages' Missing are a/Missing and
b/Missing. The daemon refuses a bare name and says what it could have meant
rather than picking one.
After you choose, the program carries on from the restart. It never unwound, so everything it had is still there.
Looking at values
C-c C-i inspects a value. Give it an expression; you get its fields, one
per line.
| Key | Does |
|---|---|
RET |
go into the field at point |
l |
back out one level |
g |
read it again |
TAB / n |
next field |
S-TAB / p |
previous field |
q |
close |
Two things worth knowing, because they are unlike other inspectors.
The view is never stale. Every step reads the program as it is now. Most inspectors show you the object as it was when you opened it.
The root expression runs again on every step. Going into a field sends a new
expression — (.pos b) where the last one was b. Appending a field name is
harmless, but the root need not be: if you inspect (spawn-enemy), you spawn one
per keystroke. That is why there is no auto-refresh and why g is a key you
press rather than a timer.
When a change is refused
Two different things wear the same refusal today, and only one of them is the design.
A changed signature — a placeholder, not a rule
The intended behaviour, and what plan.org specifies, is that changing a
function's signature makes a new version of it: new callers resolve the new
one, existing callers and any stored Fn value stay safely on the old one, and
the session warns at each tracked stale caller site so you know what to
re-evaluate. Nothing should have to restart.
That needs function versions, trampolines and caller tracking, none of which are
built yet. Until they are, the session refuses rather than letting an
indirection cell hand old arguments to a new body — a wrong answer would be
worse than a refusal. lib/session.ml says so at the refusal itself, and
plan.org tracks it as open decision #6.
So if you hit this: it is a limitation with a date on it, not how the language is meant to work.
A changed struct layout — the genuinely hard one
Rejected while live values of that struct exist, and this one plan.org does still specify as a rejection. Storage already allocated has the old shape; a new body would read its fields at the wrong offsets and nothing at run time would say so. Managed classes are the planned way through — an explicit migration at a frame boundary — and they are not built either.
The way out, for now
C-c C-x stops the program, rebuilds from source, starts it again and
reconnects. It costs the program's state, which is why it is a key you press
rather than something C-c C-c quietly falls back to.
Under the debugger
C-c C-g (flan-debug, needs flan-dape.el) builds the current file with
DWARF and stops it at main under lldb.
Breakpoints are ordinary dape breakpoints set in the .flan buffer — the line
table names your Flan file, not the generated LLVM IR. dape-breakpoint-toggle
on a line, or dape-breakpoint-global to break on a function without hunting for
its first line.
lldb needs no plugin to read Flan values. A Flan struct is a C struct, a local is an ordinary stack slot, and there are no tag words or object headers anywhere, so lldb's own C support prints them correctly with nothing taught to it.
Local variables show under their real names. One caveat: if you shadow a name —
a let inside a let, both called v — both appear, the inner one as v~2,
but plain v still answers with the outer one. Read v~2 when you are inside
the inner binding.
Writing it
TAB indents the line, and C-M-q the form under point. The rules are ported
from clojure-mode's, because Clojure has the shapes Flan has and Emacs Lisp
does not — vectors that bind, bracket variety, and keys inside braces.
A binding vector lines up name under name. The second and later bindings of a
let sit under the first one's name, not under its value:
(let [vel (+ gravity (at velocity row col))
y (min (- rows 1) (+ row (i32 vel)))]
…)
The same rule draws defn parameter lists, restart-case and handler-bind
clause parameters, and both spellings of a struct literal — they are all a vector
or a brace read in pairs, so they are all indented as one.
A body indents two. let, if, when, while, match, restart-case,
handler-bind, defn and the def… forms all put their body two columns in
from the head. What differs between them is only how many forms come before
the body and stay on the head's line — a let's binding vector, an if's test,
a restart-case's protected form — and the indenter knows that count per form.
A form it has no entry for is treated as a call: the arguments line up under the
first argument, not two in. That is the fallback, and it is what you want for
(rl/draw-rectangle x y w h) and for every function you write.
defn carries a return type between the parameter vector and the body, and it is
optional. The indenter does not need to know which — everything after the head
indents two, which is the right answer for the name, the parameters, a return type
if one is written, and the body alike.
A field is drawn as a constant, in the accessor (.x v) and as a label in
{.x 1.0}. The colon is still a constant too; it means an enum member, :green,
and a key in a map.
Nothing here needs a running program. Indentation and colouring are the major mode's, so they work in a file you have only opened.
Getting around
| Key | Does |
|---|---|
M-. |
jump to where a name is written |
M-, |
jump back |
C-c C-d |
what the running program currently defines |
C-c C-v |
help on the name at point |
C-c C-a |
disassemble a function; C-u first for its LLVM IR |
Completion, eldoc and M-. all read one cached answer rather than asking the
program per keystroke. It refreshes at the two moments the answer can have
changed: when you connect, and after an evaluation the daemon accepted.
When something is wrong
An error draws an overlay where it happened, with the message. It clears the next time that buffer's evaluation is accepted — so it disappears when you fix the thing rather than when you dismiss it.
"No .flan-dev.sock found above this buffer" — nothing is running, or you are
outside the project. Start one with M-x flan-dev.
The modeline says nothing about a program — you are not connected. C-c C-z.
A restart you picked did nothing — it should not happen silently any more, but if a restart is genuinely unreachable the buffer marks it and refuses with a reason. Read the reason.
The stack pane says it cannot show frames — that is a real limit, not a bug.
The conditions buffer reaches the program over a socket, and a socket cannot read
another process's stack. The program stopped itself; it is not being debugged.
Use C-c C-g if you need frames.
Full key reference
| Key | Does |
|---|---|
C-c C-c |
the top-level form at point, recompiled and installed |
C-c C-k |
the whole buffer, as one module |
C-x C-e |
the expression before point, evaluated in the running program |
C-c C-z |
connect (finds .flan-dev.sock upward) |
C-c C-q |
disconnect |
C-c C-o |
the running program's own output |
C-c C-r |
a prompt on the running program |
C-c C-b |
a stopped program: condition, restarts, stack |
C-c C-M-b |
the same restarts, as a one-key prompt |
C-c C-i |
inspect a value |
C-c C-a |
disassemble; C-u first for LLVM IR |
C-c C-g |
debug under lldb, through dape |
C-c C-d |
what the running program defines |
C-c C-v |
help on the name at point |
C-c C-x |
rebuild, relaunch, reconnect |
M-. / M-, |
where a name is written / back |
Commands with no key: M-x flan-dev (start a program), M-x flan-dev-quit
(stop it).
Settings
| Variable | Default | What it is |
|---|---|---|
flan-dev-command |
"flan" |
the compiler binary |
flan-dev-socket-name |
".flan-dev.sock" |
what C-c C-z searches for |
flan-dev-echo-result |
t |
print C-x C-e's value in the echo area |
flan-dev-names-shown |
4 |
how many names to list before summarising |
flan-dev-output-buffer |
"*flan-output*" |
where the program's output goes |
flan-dev-poll-interval |
1.0 |
seconds between checks for whether it stopped |
flan-dev-daemon-buffer |
"*flan-dev*" |
the daemon's own log |
flan-dev-start-timeout |
60 |
seconds to wait for a program to come up |
The files
| File | What it is |
|---|---|
flan-mode.el |
the major mode: syntax, indentation, imenu, the keymap |
flan-dev.el |
the client — the socket, evaluation, xref, eldoc, completion |
flan-repl.el |
the *flan-repl* buffer |
flan-cnr.el |
the conditions-and-restarts buffer |
flan-inspect.el |
the value inspector |
flan-dape.el |
lldb through dape; optional |
There is no Flan parser in any of them. The client sends text and the compiler answers; anything that needs to know what a form means asks.
The stack, and what a frame was holding
C-c C-b opens the conditions-and-restarts buffer, and its Stack section is no
longer empty. It lists the stopped program's frames, innermost first, each with
where it is and how many named slots it has.
TAB on a frame opens it (or f, which folds from anywhere on the frame) and shows what its locals hold — name, type and
value, rendered the same way the inspector renders anything else. They are
fetched the first time a frame is opened and then kept, because a stopped
program's locals cannot change underneath you and a round trip behind a key
that looks like folding would be a surprise.
Two kinds of frame are marked. A program frame was on the stack when the
error happened. An eval frame belongs to an expression you evaluated inside
the break loop, sitting on top of them. Those are shown rather than hidden, on
the same principle the unreachable restarts are: a frame you did not write is
better explained than silently removed.
Not everything can be shown, and what cannot is refused by name under the frame rather than left blank:
- a slot the compiler invented, which has no name in your source — showing it
as
s4would put a variable in front of you that is not in the file; - a slot whose binding had not run yet when the error happened, which has no address to read;
- a
Vecor a pointer, which render as<vec>and<ptr>here exactly as they do everywhere else.
A redefined body is refused, not guessed at. Installing a fix while the program is stopped is deliberately allowed — it is the fix-it-and-retry loop — so the frame on the stack and the body the daemon holds can be two bodies of one function. A redefinition that renames a local changes neither the slot count nor the types, and showing the new names against the old storage would be a confident wrong answer in the one place someone is working out what went wrong. Each frame therefore carries a fingerprint of its body's slots, and a frame whose fingerprint no longer matches is refused by name with that reason. Redefining one function says nothing about the others, so every untouched frame still answers.
The globals the stack is working on
Under the stack there is one more section: the globals this stopped stack
reaches. In this language that is often the more useful half — a game keeps
most of its state in top-level defvars, and sand.flan holds its entire grid
that way.
It is one section, not a fold under each frame. A global is not part of a frame; it is program state the frame happened to touch. Nesting it under one would imply an ownership that is not there and would repeat the name once per frame that reads it.
It is scoped to the stack, not to the program. The contents are the union of the globals every frame on the stack references — the compiler already knows each function's reference set, so it does the choosing. Listing all of a program's globals instead would bury the one you want under the prelude's PRNG state.
Each entry says which frames touch it, by the same number the Stack section labels them with. That recovers what per-frame nesting would have told you — "the whole chain is reading this" reads differently from "only the innermost does" — at no cost in duplication. And the order is by the innermost frame that touches it, because a deep stack makes the union large and proximity to the error is what puts the likely culprit on top.
i works on a global line exactly as it does on a local: a global's name is an
expression, so the inspector can be pointed at it with nothing new.
Two things are said rather than left out. A global whose type the structural printer has no arm for is refused by name with the reason. And a frame the daemon could not attribute — one belonging to an expression you evaluated in the break loop, a lifted handler clause, or a body redefined since it was entered — is listed under "the union is incomplete", because a short list and a complete list look identical if nothing says which it is.
One hole worth knowing. The redefinition check is a fingerprint over a body's slots. A new body that names different globals while binding exactly the same locals is not caught, and that frame's contribution will be the new body's reference set. The values shown are still read from the program's own storage and are still correct; what can be wrong is which frames an entry is annotated with.
Stopping on purpose
(pause) stops the program where it stands and hands it to the break loop.
C-c C-b then shows the stack, TAB opens a frame's locals, and taking
continue resumes at the call as though nothing happened.
It is spelled pause rather than break because break is reserved for
leaving a loop — the same word meaning "exit this loop" and "stop for
inspection" in the same position would be the worst available collision.
Nothing in the compiler implements it. It is an error under a restart-case,
written in the prelude, which is what a breakpoint is in a language that
already has conditions. One consequence worth knowing: a handler-bind above
it can intercept a Pause and decline to stop, so a release build can neuter
every breakpoint in the program without editing any of them.
Untested. It compiles and the shape is right, but nobody has run it into a real break loop yet.