flan dev takes the hand-written backend unasked now: it is what that backend was written for, it halves the C-c C-c round trip, and nothing it builds outlives the session. Every other command is LLVM by default and unmoved, which is what keeps lib/x86.ml's calling convention licensed. --debug picks LLVM on its own rather than landing on the refusal: a redefinition module from this backend carries no line table, so the one flag someone types to get a debugger must not choose the backend without one. --x86 --debug together is still refused, and now says which to drop. What the flip costs is more than the forms this backend refuses. It pushes no shadow stack, so a session built by it stops on an error and cannot say where -- and the agent answered that with "this program was not built with --dev", which is false of an x86 dev host. Dev.ask rewrites it; the IR view refuses by name instead of failing to find a define in a .s file; and a refusal at the host build or at C-c C-c names --llvm rather than stopping at "unsupported". flan-daemon-args carries the flag from Emacs, spliced into the one argument list the daemon buffer's first line is now printed from.
45 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, docs/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
Or install it as a package, which gets you autoloads and byte-compiled files
without a load-path line. package-install-file names the package after the
directory, so the directory has to be called flan-mode — symlink it once and
point Emacs at the link:
ln -s ~/Development/flan/emacs ~/.emacs.d/flan-mode
M-x package-install-file RET ~/.emacs.d/flan-mode/ RET
Every file carries Version: and Package-Requires: ((emacs "29.1")), which is
what package.el reads. dape is deliberately not in that list: flan-dape.el
reaches it through declare-function, so everything else installs and works
without it.
You also need the flan binary on your PATH. If it is somewhere else, set
flan-command.
Two settings worth knowing about before you need them. flan-start-timeout
(60s) bounds the wait for a daemon to come up, and flan-reply-timeout
(30s) bounds one request once it has. The second is the one a long first
compile can exhaust: the message says so and names *flan-dev*, where the
daemon's own build log is, so you can see whether it is still working before
you raise it.
Starting a program
Two ways in, and they are different.
M-x flan 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.
From a buffer already visiting a .flan file it starts that file and asks
nothing — the buffer has answered the question. From anywhere else it reads the
file from the minibuffer, and C-u M-x flan reads it even from a .flan
buffer, which is how you start some other program without leaving the one you
are looking at.
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-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.
Which backend the session uses, and what it costs
flan dev compiles the session with the hand-written x86-64 backend. That is
the default, and it is the reason C-c C-c is fast: about 28ms at the socket
against about 63ms through LLVM, because as does in 8ms what llc does in
45ms. On a project you are iterating in, that is the difference you feel.
It is not the whole compiler. Three things a session built by it cannot do:
- No backtrace, no locals, no globals. The inspector —
C-c C-b, and everything the break loop shows you about where a stopped program is — walks a shadow stack of frames, and this backend does not push them. A program built by it still stops on an unhandled error, still takes a restart, still answersC-x C-eat the break. It just cannot tell you where it stopped. - No
C-u C-c C-a. That view shows the LLVM IR a body was built from, and there is none; the listing this backend produced is assembly. PlainC-c C-adisassembles the object and works exactly as before. - Some programs it refuses by name. It covers a subset of the IR and says
so rather than guessing — a
signalor arestart-caseinside a global's initialiser, adeclarethat returns a struct by value, a value crossing the C boundary in a shape it has no classifier for. A refusal names the form and names--llvm, in both places you can meet one: the daemon's own build log when it starts, and the error overlay whenC-c C-chits one mid-session. It is never a bare "unsupported".
--debug is not on this list, because it picks LLVM for you. flan dev --debug — the breakpoint and flan-dape path — is an LLVM session, since a
redefinition module compiled by the x86 backend carries no line table and a
breakpoint set in one would stop firing at the first C-c C-c. Writing
--x86 --debug together is still refused, and now says which one to drop.
Asking for LLVM
Set flan-daemon-args. It is a list of strings, spliced into the daemon's
command line after the file, and it is how anything that is not the file or the
socket reaches flan dev from Emacs:
(setq flan-daemon-args '("--llvm")) ; the inspector, the IR view, every form
(setq flan-daemon-args '("--debug")) ; breakpoints, through flan-dape
The first line of *flan-dev* is the command that actually ran, so you can
always check what a session was started with.
There is no prefix argument for this and that is deliberate: C-u M-x flan
already means "ask me which file", and a second level that meant "ask me which
flags" would be two unrelated questions on one key. A setting is also the
honest shape of the thing — which backend your sessions use is a property of
the project you are working on, not of the keystroke that started this one.
If you were relying on the old default: every flan dev before this one
was an LLVM session, so a workflow built around C-c C-b or C-u C-c C-a will
find them refused now. One line in your init puts it back.
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-u C-c C-c — stop there
The same key with a prefix argument marks a form as a breakpoint. C-u C-c C-c marks the innermost form point is inside — with the cursor in (+ ticks 1)
the program stops at that (+ ...) — and C-u C-u C-c C-c marks the top-level
form itself, which means stopping on entry to it.
The buffer is not edited. The position goes to the daemon beside the code, and
the (pause) call is put into the tree after parsing, so every location in the
file stays exactly where it was and error markers, M-. and the debugger keep
pointing at the right place. The marked form is underlined while the mark is on
it.
When the program reaches it, it stops in the ordinary break loop: C-c C-b
shows the stack, and continue resumes at the call. Nothing special-cases this
— pause is a prelude function that signals a Pause condition, so a
breakpoint is just a condition nobody handled.
The mark sticks until you evaluate that form plainly. Mark it, run the game,
hit it as many times as you like; an ordinary C-c C-c over the same form (or
C-c C-k over the buffer) takes it off.
C-u C-x C-e does the same for the expression before point: it stops at the
expression instead of printing its value. That one does not stick, because there
is no definition for it to stick to.
C-c C-m — what a macro call expands to
Put point on a macro call and press it. *flan-macroexpansion* opens with the
code that call turns into, as Flan source, indented and coloured like any other.
One step by default, C-u C-c C-m for all the way. The two genuinely
differ: a macro may produce a call to another macro, so (mac/quad 3) is
(mac/twice (mac/twice 3)) after one step and (+ (+ 3 3) (+ 3 3)) at the end.
One step is the default because it is the one that can say which macro
produced what — the full expansion is tagged with the name you wrote and
nothing else.
Inside the buffer, m expands the form at point one more step in place,
a takes it to the end, g asks again — useful after you have re-evaluated
the defmacro — and q closes it.
This matters most for a macro you cannot read beside the call: a package's
macros arrive qualified, so rl/with-drawing is defined in another directory
and there is nothing next to the call site to look at.
It expands against the macros this session holds — the prelude's, the ones
its imports brought in, and every defmacro you have evaluated since it
started — not against a fresh read of the file. So it agrees with what an
evaluation does, and it sees the macro you have typed rather than the one you
last saved.
Two things the buffer says out loud. Nothing in it has a location of its own:
every node carries the call site's file, line and column with the macro's
name stamped on it, which is how an error inside expanded code points at the
call you wrote. And it is not the text of any file, so C-c C-c, C-c C-k and
C-x C-e in that buffer refuse rather than installing code nobody wrote.
A macro that never settles is refused at a bound and named, with the refusal drawn on the call — it does not hang.
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.
History. <up> and <down> walk it while you are on the line you are
typing, filtered by whatever you have typed so far — (sim then <up> reaches
only the forms that start that way. Anywhere above the prompt the same keys move
by line, so the transcript is still something you can scroll back through.
M-p, M-n and M-r are comint's own and unchanged.
It survives quitting Emacs. The file is flan-repl-history under
user-emacs-directory, so no-littering and a moved state directory both take
it with them; set flan-repl-history-file to nil to keep history for the
session only, or flan-repl-history-size to keep more or less than 500 entries.
A form typed over several lines is saved whole and comes back whole, and RET on
one recalled sends all of it.
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 the local or global at point |
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 runs again on every step. Going into a field sends a new request,
not a lookup in something remembered. Appending a field name to an expression
is harmless, but the expression itself 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.
Three ways to root a walk
There are three, they are not equally capable, and the top line of the buffer says which one you are on.
An expression — C-c C-i, and i on a global line in the break buffer.
It works on a running program and starts from anything you can write,
including a call. It cannot name a frame: an expression is evaluated where the
evaluator stands, so a local's name reaches that local only when its frame is
the innermost one. And it cannot reach an option's payload, because nothing in
the language names it.
A frame and a slot — i on a local line in the break buffer. It is exact
to one frame and one slot, so the value you get is the one the listing above it
drew. It reaches an option's payload and a union case's fields, which
have offsets but no accessor form to write. In exchange it needs a stopped
program, and it is refused — by name, with the reason — once the program
resumes or if the frame's body was redefined since the frame was entered. It
cannot start from an expression at all.
An address — M-x flan-inspect-address. A number, #x7f… or decimal, of
the kind a debugger, a valgrind report or a C shim's printf hands you. There
is no frame in it and no expression: what it shows is the (Ptr T) at that
address, followed if the storage is still live and an epitaph naming what died
there if it is not.
The type is optional, and leaving it out is the point. A dev build records the type at every allocation — the allocator's caller knew it, and a Flan value carries no header, so that is the only moment anything could — and this command is the one place that recorded name is read back and turned into a type again. Naming a type overrides it, for reading half a struct or an element the table recorded under a container's name; the reply still carries what the allocator wrote down, so you are never shown one type while the program believes another.
It needs a stopped program, for a reason of its own: whether an address is
still live is exactly what a running program is changing. And it takes no
path — RET does not go into it — because the whole answer is the pointer
arm's branch, and stepping in would step from the pointee, which is the deref
the registry has only just been asked to bless.
An address the registry has never seen is refused by name rather than rendered. That is a stack local, a global, or a pointer from C; the first two are answered by name in the stack and globals sections already.
None of the three subsumes the others, which is why all three are here. The
one you get is chosen for you by the line you press i on, or by starting an
address root by hand.
l never crosses between them, and that is structural rather than a rule
someone has to remember. Every entry on the buffer's stack carries its own
root; RET only ever lengthens the path under the root already in hand; and
starting a new root starts an empty stack. So a stack with both kinds in it
cannot be built, and l has nothing to cross into.
Where the memory went — M-x flan-allocations and M-x flan-leaks
The same registry, read as a table rather than at one address. M-x flan-allocations is every block it recorded, live and dead both, grouped by
the type the allocator's caller named and ordered biggest first by bytes. The
dead are in it on purpose: in a long-running program they are the bulk of it,
and they are what says where the allocation went rather than only where it
stayed.
M-x flan-leaks is the same walk with the dead left out — what the program
is still holding.
"Still holding" means at the moment you ask, and there is no exit report to
wait for. A program killed by a signal, which is how a program under this
editor usually ends, runs no exit handler at all, so nothing written inside it
could report anything. Asking is the answer, and you can ask at any time
including just before you quit. A program that returns from main on its own
can print the same breakdown to stderr by being run with FLAN_DEV_LEAKS set
— off by default, because a dev build's output belongs to the program.
Both are dev-build only. A release build records nothing and says so.
The watch buffer — values while the program runs
Everything above is for a program you have stopped, or one you interrupt with a
keystroke. M-x flan-watch is the other thing: a small buffer that shows
values while the game runs, updating at frame rate. M-x flan-watch-stop
closes it down, and killing the buffer does the same.
The program decides what is shown. There is no watch list to maintain, no per-variable registration, no place to type an expression. The program says what it wants seen, from inside its own loop:
(declare-c watch-i64 [name string x i64] i32 "flan_dev_watch_i64")
(declare-c watch-f64 [name string x f64] i32 "flan_dev_watch_f64")
(declare-c watch-str [name string s string] i32 "flan_dev_watch_str")
(defn step [] i64
(set ticks (+ ticks 1))
(watch-i64 "ticks" ticks)
ticks)
That is the whole of it. C-c C-c on step adds or removes a watched value
the same way it changes anything else, so the watch list is edited in the place
you were already looking.
Why it is pushed rather than polled. Every other listing in this manual is
a question the editor asks, which the daemon answers by compiling a small
module and handing it to the program. That is fine at the rate you press a key
and ruinous at the rate a HUD refreshes — each one is a new .so. So the
direction is inverted: the program writes into a table, and the editor reads
the table, which is memory. Two things follow that a poll could not have given.
The values are as fresh as the last frame, whatever the repaint interval is set
to. And they are still there while the program is stopped — a break loop is
exactly when nothing can be run at a frame boundary, and exactly when you want
to see what the last frame held.
What it costs when you are not watching. Nothing writes the table until a
watch buffer is open; M-x flan-watch tells the program somebody is looking
and closing it tells the program to stop. So a watch-i64 call in a program
nobody is debugging is a load and a branch that is not taken, in a release
build as in a dev one.
The limits. The table holds 64 names, and names past that are dropped rather than being fatal — the buffer says how many, because a value that simply never appeared would send you looking for a bug in the program. A name is truncated at 31 bytes and a rendered value at 192, with an ellipsis where a value was clipped.
A number sampled thousands of times a frame — watch-num-i64,
watch-num-f64. The scalar entry points keep one value per name, and from a
hot inner loop that is nearly useless: you see whichever of the 91,200 cells ran
last. These two are the other half. A slot keeps count, min, max, last and
mean, and the buffer renders them as one line:
(declare-c watch-num-i64 [name string x i64] i32 "flan_dev_watch_num_i64")
(declare-c watch-num-f64 [name string x f64] i32 "flan_dev_watch_num_f64")
(watch-num-i64 "cell" (at grid i))
The slot renders as n=… min=… max=… last=… mean=…, one line per name.
Two entry points rather than one so a program need not cast at the call site.
The write path does no formatting — a sample is a load, five compares and
the slot's seqlock — and the listener thread renders once per editor tick, which
is the whole reason this exists rather than a second watch-i64. The window is
since the editor's last tick, not since the program started: a min and a max
over a whole session reach the session's extremes within seconds and then never
move again, so the two most useful of the five would go dead exactly when you
start playing. A whole number prints as one, because a spy on an array index
reading 66.0000 sends you looking for a rounding bug that is not there.
Scalars only, so far. i64, u64, f64 and string have entry points; a
struct or a slice does not. That is not an oversight in the runtime — a Flan
value carries no header, so rendering one is a walk over its type at compile
time, and a (watch "hp" hp) form in the compiler is what would do that walk.
It is not built. flan-watch.el says what it would need.
Ghost text — the same values, inline
M-x flan-watch-ghost-mode shows each watched value at the call that wrote
it, as faint text after the line, instead of in a buffer of its own:
(defn step [] i64
(set ticks (+ ticks 1))
(watch-i64 "ticks" ticks) => 4812
ticks)
It is an addition, not a replacement, and the two can be on together. The
buffer is what you want when you want everything at once; inline is what you
want when you are reading one function and the value belongs beside the code
rather than three windows away. M-x flan-watch-ghost-mode again turns it off,
and turning either one off leaves the other running.
How it finds the place. The table carries a name and a rendered string and
no source location — that would need the (watch ...) form above. It does not
have to: (watch-i64 "ticks" ticks) is in the buffer you are looking at, and
the name in the table is the string literal in it, so the call site is found by
searching the text. Nothing new is asked of the running program. If you bound
your own entry points to names that do not start with watch, set
flan-watch-ghost-call-regexp — the Flan name is yours, and only the C symbol
behind it is fixed.
When it updates. On the same timer as the buffer, from the same reply, so the two cannot disagree. As with the buffer, the interval decides how often the picture is repainted and not how fresh it is. Overlays are replaced wholesale each repaint rather than followed through your edits, so a line you moved never leaves one stranded, and a file you scroll to gets its values within a tick. Only buffers shown in a window are scanned, which is what keeps that cheap.
One name, two call sites. Both show it, and both say one slot, 2 sites.
That is the truth rather than a failure to choose: the table has one slot per
name and the last call in the frame wins, so the value really is the same at
both, and it is whichever ran last. In a loop, you see the last value
written — the same answer the buffer gives. "The last of 4000 iterations" is
often not the one you wanted, but every better answer is a UI for building a
query, which is the thing this design exists not to have.
While the program is stopped, the values are the last frame's — that is the
point of pushing rather than polling — and each one says last frame and changes
colour. The watch buffer does not need to, because you opened it on purpose and
flan:stopped(...) is already in the modeline; inline, the value sits in the
middle of code that looks perfectly live.
What gets no ghost text. A watch call produced by a macro, or one whose name
is not a literal: there is nothing in the source to find, and the editor is
reading your file rather than debug info. Those still appear in the watch
buffer. A site you have typed but not yet installed with C-c C-c has no value
yet either — that is the ordinary stale-caller situation the modeline already
tracks. And when the table is full, a site that found no slot says so rather
than showing nothing, which is the one case where silence would send you looking
for a bug in your program.
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 |
C-c C-l |
every lowering of a function: IR, -O0, -O2, x86 backend |
C-c C-m |
what the macro call at point expands to; C-u for all the way |
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.
Every lowering of one function — C-c C-l
C-c C-l on a name opens *flan-lowering*: the LLVM IR the frontend emits for
that function, what llc makes of it at -O0 and at -O2, and what the
hand-written x86 backend emits, all narrowed to the one function. It is
spike/x86/dump.sh with a buffer around it. Reading one against another is the
only way to check a lowering by eye, and the reason the second backend is
trustworthy is that the two agree.
The four are folding sections, so the buffer is a four-line summary when everything is shut:
; every lowering of step
; file ~/src/game.flan
; compiler /home/joe/src/flan/_build/default/bin/main.exe
; flags none
; showing what this file compiles to, not what a running program is
; calling for this name -- for that, C-c C-a
▸ LLVM IR 8 lines
▸ LLVM -O0 13 lines
▸ LLVM -O2 10 lines
▾ x86 backend 19 lines
0000000000012d94 <flan.step>:
12d94: push %rbp
...
Which sections are open is remembered for the rest of the Emacs session, and it
is remembered by section, not by function: if you are working on the backend
this week, C-c C-l on a different name leaves the backend open and the other
three shut. It is not written to disk — four booleans are not worth a
preference file.
| Key | Does |
|---|---|
TAB |
open or close the section point is in |
S-TAB |
close everything, or open everything if it is all shut |
c |
close every section |
e |
open every section |
n / p |
to the next or previous section heading |
g |
compile the file again and redraw all four |
r |
compile and redraw only the section point is in |
q |
bury the buffer |
r is worth the separate key because the four are not the same price: the IR
is the frontend alone, and llc -O2 over a whole program's worth of it is the
one that is felt. Refreshing one section reuses the IR the other three were
made from. Refreshing the IR drops everything, since everything is downstream
of it.
This is not C-c C-a, and no prefix argument turns one into the other.
C-c C-a asks the running program what the body it is calling for a name
actually came out as; only the daemon can answer that, because the daemon
compiled that module and still has both its .ll and its .so. C-c C-l asks
what the file on disk compiles to, by compiling it, and needs no program
running at all. The two cannot be merged: neither llc -O2 nor the x86 backend
has ever been run over an installed body, so there is no four-way answer in the
daemon to give. The showing line in each buffer's header says which of the two
you are reading.
C-u C-c C-l asks for the file as well as the name, for the case where the
function you want to read is not in the buffer you are in.
The four outputs need llc, as and objdump on PATH — the backend writes
machine code rather than mnemonics, so its section is assembled and
disassembled to be readable, which is also a check that the bytes are well
formed. A section whose tool is missing says so in place of its listing; the
other three are unaffected.
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.
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-u C-c C-c |
...and stop at the form point is inside (C-u C-u: on entry) |
C-M-x |
the same as C-c C-c, on the binding SLIME and CIDER use |
C-c C-k |
the whole buffer, as one module |
C-x C-e |
the expression before point, evaluated in the running program |
C-u C-x C-e |
...and stop at it instead of printing its value |
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-m |
what the macro call at point expands to |
C-u C-c C-m |
...all the way, rather than one step |
C-c C-a |
disassemble; C-u first for LLVM IR |
C-c C-l |
every lowering of a function, in folding sections |
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 (start a program), M-x flan-quit
(stop it), M-x flan-watch (the watch buffer), M-x flan-watch-stop,
M-x flan-watch-ghost-mode (the same values inline),
M-x flan-inspect-address (what is at an address), M-x flan-macroexpand-all
(the C-u half of C-c C-m, by name), and M-x flan-allocations /
M-x flan-leaks (where the memory went, and what is still held).
Settings
| Variable | Default | What it is |
|---|---|---|
flan-command |
"flan" |
the compiler binary |
flan-daemon-args |
nil |
extra arguments for flan dev — ("--llvm"), ("--debug") |
flan-socket-name |
".flan-dev.sock" |
what C-c C-z searches for |
flan-echo-result |
t |
print C-x C-e's value in the echo area |
flan-names-shown |
4 |
how many names to list before summarising |
flan-output-buffer |
"*flan-output*" |
where the program's output goes |
flan-poll-interval |
1.0 |
seconds between checks for whether it stopped |
flan-daemon-buffer |
"*flan-dev*" |
the daemon's own log |
flan-start-timeout |
60 |
seconds to wait for a program to come up |
flan-lower-buffer |
"*flan-lowering*" |
where C-c C-l writes |
flan-lower-program |
"flan" |
the compiler C-c C-l shells out to |
flan-lower-flags |
nil |
flags for flan emit — ("--dev") for the dev lowerings |
flan-lower-llc |
"llc" |
what the -O0 and -O2 sections are made with |
flan-watch-buffer |
"*flan-watch*" |
where watched values are painted |
flan-watch-interval |
0.2 |
seconds between repaints — not the watch rate |
flan-watch-ghost-call-regexp |
"watch\(?:-[[:alnum:]]+\)?" |
the head of a call ghost text anchors on |
Every one of these used to be spelled flan-dev-…, and so did the commands:
M-x flan-dev is now M-x flan, flan-dev-quit is flan-quit, and so on
through the file. Nothing answers to the old names — there are no aliases —
so a setq or a keybinding in your config that names one will break, and the
fix is to delete -dev from it.
The files
| File | What it is |
|---|---|
flan-mode.el |
the major mode: syntax, indentation, imenu, the keymap |
flan.el |
the client — the socket, evaluation, xref, eldoc, completion |
flan-repl.el |
the *flan-repl* buffer |
flan-watch.el |
watched values: the program pushes, this paints them in a buffer and inline |
flan-cnr.el |
the conditions-and-restarts buffer |
flan-inspect.el |
the value inspector |
flan-lower.el |
the lowering buffer: four outputs, folding, and which was open |
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, and it roots differently there than on a local —
which is the fix rather than an inconsistency. A global really is reached by
name: the thunk the daemon loads binds to the program's own storage through the
dynamic linker, so the name means the same thing wherever it is evaluated. A
local is storage in one frame, and its name evaluated anywhere else may find a
global, another binding of the same name, or nothing. So a local goes in by
frame and slot index — the same two facts the listing was drawn from — and a
global by name. See "Two ways to root a walk" above for what each can reach.
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.