diff --git a/HANDOFF-x86-debug.md b/HANDOFF-x86-debug.md new file mode 100644 index 0000000..c88e42e --- /dev/null +++ b/HANDOFF-x86-debug.md @@ -0,0 +1,84 @@ +# 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) diff --git a/lib/x86.ml b/lib/x86.ml index f525e6a..43a8eb2 100644 --- a/lib/x86.ml +++ b/lib/x86.ml @@ -2245,6 +2245,23 @@ and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) dst = let l = lvalue f a in addr_into f ~reg:rax l; store_int f.b ~src:rax ~mm:(lmem f dst ~scratch:r11) ~size:8 + (* The allocation registry's notes are the one runtime family a release + build drops on the floor, and [emit.ml:1918] drops it with this same + guard — the checker builds a [Tast.Rt] it does not know is unwanted, + because it does not know whether this is a dev build and does not have + to. [emit.ml] is careful to drop it before the arguments are walked, so + that taking the address of the container being described does not leave + an escaped alloca for mem2reg to refuse; here the arguments are not + touched until [call_rt], so answering [()] is already early enough. The + test is [emit.ml]'s byte for byte, strict [>] included: bare + [flan_dev_reg_note] is the runtime's own entry point and is never a + [Tast.Rt]; what [check.ml] builds is the [_vec], [_map] and [_pool] + wrappers, each of which is longer than the prefix. The node's type is + [Unit], so there is nothing to store and [dst] is untouched. *) + | Tast.Rt sym, _ + when (not f.md.Emit.dev) + && String.length sym > 17 + && String.equal (String.sub sym 0 17) "flan_dev_reg_note" -> () | Tast.Rt sym, _ -> call_rt f ~sym ~args ~rty:t dst | Tast.Cast target, [ a ] -> cast f a target dst | _ -> unsupported "primitive with %d arguments" (List.length args)