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.
4.0 KiB
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
- Item 2 —
flan_dev_reg_notedropped in a release build. Done; see below. - A hand-written
.debug_line, plus the minimal.debug_info/.debug_abbrevCU that makes a debugger find it. These are one step and not two:readelfwill read a bare.debug_line, but gdb reaches a line table only by walking.debug_infoand followingDW_AT_stmt_list. DW_TAG_subprogramDIEs, sobreakandlistscope 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..cfi— only if it can be placed correctly against.byteoutput. If it cannot, that is said plainly and gdb's prologue analyser is left to recognisepush rbp; mov rbp,rsp; sub rsp,N, which is canonical.- 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)