A discarded value was being stored over the return address

edn.flan crashed by jumping into .rodata, several statements after the
mistake, and the assembly at the jump read correctly. Item 15 said this
is how hand-encoding fails, and it is: the crash and the cause were in
different functions.

The cause is one line of design. A form whose value is thrown away was
handed the sink, and the sink was spelled as an address — rbp+0. That is
the saved rbp, and rbp+8 is the return address, so a non-void form in
statement position stored its value straight over both. A 16-byte slice
did it in one rep movsb.

The sink is now compared by identity and never used as an address:
anything with a value that is handed it gets a frame temporary instead,
reclaimed immediately. The point is not the temporary, it is that the
store has somewhere legal to go.

edn.flan matches the LLVM build now — 60 lines of a hand-written EDN
reader, unions, options, nested collections and all.
This commit is contained in:
Joseph Ferano 2026-09-13 15:08:38 +07:00
parent fcdaa105af
commit 58b1f49cf2

View File

@ -741,12 +741,29 @@ let emit_args f (args : arg list) =
(* ── Lowering ────────────────────────────────────────────────────────── *)
(* The destination a void expression is handed and never reads. rbp+0 is the
saved rbp; nothing below stores through a destination it was told was
void, so the address is a name and not a target. *)
(* The destination handed to an expression whose value is thrown away.
It is one allocated value and it is compared by identity, because it is not
an address and must never be used as one: rbp+0 is the saved rbp and rbp+8
is the return address, so a 16-byte slice stored "into the sink" overwrites
both and the function returns into whatever the first two words of the
value happened to be. That is not hypothetical it is how [edn.flan]
failed, by jumping into .rodata several statements after the real mistake,
and the mistake was a form of non-void type written in statement position.
So [lower] refuses the sink for anything that has a value, and spends a
frame temporary on it instead. The temporary is reclaimed at once; the
point is that the store has somewhere legal to go. *)
let sink = Lf 0
let rec lower f (e : Tast.expr) (dst : loc) : unit =
if dst == sink && not (is_void e.Tast.ty) then
scoped f (fun () ->
let o = tmp f e.Tast.ty in
lower f e (Lf o))
else lower_at f e dst
and lower_at f (e : Tast.expr) (dst : loc) : unit =
let t = e.Tast.ty in
match e.Tast.e with
| Tast.Int (n, _) -> imm_into f ~reg:rax n; store_loc f ~reg:rax dst t