261 lines
13 KiB
Markdown
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.
|