wcc/cgen: #59 append/insert struct-literal value eval-order — eval-to-scratch pre-grow + precise copy (both-stage)

append/insert of a struct-LITERAL value evaluated the literal's field
exprs AFTER the grow, so a field reading the destination (e.g. len(xs))
saw the grown length. Both stages, #263 gate-blind (cs==ww byte-identical,
both wrong — runtime is the only net). #50 fixed the scalar/boxing value
arm; the struct-lit arm still post-grew.

Fix (mirror #50, both stages): resolve the struct, fill the literal into a
fresh per-site scratch (@appendstructscr, sized esz, survives rt_ensure +
nested-append clobber) BEFORE the grow, then copy scratch -> post-grow slot.

The copy uses the precise descending 8/4/2/1 ladder (the proven N_IDENT
struct arm directly below), NOT a raw 8B-word block copy: a struct's size
rounds to maxalign (check.c:916), so a sub-8B struct packs at a 4/2/1B
slice stride and an 8B copy over-writes past the slot — at a power-of-2
capacity boundary that clobbers the adjacent allocation (heap corruption,
both stages). The ladder never reads past esz (no uninit high bytes) nor
writes past the slot; esz=8 stays a single MOVQ (byte-id preserved).

insert() rides by construction: both stages desugar it to append and
re-dispatch into this arm. The #49 aplace path already uses the precise
ladder (verified, not exposed). #59 closes the last composite-value
eval-order hole in append/insert.

Pin: 946_append_structlit_evalorder_run — append / insert / narrow-neighbor
(i32-field at the cap boundary with an adjacent-allocation survival assert)
rows, each base-fail at 39432f7 and post-pass with cs==ww byte-id.
This commit is contained in:
2026-06-07 12:10:38 +09:00
parent 39432f717c
commit 2c09d13ca3
6 changed files with 491 additions and 46 deletions

View File

@@ -27470,16 +27470,16 @@ fn cgappend(c: *cgen, n: *node) void = {
vn = vn.next;
continue;
};
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
if (vn.kind == nkind.N_STRUCTLIT) {
// #59 (#50's eval-order kin): the literal's field
// exprs still eval POST-grow here — filed, not
// folded.
let scroff: i32 = localadd(c, "@appendscr", 8, nil);
emitline("\tMOVQ\tBX, ");
emitoff(scroff: i64);
emitline("(BP)\n");
// #59 (#50's eval-order kin): resolve the struct
// info, then fill the literal into a fresh
// per-SITE scratch (must stay live across
// rt_ensure, and a nested append in a field expr
// would clobber a dedup'd slot — the @apptagscr
// rationale, #25/#31) BEFORE the grow, so the
// field exprs see the pre-grow len. Then grow,
// slot, raw-copy scratch->slot (mirror the #50
// tagged arm above).
let esi: *structinfo = structlookupchain(c, etnode);
if (esi == nil && !sndirect && esubnamed != nil) {
// FA1: no declared tnode to chain through —
@@ -27494,10 +27494,60 @@ fn cgappend(c: *cgen, n: *node) void = {
os.write(2, m34s.ptr, m34s.len: u64);
os.exit(1);
};
cgstructlitfill(c, esi, vn, 1, scroff, "", 0);
let stscr: i32 = localalloc(c, "@appendstructscr", esz, nil);
cgstructlitfillbp(c, esi, vn, stscr);
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
// Descending 8/4/2/1 ladder, not an 8B-word
// loop: a plain struct's esz rounds to maxalign
// (check.c:916), not 8, so a sub-8B / non-8B-
// multiple element packs at its own stride — an
// 8B copy of the last slot writes past the slice
// buffer (the tagged arm above is safe only
// because boxes are 8B-padded; #59). Mirrors the
// N_IDENT source arm below.
let ck: i32 = 0;
for (ck + 8 <= esz) {
emitline("\tMOVQ\t");
emitoff((stscr + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(ck: i64, "BX");
emitline("\n");
ck += 8;
};
if (ck + 4 <= esz) {
emitline("\tMOVL\t");
emitoff((stscr + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVL\tAX, ");
emitdispreg(ck: i64, "BX");
emitline("\n");
ck += 4;
};
if (ck + 2 <= esz) {
emitline("\tMOVW\t");
emitoff((stscr + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVW\tAX, ");
emitdispreg(ck: i64, "BX");
emitline("\n");
ck += 2;
};
if (ck + 1 <= esz) {
emitline("\tMOVB\t");
emitoff((stscr + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg(ck: i64, "BX");
emitline("\n");
ck += 1;
};
vn = vn.next;
continue;
};
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
if (vn.kind == nkind.N_IDENT) {
let sl: *local = localfindnode(c, vn.str);
if (sl == nil) {