From e3e6b5a820a6765f30945fd12049d75342ba8198 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 09:09:56 +0900 Subject: [PATCH] =?UTF-8?q?wcc+w6c=5Fww:=20cgplaceaddr=20resolver=20?= =?UTF-8?q?=E2=80=94=20deref-base=20assign=20stores=20(F6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (*ts)[i].field = v / OP= v (the regex run_thread hot shape, task #4) compiled to NOTHING in both stages, byte-identically: the N_DOT lhs roots at N_UN(STAR), so the arr[i].field arm (idxbase must be IDENT) and the chained-ptr-field arm (base must be *struct) both miss and the N_ASSIGN dispatch fell off the switch silently, rhs unevaluated. cgplaceaddr (one per stage) is ADDRESS COMPUTATION ONLY — N_UN(STAR) root, N_INDEX hop over a slice/array place (.ptr hop for slice), N_DOT struct-field hop with one deref for a *struct base. Call-sites keep their own emission: scalar fldstoreop store, str/slice 3-word header store staged through DX, 10-op compound template with the chained-ptr-field register roles. Ident-rooted spines stay with the enumerated arms — verified asm-neutral over the 84 fold2b probe sources against fresh master-HEAD binaries (7 diffs = the F6 family now emitting stores; 2 verdict flips = aggregate-field stores, now loud). Silent dispatch tails go LOUD for N_DOT lvalues the resolver can't address and for unresolved-identifier targets (cstage float-ident arm aligned to wwstage's resolve-first order). Aggregate-field stores loud-reject pending the follow-up resolver commit (task #23, ≤24B N_CALL rhs split to #24). The non-DOT tail stays silent deliberately: going loud there would asymmetrically surface the pre-existing str-base element-store divergence — task #22, cited at both sites. test/805: 17 rows x 2 drivers + 12 cs==ww byte-id fixtures — widths (incl narrow-compound fldloadop sign/zero-extension), all 10 compound ops (DIVQ/IDIVQ/SHLQ/SARQ/SHRQ), str + slice 3-word stores, *[N]T base, runtime call index, ident-base neutrality pins, and 5 reject rows asserting exact diagnostic text. --- Makefile | 11 + cmd/w6c/cgen.c | 251 ++++++++++- selfhost/cmd/w6c/main.combined.ww | 272 +++++++++++- selfhost/cmd/wcc/cgenexpr.ww | 272 +++++++++++- selfhost/cmd/wwdump/main.combined.ww | 272 +++++++++++- test/wcc/805_placeaddr_store.c | 611 +++++++++++++++++++++++++++ 6 files changed, 1679 insertions(+), 10 deletions(-) create mode 100644 test/wcc/805_placeaddr_store.c diff --git a/Makefile b/Makefile index 1c99f516..62f05651 100644 --- a/Makefile +++ b/Makefile @@ -389,6 +389,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tuple_sret_receive_run \ $(BIN)/test_append_wide_elem \ $(BIN)/test_delete_elem \ + $(BIN)/test_placeaddr_store \ $(BIN)/test_struct_tuple_field_slot \ $(BIN)/test_widen_pad_zero_run \ $(BIN)/test_named_ptr_alias_variant_widen \ @@ -1011,6 +1012,16 @@ $(BIN)/test_delete_elem: test/wcc/804_delete_elem.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# F6 (task #4, regex fold-2b): (*ts)[i].field = v / OP= v through the +# cgplaceaddr resolver — runtime rows across widths/signedness/str + +# compound ops + loud-tail reject rows w/ exact diagnostic text + cs==ww +# asm byte-id. +$(BIN)/test_placeaddr_store: test/wcc/805_placeaddr_store.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 490f52d5..2813a074 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1989,6 +1989,88 @@ aggarg_srcaddr(Cg *c, Node *src, int dst, Local *locals) return 0; } +/* cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue) + * expression into dst_reg; returns 1 when the shape is wired, 0 + * otherwise (the caller loud-stops — rule 7, never a silent drop). + * F6 resolver, commit C1: only the deref-rooted spine is wired — + * `(*p)[i].f` as N_UN(STAR) root, N_INDEX hop over a slice/array + * place, N_DOT struct-field hop with one deref for a *struct base. + * Ident-rooted spines stay with the enumerated N_ASSIGN arms so this + * resolver never perturbs their asm; the F4 read-walker and F5 + * let-copy accrete here in follow-up commits. ADDRESS COMPUTATION + * ONLY — every call-site keeps its own load/store/copy emission. + * Clobbers AX/CX (cgexpr on index / pointer operands) and balances + * its own PUSHQ/POPQ; dst_reg must not be AX or CX. */ +static int +cgplaceaddr(Cg *c, Node *n, int dst_reg, Local *locals) +{ + if (n == NULL) return 0; + if (n->kind == N_UN && n->op == TK_STAR) { + /* &(*e) is e's value — no load. */ + cgexpr(c, n->lhs, locals); + ins2(c, A_MOVQ, areg(D_AX), areg(dst_reg)); + return 1; + } + if (n->kind == N_INDEX) { + Node *base = n->lhs; + Node *idx = n->rhs; + if (base == NULL || idx == NULL) return 0; + /* Deref base only: ident/dot index bases all have + * enumerated arms; routing them here would change + * their asm. */ + if (!(base->kind == N_UN && base->op == TK_STAR)) + return 0; + Type *bu = type_chase_named(base->type); + if (bu == NULL) return 0; + if (bu->kind != TY_SLICE && bu->kind != TY_ARRAY) + return 0; + Type *et = type_chase_named(n->type); + if (et == NULL) return 0; + int esz = (int)et->size; + cgexpr(c, idx, locals); + if (esz > 1) { + ins2(c, A_MOVQ, aimm(esz), areg(D_CX)); + ins2(c, A_IMULQ, areg(D_CX), areg(D_AX)); + } + ins1(c, A_PUSHQ, areg(D_AX)); + if (!cgplaceaddr(c, base, dst_reg, locals)) return 0; + /* A slice place holds the {ptr,len,cap} header — the + * element base is its .ptr word; an array place IS the + * element storage. */ + if (bu->kind == TY_SLICE) + ins2(c, A_MOVQ, amem(dst_reg, 0), areg(dst_reg)); + ins1(c, A_POPQ, areg(D_AX)); + ins2(c, A_ADDQ, areg(D_AX), areg(dst_reg)); + return 1; + } + if (n->kind == N_DOT) { + Node *base = n->lhs; + if (base == NULL) return 0; + Type *bu = type_chase_named(base->type); + if (bu == NULL) return 0; + int viaptr = 0; + Type *st = NULL; + if (bu->kind == TY_PTR) { + Type *p = type_chase_named(bu->sub); + if (p && p->kind == TY_STRUCT) { st = p; viaptr = 1; } + } else if (bu->kind == TY_STRUCT) { + st = bu; + } + if (st == NULL) return 0; + Tfield *f = NULL; + for (Tfield *fl = st->fields; fl; fl = fl->next) + if (strcmp(fl->name, n->str) == 0) { f = fl; break; } + if (f == NULL) return 0; + if (!cgplaceaddr(c, base, dst_reg, locals)) return 0; + if (viaptr) + ins2(c, A_MOVQ, amem(dst_reg, 0), areg(dst_reg)); + if ((int)f->offset != 0) + ins2(c, A_ADDQ, aimm((int)f->offset), areg(dst_reg)); + return 1; + } + return 0; +} + /* cg_structlit_fill modes — see helper docstring. */ enum { DST_BP = 0, @@ -5167,7 +5249,14 @@ cgexpr(Cg *c, Node *n, Local *locals) int divop = op_for(n, A_DIVSD, A_DIVSS); int off = localfind(locals, n->lhs->str); int isglobal = (off == 0) && let_islet(n->lhs->str); - if (off == 0 && !isglobal) break; + /* Loud twin of the IDENT-tail unresolved-name stop + * below (C1): wwstage resolves the name BEFORE its + * float dispatch, so a silent break here would make + * the stages disagree on the build verdict. */ + if (off == 0 && !isglobal) + fatal("unsupported assign target: " + "unresolved identifier '%s'", + n->lhs->str); if (n->op == TK_ASSIGN) { if (off != 0) { ins2(c, mvop, areg(D_X0), amem(D_BP, off)); @@ -5946,16 +6035,157 @@ cgexpr(Cg *c, Node *n, Local *locals) } } } + /* F6 (cgplaceaddr, commit C1): an N_DOT lvalue none of the + * enumerated arms above matched — today the deref-rooted + * spine `(*p)[i].f = v` / `OP= v`. Base-address derivation + * routes through cgplaceaddr; the load/store emission stays + * here. Any N_DOT shape the resolver can't address dies + * LOUD below: the pre-C1 dispatch tail silently emitted + * NOTHING (rhs unevaluated) for every such shape. */ + if (n->lhs && n->lhs->kind == N_DOT) { + Type *ft = n->lhs->type; + Type *fu = type_chase_named(ft); + int fsz = (int)(ft ? ft->size : 8); + int pa_isf32 = 0; + if (fld_isfloat(ft, &pa_isf32)) + fatal("assign-resolver: float field not " + "wired (rule-7)"); + if (fu && fu->kind == TY_TAGGED) + fatal("assign-resolver: tagged field not " + "wired (rule-7)"); + if (fu && (fu->kind == TY_STRUCT + || fu->kind == TY_ARRAY + || fu->kind == TY_TUPLE)) + fatal("assign-resolver: aggregate field not " + "wired (rule-7)"); + if (fu && (fu->kind == TY_STR + || fu->kind == TY_SLICE)) { + if (n->op != TK_ASSIGN) + fatal("assign-resolver: compound on " + "str/slice field not wired " + "(rule-7)"); + /* str IS []u8: store the whole {ptr,len,cap} + * triple from (AX,BX,CX); the place address + * goes in DX so the three pops survive + * (#1/Phase 3). */ + cgexpr(c, n->rhs, locals); + ins1(c, A_PUSHQ, areg(D_CX)); + ins1(c, A_PUSHQ, areg(D_BX)); + ins1(c, A_PUSHQ, areg(D_AX)); + if (cgplaceaddr(c, n->lhs, D_DX, locals)) { + ins1(c, A_POPQ, areg(D_AX)); + ins1(c, A_POPQ, areg(D_BX)); + ins1(c, A_POPQ, areg(D_CX)); + 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)); + break; + } + } else if (n->op == TK_ASSIGN) { + cgexpr(c, n->rhs, locals); + ins1(c, A_PUSHQ, areg(D_AX)); + if (cgplaceaddr(c, n->lhs, D_BX, locals)) { + ins1(c, A_POPQ, areg(D_AX)); + ins2(c, fldstoreop(ft, fsz), + areg(D_AX), amem(D_BX, 0)); + break; + } + } else { + /* Compound: AX=old, CX=rhs, BX=addr — the + * same register roles as the chained-ptr- + * field compound template above. */ + cgexpr(c, n->rhs, locals); + ins1(c, A_PUSHQ, areg(D_AX)); + if (cgplaceaddr(c, n->lhs, D_BX, locals)) { + ins2(c, fldloadop(ft, fsz), + amem(D_BX, 0), areg(D_AX)); + ins1(c, A_POPQ, areg(D_CX)); + int unsignd = type_isunsigned(ft); + switch (n->op) { + case TK_PLUSEQ: + ins2(c, A_ADDQ, areg(D_CX), + areg(D_AX)); + break; + case TK_MINUSEQ: + ins2(c, A_SUBQ, areg(D_CX), + areg(D_AX)); + break; + case TK_STAREQ: + ins2(c, A_IMULQ, areg(D_CX), + areg(D_AX)); + break; + case TK_AMPEQ: + ins2(c, A_ANDQ, areg(D_CX), + areg(D_AX)); + break; + case TK_PIPEEQ: + ins2(c, A_ORQ, areg(D_CX), + areg(D_AX)); + break; + case TK_CARETEQ: + ins2(c, A_XORQ, areg(D_CX), + areg(D_AX)); + break; + case TK_SLASHEQ: + if (unsignd) + ins2(c, A_MOVQ, + aimm(0), + areg(D_DX)); + else + ins0(c, A_CQO); + ins1(c, unsignd ? A_DIVQ + : A_IDIVQ, areg(D_CX)); + break; + case TK_PERCENTEQ: + if (unsignd) + ins2(c, A_MOVQ, + aimm(0), + areg(D_DX)); + else + ins0(c, A_CQO); + ins1(c, unsignd ? A_DIVQ + : A_IDIVQ, areg(D_CX)); + ins2(c, A_MOVQ, areg(D_DX), + areg(D_AX)); + break; + case TK_LSHIFTEQ: + ins2(c, A_SHLQ, areg(D_CX), + areg(D_AX)); + break; + case TK_RSHIFTEQ: + ins2(c, unsignd ? A_SHRQ + : A_SARQ, areg(D_CX), + areg(D_AX)); + break; + default: + fatal("assign-resolver: " + "unknown compound op " + "(rule-7)"); + } + ins2(c, fldstoreop(ft, fsz), + areg(D_AX), amem(D_BX, 0)); + break; + } + } + fatal("unsupported assign target shape"); + } if (n->lhs->kind == N_IDENT) { int off = localfind(locals, n->lhs->str); if (off == 0) { /* Top-level let target — RIP-relative store - * (or load→combine→store for compound). Names - * we don't recognise as scalar lets fall through - * to the existing drop behaviour, which produces - * a clean link-time undefined-symbol error if - * the binding was ever supposed to exist. */ - if (!let_islet(n->lhs->str)) break; + * (or load→combine→store for compound). A + * name that is neither a local nor a let + * dies LOUD: the pre-C1 break dropped the + * whole statement silently (no symbol was + * ever referenced, so not even a link error + * surfaced). */ + if (!let_islet(n->lhs->str)) + fatal("unsupported assign target: " + "unresolved identifier '%s'", + n->lhs->str); cgexpr(c, n->rhs, locals); if (n->op == TK_ASSIGN) { ins2(c, A_MOVQ, areg(D_AX), @@ -6113,6 +6343,13 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off)); skip_assign_store: ; } + /* C1 residual (task #22): a non-DOT lvalue no arm above + * matched still falls out SILENT here — the known member is + * the str-base element store family (`s[i] = v`: cstage + * drops, wwstage emits MOVB; pre-existing gate-blind + * divergence) plus tuple-member writes. The tail goes loud + * for the remaining kinds with #22, after the family gets a + * symmetric verdict. */ break; } case N_CALL: { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index e2026f80..8ebc41d8 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -21209,6 +21209,124 @@ fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = { return false; }; +// cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue) +// expression into dstreg; returns true when the shape is wired, false +// otherwise (the caller loud-stops — rule 7, never a silent drop). +// F6 resolver, commit C1 — mirror of cstage cmd/w6c/cgen.c +// cgplaceaddr: only the deref-rooted spine is wired (`(*p)[i].f` as +// N_UN(STAR) root, N_INDEX hop over a slice/array place, N_DOT +// struct-field hop with one deref for a *struct base). All type keys +// come off the checker-STAMPED tinfo (.type_), never tnode names — +// the #209/#211 discipline. Ident-rooted spines stay with the +// enumerated cgassign arms so this resolver never perturbs their +// asm. ADDRESS COMPUTATION ONLY — every call-site keeps its own +// load/store/copy emission. Clobbers AX/CX (cgexpr on index / +// pointer operands) and balances its own PUSHQ/POPQ; dstreg must +// not be AX or CX. +fn cgplaceaddr(c: *cgen, n: *node, dstreg: str) bool = { + if (n == nil) { return false; }; + if (n.kind == nkind.N_UN) { + if (n.op != tkind.TK_STAR) { return false; }; + // &(*e) is e's value — no load. + cgexpr(c, n.lhs); + emitline("\tMOVQ\tAX, "); + emitline(dstreg); + emitline("\n"); + return true; + }; + if (n.kind == nkind.N_INDEX) { + let base: *node = n.lhs; + let idx: *node = n.rhs; + if (base == nil || idx == nil) { return false; }; + // Deref base only: ident/dot index bases all have + // enumerated arms; routing them here would change + // their asm. + if (base.kind != nkind.N_UN) { return false; }; + if (base.op != tkind.TK_STAR) { return false; }; + let bu: *tinfo = base.type_: *tinfo; + for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; + if (bu == nil) { return false; }; + if (bu.kind != tykind.TY_SLICE && bu.kind != tykind.TY_ARRAY) { + return false; + }; + let et: *tinfo = n.type_: *tinfo; + for (et != nil && et.kind == tykind.TY_NAMED) { et = et.under; }; + if (et == nil) { return false; }; + let esz: i32 = et.size: i32; + cgexpr(c, idx); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + if (!cgplaceaddr(c, base, dstreg)) { return false; }; + // A slice place holds the {ptr,len,cap} header — the + // element base is its .ptr word; an array place IS the + // element storage. + if (bu.kind == tykind.TY_SLICE) { + emitline("\tMOVQ\t("); + emitline(dstreg); + emitline("), "); + emitline(dstreg); + emitline("\n"); + }; + emitline("\tPOPQ\tAX\n"); + emitline("\tADDQ\tAX, "); + emitline(dstreg); + emitline("\n"); + return true; + }; + if (n.kind == nkind.N_DOT) { + let base: *node = n.lhs; + if (base == nil) { return false; }; + let bu: *tinfo = base.type_: *tinfo; + for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; + if (bu == nil) { return false; }; + let viaptr: bool = false; + let st: *tinfo = nil; + if (bu.kind == tykind.TY_PTR) { + let p: *tinfo = bu.sub; + for (p != nil && p.kind == tykind.TY_NAMED) { p = p.under; }; + if (p != nil) { if (p.kind == tykind.TY_STRUCT) { + st = p; + viaptr = true; + }; }; + } else { if (bu.kind == tykind.TY_STRUCT) { + st = bu; + }; }; + if (st == nil) { return false; }; + let f: *tfield = st.fields; + let foff: i64 = -1; + for (f != nil) { + if (streq(f.name, n.str)) { + foff = f.offset: i64; + break; + }; + f = f.tnext; + }; + if (foff < 0) { return false; }; + if (!cgplaceaddr(c, base, dstreg)) { return false; }; + if (viaptr) { + emitline("\tMOVQ\t("); + emitline(dstreg); + emitline("), "); + emitline(dstreg); + emitline("\n"); + }; + if (foff != 0) { + emitline("\tADDQ\t$"); + emitint(foff); + emitline(", "); + emitline(dstreg); + emitline("\n"); + }; + return true; + }; + return false; +}; + fn cgindex(c: *cgen, n: *node) void = { // Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL, // str → (ptr, len) into (AX, BX), everything else → MOVQ. Fast @@ -28076,7 +28194,18 @@ fn cgassign(c: *cgen, n: *node) void = { // halves (plus cap for slice — stashed via // DI since LEAQ overwrites CX); the asm has // no `name+8(SB)` operand form. - if (!isletvar(c, nm)) { return; }; + // C1: a name that is neither a local nor a + // let dies LOUD — the pre-C1 return dropped + // the whole statement silently (cstage twin: + // the N_ASSIGN IDENT-tail fatal). + if (!isletvar(c, nm)) { + let mi1: str = "unsupported assign target: unresolved identifier '"; + os.write(2, mi1.ptr, mi1.len: u64); + os.write(2, nm.ptr, nm.len: u64); + let mi2: str = "'\n"; + os.write(2, mi2.ptr, mi2.len: u64); + os.exit(1); + }; // Float global: rhs lands in X0; store via // LEAQ+indirect since MOVSS/MOVSD have no // D_EXTERN operand form. @@ -28710,6 +28839,147 @@ fn cgassign(c: *cgen, n: *node) void = { return; }; }; + // F6 (cgplaceaddr, commit C1): an N_DOT lvalue none of the + // enumerated arms above matched — today the deref-rooted spine + // `(*p)[i].f = v` / `OP= v`. Base-address derivation routes + // through cgplaceaddr; the load/store emission stays here. Any + // N_DOT shape the resolver can't address dies LOUD below: the + // pre-C1 fall-off-the-function tail silently emitted NOTHING + // (rhs unevaluated). Mirror of the cstage cgen.c N_ASSIGN arm. + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT) { + let ft: *tinfo = lhs.type_: *tinfo; + let fu: *tinfo = ft; + for (fu != nil && fu.kind == tykind.TY_NAMED) { + fu = fu.under; + }; + let fsz: i32 = 8; + if (ft != nil) { fsz = ft.size: i32; }; + if (typeisfloat(ft)) { + let mf: str = "assign-resolver: float field not wired (rule-7)\n"; + os.write(2, mf.ptr, mf.len: u64); + os.exit(1); + }; + if (fu != nil) { + if (fu.kind == tykind.TY_TAGGED) { + let mt: str = "assign-resolver: tagged field not wired (rule-7)\n"; + os.write(2, mt.ptr, mt.len: u64); + os.exit(1); + }; + if (fu.kind == tykind.TY_STRUCT + || fu.kind == tykind.TY_ARRAY + || fu.kind == tykind.TY_TUPLE) { + let ma: str = "assign-resolver: aggregate field not wired (rule-7)\n"; + os.write(2, ma.ptr, ma.len: u64); + os.exit(1); + }; + }; + let fstrsl: bool = false; + if (fu != nil) { + if (fu.kind == tykind.TY_STR + || fu.kind == tykind.TY_SLICE) { + fstrsl = true; + }; + }; + if (fstrsl) { + if (n.op != tkind.TK_ASSIGN) { + let ms: str = "assign-resolver: compound on str/slice field not wired (rule-7)\n"; + os.write(2, ms.ptr, ms.len: u64); + os.exit(1); + }; + // str IS []u8: store the whole {ptr,len,cap} + // triple from (AX,BX,CX); the place address + // goes in DX so the three pops survive + // (#1/Phase 3). + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tCX\n"); + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tAX\n"); + if (cgplaceaddr(c, lhs, "DX")) { + emitline("\tPOPQ\tAX\n"); + emitline("\tPOPQ\tBX\n"); + emitline("\tPOPQ\tCX\n"); + emitline("\tMOVQ\tAX, (DX)\n"); + emitline("\tMOVQ\tBX, 8(DX)\n"); + emitline("\tMOVQ\tCX, 16(DX)\n"); + return; + }; + } else { if (n.op == tkind.TK_ASSIGN) { + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tAX\n"); + if (cgplaceaddr(c, lhs, "BX")) { + emitline("\tPOPQ\tAX\n"); + let sop: str = "MOVQ"; + if (fsz == 1) { sop = "MOVB"; }; + if (fsz == 2) { sop = "MOVW"; }; + if (fsz == 4) { sop = "MOVL"; }; + emitline("\t"); + emitline(sop); + emitline("\tAX, (BX)\n"); + return; + }; + } else { + // Compound: AX=old, CX=rhs, BX=addr — the same + // register roles as the chained-ptr-field + // compound template above. + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tAX\n"); + if (cgplaceaddr(c, lhs, "BX")) { + let lop: str = loadopsz(typeissigned(ft), fsz); + emitline("\t"); + emitline(lop); + emitline("\t(BX), AX\n"); + emitline("\tPOPQ\tCX\n"); + let unsignd: bool = typeisunsigned(ft); + let wired: bool = false; + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_SLASHEQ) { + if (unsignd) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } + else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; + wired = true; + }; + if (n.op == tkind.TK_PERCENTEQ) { + if (unsignd) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } + else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; + emitline("\tMOVQ\tDX, AX\n"); + wired = true; + }; + if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_RSHIFTEQ) { + if (unsignd) { emitline("\tSHRQ\tCX, AX\n"); } + else { emitline("\tSARQ\tCX, AX\n"); }; + wired = true; + }; + if (!wired) { + let mu: str = "assign-resolver: unknown compound op (rule-7)\n"; + os.write(2, mu.ptr, mu.len: u64); + os.exit(1); + }; + let sop: str = "MOVQ"; + if (fsz == 1) { sop = "MOVB"; }; + if (fsz == 2) { sop = "MOVW"; }; + if (fsz == 4) { sop = "MOVL"; }; + emitline("\t"); + emitline(sop); + emitline("\tAX, (BX)\n"); + return; + }; + }; }; + let mtl: str = "unsupported assign target shape\n"; + os.write(2, mtl.ptr, mtl.len: u64); + os.exit(1); + }; + }; + // C1 residual (task #22): a non-DOT lvalue no arm above matched + // still falls out SILENT here — known member: the str-base element + // store family (`s[i] = v`: cstage drops, wwstage emits MOVB; + // pre-existing gate-blind divergence) plus tuple-member writes. + // The tail goes loud for the remaining kinds with #22. return; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 1d18ff6c..ae6d01bf 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -1299,6 +1299,124 @@ fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = { return false; }; +// cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue) +// expression into dstreg; returns true when the shape is wired, false +// otherwise (the caller loud-stops — rule 7, never a silent drop). +// F6 resolver, commit C1 — mirror of cstage cmd/w6c/cgen.c +// cgplaceaddr: only the deref-rooted spine is wired (`(*p)[i].f` as +// N_UN(STAR) root, N_INDEX hop over a slice/array place, N_DOT +// struct-field hop with one deref for a *struct base). All type keys +// come off the checker-STAMPED tinfo (.type_), never tnode names — +// the #209/#211 discipline. Ident-rooted spines stay with the +// enumerated cgassign arms so this resolver never perturbs their +// asm. ADDRESS COMPUTATION ONLY — every call-site keeps its own +// load/store/copy emission. Clobbers AX/CX (cgexpr on index / +// pointer operands) and balances its own PUSHQ/POPQ; dstreg must +// not be AX or CX. +fn cgplaceaddr(c: *cgen, n: *node, dstreg: str) bool = { + if (n == nil) { return false; }; + if (n.kind == nkind.N_UN) { + if (n.op != tkind.TK_STAR) { return false; }; + // &(*e) is e's value — no load. + cgexpr(c, n.lhs); + emitline("\tMOVQ\tAX, "); + emitline(dstreg); + emitline("\n"); + return true; + }; + if (n.kind == nkind.N_INDEX) { + let base: *node = n.lhs; + let idx: *node = n.rhs; + if (base == nil || idx == nil) { return false; }; + // Deref base only: ident/dot index bases all have + // enumerated arms; routing them here would change + // their asm. + if (base.kind != nkind.N_UN) { return false; }; + if (base.op != tkind.TK_STAR) { return false; }; + let bu: *tinfo = base.type_: *tinfo; + for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; + if (bu == nil) { return false; }; + if (bu.kind != tykind.TY_SLICE && bu.kind != tykind.TY_ARRAY) { + return false; + }; + let et: *tinfo = n.type_: *tinfo; + for (et != nil && et.kind == tykind.TY_NAMED) { et = et.under; }; + if (et == nil) { return false; }; + let esz: i32 = et.size: i32; + cgexpr(c, idx); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + if (!cgplaceaddr(c, base, dstreg)) { return false; }; + // A slice place holds the {ptr,len,cap} header — the + // element base is its .ptr word; an array place IS the + // element storage. + if (bu.kind == tykind.TY_SLICE) { + emitline("\tMOVQ\t("); + emitline(dstreg); + emitline("), "); + emitline(dstreg); + emitline("\n"); + }; + emitline("\tPOPQ\tAX\n"); + emitline("\tADDQ\tAX, "); + emitline(dstreg); + emitline("\n"); + return true; + }; + if (n.kind == nkind.N_DOT) { + let base: *node = n.lhs; + if (base == nil) { return false; }; + let bu: *tinfo = base.type_: *tinfo; + for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; + if (bu == nil) { return false; }; + let viaptr: bool = false; + let st: *tinfo = nil; + if (bu.kind == tykind.TY_PTR) { + let p: *tinfo = bu.sub; + for (p != nil && p.kind == tykind.TY_NAMED) { p = p.under; }; + if (p != nil) { if (p.kind == tykind.TY_STRUCT) { + st = p; + viaptr = true; + }; }; + } else { if (bu.kind == tykind.TY_STRUCT) { + st = bu; + }; }; + if (st == nil) { return false; }; + let f: *tfield = st.fields; + let foff: i64 = -1; + for (f != nil) { + if (streq(f.name, n.str)) { + foff = f.offset: i64; + break; + }; + f = f.tnext; + }; + if (foff < 0) { return false; }; + if (!cgplaceaddr(c, base, dstreg)) { return false; }; + if (viaptr) { + emitline("\tMOVQ\t("); + emitline(dstreg); + emitline("), "); + emitline(dstreg); + emitline("\n"); + }; + if (foff != 0) { + emitline("\tADDQ\t$"); + emitint(foff); + emitline(", "); + emitline(dstreg); + emitline("\n"); + }; + return true; + }; + return false; +}; + fn cgindex(c: *cgen, n: *node) void = { // Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL, // str → (ptr, len) into (AX, BX), everything else → MOVQ. Fast @@ -8166,7 +8284,18 @@ fn cgassign(c: *cgen, n: *node) void = { // halves (plus cap for slice — stashed via // DI since LEAQ overwrites CX); the asm has // no `name+8(SB)` operand form. - if (!isletvar(c, nm)) { return; }; + // C1: a name that is neither a local nor a + // let dies LOUD — the pre-C1 return dropped + // the whole statement silently (cstage twin: + // the N_ASSIGN IDENT-tail fatal). + if (!isletvar(c, nm)) { + let mi1: str = "unsupported assign target: unresolved identifier '"; + os.write(2, mi1.ptr, mi1.len: u64); + os.write(2, nm.ptr, nm.len: u64); + let mi2: str = "'\n"; + os.write(2, mi2.ptr, mi2.len: u64); + os.exit(1); + }; // Float global: rhs lands in X0; store via // LEAQ+indirect since MOVSS/MOVSD have no // D_EXTERN operand form. @@ -8800,6 +8929,147 @@ fn cgassign(c: *cgen, n: *node) void = { return; }; }; + // F6 (cgplaceaddr, commit C1): an N_DOT lvalue none of the + // enumerated arms above matched — today the deref-rooted spine + // `(*p)[i].f = v` / `OP= v`. Base-address derivation routes + // through cgplaceaddr; the load/store emission stays here. Any + // N_DOT shape the resolver can't address dies LOUD below: the + // pre-C1 fall-off-the-function tail silently emitted NOTHING + // (rhs unevaluated). Mirror of the cstage cgen.c N_ASSIGN arm. + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT) { + let ft: *tinfo = lhs.type_: *tinfo; + let fu: *tinfo = ft; + for (fu != nil && fu.kind == tykind.TY_NAMED) { + fu = fu.under; + }; + let fsz: i32 = 8; + if (ft != nil) { fsz = ft.size: i32; }; + if (typeisfloat(ft)) { + let mf: str = "assign-resolver: float field not wired (rule-7)\n"; + os.write(2, mf.ptr, mf.len: u64); + os.exit(1); + }; + if (fu != nil) { + if (fu.kind == tykind.TY_TAGGED) { + let mt: str = "assign-resolver: tagged field not wired (rule-7)\n"; + os.write(2, mt.ptr, mt.len: u64); + os.exit(1); + }; + if (fu.kind == tykind.TY_STRUCT + || fu.kind == tykind.TY_ARRAY + || fu.kind == tykind.TY_TUPLE) { + let ma: str = "assign-resolver: aggregate field not wired (rule-7)\n"; + os.write(2, ma.ptr, ma.len: u64); + os.exit(1); + }; + }; + let fstrsl: bool = false; + if (fu != nil) { + if (fu.kind == tykind.TY_STR + || fu.kind == tykind.TY_SLICE) { + fstrsl = true; + }; + }; + if (fstrsl) { + if (n.op != tkind.TK_ASSIGN) { + let ms: str = "assign-resolver: compound on str/slice field not wired (rule-7)\n"; + os.write(2, ms.ptr, ms.len: u64); + os.exit(1); + }; + // str IS []u8: store the whole {ptr,len,cap} + // triple from (AX,BX,CX); the place address + // goes in DX so the three pops survive + // (#1/Phase 3). + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tCX\n"); + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tAX\n"); + if (cgplaceaddr(c, lhs, "DX")) { + emitline("\tPOPQ\tAX\n"); + emitline("\tPOPQ\tBX\n"); + emitline("\tPOPQ\tCX\n"); + emitline("\tMOVQ\tAX, (DX)\n"); + emitline("\tMOVQ\tBX, 8(DX)\n"); + emitline("\tMOVQ\tCX, 16(DX)\n"); + return; + }; + } else { if (n.op == tkind.TK_ASSIGN) { + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tAX\n"); + if (cgplaceaddr(c, lhs, "BX")) { + emitline("\tPOPQ\tAX\n"); + let sop: str = "MOVQ"; + if (fsz == 1) { sop = "MOVB"; }; + if (fsz == 2) { sop = "MOVW"; }; + if (fsz == 4) { sop = "MOVL"; }; + emitline("\t"); + emitline(sop); + emitline("\tAX, (BX)\n"); + return; + }; + } else { + // Compound: AX=old, CX=rhs, BX=addr — the same + // register roles as the chained-ptr-field + // compound template above. + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tAX\n"); + if (cgplaceaddr(c, lhs, "BX")) { + let lop: str = loadopsz(typeissigned(ft), fsz); + emitline("\t"); + emitline(lop); + emitline("\t(BX), AX\n"); + emitline("\tPOPQ\tCX\n"); + let unsignd: bool = typeisunsigned(ft); + let wired: bool = false; + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_SLASHEQ) { + if (unsignd) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } + else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; + wired = true; + }; + if (n.op == tkind.TK_PERCENTEQ) { + if (unsignd) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } + else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; + emitline("\tMOVQ\tDX, AX\n"); + wired = true; + }; + if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_RSHIFTEQ) { + if (unsignd) { emitline("\tSHRQ\tCX, AX\n"); } + else { emitline("\tSARQ\tCX, AX\n"); }; + wired = true; + }; + if (!wired) { + let mu: str = "assign-resolver: unknown compound op (rule-7)\n"; + os.write(2, mu.ptr, mu.len: u64); + os.exit(1); + }; + let sop: str = "MOVQ"; + if (fsz == 1) { sop = "MOVB"; }; + if (fsz == 2) { sop = "MOVW"; }; + if (fsz == 4) { sop = "MOVL"; }; + emitline("\t"); + emitline(sop); + emitline("\tAX, (BX)\n"); + return; + }; + }; }; + let mtl: str = "unsupported assign target shape\n"; + os.write(2, mtl.ptr, mtl.len: u64); + os.exit(1); + }; + }; + // C1 residual (task #22): a non-DOT lvalue no arm above matched + // still falls out SILENT here — known member: the str-base element + // store family (`s[i] = v`: cstage drops, wwstage emits MOVB; + // pre-existing gate-blind divergence) plus tuple-member writes. + // The tail goes loud for the remaining kinds with #22. return; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 90225cf7..4cf925ae 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -21209,6 +21209,124 @@ fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = { return false; }; +// cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue) +// expression into dstreg; returns true when the shape is wired, false +// otherwise (the caller loud-stops — rule 7, never a silent drop). +// F6 resolver, commit C1 — mirror of cstage cmd/w6c/cgen.c +// cgplaceaddr: only the deref-rooted spine is wired (`(*p)[i].f` as +// N_UN(STAR) root, N_INDEX hop over a slice/array place, N_DOT +// struct-field hop with one deref for a *struct base). All type keys +// come off the checker-STAMPED tinfo (.type_), never tnode names — +// the #209/#211 discipline. Ident-rooted spines stay with the +// enumerated cgassign arms so this resolver never perturbs their +// asm. ADDRESS COMPUTATION ONLY — every call-site keeps its own +// load/store/copy emission. Clobbers AX/CX (cgexpr on index / +// pointer operands) and balances its own PUSHQ/POPQ; dstreg must +// not be AX or CX. +fn cgplaceaddr(c: *cgen, n: *node, dstreg: str) bool = { + if (n == nil) { return false; }; + if (n.kind == nkind.N_UN) { + if (n.op != tkind.TK_STAR) { return false; }; + // &(*e) is e's value — no load. + cgexpr(c, n.lhs); + emitline("\tMOVQ\tAX, "); + emitline(dstreg); + emitline("\n"); + return true; + }; + if (n.kind == nkind.N_INDEX) { + let base: *node = n.lhs; + let idx: *node = n.rhs; + if (base == nil || idx == nil) { return false; }; + // Deref base only: ident/dot index bases all have + // enumerated arms; routing them here would change + // their asm. + if (base.kind != nkind.N_UN) { return false; }; + if (base.op != tkind.TK_STAR) { return false; }; + let bu: *tinfo = base.type_: *tinfo; + for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; + if (bu == nil) { return false; }; + if (bu.kind != tykind.TY_SLICE && bu.kind != tykind.TY_ARRAY) { + return false; + }; + let et: *tinfo = n.type_: *tinfo; + for (et != nil && et.kind == tykind.TY_NAMED) { et = et.under; }; + if (et == nil) { return false; }; + let esz: i32 = et.size: i32; + cgexpr(c, idx); + if (esz > 1) { + emitline("\tMOVQ\t$"); + emitint(esz: i64); + emitline(", CX\n"); + emitline("\tIMULQ\tCX, AX\n"); + }; + emitline("\tPUSHQ\tAX\n"); + if (!cgplaceaddr(c, base, dstreg)) { return false; }; + // A slice place holds the {ptr,len,cap} header — the + // element base is its .ptr word; an array place IS the + // element storage. + if (bu.kind == tykind.TY_SLICE) { + emitline("\tMOVQ\t("); + emitline(dstreg); + emitline("), "); + emitline(dstreg); + emitline("\n"); + }; + emitline("\tPOPQ\tAX\n"); + emitline("\tADDQ\tAX, "); + emitline(dstreg); + emitline("\n"); + return true; + }; + if (n.kind == nkind.N_DOT) { + let base: *node = n.lhs; + if (base == nil) { return false; }; + let bu: *tinfo = base.type_: *tinfo; + for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; + if (bu == nil) { return false; }; + let viaptr: bool = false; + let st: *tinfo = nil; + if (bu.kind == tykind.TY_PTR) { + let p: *tinfo = bu.sub; + for (p != nil && p.kind == tykind.TY_NAMED) { p = p.under; }; + if (p != nil) { if (p.kind == tykind.TY_STRUCT) { + st = p; + viaptr = true; + }; }; + } else { if (bu.kind == tykind.TY_STRUCT) { + st = bu; + }; }; + if (st == nil) { return false; }; + let f: *tfield = st.fields; + let foff: i64 = -1; + for (f != nil) { + if (streq(f.name, n.str)) { + foff = f.offset: i64; + break; + }; + f = f.tnext; + }; + if (foff < 0) { return false; }; + if (!cgplaceaddr(c, base, dstreg)) { return false; }; + if (viaptr) { + emitline("\tMOVQ\t("); + emitline(dstreg); + emitline("), "); + emitline(dstreg); + emitline("\n"); + }; + if (foff != 0) { + emitline("\tADDQ\t$"); + emitint(foff); + emitline(", "); + emitline(dstreg); + emitline("\n"); + }; + return true; + }; + return false; +}; + fn cgindex(c: *cgen, n: *node) void = { // Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL, // str → (ptr, len) into (AX, BX), everything else → MOVQ. Fast @@ -28076,7 +28194,18 @@ fn cgassign(c: *cgen, n: *node) void = { // halves (plus cap for slice — stashed via // DI since LEAQ overwrites CX); the asm has // no `name+8(SB)` operand form. - if (!isletvar(c, nm)) { return; }; + // C1: a name that is neither a local nor a + // let dies LOUD — the pre-C1 return dropped + // the whole statement silently (cstage twin: + // the N_ASSIGN IDENT-tail fatal). + if (!isletvar(c, nm)) { + let mi1: str = "unsupported assign target: unresolved identifier '"; + os.write(2, mi1.ptr, mi1.len: u64); + os.write(2, nm.ptr, nm.len: u64); + let mi2: str = "'\n"; + os.write(2, mi2.ptr, mi2.len: u64); + os.exit(1); + }; // Float global: rhs lands in X0; store via // LEAQ+indirect since MOVSS/MOVSD have no // D_EXTERN operand form. @@ -28710,6 +28839,147 @@ fn cgassign(c: *cgen, n: *node) void = { return; }; }; + // F6 (cgplaceaddr, commit C1): an N_DOT lvalue none of the + // enumerated arms above matched — today the deref-rooted spine + // `(*p)[i].f = v` / `OP= v`. Base-address derivation routes + // through cgplaceaddr; the load/store emission stays here. Any + // N_DOT shape the resolver can't address dies LOUD below: the + // pre-C1 fall-off-the-function tail silently emitted NOTHING + // (rhs unevaluated). Mirror of the cstage cgen.c N_ASSIGN arm. + if (lhs != nil) { + if (lhs.kind == nkind.N_DOT) { + let ft: *tinfo = lhs.type_: *tinfo; + let fu: *tinfo = ft; + for (fu != nil && fu.kind == tykind.TY_NAMED) { + fu = fu.under; + }; + let fsz: i32 = 8; + if (ft != nil) { fsz = ft.size: i32; }; + if (typeisfloat(ft)) { + let mf: str = "assign-resolver: float field not wired (rule-7)\n"; + os.write(2, mf.ptr, mf.len: u64); + os.exit(1); + }; + if (fu != nil) { + if (fu.kind == tykind.TY_TAGGED) { + let mt: str = "assign-resolver: tagged field not wired (rule-7)\n"; + os.write(2, mt.ptr, mt.len: u64); + os.exit(1); + }; + if (fu.kind == tykind.TY_STRUCT + || fu.kind == tykind.TY_ARRAY + || fu.kind == tykind.TY_TUPLE) { + let ma: str = "assign-resolver: aggregate field not wired (rule-7)\n"; + os.write(2, ma.ptr, ma.len: u64); + os.exit(1); + }; + }; + let fstrsl: bool = false; + if (fu != nil) { + if (fu.kind == tykind.TY_STR + || fu.kind == tykind.TY_SLICE) { + fstrsl = true; + }; + }; + if (fstrsl) { + if (n.op != tkind.TK_ASSIGN) { + let ms: str = "assign-resolver: compound on str/slice field not wired (rule-7)\n"; + os.write(2, ms.ptr, ms.len: u64); + os.exit(1); + }; + // str IS []u8: store the whole {ptr,len,cap} + // triple from (AX,BX,CX); the place address + // goes in DX so the three pops survive + // (#1/Phase 3). + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tCX\n"); + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tAX\n"); + if (cgplaceaddr(c, lhs, "DX")) { + emitline("\tPOPQ\tAX\n"); + emitline("\tPOPQ\tBX\n"); + emitline("\tPOPQ\tCX\n"); + emitline("\tMOVQ\tAX, (DX)\n"); + emitline("\tMOVQ\tBX, 8(DX)\n"); + emitline("\tMOVQ\tCX, 16(DX)\n"); + return; + }; + } else { if (n.op == tkind.TK_ASSIGN) { + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tAX\n"); + if (cgplaceaddr(c, lhs, "BX")) { + emitline("\tPOPQ\tAX\n"); + let sop: str = "MOVQ"; + if (fsz == 1) { sop = "MOVB"; }; + if (fsz == 2) { sop = "MOVW"; }; + if (fsz == 4) { sop = "MOVL"; }; + emitline("\t"); + emitline(sop); + emitline("\tAX, (BX)\n"); + return; + }; + } else { + // Compound: AX=old, CX=rhs, BX=addr — the same + // register roles as the chained-ptr-field + // compound template above. + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tAX\n"); + if (cgplaceaddr(c, lhs, "BX")) { + let lop: str = loadopsz(typeissigned(ft), fsz); + emitline("\t"); + emitline(lop); + emitline("\t(BX), AX\n"); + emitline("\tPOPQ\tCX\n"); + let unsignd: bool = typeisunsigned(ft); + let wired: bool = false; + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_SLASHEQ) { + if (unsignd) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } + else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; + wired = true; + }; + if (n.op == tkind.TK_PERCENTEQ) { + if (unsignd) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } + else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; + emitline("\tMOVQ\tDX, AX\n"); + wired = true; + }; + if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired = true; }; + if (n.op == tkind.TK_RSHIFTEQ) { + if (unsignd) { emitline("\tSHRQ\tCX, AX\n"); } + else { emitline("\tSARQ\tCX, AX\n"); }; + wired = true; + }; + if (!wired) { + let mu: str = "assign-resolver: unknown compound op (rule-7)\n"; + os.write(2, mu.ptr, mu.len: u64); + os.exit(1); + }; + let sop: str = "MOVQ"; + if (fsz == 1) { sop = "MOVB"; }; + if (fsz == 2) { sop = "MOVW"; }; + if (fsz == 4) { sop = "MOVL"; }; + emitline("\t"); + emitline(sop); + emitline("\tAX, (BX)\n"); + return; + }; + }; }; + let mtl: str = "unsupported assign target shape\n"; + os.write(2, mtl.ptr, mtl.len: u64); + os.exit(1); + }; + }; + // C1 residual (task #22): a non-DOT lvalue no arm above matched + // still falls out SILENT here — known member: the str-base element + // store family (`s[i] = v`: cstage drops, wwstage emits MOVB; + // pre-existing gate-blind divergence) plus tuple-member writes. + // The tail goes loud for the remaining kinds with #22. return; }; diff --git a/test/wcc/805_placeaddr_store.c b/test/wcc/805_placeaddr_store.c new file mode 100644 index 00000000..ef3c69d9 --- /dev/null +++ b/test/wcc/805_placeaddr_store.c @@ -0,0 +1,611 @@ +/* + * 805_placeaddr_store — cstage and wwstage agree, byte-for-byte and at + * runtime, that a store through a deref-of-pointer-to-slice/array + * element field — `(*ts)[i].field = v` and `(*ts)[i].field OP= v` — + * lands the value (F6, task #4 of the regex fold-2b blockers; the + * run_thread hot shape `threads: *[]thread`). + * + * Pre-C1 BOTH stages compiled this shape to NOTHING, byte-identically: + * the lhs N_DOT spine roots at N_UN(STAR), so the `arr[i].field` arm + * (idxbase must be N_IDENT) and the chained-ptr-field arm (base must + * be *struct) both miss, and the N_ASSIGN dispatch fell off the switch + * silently — rhs never even evaluated. The fix routes base-address + * derivation through the cgplaceaddr resolver (cmd/w6c/cgen.c, mirror + * selfhost/cmd/wcc/cgenexpr.ww); each call-site keeps its own + * load/store emission, and every N_DOT lvalue the resolver can't + * address now dies LOUD ("unsupported assign target shape") instead of + * silently dropping (rule 7). Field kinds the resolver arm does not + * wire yet (float / tagged / aggregate / str-slice compound) hard-stop + * with their own diagnostics — the reject rows pin the exact text on + * BOTH stages. Non-DOT lvalue tail residue is task #22. + * + * row | shape | want + * --------------------+----------------------------------------+------ + * store_size_neighbor | (*ts)[1].pc = 9, elem 0 intact | 19 + * store_bool_compound | p7a4 verbatim: =, +=, bool store | 0 + * widths_unsigned | u8/u16/u32 stores (MOVB/MOVW/MOVL) | 42 + * widths_signed | i8/i16/i32 negative stores + readback | 43 + * compound_ops | += -= *= |= /= (DIVQ + IDIVQ) %= <<= | 44 + * str_field_member | (*ts)[i].name = "hi" (3-word header) | 45 + * array_base_deref | (*ta)[i].pc via *[3]t, = and += | 46 + * runtime_call_idx | (*ts)[geti()].pc — call-idx clobber | 47 + * compound_bits_shr | &= ^= >>= signed (SARQ) + uns (SHRQ) | 48 + * compound_narrow | u8/i8/i16/u16/i32/u32 OP= (narrow | 49 + * | fldloadop: MOVSBQ/MOVZBQ/MOVSWQ/...) | + * slice_field_member | (*ts)[i].xs = s ([]i64, 3-word header) | 50 + * neutral_ident_bases | x.f / a[i].f / p.f = and += (untouched | 51 + * | enumerated arms — runtime-pins the | + * | resolver's asm-neutrality claim) | + * reject_aggregate | struct-typed field store | BUILD_FAIL + * reject_float | f64 field store | BUILD_FAIL + * reject_tagged | tagged-union field store | BUILD_FAIL + * reject_str_compound | (*ts)[i].name += — str/slice compound | BUILD_FAIL + * reject_tail | h.arr[1].pc (resolver-unwired N_DOT) | BUILD_FAIL + * + * 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. + */ +#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[] = { + { "store_size_neighbor", + "package main;\n" + "type thread = struct { pc: size, matched: bool };\n" + "fn setpc(ts: *[]thread, i: size) void = { (*ts)[i].pc = 9; };\n" + "export fn main() i32 = {\n" + "\tlet ts: []thread = [];\n" + "\tappend(ts, thread { pc = 1, matched = false });\n" + "\tappend(ts, thread { pc = 2, matched = false });\n" + "\tsetpc(&ts, 1);\n" + "\treturn (ts[0].pc*10 + ts[1].pc): i32;\n" + "};\n", + 19, NULL }, + + /* The original F6 probe (scratch/fold2b_probes/p7a4), graduated + * verbatim: plain scalar store, compound +=, bool store. */ + { "store_bool_compound", + "package main;\n" + "type thread = struct { pc: size, matched: bool };\n" + "fn setpc(ts: *[]thread, i: size) void = {\n" + "\t(*ts)[i].pc = 9;\n" + "};\n" + "fn bump(ts: *[]thread, i: size) void = {\n" + "\t(*ts)[i].pc += 1;\n" + "};\n" + "fn markm(ts: *[]thread, i: size) void = {\n" + "\t(*ts)[i].matched = true;\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet ts: []thread = [];\n" + "\tappend(ts, thread { pc = 7, matched = false });\n" + "\tsetpc(&ts, 0);\n" + "\tif (ts[0].pc != 9) { return 1; };\n" + "\tbump(&ts, 0);\n" + "\tif (ts[0].pc != 10) { return 2; };\n" + "\tmarkm(&ts, 0);\n" + "\tif (!ts[0].matched) { return 3; };\n" + "\treturn 0;\n" + "};\n", + 0, NULL }, + + { "widths_unsigned", + "package main;\n" + "type t = struct { a: u8, b: u16, c: u32 };\n" + "fn seta(ts: *[]t, i: size) void = { (*ts)[i].a = 200u8; };\n" + "fn setb(ts: *[]t, i: size) void = { (*ts)[i].b = 60000u16; };\n" + "fn setc(ts: *[]t, i: size) void = { (*ts)[i].c = 70000u32; };\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { a = 5u8, b = 6u16, c = 7u32 });\n" + "\tappend(ts, t { a = 5u8, b = 6u16, c = 7u32 });\n" + "\tseta(&ts, 1); setb(&ts, 1); setc(&ts, 1);\n" + "\tif (ts[1].a != 200u8) { return 1; };\n" + "\tif (ts[1].b != 60000u16) { return 2; };\n" + "\tif (ts[1].c != 70000u32) { return 3; };\n" + "\tif (ts[0].a != 5u8) { return 4; };\n" + "\tif (ts[0].c != 7u32) { return 5; };\n" + "\treturn 42;\n" + "};\n", + 42, NULL }, + + { "widths_signed", + "package main;\n" + "type t = struct { a: i8, b: i16, c: i32 };\n" + "fn seta(ts: *[]t, i: size) void = { (*ts)[i].a = -7i8; };\n" + "fn setb(ts: *[]t, i: size) void = { (*ts)[i].b = -300i16; };\n" + "fn setc(ts: *[]t, i: size) void = { (*ts)[i].c = -70000i32; };\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { a = 1i8, b = 2i16, c = 3i32 });\n" + "\tappend(ts, t { a = 1i8, b = 2i16, c = 3i32 });\n" + "\tseta(&ts, 1); setb(&ts, 1); setc(&ts, 1);\n" + "\tif (ts[1].a != -7i8) { return 1; };\n" + "\tif (ts[1].b != -300i16) { return 2; };\n" + "\tif (ts[1].c != -70000i32) { return 3; };\n" + "\tif (ts[0].a != 1i8) { return 4; };\n" + "\treturn 43;\n" + "};\n", + 43, NULL }, + + /* All-arm compound coverage: unsigned size /= (DIVQ), signed int + * /= and i64 %= on negatives (CQO+IDIVQ), <<=, and the plain + * += -= *= |= trio on a fourth field. */ + { "compound_ops", + "package main;\n" + "type t = struct { s: int, u: size, n: i64, m: i64 };\n" + "fn divs(ts: *[]t, i: size) void = { (*ts)[i].s /= 4; };\n" + "fn modn(ts: *[]t, i: size) void = { (*ts)[i].n %= 5; };\n" + "fn divu(ts: *[]t, i: size) void = { (*ts)[i].u /= 3; };\n" + "fn shl(ts: *[]t, i: size) void = { (*ts)[i].u <<= 2; };\n" + "fn addm(ts: *[]t, i: size) void = { (*ts)[i].m += 7; };\n" + "fn subm(ts: *[]t, i: size) void = { (*ts)[i].m -= 2; };\n" + "fn mulm(ts: *[]t, i: size) void = { (*ts)[i].m *= 3; };\n" + "fn orm(ts: *[]t, i: size) void = { (*ts)[i].m |= 4; };\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { s = 4, u = 5, n = 6, m = 1 });\n" + "\tappend(ts, t { s = -12, u = 9, n = -13, m = 4 });\n" + "\tdivs(&ts, 1);\n" + "\tif (ts[1].s != -3) { return 1; };\n" + "\tmodn(&ts, 1);\n" + "\tif (ts[1].n != -3) { return 2; };\n" + "\tdivu(&ts, 1);\n" + "\tif (ts[1].u != 3) { return 3; };\n" + "\tshl(&ts, 1);\n" + "\tif (ts[1].u != 12) { return 4; };\n" + "\taddm(&ts, 1);\n" + "\tsubm(&ts, 1);\n" + "\tmulm(&ts, 1);\n" + "\torm(&ts, 1);\n" + "\tif (ts[1].m != 31) { return 5; };\n" + "\tif (ts[0].s != 4) { return 6; };\n" + "\treturn 44;\n" + "};\n", + 44, NULL }, + + /* str field: the rhs leaves {ptr,len,cap} in (AX,BX,CX); the + * place address stages through DX so the triple survives. */ + { "str_field_member", + "package main;\n" + "type t = struct { pc: size, name: str };\n" + "fn setname(ts: *[]t, i: size) void = { (*ts)[i].name = \"hi\"; };\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { pc = 1, name = \"\" });\n" + "\tappend(ts, t { pc = 2, name = \"\" });\n" + "\tsetname(&ts, 1);\n" + "\tlet nm: str = ts[1].name;\n" + "\tif (nm.len != 2) { return 1; };\n" + "\tif (nm[0] != 104u8) { return 2; };\n" + "\tif (ts[0].name.len != 0) { return 3; };\n" + "\treturn 45;\n" + "};\n", + 45, NULL }, + + /* *[N]T base: the array place IS the element storage (no .ptr + * hop), unlike the slice-header deref the rows above take. */ + { "array_base_deref", + "package main;\n" + "type t = struct { pc: size, matched: bool };\n" + "fn setarr(ta: *[3]t, i: size) void = { (*ta)[i].pc = 9; };\n" + "fn bumparr(ta: *[3]t, i: size) void = { (*ta)[i].pc += 2; };\n" + "export fn main() i32 = {\n" + "\tlet ta: [3]t;\n" + "\tta[0].pc = 1; ta[1].pc = 2; ta[2].pc = 3;\n" + "\tsetarr(&ta, 1);\n" + "\tif (ta[1].pc != 9) { return 1; };\n" + "\tbumparr(&ta, 1);\n" + "\tif (ta[1].pc != 11) { return 2; };\n" + "\tif (ta[0].pc != 1) { return 3; };\n" + "\tif (ta[2].pc != 3) { return 4; };\n" + "\treturn 46;\n" + "};\n", + 46, NULL }, + + /* Call-result index: cgexpr(idx) inside the resolver clobbers + * caller-saved regs; the spilled rhs and place address must + * survive. */ + { "runtime_call_idx", + "package main;\n" + "type t = struct { pc: size, matched: bool };\n" + "fn geti() size = { return 1; };\n" + "fn setpc(ts: *[]t) void = { (*ts)[geti()].pc = 9; };\n" + "fn bump(ts: *[]t) void = { (*ts)[geti()].pc += 1; };\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { pc = 1, matched = false });\n" + "\tappend(ts, t { pc = 2, matched = false });\n" + "\tsetpc(&ts);\n" + "\tif (ts[1].pc != 9) { return 1; };\n" + "\tbump(&ts);\n" + "\tif (ts[1].pc != 10) { return 2; };\n" + "\tif (ts[0].pc != 1) { return 3; };\n" + "\treturn 47;\n" + "};\n", + 47, NULL }, + + /* The remaining compound arms: ANDQ/XORQ, and BOTH >>= forms — + * SARQ (signed, sign bit must replicate) vs SHRQ (unsigned). */ + { "compound_bits_shr", + "package main;\n" + "type t = struct { s: int, u: size, n: i64, m: i64 };\n" + "fn ands(ts: *[]t, i: size) void = { (*ts)[i].m &= 6; };\n" + "fn xors(ts: *[]t, i: size) void = { (*ts)[i].m ^= 3; };\n" + "fn sars(ts: *[]t, i: size) void = { (*ts)[i].s >>= 2; };\n" + "fn shrs(ts: *[]t, i: size) void = { (*ts)[i].u >>= 1; };\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { s = -16, u = 8, n = 0, m = 7 });\n" + "\tands(&ts, 0);\n" + "\tif (ts[0].m != 6) { return 1; };\n" + "\txors(&ts, 0);\n" + "\tif (ts[0].m != 5) { return 2; };\n" + "\tsars(&ts, 0);\n" + "\tif (ts[0].s != -4) { return 3; };\n" + "\tshrs(&ts, 0);\n" + "\tif (ts[0].u != 4) { return 4; };\n" + "\treturn 48;\n" + "};\n", + 48, NULL }, + + /* Narrow-width compounds: the widths rows above only exercise + * plain `=` (fldstoreop); compounds also take the fldloadop + * narrow LOAD (MOVSBQ/MOVZBQ/MOVSWQ/MOVZWQ/MOVSXD/MOVL) — i8/i16 + * negatives pin the sign-extension, u8 250 pins zero-extension. */ + { "compound_narrow", + "package main;\n" + "type t = struct { a: i8, b: u8, c: i16, d: u16, e: i32, f: u32 };\n" + "fn adda(ts: *[]t, i: size) void = { (*ts)[i].a += 1i8; };\n" + "fn addb(ts: *[]t, i: size) void = { (*ts)[i].b += 200u8; };\n" + "fn subc(ts: *[]t, i: size) void = { (*ts)[i].c -= 300i16; };\n" + "fn shld(ts: *[]t, i: size) void = { (*ts)[i].d <<= 3u16; };\n" + "fn mule(ts: *[]t, i: size) void = { (*ts)[i].e *= -3i32; };\n" + "fn orf(ts: *[]t, i: size) void = { (*ts)[i].f |= 8u32; };\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { a = -5i8, b = 50u8, c = 100i16, d = 2u16, e = 7i32, f = 5u32 });\n" + "\tadda(&ts, 0);\n" + "\tif (ts[0].a != -4i8) { return 1; };\n" + "\taddb(&ts, 0);\n" + "\tif (ts[0].b != 250u8) { return 2; };\n" + "\tsubc(&ts, 0);\n" + "\tif (ts[0].c != -200i16) { return 3; };\n" + "\tshld(&ts, 0);\n" + "\tif (ts[0].d != 16u16) { return 4; };\n" + "\tmule(&ts, 0);\n" + "\tif (ts[0].e != -21i32) { return 5; };\n" + "\torf(&ts, 0);\n" + "\tif (ts[0].f != 13u32) { return 6; };\n" + "\treturn 49;\n" + "};\n", + 49, NULL }, + + /* Slice field, same 3-word path as str. Field init goes through + * an ident-bound empty slice, NOT `xs = []`: a bare empty-slice + * literal as a struct-lit field inside an append arg leaves a + * garbage header on MASTER too (pre-existing, task #9 family) — + * this row pins the F6 store, not that bug. */ + { "slice_field_member", + "package main;\n" + "type t = struct { pc: size, xs: []i64 };\n" + "fn setxs(ts: *[]t, i: size, s: []i64) void = { (*ts)[i].xs = s; };\n" + "export fn main() i32 = {\n" + "\tlet empty: []i64 = [];\n" + "\tlet ts: []t = [];\n" + "\tappend(ts, t { pc = 1, xs = empty });\n" + "\tappend(ts, t { pc = 2, xs = empty });\n" + "\tlet s: []i64 = [];\n" + "\tappend(s, 11);\n" + "\tappend(s, 22);\n" + "\tsetxs(&ts, 1, s);\n" + "\tif (ts[1].xs.len != 2) { return 1; };\n" + "\tif (ts[1].xs[1] != 22) { return 2; };\n" + "\tif (ts[0].xs.len != 0) { return 3; };\n" + "\tif (ts[0].pc != 1) { return 4; };\n" + "\tif (ts[1].pc != 2) { return 5; };\n" + "\treturn 50;\n" + "};\n", + 50, NULL }, + + /* Ident-rooted lvalues stay with the enumerated arms (the + * resolver must never fire for them) — this row runtime-pins the + * shapes the 78-probe asm-neutrality sweep diffed statically. */ + { "neutral_ident_bases", + "package main;\n" + "type t = struct { pc: size, matched: bool };\n" + "export fn main() i32 = {\n" + "\tlet x: t = t { pc = 1, matched = false };\n" + "\tx.pc = 5;\n" + "\tx.pc += 2;\n" + "\tlet a: [2]t;\n" + "\ta[0].pc = 3; a[1].pc = 4;\n" + "\ta[1].pc += 10;\n" + "\tlet p: *t = &x;\n" + "\tp.pc = 20;\n" + "\tp.pc += 1;\n" + "\tif (x.pc != 21) { return 1; };\n" + "\tif (a[1].pc != 14) { return 2; };\n" + "\tif (a[0].pc != 3) { return 3; };\n" + "\treturn 51;\n" + "};\n", + 51, NULL }, + + /* The fold-2b 40B capture store (p7b/p7_composed) — wired in a + * follow-up resolver commit; until then it must die LOUD, never + * the pre-C1 silent drop. */ + { "reject_aggregate", + "package main;\n" + "type capture = struct { content: str, start: size, end: size };\n" + "type t = struct { pc: size, cap: capture };\n" + "fn setcap(ts: *[]t, i: size) void = {\n" + "\t(*ts)[i].cap = capture { content = \"x\", start = 1, end = 2 };\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\tsetcap(&ts, 0);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "assign-resolver: aggregate field not wired (rule-7)" }, + + { "reject_float", + "package main;\n" + "type t = struct { pc: size, f: f64 };\n" + "fn setf(ts: *[]t, i: size) void = { (*ts)[i].f = 1.5; };\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\tsetf(&ts, 0);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "assign-resolver: float field not wired (rule-7)" }, + + { "reject_tagged", + "package main;\n" + "type v = (i64 | bool);\n" + "type t = struct { pc: size, tg: v };\n" + "fn settg(ts: *[]t, i: size) void = { (*ts)[i].tg = 5; };\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\tsettg(&ts, 0);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "assign-resolver: tagged field not wired (rule-7)" }, + + /* Compound on a str field reaches cgen (the checker admits it) — + * it must hit the resolver's own loud stop, not the silent tail. */ + { "reject_str_compound", + "package main;\n" + "type t = struct { pc: size, name: str };\n" + "fn addname(ts: *[]t, i: size) void = { (*ts)[i].name += \"x\"; };\n" + "export fn main() i32 = {\n" + "\tlet ts: []t = [];\n" + "\taddname(&ts, 0);\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, + "assign-resolver: compound on str/slice field not wired (rule-7)" }, + + /* An N_DOT lvalue the C1 resolver does not wire (index base is a + * dot chain, not a deref) — pre-C1 this was a SILENT no-op store; + * the loud dispatch tail is the close-by-construction net. */ + { "reject_tail", + "package main;\n" + "type t = struct { pc: size, matched: bool };\n" + "type holder = struct { arr: [3]t, n: i64 };\n" + "export fn main() i32 = {\n" + "\tlet h: holder = holder { n = 0, ... };\n" + "\th.arr[1].pc = 5;\n" + "\treturn 0;\n" + "};\n", + BUILD_FAIL, "unsupported assign target shape" }, +}; + +/* 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/plad_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/plad_%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 resolver emission is written fresh on both + * sides, so this is the converged-by-construction gate: any drift in + * the idx-scale/spill/deref 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/plad_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/plad_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/plad_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, "placeaddr_store: 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, + "placeaddr_store[%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, + "placeaddr_store: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("placeaddr_store: %d fixtures passed\n", total); + return 0; +}