flan/docs/handoffs/HANDOFF-lowering-buffer.md

261 lines
13 KiB
Markdown

# Handoff — one buffer for every lowering
Branch `dev-loop`, worktree `agent-a9d4028fa5752ba39`, from `f772162`.
`spike/x86/dump.sh` landed at `f772162` and prints four lowerings of one function side by side: the LLVM IR
the frontend emits, what `llc` makes of it at `-O0` and at `-O2`, and what the hand-written x86 backend emits.
Reading one against another is the only way to check a lowering by eye. A shell script that prints all four
into a scrollback is not how anyone reads them, though: three of the four are noise on any given day, and the
one that is not is the one you are elbow-deep in. So the same four go into an Emacs buffer with collapsible
sections, and the buffer remembers which of them you had open.
`M-x flan-lowering`, `C-c C-l` in `flan-mode`.
## The three decisions
### 1. It compiles the file; it does not ask the daemon
`flan-disassemble` (`C-c C-a`) asks the running program what the body it is calling for a name actually came
out as. This asks a different question — what does this source compile to, through four different lowerings —
and the two cannot be merged, for a concrete reason rather than a stylistic one: **the daemon has the
installed body's `.ll` and its `.so`, and neither `llc -O2` nor the hand-written backend has ever been run
over it.** There is no answer in the daemon to give. `llc -O2` on the installed IR would be a fifth thing,
built here and never run anywhere, and the backend's output for an installed body does not exist at all
unless the daemon was started `--x86`.
So the two commands sit beside each other and each says which question it answers. `flan-disassemble` keeps
its header, whose `showing` line is the daemon's own account of how much its answer claims. This buffer's
header says the opposite thing just as plainly: it names the file and the compiler flags, and says the
listing is what the source on disk compiles to and not what the program is running. Both docstrings
cross-reference the other.
### 2. `outline-minor-mode`, not `magit-section` and not overlays of my own
Nothing under `emacs/` requires a package that is not in Emacs — the whole client is `subr-x`, `seq`,
`pcase`, `cl-lib`, `xref`, `eldoc`, `comint`, `imenu` and its own files. Taking a dependency on Magit for a
four-section buffer would be the first, and it would be for folding, which Emacs has had since before Magit
existed.
`outline-minor-mode` on GNU Emacs 30.2.50 is the whole of what was asked for: `outline-cycle` is `TAB` on a
heading, `outline-hide-body` and `outline-show-all` collapse and expand everything, and
`outline-next-visible-heading` / `outline-previous-visible-heading` are `n` and `p`. Hand-rolling it would be
a hundred lines re-implementing that badly.
`outline-regexp` is `"[▸▾] "` — the fold arrow that starts each heading, and nothing else. That is not
decoration standing in for structure, it is the one anchor available: LLVM IR has `;` comments, assembler
output has `#` and `;` comments and lines that start with `.`, and `objdump` prints `<flan.step>:` label
lines, so a regexp loose enough to match four section headers written in ordinary characters also matches
inside three of the four bodies, and folding then fragments in the middle of a listing. Neither arrow can
begin a line of any of the three. `outline-search-function` would have done the same job keyed to a text
property, at the cost of a four-argument protocol to get exactly right; the arrow is simpler and it is also
the affordance, so it earns its place twice.
### 3. The open/closed state is a global alist keyed by section, and nothing else
One `defvar`, four entries, alive for the Emacs session and no longer. Not persisted to disk: it is four
booleans, a preference file for it would one day have to be migrated, and nobody has ever wanted yesterday's
fold state back.
Keyed to the **section**, not to the function. The workflow is "I am working on the backend today", which is
a fact about the section and holds across every function the author looks at while he is doing it. Keyed to
the function it would forget on every `M-x flan-lowering` of a different name, which is exactly the thing he
asked not to happen.
The alist is the single source of truth. A toggle mutates it and then applies it to the buffer; a refresh
applies it to the newly drawn buffer. Visibility is never read back out — that would couple this to whether
outline folds with overlays or with text properties, which is a thing that has changed between Emacs
versions and is none of this file's business.
## What was built
| file | what |
|---|---|
| `emacs/flan-lower.el` | the whole feature: the four fetches, the narrowing, the renderer, the folding and the memory of it |
| `emacs/flan-mode.el` | `C-c C-l` and the autoload |
| `emacs/test-flan-dev.el` | 30 checks, after the disassembly section |
| `emacs/MANUAL.md` | its own section, plus rows in both key tables, the settings table and the file table |
## The keymap
| Key | Command | Does |
|---|---|---|
| `TAB`, `RET`, `mouse-1` | `flan-lower-toggle` | open or close the section point is in |
| `S-TAB` | `flan-lower-cycle-all` | close everything, or open everything if it is all shut |
| `c` | `flan-lower-collapse-all` | close every section |
| `e` | `flan-lower-expand-all` | open every section |
| `n` / `p` | `outline-next-visible-heading` / `outline-previous-visible-heading` | between headings |
| `g` | `flan-lower-refresh` | compile again, redraw all four |
| `r` | `flan-lower-refresh-section` | compile and redraw only this one |
| `q` | `quit-window` | |
`n` and `p` are outline's own commands bound straight into the map, which is
the same thing they mean in `flan-inspect-mode` and `flan-cnr-mode`: move
between the buffer's own units. `special-mode` binds the digits and `-` to
`digit-argument` and `negative-argument`, so the collapse and expand keys are
letters rather than the `+`/`-` pair they would otherwise have been.
`r` exists because the four are not the same price. The IR is the frontend
alone; `llc -O2` over a whole program's worth of it is the one that is felt.
The intermediates live in a scratch directory per buffer, so refreshing one
section reuses the IR the other three were made from. Refreshing the IR drops
every intermediate, because everything is downstream of it and the next `r` on
another section must not answer out of a file the old IR produced. The
directory goes with the buffer, on `kill-buffer-hook`, and there is a check
for that -- four intermediates per invocation left in `/tmp` would add up.
## Two things worth knowing
**The narrowing is `dump.sh`'s awk, ported.** The IR runs from `define
...@"flan.NAME"(` to `}`; the two assembler sections from the `flan.NAME:`
label to the `.size` that ends it; the disassembly from `<flan.NAME>:` to the
blank line after it. All four keep their last line, which is what makes a
closing brace part of the listing rather than the start of the next one. The
name is the Flan one -- `step`, not `flan.step` -- and a packaged function is
written the way the program names it, `sim/settle`, which is why every pattern
allows for LLVM's quoting.
**A section's failure is that section's text, not the buffer's.** `llc` can be
missing while the IR is perfectly readable, and a buffer that refused
altogether would be the wrong answer to that. Each fetch is wrapped, and the
error message -- which is the tool's own stderr -- becomes the body.
## Testing
30 checks in `emacs/test-flan-dev.el`, immediately after the disassembly
section and needing no daemon, which is itself the distinction between the two
commands.
Twenty-two of them drive the renderer with `flan-lower-fetch-function` rebound
to canned text. That is deliberate: folding, and the memory of what was
folded, are renderer properties, and putting four compilers and an `llc -O2`
behind every redraw would have cost seconds per redraw and proved nothing
about folding. The real fetch is exercised once at the end, guarded on `llc`,
`as` and `objdump`, and that one costs about 2.2s plus one more `llc -O2` for
the `r` check.
`r` is checked where the real fetch already ran, because the claim it makes is
about files rather than about text: the IR's mtime is unchanged after one
section is refreshed, which is the only evidence available that the cache does
what it says — a re-emitted `out.ll` would have the same contents as the one
it replaced. `g` is checked twice: from inside a listing, where point comes
back to the same line *and the same text on it*, and from the header, where
there is no section to come back to and the answer has to be the top rather
than wherever the redraw finished.
`buffer-string` is useless for any of it, because it returns hidden text too:
"all four are still named when everything is shut" would pass without anything
being shut. Every visibility check is `invisible-p`, which means what it says
whether outline folded with an overlay or with a text property.
- `dune test --root .` exit 0, every suite green, `emacs: all tests passed`.
- `dune build --root . @page` and `@cells` both exit 0.
## Transcript
`emacs -Q --batch -L emacs -l try.el -- _build/default/bin/main.exe
test/programs/dev-repl.flan`, printing the buffer *as displayed* — invisible
characters dropped, so a folded section really is absent rather than merely
unprinted.
Opened on `step`, with the remembered default (the IR open):
```
== M-x flan-lowering step ==
; every lowering of step
; file ~/Development/flan/.claude/worktrees/agent-a9d4028fa5752ba39/test/programs/dev-repl.flan
; compiler /home/joe/Development/flan/.claude/worktrees/agent-a9d4028fa5752ba39/_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
define i64 @"flan.step"(ptr %xfer) {
entry:
%t1 = load i64, ptr @"flan.ticks"
%t2 = add i64 %t1, 1
store i64 %t2, ptr @"flan.ticks"
%t3 = load i64, ptr @"flan.ticks"
ret i64 %t3
}
▸ LLVM -O0 13 lines
▸ LLVM -O2 10 lines
▸ x86 backend 18 lines
```
`TAB` on the x86 heading, then `TAB` on the IR heading — the state the author
described, his own backend open and the other three shut:
```
== TAB on the x86 heading, TAB on the IR heading ==
; every lowering of step
; file ~/Development/flan/.claude/worktrees/agent-a9d4028fa5752ba39/test/programs/dev-repl.flan
; compiler /home/joe/Development/flan/.claude/worktrees/agent-a9d4028fa5752ba39/_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 18 lines
0000000000012d94 <flan.step>:
12d94: push %rbp
12d95: mov %rsp,%rbp
...
```
The command run again on a *different* function. The preference is the
section's, so the backend is still the open one:
```
== M-x flan-lowering main -- a different function, same preference ==
; every lowering of main
; file ~/Development/flan/.claude/worktrees/agent-a9d4028fa5752ba39/test/programs/dev-repl.flan
; compiler /home/joe/Development/flan/.claude/worktrees/agent-a9d4028fa5752ba39/_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 48 lines
▸ LLVM -O0 63 lines
▸ LLVM -O2 58 lines
▾ x86 backend 69 lines
0000000000012df4 <flan.main>:
12df4: push %rbp
```
`c`, then `e`, then `n`/`p`. Collapsed, all four are still named with their
line counts, which is the buffer as a summary:
```
== c, collapse all ==
; every lowering of main
; file ~/Development/flan/.claude/worktrees/agent-a9d4028fa5752ba39/test/programs/dev-repl.flan
; compiler /home/joe/Development/flan/.claude/worktrees/agent-a9d4028fa5752ba39/_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 48 lines
▸ LLVM -O0 63 lines
▸ LLVM -O2 58 lines
▸ x86 backend 69 lines
== e, expand all, then n/p between headings ==
== e, expand all, then n/p between headings ==
n -> ▾ LLVM IR 48 lines
n n -> ▾ LLVM -O2 58 lines
p -> ▾ LLVM -O0 63 lines
```
## What is not here
- `C-c C-l` is not in `web/index.html`, so `web/examples/quotes.sh` does not
quote it. Its list of bindings is fixed and the page is a separate artefact;
adding a row there is a page change, not this one.
- A `desktop` or `savehist` story for the fold state, deliberately: it lives
for the Emacs session and no longer.
- The batch run leaves its scratch directory behind, because `emacs -batch`
exiting runs no `kill-buffer-hook`. Interactively the hook fires.