From faade485132c3951b38b01a2f05aee94c10a5bd5 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 02:14:14 +0900 Subject: [PATCH] w6c+w6c_ww: append() stores the full element width per element kind (fix #34) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 12 + cmd/w6c/cgen.c | 171 +++++++++++ selfhost/cmd/w6c/main.combined.ww | 259 +++++++++++++++- selfhost/cmd/wcc/cgenexpr.ww | 259 +++++++++++++++- selfhost/cmd/wwdump/main.combined.ww | 259 +++++++++++++++- test/wcc/800_append_wide_elem.c | 425 +++++++++++++++++++++++++++ 6 files changed, 1382 insertions(+), 3 deletions(-) create mode 100644 test/wcc/800_append_wide_elem.c diff --git a/Makefile b/Makefile index bfbcf582..0f8fd119 100644 --- a/Makefile +++ b/Makefile @@ -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 \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index f41a62b8..1826440a 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index af719fd6..0db6a6e8 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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, "); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index ae9e92ca..1aa52644 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -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, "); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index d4941372..9e1ac5b8 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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, "); diff --git a/test/wcc/800_append_wide_elem.c b/test/wcc/800_append_wide_elem.c new file mode 100644 index 00000000..dcaa58e6 --- /dev/null +++ b/test/wcc/800_append_wide_elem.c @@ -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 +#include +#include +#include +#include +#include + +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; +}