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

@@ -382,6 +382,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_globalidx_run \
$(BIN)/test_tuple_sret_callee \
$(BIN)/test_tuple_sret_receive_run \
$(BIN)/test_append_wide_elem \
$(BIN)/test_struct_tuple_field_slot \
$(BIN)/test_widen_pad_zero_run \
$(BIN)/test_named_ptr_alias_variant_widen \
@@ -983,6 +984,17 @@ $(BIN)/test_tuple_sret_receive_run: test/wcc/799_tuple_sret_receive_run.c \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
# #34: append() stores the full element width per element kind — runtime
# readback (tagged box / str+slice 3-word header / struct fill / spread
# word-copy / 2-append realloc survival) + per-row cs==ww byte-id (which
# pins the wwstage elemsizeofc membsz, scratch order, and frame canary)
# + the rule-7 loud-stop for the deferred struct-from-call source shape.
$(BIN)/test_append_wide_elem: test/wcc/800_append_wide_elem.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
# #237: a tuple-typed struct field must contribute its real slot width to
# the enclosing struct's slotsize — pure cs==ww .s byte-id (frame size).
$(BIN)/test_struct_tuple_field_slot: test/wcc/930_struct_tuple_field_slot.c \

View File

@@ -87,6 +87,12 @@ static int cg_tagbase;
static int cg_tagbase_sz;
static int cg_tagscr;
static int cg_tagscr_sz;
/* #34: per-fn @appendscr — 8B dst-pointer spill for the append()
* struct-literal element fill (cg_structlit_fill DST_PTR_LOCAL needs
* a BP-rooted slot to reload BX from across its internal cgexprs).
* Cached per name per fn to mirror wwstage's localadd `@`-prefix
* dedup, else two struct appends in one fn diverge the frame. */
static int cg_appendscr;
/* System V AMD64 sret discipline (task #23). Plain TY_STRUCT returns
* with size > 24B are passed via a hidden first-arg pointer (RDI) to
* a caller-prealloc dest; the callee writes through that pointer and
@@ -6335,6 +6341,21 @@ cgexpr(Cg *c, Node *n, Local *locals)
int sn_off = (sn->kind == N_IDENT)
? localfind(locals, sn->str) : 0;
int store_op = fldstoreop(esub, 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).
* Mirrors the #270/#12/#20 array-literal element
* dispatch (cg_arrlit_fill_bp). */
Type *esubu = type_chase_named(esub);
int el_str = type_isstr(esub);
int el_slice = type_isslice(esub);
int el_tagged = esubu && esubu->kind == TY_TAGGED;
int el_struct = esubu && esubu->kind == TY_STRUCT;
int el_wide = el_str || el_slice || el_tagged ||
el_struct;
if (!el_wide && esz > 8)
fatal("#34: append() element kind "
"unsupported (rule-7)");
for (Node *vn = sn->next; vn; vn = vn->next) {
if (vn->kind == N_SPREAD &&
vn->lhs && vn->lhs->kind == N_IDENT) {
@@ -6350,6 +6371,61 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_MOVQ, amem(D_BP, it_off + 8), areg(D_DX));
ins2(c, A_CMPQ, areg(D_DX), areg(D_CX));
ins1(c, A_JGE, abranch(le));
if (el_wide) {
/* #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). */
ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8));
ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_DI));
ins2(c, A_MOVQ, aimm(esz), areg(D_SI));
ins1(c, A_CALL, masym(c, "rt_ensure"));
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
ins2(c, A_MOVQ, amem(D_BP, it_off), areg(D_BX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
ins2(c, A_MOVQ, amem(D_BP, sn_off + 8), areg(D_CX));
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
ins2(c, A_MOVQ, amem(D_BP, sn_off), areg(D_DX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_DX));
int k = 0;
for (; k + 8 <= esz; k += 8) {
ins2(c, A_MOVQ, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, k));
}
if (k + 4 <= esz) {
ins2(c, A_MOVL, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVL, areg(D_AX), amem(D_DX, k));
k += 4;
}
if (k + 2 <= esz) {
ins2(c, A_MOVW, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVW, areg(D_AX), amem(D_DX, k));
k += 2;
}
if (k + 1 <= esz) {
ins2(c, A_MOVB, amem(D_BX, k), areg(D_AX));
ins2(c, A_MOVB, areg(D_AX), amem(D_DX, k));
k += 1;
}
ins2(c, A_ADDQ, aimm(1), amem(D_SP, 0));
ins1(c, A_JMP, abranch(ll));
label(c, le);
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
continue;
}
/* AX = items.ptr[i] */
ins2(c, A_MOVQ, amem(D_BP, it_off), areg(D_BX));
if (esz > 1) {
@@ -6381,6 +6457,100 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_ADDQ, aimm(8), areg(D_SP));
continue;
}
if (el_str || el_slice) {
/* #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, locals);
ins1(c, A_PUSHQ, areg(D_AX));
ins1(c, A_PUSHQ, areg(D_BX));
ins1(c, A_PUSHQ, areg(D_CX));
ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8));
ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_DI));
ins2(c, A_MOVQ, aimm(esz), areg(D_SI));
ins1(c, A_CALL, masym(c, "rt_ensure"));
ins2(c, A_MOVQ, amem(D_BP, sn_off + 8), areg(D_CX));
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
ins2(c, A_MOVQ, amem(D_BP, sn_off), areg(D_DX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_DX));
ins1(c, A_POPQ, areg(D_CX));
ins1(c, A_POPQ, areg(D_BX));
ins1(c, A_POPQ, areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, 8));
ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, 16));
continue;
}
if (el_tagged || el_struct) {
/* #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). */
ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8));
ins2(c, A_LEAQ, amem(D_BP, sn_off), areg(D_DI));
ins2(c, A_MOVQ, aimm(esz), areg(D_SI));
ins1(c, A_CALL, masym(c, "rt_ensure"));
ins2(c, A_MOVQ, amem(D_BP, sn_off + 8), areg(D_CX));
ins2(c, A_SUBQ, aimm(1), areg(D_CX));
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_AX));
ins2(c, A_IMULQ, areg(D_AX), areg(D_CX));
}
ins2(c, A_MOVQ, amem(D_BP, sn_off), areg(D_BX));
ins2(c, A_ADDQ, areg(D_CX), areg(D_BX));
if (el_tagged) {
cg_widen_tagged_store(c, &locals,
esub, vn, D_BX, 0, esz);
continue;
}
if (vn->kind == N_STRUCTLIT) {
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)
fatal("#34: append() struct "
"element source ident is "
"not a local (rule-7)");
int k = 0;
for (; k + 8 <= esz; k += 8) {
ins2(c, A_MOVQ, amem(D_BP, soff + k), areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, k));
}
if (k + 4 <= esz) {
ins2(c, A_MOVL, amem(D_BP, soff + k), areg(D_AX));
ins2(c, A_MOVL, areg(D_AX), amem(D_BX, k));
k += 4;
}
if (k + 2 <= esz) {
ins2(c, A_MOVW, amem(D_BP, soff + k), areg(D_AX));
ins2(c, A_MOVW, areg(D_AX), amem(D_BX, k));
k += 2;
}
if (k + 1 <= esz) {
ins2(c, A_MOVB, amem(D_BP, soff + k), areg(D_AX));
ins2(c, A_MOVB, areg(D_AX), amem(D_BX, k));
k += 1;
}
continue;
}
fatal("#34: append() struct element source "
"shape unsupported (rule-7)");
}
cgexpr(c, vn, locals); /* val → AX */
ins1(c, A_PUSHQ, areg(D_AX));
ins2(c, A_ADDQ, aimm(1), amem(D_BP, sn_off + 8));
@@ -10839,6 +11009,7 @@ cgfn(Cg *c, FILE *out, Node *fn)
cg_tagbase_sz = 0;
cg_tagscr = 0;
cg_tagscr_sz = 0;
cg_appendscr = 0;
cg_sret_arg_off = 0;
cg_sret_dest_off = 0;
cg_sret_dest_sym = NULL;

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, ");

