A release build stops calling into a registry that is switched off
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.
This commit is contained in:
parent
eacf7c489f
commit
853bc35be7
84
HANDOFF-x86-debug.md
Normal file
84
HANDOFF-x86-debug.md
Normal file
@ -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)
|
||||
17
lib/x86.ml
17
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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user