117 lines
8.2 KiB
Markdown
117 lines
8.2 KiB
Markdown
# Handoff — a redefinition emitter in `lib/x86.ml`
|
||
|
||
Branch `dev-loop`, worktree `agent-a884e1e2fcffe5052`, from `b1cc67b`. Item 1 of `HANDOFF-x86-rt.md` §6.
|
||
|
||
**It works and it is checked by running it.** An `--x86` host dlopens an `--x86` redefinition module, the
|
||
module publishes a new body into the host's cell, and the host's un-rebuilt call site follows it.
|
||
`test/test_reload.ml` is green on both backends.
|
||
|
||
## Baseline, measured before and after
|
||
|
||
| | before (`b1cc67b`) | after |
|
||
|---|---|---|
|
||
| `spike/x86/survey.sh` | 97 MATCH / 0 DIFFER / 0 refused | (see §"After", below) |
|
||
| `spike/x86/cells.sh` | 4/4 ok | 4/4 ok |
|
||
| `dune test --root .` | exit 0 | exit 0 |
|
||
|
||
`dune test` prints two `ld` / `clang` failures from inside the `dev-robust` fixture and still exits 0. Check the
|
||
**exit code**, not the output; grepping will mislead you.
|
||
|
||
## Why this had to exist
|
||
|
||
`x86.ml` licenses its own calling convention on the grounds that a dev build is compiled entirely by it and a
|
||
release build entirely by LLVM, *and the two never meet in one process*. The conventions agree on every scalar
|
||
and disagree on **every aggregate** (here: every aggregate by pointer with a hidden `sret` in the first integer
|
||
register; LLVM classifies by eightbyte). An `Emit.redefinition` module dlopened into an `--x86` host is correct
|
||
until the first redefined function takes or returns a struct. So an `--x86` host gets `--x86` modules.
|
||
**There is still no aggregate classifier and there must not be one.**
|
||
|
||
## The crux, and the answer
|
||
|
||
`program` emits an executable; every symbol it names it defines, or the runtime does, and a pc-relative `PC32`
|
||
reference is right for all of them. A redefinition module is a `.so` and almost everything it names belongs to
|
||
the **host executable** — the cells `flan.cell.<n>`, the globals `flan.<g>`, the runtime's entry points. A
|
||
`PC32` relocation against an undefined symbol cannot be used in a `-shared` link; `ld` refuses it.
|
||
|
||
So a reference to a symbol the object does not define goes through the GOT. Measured, not recalled:
|
||
|
||
- The spelling is `.long sym@GOTPCREL - 4`. Quoted symbols (`"flan.cell.x"@GOTPCREL`) are accepted.
|
||
- **`- .` is wrong.** `@GOTPCREL` is already pc-relative; written `- . - 4` as the plain form needs, `as`
|
||
produces an addend of **-8** and the load reads the wrong slot. `readelf -r` on the object is how you see it:
|
||
want `flan.cell.x - 4`.
|
||
- `llc -relocation-model=pic` emits `movq x@GOTPCREL(%rip), %rbx` for `@x = external global i64`, which is the
|
||
same instruction bytes this backend already emits for a `Sym` operand. Only the relocation changes.
|
||
|
||
## What was built
|
||
|
||
| file | what |
|
||
|---|---|
|
||
| `lib/x86.ml` — `modrm_got`, `mem`'s new `Got` case | the `@GOTPCREL` field. `mem_op` and `store_int`'s byte case were the only two matches on `mem` |
|
||
| `lib/x86.ml` — `loc`'s new `Lgot` case | a symbol the object does not define. `lmem` loads the GOT slot into `scratch` and answers `Reg (scratch, d)` — the `Lp` shape, so a field offset is arithmetic on a register and never on the relocation |
|
||
| `lib/x86.ml` — `fnctx.ext` | `string -> bool`, true of a symbol this object does not define. **Default `false`**, so the whole-program path emits byte-identical output and the survey is protected structurally |
|
||
| `lib/x86.ml` — `sym_loc`, `addr_sym`, `load_sym` | the three spellings of naming a symbol; each picks pc-relative or GOT off `ext` |
|
||
| `lib/x86.ml` — `emit_fn ?ext ?hidden` | `.hidden` on a module's own bodies |
|
||
| `lib/x86.ml` — `program`, the no-`main` case | was `unsupported "no main"`, now emits none, as `emit.ml` does. A program linked against a C host that brings its own entry point is legitimate — `reload_host.c` is exactly that, and the refusal made an `--x86` host for the reload tests impossible to build |
|
||
| `lib/x86.ml` — `redefinition` | the emitter. No `main`, no `.init_array`, no `flan..init-globals`, no `flan_dev_reg_enable` ctor, no `.bss` for globals, no `.data` for cells. Bodies `.hidden`; one `flan_reload_install` that reads each cell's address out of the GOT and stores the new body's `lea` into it |
|
||
| `lib/build.ml` — `shared_x86` | `as` + `ld -shared`, the same link `shared` does. No `-relocation-model=pic` to ask for: the emitter already writes every foreign reference through the GOT |
|
||
| `test/test_reload.ml` | the x86 section: an `--x86` host, two `--x86` modules, the same transcript, plus the refusal check |
|
||
|
||
## The one real bug, because it is the shape of the next one
|
||
|
||
`call_flan`'s `` `Cell `` target loaded **one** level: `mov r11, cell@GOTPCREL(%rip)` then `call *%r11`. That
|
||
reads as "call through the cell" in a disassembly and in fact *calls the cell*. The GOT slot holds the cell's
|
||
**address**; reading what the cell holds is two loads, not one. Found by the segfault, not by reading — which is
|
||
DISCUSS.md item 15's claim, now with a sixth instance. `load_sym` is the fix and is the only helper that
|
||
double-loads; `Lgot` gets it right for free because `lmem` already had the shape.
|
||
|
||
## Scope, deliberately narrower than `Emit.redefinition`
|
||
|
||
Only names the **host already has**. Refused by name, each with the reason in the message:
|
||
|
||
- a function new to the session — needs `flan_dev_cell` and `Emit.cellptr`'s second spelling (a module-local
|
||
slot resolved by string at install time)
|
||
- a global new to the session — needs `flan_dev_global`, plus the init constant that travels with it
|
||
- `consts` — republishing a `defconst`
|
||
- `call` — the transient `flan_reload_call` thunk and the `@flan_reload_transient` marker the agent unloads on
|
||
|
||
That is `C-c C-c` on an existing `defn`, which is the demo, and it is what `test/programs/reload.flan` and
|
||
`reload-v2.flan` exercise (v3 and v4 are the new-name cases and are asserted to refuse).
|
||
|
||
## What remains
|
||
|
||
1. **The new-name path.** `flan_dev_cell` / `flan_dev_global` and `Emit.cellptr`'s spelling, so that a `C-c C-k`
|
||
introducing a function or a `defvar` works on `--x86`. `test/test_reload.ml`'s v3/v4/v5 are the fixtures
|
||
already written; the refusal check there is what to replace.
|
||
2. **The transient thunk** — `flan_reload_call` plus `@flan_reload_transient`, which is what an *expression*
|
||
evaluation (the break loop, `C-x C-e`) compiles to. Note `emit.ml`'s three conditions for emitting the
|
||
marker, including `m.nstr = 0`: a module holding a string literal can never say it is transient.
|
||
3. **Wiring it to `flan dev` / `flan reload`.** Today nothing reaches this: `--x86` is read only by
|
||
`flan build`'s argument list in `bin/main.ml`, and the daemon builds host and module through
|
||
`Build.executable` / `Build.shared` with no `x86` field set. When it is wired, the host's backend and the
|
||
module's backend must be chosen together — that is the whole licence.
|
||
4. **A redefined function that takes and returns a struct.** This is the case that motivated the whole lane —
|
||
the two backends agree on every scalar and disagree on every aggregate — and it is exercised by *nothing*,
|
||
on either side of the reload boundary. `reload.flan`'s `bump` is `-> i64` and `helper` is `i64 -> i64`. The
|
||
argument that an `--x86` host plus `--x86` modules is same-convention-by-construction is sound, but it is an
|
||
argument and not a measurement. Do not edit `reload.flan` for it — its transcript is shared with the LLVM
|
||
path; add a fixture beside it.
|
||
5. Items 2–7 of `HANDOFF-x86-rt.md` §6, unchanged.
|
||
|
||
Also still true and still worth doing: **run `spike/x86/survey.sh` in CI**.
|
||
|
||
## After, measured
|
||
|
||
| | before (`b1cc67b`) | after |
|
||
|---|---|---|
|
||
| `spike/x86/survey.sh` | 97 MATCH / 0 DIFFER / 0 refused | **97 / 0 / 0**, skips `28 does-not-compile / 6 no-main / 2 runs-forever` — identical |
|
||
| `spike/x86/cells.sh` | 4/4 ok | **4/4 ok** |
|
||
| `dune test --root .` | exit 0 | **exit 0**, run without a pipe |
|
||
|
||
The skip breakdown is the line that matters beyond the MATCH count, because the no-`main` relaxation is the one
|
||
change here not gated behind `ext` being false. It cannot in fact move it: `survey.sh:70` classifies `no-main`
|
||
off the **LLVM** build failing at the link, before it ever runs the x86 one.
|
||
|
||
And run `dune test --root .` **without a pipe**: piping to `tail` gives you `tail`'s exit status, which is the
|
||
trap this file warns about at the top, and the `dev-robust` fixture puts `ld` and `clang` failure text in the
|
||
output either way.
|