View File

@@ -4107,7 +4107,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;
@@ -4116,6 +4121,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) {
@@ -4138,6 +4167,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");
@@ -4183,6 +4299,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, ");

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, ");

View File

@@ -0,0 +1,425 @@
/*
* 800_append_wide_elem — cstage and wwstage agree, byte-for-byte and at
* runtime, that `append(s, v)` stores the FULL element width for every
* element kind (task #34; the regex fold-2a blocker).
*
* The bug: both stages lowered the append element store as ONE sized
* mov from AX (`MOV* AX, (BX)`) — correct only for scalars <= 8B. A
* str/slice element (24B {ptr,len,cap}, cgexpr -> AX/BX/CX) kept only
* .ptr (byte-id-BLIND: both stages identical and identically wrong); a
* tagged element got the raw payload written into the tag slot (no
* boxing — the #12 pathology); 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 `MOVQ $N, SI`
* line and the stride IMUL).
*
* The fix (BOTH stages, converged byte-identical), keyed on the
* DECLARED slice local's element type (cstage su->sub; wwstage stamped
* tinfo via elemsizeofc — NEVER the value node, the #25/#31 esz=0
* trap):
* - scalar 1/2/4/8: UNTOUCHED (the u8 row's asm is unchanged vs
* pre-fix master — regression-pinned by byte-id + runtime).
* - str/slice: push AX/BX/CX across rt_ensure, dst in DX (BX holds
* the element .len after the pops — the #24 register discipline),
* 3-word store at (DX)/8(DX)/16(DX).
* - tagged: grow FIRST, dst -> BX, then the #12 widen choke-point
* (cg_widen_tagged_store / cgwidentaggedstore) boxes {tag,payload}
* through @tagbase/@tagscr (via_outer mode).
* - struct: grow first; literal -> dst spilled to @appendscr +
* structlit fill (DST_PTR_LOCAL); local ident -> word-copy; ANY
* other source shape is a rule-7 loud-stop (build fails), never a
* silent scalar fall-through.
* - spread `append(s, items...)`: 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.
*
* row | shape | want
* ---------------------+--------------------------------------+------
* u8_baseline | []u8, two appends, sum | 8
* enum_alias_esz | []ek (enum i32 alias) — pins the | 5
* | elemsizeofc swap: wwstage fed SI=$8 |
* | where cstage fed $4 (cs!=ww growth). |
* tagged_box | [](i64|bool), append 100i64, match | 100
* | readback. Pre-fix: raw 100 landed in |
* | the tag slot, no arm matched. |
* tagged_two_append | two appends — element 0 survives the | 42
* | realloc full-width. |
* str_len_bytes | []str, append, len + byte readback | 11
* str_two_append | two str appends, len0*10 + len1 | 32
* slice_elem | [][]u8, append, len + byte readback | 12
* struct_first8 | []pt (16B), x+y (first qword — ken's | 8
* | characterization row) |
* struct_tail_word | []pt, z at +8 (the word the 1-word | 8
* | store dropped) |
* struct_lit_two | two struct-LITERAL appends (pins the | 7
* | @appendscr per-fn dedup: frame + |
* | byte-id diverge if cstage allocs two |
* | scratch slots where wwstage dedups) |
* spread_str | append(ys, xs...) of []str + single | 7
* spread_tagged | append(ys, xs...) of [](i64|bool) | 42
* struct_call_loudstop | append(xs, f()) struct-from-call is | BUILD_FAIL
* | the deferred source shape — must |
* | fail LOUD on both stages (rule-7), |
* | never silently store one word. |
*
* Every non-BUILD_FAIL row also asserts cstage/wwstage asm byte-id,
* which subsumes the frame-size canary (TEXT main,$N) — the @tagbase/
* @tagscr/@appendscr scratch allocations must land in the same order
* and size on both stages (#25/#31 lesson).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return -1;
}
/* want == BUILD_FAIL: the row must FAIL to build on both stages (the
* rule-7 loud-stop for a deferred source shape). run_driver returns -1
* exactly when the build fails, so `got == -1` is the pass. */
#define BUILD_FAIL (-2147483647 - 1)
struct row { const char *label; const char *src; int want; };
static const struct row rows[] = {
{ "u8_baseline",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet xs: []u8 = [];\n"
"\tappend(xs, 5u8);\n"
"\tappend(xs, 3u8);\n"
"\treturn (xs[0] + xs[1]): i32;\n"
"};\n",
8 },
{ "enum_alias_esz",
"package main;\n"
"type ek = enum { A = 5, B = 9 };\n"
"export fn main() i32 = {\n"
"\tlet xs: []ek = [];\n"
"\tappend(xs, ek.A);\n"
"\treturn xs[0]: i32;\n"
"};\n",
5 },
{ "tagged_box",
"package main;\n"
"type cell = (i64 | bool);\n"
"export fn main() i32 = {\n"
"\tlet xs: []cell = [];\n"
"\tappend(xs, 100i64);\n"
"\tlet r: i32 = 1;\n"
"\tmatch (xs[0]) {\n"
"\tcase let v: i64 => r = v: i32;\n"
"\tcase bool => r = 2;\n"
"\t};\n"
"\treturn r;\n"
"};\n",
100 },
{ "tagged_two_append",
"package main;\n"
"type cell = (i64 | bool);\n"
"export fn main() i32 = {\n"
"\tlet xs: []cell = [];\n"
"\tappend(xs, 40i64);\n"
"\tappend(xs, 2i64);\n"
"\tlet r: i32 = 0;\n"
"\tmatch (xs[0]) {\n"
"\tcase let v: i64 => r += v: i32;\n"
"\tcase bool => r = 99;\n"
"\t};\n"
"\tmatch (xs[1]) {\n"
"\tcase let v: i64 => r += v: i32;\n"
"\tcase bool => r = 98;\n"
"\t};\n"
"\treturn r;\n"
"};\n",
42 },
{ "str_len_bytes",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet xs: []str = [];\n"
"\tlet s: str = \"abcdef\";\n"
"\tappend(xs, s);\n"
"\treturn (xs[0].len: i32) + (xs[0][5]: i32) - (xs[0][0]: i32);\n"
"};\n",
11 },
{ "str_two_append",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet xs: []str = [];\n"
"\tlet a: str = \"abc\";\n"
"\tlet b: str = \"fg\";\n"
"\tappend(xs, a);\n"
"\tappend(xs, b);\n"
"\treturn (xs[0].len * 10 + xs[1].len): i32;\n"
"};\n",
32 },
{ "slice_elem",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet hb: [4]u8; hb[0] = 9u8; hb[1] = 4u8;\n"
"\tlet a: []u8; a.ptr = &hb[0]; a.len = 3; a.cap = 4;\n"
"\tlet ys: [][]u8 = [];\n"
"\tappend(ys, a);\n"
"\treturn (ys[0].len: i32) + (ys[0][0]: i32);\n"
"};\n",
12 },
{ "struct_first8",
"package main;\n"
"type pt = struct { x: i32, y: i32, z: i64 };\n"
"export fn main() i32 = {\n"
"\tlet xs: []pt = [];\n"
"\tlet p: pt = pt { x = 3, y = 5, z = 9 };\n"
"\tappend(xs, p);\n"
"\treturn (xs[0].x + xs[0].y): i32;\n"
"};\n",
8 },
{ "struct_tail_word",
"package main;\n"
"type pt = struct { x: i32, y: i32, z: i64 };\n"
"export fn main() i32 = {\n"
"\tlet xs: []pt = [];\n"
"\tlet p: pt = pt { x = 3, y = 5, z = 9 };\n"
"\tappend(xs, p);\n"
"\treturn (xs[0].z - 1): i32;\n"
"};\n",
8 },
{ "struct_lit_two",
"package main;\n"
"type pt = struct { x: i32, y: i32, z: i64 };\n"
"export fn main() i32 = {\n"
"\tlet xs: []pt = [];\n"
"\tappend(xs, pt { x = 1, y = 2, z = 5 });\n"
"\tappend(xs, pt { x = 3, y = 4, z = 7 });\n"
"\treturn (xs[0].z + xs[1].z): i32 - xs[0].x - xs[1].y;\n"
"};\n",
7 },
{ "spread_str",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet xs: []str = [];\n"
"\tlet a: str = \"abc\";\n"
"\tlet b: str = \"fg\";\n"
"\tappend(xs, a);\n"
"\tlet ys: []str = [];\n"
"\tappend(ys, xs...);\n"
"\tappend(ys, b);\n"
"\treturn (ys[0].len + ys[1].len + ys.len): i32;\n"
"};\n",
7 },
{ "spread_tagged",
"package main;\n"
"type cell = (i64 | bool);\n"
"export fn main() i32 = {\n"
"\tlet xs: []cell = [];\n"
"\tappend(xs, 40i64);\n"
"\tappend(xs, 2i64);\n"
"\tlet ys: []cell = [];\n"
"\tappend(ys, xs...);\n"
"\tlet r: i32 = 0;\n"
"\tmatch (ys[0]) {\n"
"\tcase let v: i64 => r += v: i32;\n"
"\tcase bool => r = 99;\n"
"\t};\n"
"\tmatch (ys[1]) {\n"
"\tcase let v: i64 => r += v: i32;\n"
"\tcase bool => r = 98;\n"
"\t};\n"
"\treturn r;\n"
"};\n",
42 },
{ "struct_call_loudstop",
"package main;\n"
"type pt = struct { x: i32, y: i32, z: i64 };\n"
"fn mk() pt = {\n"
"\treturn pt { x = 1, y = 2, z = 3 };\n"
"};\n"
"export fn main() i32 = {\n"
"\tlet xs: []pt = [];\n"
"\tappend(xs, mk());\n"
"\treturn 0;\n"
"};\n",
BUILD_FAIL },
};
static int
run_driver(const char *driver, const struct row *r, int i)
{
char src[64], tmpdir[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/apwe_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/apwe_%d_d_%d", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null",
tmpdir, driver, src);
if (runwait(cmd) != 0) {
if (r->want != BUILD_FAIL)
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
unlink(src); rmdir(tmpdir);
return -1;
}
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = runwait(outbin);
unlink(src); unlink(outbin); rmdir(tmpdir);
return got;
}
/* asm_byte_identical — generate .s via cstage's w6c and wwstage's
* w6c_ww and diff. Subsumes the frame-size canary: a scratch-order or
* esz divergence shows up on the TEXT main,$N line or the MOVQ $N, SI
* line. Pre-fix the tagged/struct rows differed (wwstage SI=$8 vs $16);
* the str rows were byte-id-blind (both wrong identically), which the
* runtime rows above catch. */
static int
asm_byte_identical(const char *bin, const struct row *r, int i)
{
char src[64], cs[64], ws[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/apwe_asm_%d_%d.ww", getpid(), i);
snprintf(cs, sizeof cs, "/tmp/apwe_asm_%d_%d_c.s", getpid(), i);
snprintf(ws, sizeof ws, "/tmp/apwe_asm_%d_%d_w.s", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c errored\n", r->label);
unlink(src);
return -1;
}
snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null",
bin, ws, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label);
unlink(src); unlink(cs);
return -1;
}
FILE *fc = fopen(cs, "rb");
FILE *fw = fopen(ws, "rb");
int rc = 0;
if (!fc || !fw) {
rc = -1;
} else {
for (;;) {
int a = fgetc(fc);
int b = fgetc(fw);
if (a != b) { rc = -1; break; }
if (a == EOF) break;
}
}
if (fc) fclose(fc);
if (fw) fclose(fw);
if (rc != 0)
fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n",
r->label);
unlink(src); unlink(cs); unlink(ws);
return rc;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cdrv[1024];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
char wdrv[1024];
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
struct { const char *name; const char *path; int gated_on_existence; }
drivers[] = {
{ "cstage", cdrv, 0 },
{ "wwstage", wdrv, 1 },
{ NULL, NULL, 0 },
};
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int d = 0; drivers[d].name; d++) {
if (drivers[d].gated_on_existence
&& access(drivers[d].path, X_OK) != 0) {
fprintf(stderr, "append_wide_elem: skip %s (no %s)\n",
drivers[d].name, drivers[d].path);
continue;
}
for (int i = 0; i < n; i++) {
int got = run_driver(drivers[d].path, &rows[i], i);
total++;
int bad = rows[i].want == BUILD_FAIL
? (got != -1) : (got != rows[i].want);
if (bad) {
fprintf(stderr,
"append_wide_elem[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
}
if (access(wdrv, X_OK) == 0) {
for (int i = 0; i < n; i++) {
if (rows[i].want == BUILD_FAIL)
continue;
total++;
if (asm_byte_identical(bin, &rows[i], i) != 0)
fail++;
}
}
if (fail) {
fprintf(stderr,
"append_wide_elem: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("append_wide_elem: %d fixtures passed\n", total);
return 0;
}