794 lines
36 KiB
Markdown
794 lines
36 KiB
Markdown
# 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.
|
||
|
||
```elisp
|
||
(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](https://github.com/svaante/dape). It is separate on purpose — `flan-mode`
|
||
works without dape installed, and `C-c C-g` only exists once you load it.
|
||
|
||
```elisp
|
||
(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-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.
|
||
|
||
---
|
||
|
||
## 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:
|
||
|
||
1. **the condition** — what went wrong
|
||
2. **the restarts** — your choices
|
||
3. **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:
|
||
|
||
```flan
|
||
(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:
|
||
|
||
```flan
|
||
(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:
|
||
|
||
```flan
|
||
(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-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.
|
||
|
||
---
|
||
|
||
## 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-u C-c C-c` | ...and stop at the form point is inside (`C-u C-u`: on entry) |
|
||
| `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-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), `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-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 |
|
||
| `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 |
|
||
|
||
---
|
||
|
||
## 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-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-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 `s4` would 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 `Vec` or 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 `defvar`s, 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.
|