diff --git a/Makefile b/Makefile index d4f1bb6a..29e3a1fd 100644 --- a/Makefile +++ b/Makefile @@ -392,6 +392,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_delete_elem \ $(BIN)/test_placeaddr_store \ $(BIN)/test_tryprop_multisuccess \ + $(BIN)/test_append_place \ $(BIN)/test_struct_tuple_field_slot \ $(BIN)/test_widen_pad_zero_run \ $(BIN)/test_named_ptr_alias_variant_widen \ @@ -1035,6 +1036,16 @@ $(BIN)/test_tryprop_multisuccess: test/wcc/806_tryprop_multisuccess.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# FA1 (task #15, regex fold-2b): append(*p, v) / append(*p, xs...) +# through a pointer-to-slice place via the cgplaceaddr resolver — +# runtime rows across element kinds + spread + realloc/frame-integrity +# + loud-tail reject rows w/ exact diagnostic text + cs==ww asm byte-id. +$(BIN)/test_append_place: test/wcc/806_append_place.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 b29ad34f..613a1abd 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -102,6 +102,16 @@ static int cg_ntagscr; * 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; +/* FA1 (#15) @apphdrscr — 8B spill of the resolver-derived slice-header + * ADDRESS for append() through a non-ident-local target (`append(*p, + * v)`). rt_ensure may realloc .ptr but never moves the header, so the + * spilled address stays valid across the call; every access reloads + * from the slot because registers don't survive it. Allocated fresh + * per append SITE (no per-fn cache, no decl here): a nested + * append-through-pointer inside a value expression (match-yield arm) + * spills its own resolve, and a shared slot would feed the outer + * grow/slot reloads the INNER target's header — silent cross-slice + * corruption (806 reentrant_value row). */ /* 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 @@ -2088,6 +2098,50 @@ cgplaceaddr(Cg *c, Node *n, int dst_reg, Local *locals) return 0; } +/* FA1 (#15): append() header-place access, cgplaceaddr's append + * consumer. direct = ident-local header in the frame (BP-disp — the + * legacy emission, kept byte-identical); indirect = header address + * pre-spilled to @apphdrscr by the resolver. grow = len+=1, &hdr→DI, + * esz→SI, CALL rt_ensure. In indirect mode the len bump goes through + * DI so the loaded address doubles as the call argument. */ +static void +cg_append_grow(Cg *c, int direct, int off, int scr, int esz) +{ + if (direct) { + ins2(c, A_ADDQ, aimm(1), amem(D_BP, off + 8)); + ins2(c, A_LEAQ, amem(D_BP, off), areg(D_DI)); + } else { + ins2(c, A_MOVQ, amem(D_BP, scr), areg(D_DI)); + ins2(c, A_ADDQ, aimm(1), amem(D_DI, 8)); + } + ins2(c, A_MOVQ, aimm(esz), areg(D_SI)); + ins1(c, A_CALL, masym(c, "rt_ensure")); +} + +/* Post-rt_ensure slot address: CX = (len-1)*esz, dst = .ptr + CX. + * Clobbers AX (the IMUL immediate) and CX, like the emission it + * replaces; dst must not be AX or CX. */ +static void +cg_append_slot(Cg *c, int direct, int off, int scr, int esz, int dst) +{ + if (direct) { + ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_CX)); + } else { + ins2(c, A_MOVQ, amem(D_BP, scr), areg(dst)); + ins2(c, A_MOVQ, amem(dst, 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)); + } + if (direct) + ins2(c, A_MOVQ, amem(D_BP, off), areg(dst)); + else + ins2(c, A_MOVQ, amem(dst, 0), areg(dst)); + ins2(c, A_ADDQ, areg(D_CX), areg(dst)); +} + /* cg_structlit_fill modes — see helper docstring. */ enum { DST_BP = 0, @@ -6842,6 +6896,12 @@ cgexpr(Cg *c, Node *n, Local *locals) Type *esub = su ? su->sub : NULL; int sn_off = (sn->kind == N_IDENT) ? localfind(locals, sn->str) : 0; + /* FA1 (#15): a non-ident-local target used to silently + * 0-default sn_off, making 0(BP)/8(BP) the "header" — + * rt_ensure then corrupted the caller frame. Non-direct + * targets resolve through cgplaceaddr below; a shape it + * can't address is loud. */ + int sn_direct = sn_off != 0; int store_op = fldstoreop(esub, esz); /* #34 element-kind store dispatch: the scalar 1-word * store below silently gutted every wide element @@ -6858,6 +6918,22 @@ cgexpr(Cg *c, Node *n, Local *locals) if (!el_wide && esz > 8) fatal("#34: append() element kind " "unsupported (rule-7)"); + int sn_scr = 0; + if (!sn_direct) { + if (!cgplaceaddr(c, sn, D_BX, locals)) + fatal("#15: append() target place " + "unsupported (rule-7)"); + /* Fresh slot per SITE, not a per-fn cache: a + * nested append-through-pointer inside a value + * expression (match-yield arm) spills its own + * resolve; a shared slot would hand the outer + * grow/slot reloads the inner target's header. + * local_alloc never dedups by name. */ + sn_scr = local_alloc(c, &locals, + "@apphdrscr", 8, cg_frame); + ins2(c, A_MOVQ, areg(D_BX), + amem(D_BP, sn_scr)); + } for (Node *vn = sn->next; vn; vn = vn->next) { /* #34 review: a spread whose source is not a * local ident used to fall PAST the spread arm @@ -6897,10 +6973,8 @@ cgexpr(Cg *c, Node *n, Local *locals) * 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")); + cg_append_grow(c, sn_direct, sn_off, + sn_scr, esz); ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX)); if (esz > 1) { ins2(c, A_MOVQ, aimm(esz), areg(D_AX)); @@ -6908,14 +6982,8 @@ cgexpr(Cg *c, Node *n, Local *locals) } 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)); + cg_append_slot(c, sn_direct, sn_off, + sn_scr, esz, D_DX); int k = 0; for (; k + 8 <= esz; k += 8) { ins2(c, A_MOVQ, amem(D_BX, k), areg(D_AX)); @@ -6952,18 +7020,10 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, load_op, amem(D_BX, 0), areg(D_AX)); /* ensure + store one element */ ins1(c, A_PUSHQ, areg(D_AX)); - 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)); + cg_append_grow(c, sn_direct, sn_off, + sn_scr, esz); + cg_append_slot(c, sn_direct, sn_off, + sn_scr, esz, D_BX); ins1(c, A_POPQ, areg(D_AX)); ins2(c, store_op, areg(D_AX), amem(D_BX, 0)); /* loop tail */ @@ -6983,16 +7043,10 @@ cgexpr(Cg *c, Node *n, Local *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)); + cg_append_grow(c, sn_direct, sn_off, + sn_scr, esz); + cg_append_slot(c, sn_direct, sn_off, + sn_scr, esz, D_DX); ins1(c, A_POPQ, areg(D_CX)); ins1(c, A_POPQ, areg(D_BX)); ins1(c, A_POPQ, areg(D_AX)); @@ -7007,18 +7061,10 @@ cgexpr(Cg *c, Node *n, Local *locals) * 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)); + cg_append_grow(c, sn_direct, sn_off, + sn_scr, esz); + cg_append_slot(c, sn_direct, sn_off, + sn_scr, esz, D_BX); if (el_tagged) { cg_widen_tagged_store(c, &locals, esub, vn, D_BX, 0, esz); @@ -7069,18 +7115,10 @@ cgexpr(Cg *c, Node *n, Local *locals) } 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)); - 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)); + cg_append_grow(c, sn_direct, sn_off, + sn_scr, esz); + cg_append_slot(c, sn_direct, sn_off, + sn_scr, esz, D_BX); ins1(c, A_POPQ, areg(D_AX)); ins2(c, store_op, areg(D_AX), amem(D_BX, 0)); } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 362d8f54..5206ddf2 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -24402,6 +24402,77 @@ fn cgalloc(c: *cgen, n: *node) void = { emitlabel(donel); }; +// cgappendgrow — FA1 (#15): append() header-place grow, cgplaceaddr's +// append consumer. direct = ident-local header in the frame (BP-disp — +// the legacy emission, kept byte-identical); indirect = header address +// pre-spilled to @apphdrscr by the resolver. len+=1, &hdr→DI, esz→SI, +// CALL rt_ensure. In indirect mode the len bump goes through DI so the +// loaded address doubles as the call argument. Mirrors cstage +// cg_append_grow. +fn cgappendgrow(c: *cgen, direct: bool, off: i32, scr: i32, esz: i32) void = { + if (direct) { + emitline("\tADDQ\t$1, "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + emitline("\tLEAQ\t"); + emitoff(off: i64); + emitline("(BP), DI\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(scr: i64); + emitline("(BP), DI\n"); + emitline("\tADDQ\t$1, 8(DI)\n"); + }; + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", SI\n"); + emitline("\tCALL\trt_ensure(SB)\n"); +}; + +// cgappendslot — post-rt_ensure slot address: CX = (len-1)*esz, +// dst = .ptr + CX. Clobbers AX (the IMUL immediate) and CX, like the +// emission it replaces; dst must not be AX or CX. Mirrors cstage +// cg_append_slot. +fn cgappendslot(c: *cgen, direct: bool, off: i32, scr: i32, esz: i32, dst: str) void = { + if (direct) { + emitline("\tMOVQ\t"); + emitoff((off + 8): i64); + emitline("(BP), CX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(scr: i64); + emitline("(BP), "); + emitline(dst); + emitline("\n"); + emitline("\tMOVQ\t8("); + emitline(dst); + emitline("), 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"); + }; + if (direct) { + emitline("\tMOVQ\t"); + emitoff(off: i64); + emitline("(BP), "); + emitline(dst); + emitline("\n"); + } else { + emitline("\tMOVQ\t("); + emitline(dst); + emitline("), "); + emitline(dst); + emitline("\n"); + }; + emitline("\tADDQ\tCX, "); + emitline(dst); + emitline("\n"); +}; + // cgappend — Hare-style `append(s, v)` / `append(s, items...)` lowering. // Mirrors cmd/w6c/cgen.c's N_CALL append branch (rt::ensure model). // Each value gets: @@ -24422,22 +24493,52 @@ fn cgalloc(c: *cgen, n: *node) void = { fn cgappend(c: *cgen, n: *node) void = { let sn: *node = n.list; if (sn == nil) { return; }; - if (sn.kind != nkind.N_IDENT) { return; }; - let snlocal: *local = localfindnode(c, sn.str); - if (snlocal == nil) { return; }; - let sn_off: i32 = snlocal.off; - // #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); + // FA1 (#15): the old `sn.kind != N_IDENT → return` and + // `snlocal == nil → return` gates were SILENT zero-emission + // (gate-blind cs≠ww: cstage 0-defaulted the header base and + // corrupted the caller frame instead). A non-ident-local target + // now resolves its header address through cgplaceaddr; a shape + // the resolver can't address is loud. + let sndirect: bool = false; + let sn_off: i32 = 0; + let snlocal: *local = nil; + if (sn.kind == nkind.N_IDENT) { + snlocal = localfindnode(c, sn.str); + if (snlocal != nil) { + sndirect = true; + sn_off = snlocal.off; + }; + }; + let esz: i32 = 0; let etnode: *node = nil; - if (snlocal.tnode != nil) { - let stk: nkind = snlocal.tnode.kind; - if (stk == nkind.N_TSLICE) { etnode = snlocal.tnode.lhs; }; - if (stk == nkind.N_TARRAY) { etnode = snlocal.tnode.lhs; }; - if (stk == nkind.N_TPTR) { etnode = snlocal.tnode.lhs; }; + let sti: *tinfo = nil; + if (sndirect) { + // #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. + esz = elemsizeofc(c, snlocal.tnode); + if (snlocal.tnode != nil) { + let stk: nkind = snlocal.tnode.kind; + if (stk == nkind.N_TSLICE) { etnode = snlocal.tnode.lhs; }; + if (stk == nkind.N_TARRAY) { etnode = snlocal.tnode.lhs; }; + if (stk == nkind.N_TPTR) { etnode = snlocal.tnode.lhs; }; + sti = snlocal.tnode.type_: *tinfo; + }; + } else { + // FA1: `*p` has no declared tnode — key esz/element kind off + // the checker-STAMPED target tinfo (#209/#211 discipline), the + // same source cstage reads (sn->type → su->sub->size). + sti = sn.type_: *tinfo; + let fsti: *tinfo = sti; + for (fsti != nil && fsti.kind == tykind.TY_NAMED) { fsti = fsti.under; }; + if (fsti != nil && fsti.sub != nil) { esz = fsti.sub.size: i32; }; + if (esz <= 0) { + let m15z: str = "#15: append() target element size unresolved (rule-7)\n"; + os.write(2, m15z.ptr, m15z.len: u64); + os.exit(1); + }; }; let store_op: str = tnodestoreop(c, etnode, esz); // #34 element-kind store dispatch: the scalar 1-word store below @@ -24446,11 +24547,13 @@ fn cgappend(c: *cgen, n: *node) void = { // 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; }; + // FA1: pre-peel handle — the indirect struct-lit fill keys its + // structinfo off the NAMED element tinfo's name (the same leaf + // structlookupchain resolves from the declared tnode). + let esubnamed: *tinfo = esubti; for (esubti != nil && esubti.kind == tykind.TY_NAMED) { esubti = esubti.under; }; @@ -24464,6 +24567,26 @@ fn cgappend(c: *cgen, n: *node) void = { os.write(2, m34k.ptr, m34k.len: u64); os.exit(1); }; + let snscr: i32 = 0; + if (!sndirect) { + if (!cgplaceaddr(c, sn, "BX")) { + let m15p: str = "#15: append() target place unsupported (rule-7)\n"; + os.write(2, m15p.ptr, m15p.len: u64); + os.exit(1); + }; + // Spill across rt_ensure: realloc moves .ptr, never the + // header, so the slot stays valid for every later reload. + // Fresh slot per SITE via localalloc (NOT localadd: its `@` + // dedup would share one slot per fn, and a nested + // append-through-pointer inside a value expression — + // match-yield arm — would clobber the outer's spilled header + // address: silent cross-slice corruption. Mirrors cstage's + // never-deduping local_alloc at the same point.) + snscr = localalloc(c, "@apphdrscr", 8, nil); + emitline("\tMOVQ\tBX, "); + emitoff(snscr: i64); + emitline("(BP)\n"); + }; let vn: *node = sn.next; for (vn != nil) { @@ -24487,6 +24610,12 @@ fn cgappend(c: *cgen, n: *node) void = { }; let it_off: i32 = itlocal.off; let load_op: str = tnodeloadop(c, etnode, esz); + if (!sndirect) { + // FA1: no etnode behind `*p` — signedness off the + // stamped element tinfo, the predicate cstage's + // fldloadop applies to su->sub. + load_op = loadopsz(typeissigned(esubti), esz); + }; emitline("\tSUBQ\t$8, SP\n"); emitline("\tMOVQ\t$0, (SP)\n"); let ll: str = mklabel(c, "spr_l"); @@ -24506,16 +24635,7 @@ fn cgappend(c: *cgen, n: *node) void = { // 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); emitline("\tMOVQ\t(SP), CX\n"); if (esz > 1) { emitline("\tMOVQ\t$"); @@ -24527,20 +24647,7 @@ fn cgappend(c: *cgen, n: *node) void = { 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"); + cgappendslot(c, sndirect, sn_off, snscr, esz, "DX"); let wk: i32 = 0; for (wk + 8 <= esz) { emitline("\tMOVQ\t"); @@ -24597,30 +24704,8 @@ fn cgappend(c: *cgen, n: *node) void = { emitline("\tADDQ\tCX, BX\n"); emitline("\t"); emitline(load_op); emitline("\t(BX), AX\n"); emitline("\tPUSHQ\tAX\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"); - 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); emitline("\tPOPQ\tAX\n"); emitline("\t"); emitline(store_op); emitline("\tAX, (BX)\n"); emitline("\tADDQ\t$1, (SP)\n"); @@ -24639,28 +24724,8 @@ fn cgappend(c: *cgen, n: *node) void = { 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "DX"); emitline("\tPOPQ\tCX\n"); emitline("\tPOPQ\tBX\n"); emitline("\tPOPQ\tAX\n"); @@ -24675,30 +24740,8 @@ fn cgappend(c: *cgen, n: *node) void = { // 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); if (eltagged) { cgwidentaggedstore(c, esubti, vn, "BX", 0, esz); vn = vn.next; @@ -24710,6 +24753,14 @@ fn cgappend(c: *cgen, n: *node) void = { emitoff(scroff: i64); emitline("(BP)\n"); let esi: *structinfo = structlookupchain(c, etnode); + if (esi == nil && !sndirect && esubnamed != nil) { + // FA1: no declared tnode to chain through — + // the stamped NAMED element tinfo carries the + // same leaf structlookupchain would resolve. + if (esubnamed.kind == tykind.TY_NAMED) { + esi = structlookup(c, esubnamed.name); + }; + }; if (esi == nil) { let m34s: str = "#34: append() struct element has no structinfo (rule-7)\n"; os.write(2, m34s.ptr, m34s.len: u64); @@ -24773,30 +24824,8 @@ fn cgappend(c: *cgen, n: *node) void = { }; cgexpr(c, vn); emitline("\tPUSHQ\tAX\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"); - 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); emitline("\tPOPQ\tAX\n"); emitline("\t"); emitline(store_op); emitline("\tAX, (BX)\n"); vn = vn.next; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 5a384ef3..563f262c 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -4324,6 +4324,77 @@ fn cgalloc(c: *cgen, n: *node) void = { emitlabel(donel); }; +// cgappendgrow — FA1 (#15): append() header-place grow, cgplaceaddr's +// append consumer. direct = ident-local header in the frame (BP-disp — +// the legacy emission, kept byte-identical); indirect = header address +// pre-spilled to @apphdrscr by the resolver. len+=1, &hdr→DI, esz→SI, +// CALL rt_ensure. In indirect mode the len bump goes through DI so the +// loaded address doubles as the call argument. Mirrors cstage +// cg_append_grow. +fn cgappendgrow(c: *cgen, direct: bool, off: i32, scr: i32, esz: i32) void = { + if (direct) { + emitline("\tADDQ\t$1, "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + emitline("\tLEAQ\t"); + emitoff(off: i64); + emitline("(BP), DI\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(scr: i64); + emitline("(BP), DI\n"); + emitline("\tADDQ\t$1, 8(DI)\n"); + }; + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", SI\n"); + emitline("\tCALL\trt_ensure(SB)\n"); +}; + +// cgappendslot — post-rt_ensure slot address: CX = (len-1)*esz, +// dst = .ptr + CX. Clobbers AX (the IMUL immediate) and CX, like the +// emission it replaces; dst must not be AX or CX. Mirrors cstage +// cg_append_slot. +fn cgappendslot(c: *cgen, direct: bool, off: i32, scr: i32, esz: i32, dst: str) void = { + if (direct) { + emitline("\tMOVQ\t"); + emitoff((off + 8): i64); + emitline("(BP), CX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(scr: i64); + emitline("(BP), "); + emitline(dst); + emitline("\n"); + emitline("\tMOVQ\t8("); + emitline(dst); + emitline("), 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"); + }; + if (direct) { + emitline("\tMOVQ\t"); + emitoff(off: i64); + emitline("(BP), "); + emitline(dst); + emitline("\n"); + } else { + emitline("\tMOVQ\t("); + emitline(dst); + emitline("), "); + emitline(dst); + emitline("\n"); + }; + emitline("\tADDQ\tCX, "); + emitline(dst); + emitline("\n"); +}; + // cgappend — Hare-style `append(s, v)` / `append(s, items...)` lowering. // Mirrors cmd/w6c/cgen.c's N_CALL append branch (rt::ensure model). // Each value gets: @@ -4344,22 +4415,52 @@ fn cgalloc(c: *cgen, n: *node) void = { fn cgappend(c: *cgen, n: *node) void = { let sn: *node = n.list; if (sn == nil) { return; }; - if (sn.kind != nkind.N_IDENT) { return; }; - let snlocal: *local = localfindnode(c, sn.str); - if (snlocal == nil) { return; }; - let sn_off: i32 = snlocal.off; - // #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); + // FA1 (#15): the old `sn.kind != N_IDENT → return` and + // `snlocal == nil → return` gates were SILENT zero-emission + // (gate-blind cs≠ww: cstage 0-defaulted the header base and + // corrupted the caller frame instead). A non-ident-local target + // now resolves its header address through cgplaceaddr; a shape + // the resolver can't address is loud. + let sndirect: bool = false; + let sn_off: i32 = 0; + let snlocal: *local = nil; + if (sn.kind == nkind.N_IDENT) { + snlocal = localfindnode(c, sn.str); + if (snlocal != nil) { + sndirect = true; + sn_off = snlocal.off; + }; + }; + let esz: i32 = 0; let etnode: *node = nil; - if (snlocal.tnode != nil) { - let stk: nkind = snlocal.tnode.kind; - if (stk == nkind.N_TSLICE) { etnode = snlocal.tnode.lhs; }; - if (stk == nkind.N_TARRAY) { etnode = snlocal.tnode.lhs; }; - if (stk == nkind.N_TPTR) { etnode = snlocal.tnode.lhs; }; + let sti: *tinfo = nil; + if (sndirect) { + // #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. + esz = elemsizeofc(c, snlocal.tnode); + if (snlocal.tnode != nil) { + let stk: nkind = snlocal.tnode.kind; + if (stk == nkind.N_TSLICE) { etnode = snlocal.tnode.lhs; }; + if (stk == nkind.N_TARRAY) { etnode = snlocal.tnode.lhs; }; + if (stk == nkind.N_TPTR) { etnode = snlocal.tnode.lhs; }; + sti = snlocal.tnode.type_: *tinfo; + }; + } else { + // FA1: `*p` has no declared tnode — key esz/element kind off + // the checker-STAMPED target tinfo (#209/#211 discipline), the + // same source cstage reads (sn->type → su->sub->size). + sti = sn.type_: *tinfo; + let fsti: *tinfo = sti; + for (fsti != nil && fsti.kind == tykind.TY_NAMED) { fsti = fsti.under; }; + if (fsti != nil && fsti.sub != nil) { esz = fsti.sub.size: i32; }; + if (esz <= 0) { + let m15z: str = "#15: append() target element size unresolved (rule-7)\n"; + os.write(2, m15z.ptr, m15z.len: u64); + os.exit(1); + }; }; let store_op: str = tnodestoreop(c, etnode, esz); // #34 element-kind store dispatch: the scalar 1-word store below @@ -4368,11 +4469,13 @@ fn cgappend(c: *cgen, n: *node) void = { // 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; }; + // FA1: pre-peel handle — the indirect struct-lit fill keys its + // structinfo off the NAMED element tinfo's name (the same leaf + // structlookupchain resolves from the declared tnode). + let esubnamed: *tinfo = esubti; for (esubti != nil && esubti.kind == tykind.TY_NAMED) { esubti = esubti.under; }; @@ -4386,6 +4489,26 @@ fn cgappend(c: *cgen, n: *node) void = { os.write(2, m34k.ptr, m34k.len: u64); os.exit(1); }; + let snscr: i32 = 0; + if (!sndirect) { + if (!cgplaceaddr(c, sn, "BX")) { + let m15p: str = "#15: append() target place unsupported (rule-7)\n"; + os.write(2, m15p.ptr, m15p.len: u64); + os.exit(1); + }; + // Spill across rt_ensure: realloc moves .ptr, never the + // header, so the slot stays valid for every later reload. + // Fresh slot per SITE via localalloc (NOT localadd: its `@` + // dedup would share one slot per fn, and a nested + // append-through-pointer inside a value expression — + // match-yield arm — would clobber the outer's spilled header + // address: silent cross-slice corruption. Mirrors cstage's + // never-deduping local_alloc at the same point.) + snscr = localalloc(c, "@apphdrscr", 8, nil); + emitline("\tMOVQ\tBX, "); + emitoff(snscr: i64); + emitline("(BP)\n"); + }; let vn: *node = sn.next; for (vn != nil) { @@ -4409,6 +4532,12 @@ fn cgappend(c: *cgen, n: *node) void = { }; let it_off: i32 = itlocal.off; let load_op: str = tnodeloadop(c, etnode, esz); + if (!sndirect) { + // FA1: no etnode behind `*p` — signedness off the + // stamped element tinfo, the predicate cstage's + // fldloadop applies to su->sub. + load_op = loadopsz(typeissigned(esubti), esz); + }; emitline("\tSUBQ\t$8, SP\n"); emitline("\tMOVQ\t$0, (SP)\n"); let ll: str = mklabel(c, "spr_l"); @@ -4428,16 +4557,7 @@ fn cgappend(c: *cgen, n: *node) void = { // 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); emitline("\tMOVQ\t(SP), CX\n"); if (esz > 1) { emitline("\tMOVQ\t$"); @@ -4449,20 +4569,7 @@ fn cgappend(c: *cgen, n: *node) void = { 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"); + cgappendslot(c, sndirect, sn_off, snscr, esz, "DX"); let wk: i32 = 0; for (wk + 8 <= esz) { emitline("\tMOVQ\t"); @@ -4519,30 +4626,8 @@ fn cgappend(c: *cgen, n: *node) void = { emitline("\tADDQ\tCX, BX\n"); emitline("\t"); emitline(load_op); emitline("\t(BX), AX\n"); emitline("\tPUSHQ\tAX\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"); - 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); emitline("\tPOPQ\tAX\n"); emitline("\t"); emitline(store_op); emitline("\tAX, (BX)\n"); emitline("\tADDQ\t$1, (SP)\n"); @@ -4561,28 +4646,8 @@ fn cgappend(c: *cgen, n: *node) void = { 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "DX"); emitline("\tPOPQ\tCX\n"); emitline("\tPOPQ\tBX\n"); emitline("\tPOPQ\tAX\n"); @@ -4597,30 +4662,8 @@ fn cgappend(c: *cgen, n: *node) void = { // 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); if (eltagged) { cgwidentaggedstore(c, esubti, vn, "BX", 0, esz); vn = vn.next; @@ -4632,6 +4675,14 @@ fn cgappend(c: *cgen, n: *node) void = { emitoff(scroff: i64); emitline("(BP)\n"); let esi: *structinfo = structlookupchain(c, etnode); + if (esi == nil && !sndirect && esubnamed != nil) { + // FA1: no declared tnode to chain through — + // the stamped NAMED element tinfo carries the + // same leaf structlookupchain would resolve. + if (esubnamed.kind == tykind.TY_NAMED) { + esi = structlookup(c, esubnamed.name); + }; + }; if (esi == nil) { let m34s: str = "#34: append() struct element has no structinfo (rule-7)\n"; os.write(2, m34s.ptr, m34s.len: u64); @@ -4695,30 +4746,8 @@ fn cgappend(c: *cgen, n: *node) void = { }; cgexpr(c, vn); emitline("\tPUSHQ\tAX\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"); - 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); emitline("\tPOPQ\tAX\n"); emitline("\t"); emitline(store_op); emitline("\tAX, (BX)\n"); vn = vn.next; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index f157d3f4..aecd5cb3 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -24402,6 +24402,77 @@ fn cgalloc(c: *cgen, n: *node) void = { emitlabel(donel); }; +// cgappendgrow — FA1 (#15): append() header-place grow, cgplaceaddr's +// append consumer. direct = ident-local header in the frame (BP-disp — +// the legacy emission, kept byte-identical); indirect = header address +// pre-spilled to @apphdrscr by the resolver. len+=1, &hdr→DI, esz→SI, +// CALL rt_ensure. In indirect mode the len bump goes through DI so the +// loaded address doubles as the call argument. Mirrors cstage +// cg_append_grow. +fn cgappendgrow(c: *cgen, direct: bool, off: i32, scr: i32, esz: i32) void = { + if (direct) { + emitline("\tADDQ\t$1, "); + emitoff((off + 8): i64); + emitline("(BP)\n"); + emitline("\tLEAQ\t"); + emitoff(off: i64); + emitline("(BP), DI\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(scr: i64); + emitline("(BP), DI\n"); + emitline("\tADDQ\t$1, 8(DI)\n"); + }; + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", SI\n"); + emitline("\tCALL\trt_ensure(SB)\n"); +}; + +// cgappendslot — post-rt_ensure slot address: CX = (len-1)*esz, +// dst = .ptr + CX. Clobbers AX (the IMUL immediate) and CX, like the +// emission it replaces; dst must not be AX or CX. Mirrors cstage +// cg_append_slot. +fn cgappendslot(c: *cgen, direct: bool, off: i32, scr: i32, esz: i32, dst: str) void = { + if (direct) { + emitline("\tMOVQ\t"); + emitoff((off + 8): i64); + emitline("(BP), CX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(scr: i64); + emitline("(BP), "); + emitline(dst); + emitline("\n"); + emitline("\tMOVQ\t8("); + emitline(dst); + emitline("), 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"); + }; + if (direct) { + emitline("\tMOVQ\t"); + emitoff(off: i64); + emitline("(BP), "); + emitline(dst); + emitline("\n"); + } else { + emitline("\tMOVQ\t("); + emitline(dst); + emitline("), "); + emitline(dst); + emitline("\n"); + }; + emitline("\tADDQ\tCX, "); + emitline(dst); + emitline("\n"); +}; + // cgappend — Hare-style `append(s, v)` / `append(s, items...)` lowering. // Mirrors cmd/w6c/cgen.c's N_CALL append branch (rt::ensure model). // Each value gets: @@ -24422,22 +24493,52 @@ fn cgalloc(c: *cgen, n: *node) void = { fn cgappend(c: *cgen, n: *node) void = { let sn: *node = n.list; if (sn == nil) { return; }; - if (sn.kind != nkind.N_IDENT) { return; }; - let snlocal: *local = localfindnode(c, sn.str); - if (snlocal == nil) { return; }; - let sn_off: i32 = snlocal.off; - // #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); + // FA1 (#15): the old `sn.kind != N_IDENT → return` and + // `snlocal == nil → return` gates were SILENT zero-emission + // (gate-blind cs≠ww: cstage 0-defaulted the header base and + // corrupted the caller frame instead). A non-ident-local target + // now resolves its header address through cgplaceaddr; a shape + // the resolver can't address is loud. + let sndirect: bool = false; + let sn_off: i32 = 0; + let snlocal: *local = nil; + if (sn.kind == nkind.N_IDENT) { + snlocal = localfindnode(c, sn.str); + if (snlocal != nil) { + sndirect = true; + sn_off = snlocal.off; + }; + }; + let esz: i32 = 0; let etnode: *node = nil; - if (snlocal.tnode != nil) { - let stk: nkind = snlocal.tnode.kind; - if (stk == nkind.N_TSLICE) { etnode = snlocal.tnode.lhs; }; - if (stk == nkind.N_TARRAY) { etnode = snlocal.tnode.lhs; }; - if (stk == nkind.N_TPTR) { etnode = snlocal.tnode.lhs; }; + let sti: *tinfo = nil; + if (sndirect) { + // #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. + esz = elemsizeofc(c, snlocal.tnode); + if (snlocal.tnode != nil) { + let stk: nkind = snlocal.tnode.kind; + if (stk == nkind.N_TSLICE) { etnode = snlocal.tnode.lhs; }; + if (stk == nkind.N_TARRAY) { etnode = snlocal.tnode.lhs; }; + if (stk == nkind.N_TPTR) { etnode = snlocal.tnode.lhs; }; + sti = snlocal.tnode.type_: *tinfo; + }; + } else { + // FA1: `*p` has no declared tnode — key esz/element kind off + // the checker-STAMPED target tinfo (#209/#211 discipline), the + // same source cstage reads (sn->type → su->sub->size). + sti = sn.type_: *tinfo; + let fsti: *tinfo = sti; + for (fsti != nil && fsti.kind == tykind.TY_NAMED) { fsti = fsti.under; }; + if (fsti != nil && fsti.sub != nil) { esz = fsti.sub.size: i32; }; + if (esz <= 0) { + let m15z: str = "#15: append() target element size unresolved (rule-7)\n"; + os.write(2, m15z.ptr, m15z.len: u64); + os.exit(1); + }; }; let store_op: str = tnodestoreop(c, etnode, esz); // #34 element-kind store dispatch: the scalar 1-word store below @@ -24446,11 +24547,13 @@ fn cgappend(c: *cgen, n: *node) void = { // 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; }; + // FA1: pre-peel handle — the indirect struct-lit fill keys its + // structinfo off the NAMED element tinfo's name (the same leaf + // structlookupchain resolves from the declared tnode). + let esubnamed: *tinfo = esubti; for (esubti != nil && esubti.kind == tykind.TY_NAMED) { esubti = esubti.under; }; @@ -24464,6 +24567,26 @@ fn cgappend(c: *cgen, n: *node) void = { os.write(2, m34k.ptr, m34k.len: u64); os.exit(1); }; + let snscr: i32 = 0; + if (!sndirect) { + if (!cgplaceaddr(c, sn, "BX")) { + let m15p: str = "#15: append() target place unsupported (rule-7)\n"; + os.write(2, m15p.ptr, m15p.len: u64); + os.exit(1); + }; + // Spill across rt_ensure: realloc moves .ptr, never the + // header, so the slot stays valid for every later reload. + // Fresh slot per SITE via localalloc (NOT localadd: its `@` + // dedup would share one slot per fn, and a nested + // append-through-pointer inside a value expression — + // match-yield arm — would clobber the outer's spilled header + // address: silent cross-slice corruption. Mirrors cstage's + // never-deduping local_alloc at the same point.) + snscr = localalloc(c, "@apphdrscr", 8, nil); + emitline("\tMOVQ\tBX, "); + emitoff(snscr: i64); + emitline("(BP)\n"); + }; let vn: *node = sn.next; for (vn != nil) { @@ -24487,6 +24610,12 @@ fn cgappend(c: *cgen, n: *node) void = { }; let it_off: i32 = itlocal.off; let load_op: str = tnodeloadop(c, etnode, esz); + if (!sndirect) { + // FA1: no etnode behind `*p` — signedness off the + // stamped element tinfo, the predicate cstage's + // fldloadop applies to su->sub. + load_op = loadopsz(typeissigned(esubti), esz); + }; emitline("\tSUBQ\t$8, SP\n"); emitline("\tMOVQ\t$0, (SP)\n"); let ll: str = mklabel(c, "spr_l"); @@ -24506,16 +24635,7 @@ fn cgappend(c: *cgen, n: *node) void = { // 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); emitline("\tMOVQ\t(SP), CX\n"); if (esz > 1) { emitline("\tMOVQ\t$"); @@ -24527,20 +24647,7 @@ fn cgappend(c: *cgen, n: *node) void = { 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"); + cgappendslot(c, sndirect, sn_off, snscr, esz, "DX"); let wk: i32 = 0; for (wk + 8 <= esz) { emitline("\tMOVQ\t"); @@ -24597,30 +24704,8 @@ fn cgappend(c: *cgen, n: *node) void = { emitline("\tADDQ\tCX, BX\n"); emitline("\t"); emitline(load_op); emitline("\t(BX), AX\n"); emitline("\tPUSHQ\tAX\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"); - 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); emitline("\tPOPQ\tAX\n"); emitline("\t"); emitline(store_op); emitline("\tAX, (BX)\n"); emitline("\tADDQ\t$1, (SP)\n"); @@ -24639,28 +24724,8 @@ fn cgappend(c: *cgen, n: *node) void = { 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "DX"); emitline("\tPOPQ\tCX\n"); emitline("\tPOPQ\tBX\n"); emitline("\tPOPQ\tAX\n"); @@ -24675,30 +24740,8 @@ fn cgappend(c: *cgen, n: *node) void = { // 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); if (eltagged) { cgwidentaggedstore(c, esubti, vn, "BX", 0, esz); vn = vn.next; @@ -24710,6 +24753,14 @@ fn cgappend(c: *cgen, n: *node) void = { emitoff(scroff: i64); emitline("(BP)\n"); let esi: *structinfo = structlookupchain(c, etnode); + if (esi == nil && !sndirect && esubnamed != nil) { + // FA1: no declared tnode to chain through — + // the stamped NAMED element tinfo carries the + // same leaf structlookupchain would resolve. + if (esubnamed.kind == tykind.TY_NAMED) { + esi = structlookup(c, esubnamed.name); + }; + }; if (esi == nil) { let m34s: str = "#34: append() struct element has no structinfo (rule-7)\n"; os.write(2, m34s.ptr, m34s.len: u64); @@ -24773,30 +24824,8 @@ fn cgappend(c: *cgen, n: *node) void = { }; cgexpr(c, vn); emitline("\tPUSHQ\tAX\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"); - 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"); + cgappendgrow(c, sndirect, sn_off, snscr, esz); + cgappendslot(c, sndirect, sn_off, snscr, esz, "BX"); emitline("\tPOPQ\tAX\n"); emitline("\t"); emitline(store_op); emitline("\tAX, (BX)\n"); vn = vn.next; diff --git a/test/wcc/806_append_place.c b/test/wcc/806_append_place.c new file mode 100644 index 00000000..40c29e44 --- /dev/null +++ b/test/wcc/806_append_place.c @@ -0,0 +1,587 @@ +/* + * 806_append_place — cstage and wwstage agree, byte-for-byte and at + * runtime, that `append(*p, v)` / `append(*p, items...)` through a + * pointer-to-slice place lands the element (FA1, task #15 of the regex + * fold-2b blockers; the add_thread hot shape `threads: *[]thread`). + * + * Pre-fix this was ONE mirrored choke-point with TWO failure modes: + * cstage 0-defaulted the header base for any non-ident target + * (`sn_off = (sn->kind==N_IDENT) ? localfind : 0`), so 0(BP)/8(BP) + * became the "slice header" and rt_ensure corrupted the CALLER frame + * (SIGSEGV); wwstage cgappend silently emitted NOTHING (gate-blind + * cs≠ww). The fix re-keys the append lowering from BP-displacement + * assumptions onto a resolver-provided header PLACE (cgplaceaddr's + * third consumer after C1 assign-stores and C1.25 aggregate-field + * stores): the derived header address is spilled to a per-fn + * @apphdrscr slot so it survives rt_ensure (realloc moves .ptr, never + * the header), and every header access reloads from the slot. + * Ident-local targets keep the legacy BP-disp emission byte-identical. + * Any target place the resolver can't address dies LOUD (rule 7) — + * the old silent corruption can't come back through an unwired shape. + * + * row | shape | want + * ---------------------+-----------------------------------------+------ + * scalar_param | append(*p, v) i64 via *[]i64 param | 60 + * scalar_u8_multi | append(*q, a, b) u8 via local ptr | 61 + * spread_param | append(*p, src...) i64 spread | 62 + * spread_narrow_signed | append(*p, src...) i32 (MOVSXD load | 63 + * | keyed off stamped tinfo, no tnode) | + * spread_wide | append(*p, src...) str (24B word-copy) | 64 + * str_elem | append(*p, "hi") 3-word header | 65 + * slice_elem | append(*p, one) [][]u8 3-word header | 66 + * tagged_elem | append(*p, 42) (i64|void) widen-box | 67 + * struct_lit | pA6 verbatim: append(*p, box{...}) | 68 + * struct_ident | append(*p, b) local struct word-copy | 69 + * realloc_loop | 100 appends via param, branched callee, | 70 + * | cap-crossing reallocs, full multi-field | + * | readback + caller-frame sentinels | + * deref_spine | append((*q)[i].xs, v) resolver spine | 71 + * neutral_direct | ident-local appends (the legacy arm — | 72 + * | runtime-pins the asm-neutrality claim) | + * reentrant_value | append(*p, match{.. append(*q,..) ..}) | 33 + * | — nested indirect append inside the | + * | outer's value expr; pins the per-SITE | + * | @apphdrscr slot (a shared slot clobbers)| + * reject_identroot_dot | h.xs via *holder param (resolver ident | BUILD_FAIL + * | root unwired — was silent corruption) | + * reject_spread_src | non-ident spread SOURCE through a deref | BUILD_FAIL + * | target (the #35 designed boundary) | + * + * BUILD_FAIL rows also assert the diagnostic TEXT (stderr substring, + * both stages) — a build that fails for any other reason (parse error, + * crash) is a vacuous reject and fails the row. + * + * Every non-BUILD_FAIL row also asserts cstage/wwstage asm byte-id. + * + * Readbacks deliberately avoid the still-open F2/F5 read shapes + * (`len(xs[i].field)`, `let s = xs[i].field`) — those are tracked + * separately; this test pins the append WRITE path. + */ +#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 AND + * emit expect_err on stderr (rule 7 — never a silent acceptance; + * without the message check a row would pass vacuously on any + * unrelated build failure). */ +#define BUILD_FAIL (-2147483647 - 1) + +struct row { + const char *label; + const char *src; + int want; + const char *expect_err; /* BUILD_FAIL rows: required stderr substring */ +}; + +static const struct row rows[] = { + /* The FA1 minimum: two appends through a *[]i64 param, then a + * third through a local pointer binding. */ + { "scalar_param", + "package main;\n" + "fn add(p: *[]i64, v: i64) void = { append(*p, v); };\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tadd(&xs, 5);\n" + "\tadd(&xs, 9);\n" + "\tif (len(xs) != 2) { return 1; };\n" + "\tif (xs[0] != 5 || xs[1] != 9) { return 2; };\n" + "\tlet q: *[]i64 = &xs;\n" + "\tappend(*q, 11);\n" + "\tif (len(xs) != 3 || xs[2] != 11) { return 3; };\n" + "\treturn 60;\n" + "};\n", + 60, NULL }, + + /* esz=1 store (MOVB) + multi-value list through one resolve. */ + { "scalar_u8_multi", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet bs: []u8 = [];\n" + "\tlet q: *[]u8 = &bs;\n" + "\tappend(*q, 1u8, 2u8);\n" + "\tif (len(bs) != 2) { return 1; };\n" + "\tif (bs[0] != 1u8 || bs[1] != 2u8) { return 2; };\n" + "\treturn 61;\n" + "};\n", + 61, NULL }, + + { "spread_param", + "package main;\n" + "fn addall(p: *[]i64, src: []i64) void = { append(*p, src...); };\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 1, 2);\n" + "\tlet ys: []i64 = [];\n" + "\taddall(&ys, xs);\n" + "\taddall(&ys, xs);\n" + "\tif (len(ys) != 4) { return 1; };\n" + "\tif (ys[0] != 1 || ys[1] != 2 || ys[3] != 2) { return 2; };\n" + "\treturn 62;\n" + "};\n", + 62, NULL }, + + /* Indirect mode has no declared tnode — the spread element LOAD + * op (sign extension) must come off the stamped tinfo; negative + * i32 elements pin MOVSXD. */ + { "spread_narrow_signed", + "package main;\n" + "fn addall(p: *[]i32, src: []i32) void = { append(*p, src...); };\n" + "export fn main() i32 = {\n" + "\tlet xs: []i32 = [];\n" + "\tappend(xs, -7i32, 9i32);\n" + "\tlet ys: []i32 = [];\n" + "\taddall(&ys, xs);\n" + "\tif (len(ys) != 2) { return 1; };\n" + "\tif (ys[0] != -7i32) { return 2; };\n" + "\tif (ys[1] != 9i32) { return 3; };\n" + "\treturn 63;\n" + "};\n", + 63, NULL }, + + /* Wide (24B) spread element word-copy through the deref target. */ + { "spread_wide", + "package main;\n" + "fn addall(p: *[]str, src: []str) void = { append(*p, src...); };\n" + "export fn main() i32 = {\n" + "\tlet ss: []str = [];\n" + "\tappend(ss, \"ab\");\n" + "\tappend(ss, \"cde\");\n" + "\tlet tt: []str = [];\n" + "\taddall(&tt, ss);\n" + "\tif (len(tt) != 2) { return 1; };\n" + "\tlet a: str = tt[0];\n" + "\tlet b: str = tt[1];\n" + "\tif (a.len != 2 || b.len != 3) { return 2; };\n" + "\tif (b[0] != 'c') { return 3; };\n" + "\treturn 64;\n" + "};\n", + 64, NULL }, + + /* str element: cgexpr leaves {ptr,len,cap} in AX/BX/CX; all three + * must survive rt_ensure AND the indirect header reload. */ + { "str_elem", + "package main;\n" + "fn adds(p: *[]str, v: str) void = { append(*p, v); };\n" + "export fn main() i32 = {\n" + "\tlet ss: []str = [];\n" + "\tadds(&ss, \"hi\");\n" + "\tadds(&ss, \"world\");\n" + "\tif (len(ss) != 2) { return 1; };\n" + "\tlet a: str = ss[0];\n" + "\tlet b: str = ss[1];\n" + "\tif (a.len != 2 || b.len != 5) { return 2; };\n" + "\tif (a[0] != 'h' || b[0] != 'w') { return 3; };\n" + "\treturn 65;\n" + "};\n", + 65, NULL }, + + { "slice_elem", + "package main;\n" + "fn addv(p: *[][]u8, v: []u8) void = { append(*p, v); };\n" + "export fn main() i32 = {\n" + "\tlet vv: [][]u8 = [];\n" + "\tlet one: []u8 = [];\n" + "\tappend(one, 5u8, 6u8);\n" + "\taddv(&vv, one);\n" + "\tif (len(vv) != 1) { return 1; };\n" + "\tlet got: []u8 = vv[0];\n" + "\tif (len(got) != 2 || got[1] != 6u8) { return 2; };\n" + "\treturn 66;\n" + "};\n", + 66, NULL }, + + /* Tagged element: the #12 widen choke-point boxes through the + * resolver-derived slot pointer (grow-first, no register form). */ + { "tagged_elem", + "package main;\n" + "type tu = (i64 | void);\n" + "fn addt(p: *[]tu, v: i64) void = { append(*p, v); };\n" + "export fn main() i32 = {\n" + "\tlet ts: []tu = [];\n" + "\taddt(&ts, 42);\n" + "\tif (len(ts) != 1) { return 1; };\n" + "\tlet t0: tu = ts[0];\n" + "\tif (!(t0 is i64)) { return 2; };\n" + "\treturn 67;\n" + "};\n", + 67, NULL }, + + /* The pA6 repro, verbatim shapes: struct-literal element fill + * through @appendscr while the header address sits in @apphdrscr. */ + { "struct_lit", + "package main;\n" + "type box = struct { pc: size, matched: bool };\n" + "fn add(p: *[]box, v: size) void = {\n" + "\tappend(*p, box { pc = v, matched = false });\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet bs: []box = [];\n" + "\tadd(&bs, 5);\n" + "\tadd(&bs, 9);\n" + "\tif (len(bs) != 2) { return 1; };\n" + "\tif (bs[0].pc != 5) { return 2; };\n" + "\tif (bs[1].pc != 9) { return 3; };\n" + "\treturn 68;\n" + "};\n", + 68, NULL }, + + { "struct_ident", + "package main;\n" + "type box = struct { pc: size, matched: bool };\n" + "fn add(p: *[]box, v: size) void = {\n" + "\tlet b: box = box { pc = v, matched = true };\n" + "\tappend(*p, b);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet bs: []box = [];\n" + "\tadd(&bs, 7);\n" + "\tif (len(bs) != 1) { return 1; };\n" + "\tif (bs[0].pc != 7) { return 2; };\n" + "\tif (!bs[0].matched) { return 3; };\n" + "\treturn 69;\n" + "};\n", + 69, NULL }, + + /* The corruption symptom row: 100 appends through the param in a + * loop with a branched callee — crosses several cap-doubling + * reallocs. Sentinels on BOTH sides of the slice local pin the + * caller frame (pre-fix cstage incremented 8(BP) and passed (BP) + * to rt_ensure → caller-frame corruption); the full readback pins + * every element across the realloc moves. */ + { "realloc_loop", + "package main;\n" + "type box = struct { pc: size, matched: bool };\n" + "fn add(p: *[]box, v: size) void = {\n" + "\tif (v % 2 == 0) {\n" + "\t\tappend(*p, box { pc = v, matched = true });\n" + "\t} else {\n" + "\t\tappend(*p, box { pc = v, matched = false });\n" + "\t};\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet lo: i64 = 0x5151;\n" + "\tlet bs: []box = [];\n" + "\tlet hi: i64 = 0x7272;\n" + "\tlet i: size = 0;\n" + "\tfor (i < 100) {\n" + "\t\tadd(&bs, i);\n" + "\t\ti += 1;\n" + "\t};\n" + "\tif (len(bs) != 100) { return 1; };\n" + "\tlet j: size = 0;\n" + "\tfor (j < 100) {\n" + "\t\tif (bs[j].pc != j) { return 2; };\n" + "\t\tif (bs[j].matched != (j % 2 == 0)) { return 3; };\n" + "\t\tj += 1;\n" + "\t};\n" + "\tif (lo != 0x5151 || hi != 0x7272) { return 4; };\n" + "\treturn 70;\n" + "};\n", + 70, NULL }, + + /* Deeper resolver spine: the header is a slice FIELD of an element + * behind a deref ((*q)[i].xs — N_DOT over N_INDEX over N_UN). + * Readback via a *holder ptr-dot, a sound read path. */ + { "deref_spine", + "package main;\n" + "type holder = struct { tag: i64, xs: []i64 };\n" + "fn addspine(q: *[]holder, i: i64, v: i64) void = {\n" + "\tappend((*q)[i].xs, v);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet hs: []holder = [];\n" + "\tlet h0: holder = holder { tag = 1, xs = [] };\n" + "\tappend(hs, h0);\n" + "\taddspine(&hs, 0, 41);\n" + "\taddspine(&hs, 0, 43);\n" + "\tlet hp: *holder = &hs[0];\n" + "\tlet g: []i64 = hp.xs;\n" + "\tif (len(g) != 2) { return 1; };\n" + "\tif (g[0] != 41 || g[1] != 43) { return 2; };\n" + "\tif (hp.tag != 1) { return 3; };\n" + "\treturn 71;\n" + "};\n", + 71, NULL }, + + /* Ident-local targets stay with the legacy BP-disp emission (the + * resolver must never fire for them) — this row runtime-pins the + * shapes the before/after asm sweep diffed statically. */ + { "neutral_direct", + "package main;\n" + "type box = struct { pc: size, matched: bool };\n" + "export fn main() i32 = {\n" + "\tlet xs: []i64 = [];\n" + "\tappend(xs, 7);\n" + "\tappend(xs, 8, 9);\n" + "\tlet ys: []i64 = [];\n" + "\tappend(ys, xs...);\n" + "\tlet ss: []str = [];\n" + "\tappend(ss, \"hi\");\n" + "\tlet ps: []box = [];\n" + "\tappend(ps, box { pc = 1, matched = false });\n" + "\tif (len(xs) != 3 || xs[2] != 9) { return 1; };\n" + "\tif (len(ys) != 3 || ys[0] != 7) { return 2; };\n" + "\tif (len(ss) != 1 || len(ps) != 1) { return 3; };\n" + "\treturn 72;\n" + "};\n", + 72, NULL }, + + /* Reentrancy: a nested append-through-pointer inside the outer + * append's VALUE expression (match-yield arm) spills its own + * header address. Pins the per-SITE @apphdrscr slot — a shared + * per-fn slot hands the outer's post-rt_ensure reloads the inner + * target's header and the outer element lands in the wrong + * slice (silent, both counts wrong). */ + { "reentrant_value", + "package main;\n" + "type tu = (i64 | void);\n" + "fn nest(p: *[]i64, q: *[]i64, t: tu) void = {\n" + "\tappend(*p, match (t) {\n" + "\tcase let x: i64 => {\n" + "\t\tappend(*q, 500);\n" + "\t\tyield x;\n" + "\t};\n" + "\tcase void => {\n" + "\t\tyield 0;\n" + "\t};\n" + "\t});\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet a: []i64 = [];\n" + "\tlet b: []i64 = [];\n" + "\tlet t: tu = 7;\n" + "\tnest(&a, &b, t);\n" + "\tif (len(a) != 1) { return 1; };\n" + "\tif (len(b) != 1) { return 2; };\n" + "\tif (a[0] != 7) { return 3; };\n" + "\tif (b[0] != 500) { return 4; };\n" + "\treturn 33;\n" + "};\n", + 33, NULL }, + + /* Ident-rooted dot target (h.xs through *holder): the resolver + * does not wire ident roots (they belong to the enumerated arms; + * none exists for append yet) — pre-fix this shape silently + * corrupted the frame in cstage; now it must die LOUD. */ + { "reject_identroot_dot", + "package main;\n" + "type holder = struct { tag: i64, xs: []i64 };\n" + "fn addfield(h: *holder, v: i64) void = { append(h.xs, v); };\n" + "export fn main() i32 = {\n" + "\tlet hl: holder = holder { tag = 2, xs = [] };\n" + "\taddfield(&hl, 9);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "#15: append() target place unsupported (rule-7)" }, + + /* The FA4 designed boundary (task #35, old #37): a spread SOURCE that is + * not an ident local must stay loud even now that the deref + * TARGET resolves. */ + { "reject_spread_src", + "package main;\n" + "type holder = struct { tag: i64, xs: []i64 };\n" + "fn dup(p: *[]i64, q: *[]holder, i: i64) void = {\n" + "\tappend(*p, (*q)[i].xs...);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet ys: []i64 = [];\n" + "\tlet hs: []holder = [];\n" + "\tdup(&ys, &hs, 0);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "#34: append() spread source shape unsupported (rule-7)" }, +}; + +/* errlog_has — the build-failure stderr must carry the row's expected + * diagnostic; any other failure (parse error, crash) is a vacuous + * reject and must not pass. */ +static int +errlog_has(const char *path, const char *needle) +{ + FILE *f = fopen(path, "rb"); + if (!f) return 0; + char buf[8192]; + size_t got = fread(buf, 1, sizeof buf - 1, f); + fclose(f); + buf[got] = '\0'; + return strstr(buf, needle) != NULL; +} + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], errlog[80], cmd[1200]; + snprintf(src, sizeof src, "/tmp/applp_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/applp_%d_d_%d", getpid(), i); + snprintf(errlog, sizeof errlog, "%s.err", src); + + 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>%s", + tmpdir, driver, src, errlog); + if (runwait(cmd) != 0) { + int rc = -1; + if (r->want != BUILD_FAIL) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + } else if (r->expect_err && + !errlog_has(errlog, r->expect_err)) { + fprintf(stderr, "row[%s]: %s build failed without " + "expected diagnostic \"%s\"\n", + r->label, driver, r->expect_err); + rc = -3; /* failed, but for the wrong reason */ + } + unlink(src); unlink(errlog); rmdir(tmpdir); + return rc; + } + + 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(errlog); unlink(outbin); rmdir(tmpdir); + return got; +} + +/* asm_byte_identical — generate .s via cstage's w6c and wwstage's + * w6c_ww and diff. The header-place emission is written fresh on both + * sides, so this is the converged-by-construction gate: any drift in + * the resolve/spill/reload sequence shows here. */ +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/applp_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/applp_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/applp_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[2080]; + 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[2120]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[2120]; + 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_place: 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_place[%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_place: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("append_place: %d fixtures passed\n", total); + return 0; +}