Joseph Ferano a7e1208ee4 A stopped x86 program says where it is, and what its frames hold
The backend had a full dev story for everything except the one thing the
break loop reads: emit.ml pushes a shadow-stack frame on entry to every body
in a dev build and x86.ml pushed none, so an --x86 session answered backtrace,
locals, inspect and globals with "this program was not built with --dev",
which was false of it.

emit_fn now builds the same four words runtime/flan_dev.c describes -- the
previous head, a static descriptor, and a table of one address per slot, null
until the binding that fills it has run. The push is emitted at the head of
the body buffer rather than into the prologue's, because the prologue is where
the incoming registers still hold the arguments and every instruction of it
clobbers rax. The pop is one store at the epilogue label: this backend has one
epilogue and a return, the body's tail and the transfer exit all arrive there,
where emit.ml needs the same restore at five separate rets.

Two decisions are taken from emit.ml rather than reasoned out again, so that
the two dev builds answer a stopped frame identically. A function with no
named slot gets no table and reports no slots -- there the gate buys mem2reg
an alloca that does not escape, here it buys nothing but agreement, and
agreement is the only thing the break loop can check. And the descriptor's
bytes do not go through string_const: that counter is what decides whether an
expression thunk's module may be unloaded, and counting a descriptor in it
would pin every C-x C-e module's mapping for ever.

The descriptors are the one constant this backend emits that holds an address,
so they go in .data.rel.ro and not .rodata -- a relocation the loader applies
needs a section it may write, and a redefinition module is such an object.
Verified on an assembled module: R_X86_64_RELATIVE for the two pointers,
flan_frame_head through the GOT, no TEXTREL.

layout_ctx fills its globals table, which it never had to before. Reach's
ref_fingerprint asks whether a name is a global, and with an empty table no
name is one -- every frame would carry the hash of the empty set, the daemon
would recompute the real one, and the globals section would refuse a body
nobody had touched while its locals still read.

Release builds gain nothing: all of it is behind md.dev, and three corpus
programs emit byte-identical assembly before and after.

The new coverage stands up an --x86 daemon on dev-locals.flan and asks the
four questions, against the values the LLVM block above asserts of that same
program -- the claim is that the answers are the same and not merely
plausible. The existing --x86 two-process daemon gains one more: a backtrace
through a redefined body, which reports the installed body's own file, since
that descriptor travels in the module and a frame pointing at the host's would
name the body it replaced.
2026-09-19 04:32:15 +07:00
..