emit.ml:1918 drops the allocation registry's notes in a release build -- the checker builds a Tast.Rt it cannot know is unwanted, because it does not know whether this is a dev build. The x86 backend had no counterpart and emitted the calls for real: correct, since the registry answers nothing when it is disabled, but one call per container operation into a function that returns immediately. The guard is emit.ml's byte for byte, strict > 17 included: bare flan_dev_reg_note is the runtime's own C entry point and is never a Tast.Rt, and what check.ml builds is the _vec, _map and _pool wrappers. emit.ml drops the note before the arguments are walked so that taking the address of the container does not leave an escaped alloca behind; here the arguments are not touched until call_rt, so answering () is already early enough. Measured, call sites of flan_dev_reg_note in the disassembly: vec.flan 46 -> 1 (--dev: 46) maps.flan 55 -> 1 (--dev: 55) registry.flan 36 -> 1 (--dev: 36) The one left in each is not emitted code -- it is inside the runtime's own flan_dev_reg_note_vec. An LLVM release build of vec.flan has the same one, so the two backends now agree. HANDOFF-x86-debug.md is the stub for item 6, which is next, and it leads with the finding that changes that item's plan: .loc does not work against a backend that emits .byte blobs, so the line table has to be written out by hand.
85 lines
4.0 KiB
Markdown
85 lines
4.0 KiB
Markdown
# Handoff — dropping the registry notes, and debug information for `--x86`
|
|
|
|
Branch `dev-loop`, worktree `agent-a123a91f32f4c9779`, from `957ba07`. Items **2** and **6** of
|
|
`HANDOFF-x86-rt.md` §6.
|
|
|
|
## The finding that changes the brief, read this first
|
|
|
|
**`.loc` does not work in this backend's output, and no amount of care makes it work.** The brief called line
|
|
tables "by far the least work, since `as` will build `.debug_line` for you from `.loc` directives". That is
|
|
true of a backend that emits mnemonics. This one emits `.byte` blobs — the header says so and says why — and
|
|
GAS builds its line table from `dwarf2_emit_insn`, which only runs out of `md_assemble`. No instruction is
|
|
ever assembled, so nothing flushes the pending `.loc`.
|
|
|
|
What GAS does instead is flush the pending `.loc` when it sees the *next* `.loc`, at whatever the location
|
|
counter has reached by then. Measured on `as` 2.44:
|
|
|
|
```
|
|
.file 1 "foo.flan"
|
|
main: .loc 1 10 1
|
|
.byte 0x55
|
|
.byte 0x48,0x89,0xe5
|
|
.loc 1 11 1
|
|
.byte 0xb8,0x07,0x00,0x00,0x00
|
|
.loc 1 12 1
|
|
.byte 0xc9
|
|
.byte 0xc3
|
|
```
|
|
|
|
```
|
|
foo.flan 10 0x4 x <- should be 0x0
|
|
foo.flan 11 0x9 x <- should be 0x4
|
|
foo.flan - 0xb <- line 12 has no row at all
|
|
```
|
|
|
|
Every row is one statement late and the last statement in the function gets none. Interposing labels between
|
|
the `.loc` and the `.byte` does not help; it was tested. Do **not** try to correct this by shifting the `.loc`
|
|
directives back by one statement — the last-statement hole is a correctness gap, not just fragility, and the
|
|
behaviour being exploited is undocumented.
|
|
|
|
So the line table is written out by hand here, as raw `.byte` / `.uleb128` / `.quad` in `.debug_line`, which is
|
|
the same thing this backend already does for instructions. Scope grew accordingly; see the plan below.
|
|
|
|
## Plan, in the order it is being done
|
|
|
|
1. **Item 2 — `flan_dev_reg_note` dropped in a release build.** Done; see below.
|
|
2. A hand-written `.debug_line`, plus the minimal `.debug_info` / `.debug_abbrev` CU that makes a debugger
|
|
*find* it. These are one step and not two: `readelf` will read a bare `.debug_line`, but gdb reaches a line
|
|
table only by walking `.debug_info` and following `DW_AT_stmt_list`.
|
|
3. `DW_TAG_subprogram` DIEs, so `break` and `list` scope by function. Note that gdb already names frames from
|
|
the ELF symbol table, which this backend emits via `.globl`, so this buys less than it looks like.
|
|
4. `.cfi` — only if it can be placed correctly against `.byte` output. If it cannot, that is said plainly and
|
|
gdb's prologue analyser is left to recognise `push rbp; mov rbp,rsp; sub rsp,N`, which is canonical.
|
|
5. Locals and types, only if everything above is solid and committed.
|
|
|
|
## Item 2 — done
|
|
|
|
`lib/x86.ml`, `prim`'s `Tast.Rt` dispatch, one arm above the general one, mirroring `lib/emit.ml:1918` test for
|
|
test including the strict `> 17`: bare `flan_dev_reg_note` is the runtime's own C entry point and is never a
|
|
`Tast.Rt`; what `check.ml` builds is the `_vec`, `_map` and `_pool` wrappers. The node's type is `Unit`, so the
|
|
body is `()` and `dst` is untouched.
|
|
|
|
`emit.ml` is careful to drop the note *before* the arguments are walked, because emitting the address of the
|
|
container only to discard the call would leave an escaped alloca that mem2reg refuses. Here the arguments are
|
|
not touched until `call_rt`, so the guard is already early enough.
|
|
|
|
Measured by disassembly, `objdump -d | grep -c 'call.*flan_dev_reg_note'`:
|
|
|
|
| program | `--x86` release | `--x86 --dev` | LLVM release |
|
|
|---|---|---|---|
|
|
| `vec.flan` | 46 → **1** | 46 | 1 |
|
|
| `maps.flan` | 55 → **1** | 55 | — |
|
|
| `registry.flan` | 36 → **1** | 36 | — |
|
|
|
|
The one that remains in every release build is not emitted code: it is inside the runtime's own
|
|
`flan_dev_reg_note_vec` in `flan_rt.c`, which calls `flan_dev_reg_note`. LLVM's release build has exactly the
|
|
same one, so the two backends now agree.
|
|
|
|
## Baseline
|
|
|
|
Measured on this tree, not recalled. (Filled in below as it is measured.)
|
|
|
|
## Open questions
|
|
|
|
(none yet)
|