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

@@ -8570,26 +8570,62 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
continue;
}
if (vn->kind == N_STRUCTLIT) {
/* #59 (#50's eval-order kin):
* fill the literal into a fresh
* per-SITE scratch (must survive
* rt_ensure + a nested append in a
* field expr; 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). */
int st_scr = local_alloc(c,
&locals, "@appendstructscr",
esz, cg_frame);
cg_structlit_fill_bp(c, &locals,
esubu, vn, st_scr);
cg_append_grow(c, sn_direct, sn_off,
sn_scr, esz);
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_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. */
int ck = 0;
for (; ck + 8 <= esz; ck += 8) {
ins2(c, A_MOVQ, amem(D_BP, st_scr + ck), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, ck));
}
if (ck + 4 <= esz) {
ins2(c, A_MOVL, amem(D_BP, st_scr + ck), areg(D_AX));
ins2(c, A_MOVL, areg(D_AX), amem(D_BX, ck));
ck += 4;
}
if (ck + 2 <= esz) {
ins2(c, A_MOVW, amem(D_BP, st_scr + ck), areg(D_AX));
ins2(c, A_MOVW, areg(D_AX), amem(D_BX, ck));
ck += 2;
}
if (ck + 1 <= esz) {
ins2(c, A_MOVB, amem(D_BP, st_scr + ck), areg(D_AX));
ins2(c, A_MOVB, areg(D_AX), amem(D_BX, ck));
ck += 1;
}
continue;
}
cg_append_grow(c, sn_direct, sn_off,
sn_scr, esz);
cg_append_slot(c, sn_direct, sn_off,
sn_scr, esz, D_BX);
if (vn->kind == N_STRUCTLIT) {
/* #59 (#50's eval-order kin):
* the literal's field exprs
* still eval POST-grow here —
* filed, not folded. */
if (cg_appendscr == 0)
cg_appendscr = local_alloc(c,
&locals, "@appendscr", 8,
cg_frame);
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, cg_appendscr));
cg_structlit_fill(c, &locals, esubu,
vn, DST_PTR_LOCAL, cg_appendscr,
NULL, 0);
continue;
}
if (vn->kind == N_IDENT) {
int soff = localfind(locals, vn->str);
if (soff == 0)