w6c+w6c_ww: append() stores the full element width per element kind (fix #34)

Both stages lowered the append element store as one sized mov from AX —
correct only for scalars <= 8B. A str/slice element kept only .ptr
(byte-id-blind), a tagged element got its raw payload written into the
tag slot (the #12 pathology, no boxing), a struct element kept only its
first qword. wwstage additionally fed rt_ensure membsz from bare
elemsizeof, whose 8-sentinel under-allocated and mis-strided named
tagged/struct elements (the #8 family; cs!=ww on the SI imm + stride).

Fix, keyed on the DECLARED slice local's element type (cstage
su->sub->size as before; wwstage elemsizeofc off the stamped tnode —
never the value node, the #25/#31 esz=0 trap), applied to both the
single-value and spread bodies (2 arms x 2 stages):

- scalar 1/2/4/8: untouched (u8 asm byte-identical to pre-fix).
- str/slice: AX/BX/CX pushed across rt_ensure, dst in DX (BX holds the
  element .len after the pops — the #24 register discipline), 3-word
  store.
- tagged: grow first, dst -> BX, box via the #12 widen choke-point
  (cg_widen_tagged_store / cgwidentaggedstore via_outer).
- struct: grow first; literal -> dst spilled to per-fn @appendscr
  (cached on cstage to mirror wwstage's @-prefix localadd dedup) +
  structlit fill DST_PTR_LOCAL; local ident -> word-copy; any other
  source shape is a rule-7 loud-stop, never a silent scalar
  fall-through. struct-from-call deferred.
- spread: the source element is already a fully-formed T (tag
  included), so the wide arm grows first and whole-width word-copies
  &items[i] -> dst, recomputing both addresses from the slice headers
  after the possibly-reallocating rt_ensure.

The elemsizeofc swap also corrects the named-scalar-alias membsz
(wwstage fed SI=$8 where cstage fed $4); no in-tree consumer appended
to such a slice, so nothing was riding the wrong 8 (lib/selfhost append
sites are all u8).

Test 800_append_wide_elem: 13 rows (runtime readback per kind, 2-append
realloc survival, spread str+tagged, @appendscr dedup, enum-alias esz,
loud-stop build-fail) + per-row cs==ww byte-id, which subsumes the
frame canary.
This commit is contained in:
2026-06-04 02:14:14 +09:00
parent bf1037d8c4
commit faade48513
6 changed files with 1382 additions and 3 deletions

View File

@@ -23940,7 +23940,12 @@ fn cgappend(c: *cgen, n: *node) void = {
let snlocal: *local = localfindnode(c, sn.str);
if (snlocal == nil) { return; };
let sn_off: i32 = snlocal.off;
let esz: i32 = elemsizeof(snlocal.tnode);
// #34: esz off the DECLARED slice local's stamped tnode via
// elemsizeofc — bare elemsizeof returns the 8 sentinel for a
// named tagged/struct element (the #8 family; cgappend was never
// upgraded), under-feeding rt_ensure's membsz AND mis-striding
// the slot index vs cstage's su->sub->size.
let esz: i32 = elemsizeofc(c, snlocal.tnode);
let etnode: *node = nil;
if (snlocal.tnode != nil) {
let stk: nkind = snlocal.tnode.kind;
@@ -23949,6 +23954,30 @@ fn cgappend(c: *cgen, n: *node) void = {
if (stk == nkind.N_TPTR) { etnode = snlocal.tnode.lhs; };
};
let store_op: str = tnodestoreop(c, etnode, esz);
// #34 element-kind store dispatch: the scalar 1-word store below
// silently gutted every wide element (str/slice 24B header,
// tagged box, struct body). Kind off the stamped slice tinfo —
// the value node's literal tinfo is the #25/#31 esz=0 trap.
// Mirrors cstage cgen.c's append arm + the #270/#12/#20
// array-literal element dispatch (cgarrlitfillbp).
let sti: *tinfo = nil;
if (snlocal.tnode != nil) { sti = snlocal.tnode.type_: *tinfo; };
for (sti != nil && sti.kind == tykind.TY_NAMED) { sti = sti.under; };
let esubti: *tinfo = nil;
if (sti != nil) { esubti = sti.sub; };
for (esubti != nil && esubti.kind == tykind.TY_NAMED) {
esubti = esubti.under;
};
let elstr: bool = esubti != nil && esubti.kind == tykind.TY_STR;
let elslice: bool = esubti != nil && esubti.kind == tykind.TY_SLICE;
let eltagged: bool = esubti != nil && esubti.kind == tykind.TY_TAGGED;
let elstruct: bool = esubti != nil && esubti.kind == tykind.TY_STRUCT;
let elwide: bool = elstr || elslice || eltagged || elstruct;
if (!elwide && esz > 8) {
let m34k: str = "#34: append() element kind unsupported (rule-7)\n";
os.write(2, m34k.ptr, m34k.len: u64);
os.exit(1);
};
let vn: *node = sn.next;
for (vn != nil) {
@@ -23971,6 +24000,93 @@ fn cgappend(c: *cgen, n: *node) void = {
emitline("(BP), DX\n");
emitline("\tCMPQ\tDX, CX\n");
emitline("\tJGE\t"); emitline(le); emitline("\n");
if (elwide) {
// #34: a spread element is already a fully-formed
// T in the source slice (tag included), so a
// whole-width word-copy is the store — no boxing.
// Grow FIRST: rt_ensure may realloc, so both
// addresses are recomputed from the slice headers
// after the call (i reloads from the counter
// slot; CX was clobbered).
emitline("\tADDQ\t$1, ");
emitoff((sn_off + 8): i64);
emitline("(BP)\n");
emitline("\tLEAQ\t");
emitoff(sn_off: i64);
emitline("(BP), DI\n");
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", SI\n");
emitline("\tCALL\trt_ensure(SB)\n");
emitline("\tMOVQ\t(SP), CX\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
emitline("\tADDQ\tCX, BX\n");
emitline("\tMOVQ\t");
emitoff((sn_off + 8): i64);
emitline("(BP), CX\n");
emitline("\tSUBQ\t$1, CX\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
emitline("\tMOVQ\t");
emitoff(sn_off: i64);
emitline("(BP), DX\n");
emitline("\tADDQ\tCX, DX\n");
let wk: i32 = 0;
for (wk + 8 <= esz) {
emitline("\tMOVQ\t");
emitdispreg(wk: i64, "BX");
emitline(", AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(wk: i64, "DX");
emitline("\n");
wk += 8;
};
if (wk + 4 <= esz) {
emitline("\tMOVL\t");
emitdispreg(wk: i64, "BX");
emitline(", AX\n");
emitline("\tMOVL\tAX, ");
emitdispreg(wk: i64, "DX");
emitline("\n");
wk += 4;
};
if (wk + 2 <= esz) {
emitline("\tMOVW\t");
emitdispreg(wk: i64, "BX");
emitline(", AX\n");
emitline("\tMOVW\tAX, ");
emitdispreg(wk: i64, "DX");
emitline("\n");
wk += 2;
};
if (wk + 1 <= esz) {
emitline("\tMOVB\t");
emitdispreg(wk: i64, "BX");
emitline(", AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg(wk: i64, "DX");
emitline("\n");
wk += 1;
};
emitline("\tADDQ\t$1, (SP)\n");
emitline("\tJMP\t"); emitline(ll); emitline("\n");
emitlabel(le);
emitline("\tADDQ\t$8, SP\n");
vn = vn.next;
continue;
};
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
@@ -24016,6 +24132,147 @@ fn cgappend(c: *cgen, n: *node) void = {
vn = vn.next;
continue;
};
if (elstr || elslice) {
// #34: 24B {ptr,len,cap} header. cgexpr leaves
// AX/BX/CX; all three must survive rt_ensure. dst
// lands in DX, NOT BX — the pops put the element
// .len back in BX (the #24 register discipline).
cgexpr(c, vn);
emitline("\tPUSHQ\tAX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tCX\n");
emitline("\tADDQ\t$1, ");
emitoff((sn_off + 8): i64);
emitline("(BP)\n");
emitline("\tLEAQ\t");
emitoff(sn_off: i64);
emitline("(BP), DI\n");
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", SI\n");
emitline("\tCALL\trt_ensure(SB)\n");
emitline("\tMOVQ\t");
emitoff((sn_off + 8): i64);
emitline("(BP), CX\n");
emitline("\tSUBQ\t$1, CX\n");
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
emitline("\tMOVQ\t");
emitoff(sn_off: i64);
emitline("(BP), DX\n");
emitline("\tADDQ\tCX, DX\n");
emitline("\tPOPQ\tCX\n");
emitline("\tPOPQ\tBX\n");
emitline("\tPOPQ\tAX\n");
emitline("\tMOVQ\tAX, (DX)\n");
emitline("\tMOVQ\tBX, 8(DX)\n");
emitline("\tMOVQ\tCX, 16(DX)\n");
vn = vn.next;
continue;
};
if (eltagged || elstruct) {
// #34: no register form survives rt_ensure for these —
// grow FIRST, then fill through the dst pointer
// (tagged: the #12 widen choke-point cgexprs the value
// internally; struct: literal fill / ident word-copy).
emitline("\tADDQ\t$1, ");
emitoff((sn_off + 8): i64);
emitline("(BP)\n");
emitline("\tLEAQ\t");
emitoff(sn_off: i64);
emitline("(BP), DI\n");
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", SI\n");
emitline("\tCALL\trt_ensure(SB)\n");
emitline("\tMOVQ\t");
emitoff((sn_off + 8): i64);
emitline("(BP), CX\n");
emitline("\tSUBQ\t$1, CX\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
emitline("\tMOVQ\t");
emitoff(sn_off: i64);
emitline("(BP), BX\n");
emitline("\tADDQ\tCX, BX\n");
if (eltagged) {
cgwidentaggedstore(c, esubti, vn, "BX", 0, esz);
vn = vn.next;
continue;
};
if (vn.kind == nkind.N_STRUCTLIT) {
let scroff: i32 = localadd(c, "@appendscr", 8, nil);
emitline("\tMOVQ\tBX, ");
emitoff(scroff: i64);
emitline("(BP)\n");
let esi: *structinfo = structlookupchain(c, etnode);
if (esi == nil) {
let m34s: str = "#34: append() struct element has no structinfo (rule-7)\n";
os.write(2, m34s.ptr, m34s.len: u64);
os.exit(1);
};
cgstructlitfill(c, esi, vn, 1, scroff, "", 0);
vn = vn.next;
continue;
};
if (vn.kind == nkind.N_IDENT) {
let sl: *local = localfindnode(c, vn.str);
if (sl == nil) {
let m34i: str = "#34: append() struct element source ident is not a local (rule-7)\n";
os.write(2, m34i.ptr, m34i.len: u64);
os.exit(1);
};
let soff: i32 = sl.off;
let ck: i32 = 0;
for (ck + 8 <= esz) {
emitline("\tMOVQ\t");
emitoff((soff + 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((soff + 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((soff + 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((soff + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg(ck: i64, "BX");
emitline("\n");
ck += 1;
};
vn = vn.next;
continue;
};
let m34e: str = "#34: append() struct element source shape unsupported (rule-7)\n";
os.write(2, m34e.ptr, m34e.len: u64);
os.exit(1);
};
cgexpr(c, vn);
emitline("\tPUSHQ\tAX\n");
emitline("\tADDQ\t$1, ");