From 3986818172070fbe5c3f4c2ecd955464728cd284 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 26 May 2026 22:27:32 +0900 Subject: [PATCH] wcc: compound-assign load-op-store for indexed + chained-ptr-field lvalues (#133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both stages had silent miscompiles on compound assignment for two shapes: indexed lvalue (`arr[i] OP= v`) and chained-pointer-field (`d.fld.fld OP= v` through a *struct chain). The cstage N_INDEX-lhs branch did not gate on TK_ASSIGN and silently DEMOTED compound ops to plain stores (RHS stored, no load, no op). The wwstage equivalents silently DROPPED the line entirely (no instructions emitted). The chained-pointer-field compound template at cgen.c:3281-3317 also silently identity-stored on unwired compound ops (SLASHEQ / PERCENTEQ / LSHIFTEQ / RSHIFTEQ all fell to the switch default = no-op = load, pop RHS, store ORIGINAL value back) and silently no-op'd on float / str / slice / tagged element compound; its wwstage twin at cgenexpr.ww:5471 only handled TK_ASSIGN, dropping any chained-ptr-field compound entirely. Wire all 10 integer compound ops (PLUSEQ MINUSEQ STAREQ AMPEQ PIPEEQ CARETEQ SLASHEQ PERCENTEQ LSHIFTEQ RSHIFTEQ) at all 4 sites in both stages: SLASHEQ/PERCENTEQ via CQO+IDIVQ (signed) or zero-DX+DIVQ (unsigned), with PERCENTEQ moving DX->AX for the result; LSHIFTEQ/ RSHIFTEQ via SHLQ/SHRQ on CX (rhs already in CX after the pop). Signedness keyed off the field/element type via type_isunsigned / typeisunsigned. Float / str / slice / tagged element compound now LOUD-ERRORS at codegen with a distinct per-site diagnostic citing #133/rule-7 instead of silent fall-through. Site 3 (the wwstage chained-pointer-field compound) is ADDED FROM SCRATCH alongside the existing TK_ASSIGN-only arm — pre-#133 wwstage emitted zero instructions for any `d.i.v OP= v` shape, a rule-10 silent divergence from the cstage which handled the same shape correctly. Multi-fix carve-out (rule 11): the 10 wired ops at 4 sites + hard-error gate on 4 unwired payload kinds at 4 sites are ONE silent-misbehavior class closure on indexed/chained-ptr-field compound assignment. Splitting would muddle bisect on related cgen surfaces — the wired ops, the hard-error gate, and the rule-10 cstage/wwstage symmetry are inseparable correctness facts at each site. The inherited template default-break silent-identity (cgen.c:3281-3317) was the originating class root; close it everywhere or leave the class open. 948_idx_compound_run: 21 rows total. 11 runtime+byte-id rows for the original 6 ops on u8/i32/i64/u32 array bases and one slice base, with a plain-assign control row asserting the ASSIGN path is byte-id- unchanged. 7 new runtime+byte-id rows for SLASHEQ/PERCENTEQ on signed i32 + unsigned u32, LSHIFTEQ on i32, RSHIFTEQ on signed-positive i32 and unsigned u32. 3 builderr rows (he_float_indexed, he_str_indexed, he_float_chained_ptr) asserting both stages exit non-zero AND stderr carries the cited diagnostic substring (rule-7 — never silent). Mirrors 945_tuple_nary's builderr/experr pattern. Bootstrap NEUTRAL — `grep -rE '\][[:space:]]*(\+=|-=|\*=|/=|&=|\|=|\^=|<<=|>>=)' lib/ selfhost/` (excluding combined.ww) returns ZERO existing callers for the indexed compound shape, and the chained-ptr-field compound shape was silent- no-op in wwstage pre-fix (no working caller possible). 990-997 byte- id gates green, 994 explicit confirms 18 corpus inputs identical pre/post. combined.ww (w6c + wwdump) regen deterministic across re-touch+rebuild. A_SARQ is not in w6a's opcode table; signed RSHIFTEQ uses SHRQ at all 4 sites for parity with the pre-existing deref-lvalue compound site (TK_RSHIFTEQ→A_SHRQ at cgen.c:4145). Documented technical debt filed as #136 — pre-existing concern that a fix would need w6a opcode addition + cgen sweep across every SHRQ-for-signed-RSHIFT site, out of scope for this fold. --- Makefile | 6 + cmd/w6c/cgen.c | 201 ++++++++++++- selfhost/cmd/w6c/main.combined.ww | 253 ++++++++++++++++ selfhost/cmd/wcc/cgenexpr.ww | 253 ++++++++++++++++ selfhost/cmd/wwdump/main.combined.ww | 253 ++++++++++++++++ test/wcc/948_idx_compound_run.c | 421 +++++++++++++++++++++++++++ 6 files changed, 1383 insertions(+), 4 deletions(-) create mode 100644 test/wcc/948_idx_compound_run.c diff --git a/Makefile b/Makefile index c626af3a..e0aad411 100644 --- a/Makefile +++ b/Makefile @@ -330,6 +330,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_checked_run \ $(BIN)/test_floatarr_run \ $(BIN)/test_deref_narrow_run \ + $(BIN)/test_idx_compound_run \ $(BIN)/test_f64cgen_run \ $(BIN)/test_f64crossmod_run \ $(BIN)/test_tuprecv_run \ @@ -1094,6 +1095,11 @@ $(BIN)/test_deref_narrow_run: test/wcc/947_deref_narrow_run.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_idx_compound_run: test/wcc/948_idx_compound_run.c $(BIN)/ww \ + $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_f64cgen_run: test/wcc/951_f64cgen_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index eaf5dec8..b9148db2 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -3280,8 +3280,41 @@ cgexpr(Cg *c, Node *n, Local *locals) } /* compound op: AX=rhs → push; eval ptr → push; * load old field → AX; pop ptr→BX, rhs→CX; - * combine; store. Float/str compound on a - * chained pointer-field is not wired. */ + * combine; store. #133-expanded: all 10 integer + * compound ops wired; SLASHEQ/PERCENTEQ via + * CQO+IDIV (signed) or zero-DX+DIV (unsigned); + * LSHIFTEQ/RSHIFTEQ via SHLQ/SHRQ on CX. Float / + * str / slice / tagged element compound hard- + * errors LOUD (rule-7 — replaces prior silent + * fall-through-to-default-break). Note: A_SARQ + * isn't in w6a today; signed RSHIFTEQ uses SHRQ + * for parity with the pre-existing deref-lvalue + * compound site (TK_RSHIFTEQ→A_SHRQ below); + * signed-RSHIFT-on-negatives separate concern, + * filed as #136, out of scope for #133. */ + { + int compound_isf32 = 0; + if (fld_isfloat(ft, &compound_isf32)) + fatal("chained-ptr-field compound on " + "float element not wired " + "(#133/rule-7); field='%s'", + n->lhs->str); + Type *fchk = type_chase_named(ft); + if (fchk && fchk->kind == TY_STR) + fatal("chained-ptr-field compound on " + "str element not wired " + "(#133/rule-7); field='%s'", + n->lhs->str); + if (fchk && fchk->kind == TY_SLICE) + fatal("chained-ptr-field compound on " + "slice element not wired " + "(#133/rule-7); field='%s'", + n->lhs->str); + if (fchk && fchk->kind == TY_TAGGED) + fatal("chained-ptr-field compound on " + "tagged element not wired " + "(#133/rule-7); field='%s'", + n->lhs->str); cgexpr(c, n->rhs, locals); ins1(c, A_PUSHQ, areg(D_AX)); cgexpr(c, n->lhs->lhs, locals); @@ -3291,6 +3324,7 @@ cgexpr(Cg *c, Node *n, Local *locals) areg(D_AX)); ins1(c, A_POPQ, areg(D_BX)); 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)); @@ -3310,10 +3344,43 @@ cgexpr(Cg *c, Node *n, Local *locals) case TK_CARETEQ: ins2(c, A_XORQ, areg(D_CX), areg(D_AX)); break; - default: 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, A_SHRQ, areg(D_CX), + areg(D_AX)); + break; + default: + fatal("chained-ptr-field compound: " + "unknown op tk=%d (#133/rule-7); " + "field='%s'", n->op, + n->lhs->str); } ins2(c, store_op, areg(D_AX), amem(D_BX, foff)); + } break; } } @@ -3775,7 +3842,7 @@ cgexpr(Cg *c, Node *n, Local *locals) } break; } - if (is_arr || is_sl || is_ptr) { + if ((is_arr || is_sl || is_ptr) && n->op == TK_ASSIGN) { cgexpr(c, n->rhs, locals); /* AX=ptr (BX=len,CX=cap if str) */ /* str/slice: stash cap+len so all three store * (#1/Phase 3). */ @@ -3850,6 +3917,132 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, store_op, areg(D_AX), amem(D_BX, 0)); break; } + /* Compound assign on an indexed scalar element + * (`arr[i] OP= v`). Pre-#133 this branch had no TK_ASSIGN + * gate above and silently DEMOTED compound ops to plain + * stores (no load, no op). Mirror the chained-pointer- + * field compound template at cgen.c:3281-3317: same + * address computation as the ASSIGN body above, then + * load_op (BX)→AX, pop rhs→CX, combine, store_op. + * #133-expanded: all 10 integer compound ops wired; + * float/str/slice/tagged element compound HARD-ERRORS + * loud (rule-7, replaces prior silent fall-through). + * Signed RSHIFTEQ uses SHRQ — parity with the pre- + * existing deref-lvalue compound site (TK_RSHIFTEQ→ + * A_SHRQ below). A_SARQ not in w6a; pre-existing + * signed-RSHIFT-on-negatives is filed as #136, out of + * scope for #133. */ + if ((is_arr || is_sl || is_ptr) && n->op != TK_ASSIGN) { + if (elem_is_str) + fatal("indexed-lvalue compound on " + "str element not wired " + "(#133/rule-7)"); + if (elem_is_slice) + fatal("indexed-lvalue compound on " + "slice element not wired " + "(#133/rule-7)"); + if (elem_tagged) + fatal("indexed-lvalue compound on " + "tagged element not wired " + "(#133/rule-7)"); + if (esub && type_isfloat(esub)) + fatal("indexed-lvalue compound on " + "float element not wired " + "(#133/rule-7)"); + cgexpr(c, n->rhs, locals); + ins1(c, A_PUSHQ, areg(D_AX)); + cgexpr(c, n->lhs->rhs, 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 (base->kind == N_IDENT) { + int off = localfind(locals, base->str); + int isglobal = (off == 0) && + let_islet(base->str); + if (isglobal && is_arr) { + ins2(c, A_LEAQ, + masym(c, base->str), + areg(D_BX)); + } else if (isglobal) { + ins2(c, A_MOVQ, + masym(c, base->str), + areg(D_BX)); + } else if (is_arr) { + ins2(c, A_LEAQ, + amem(D_BP, off), + areg(D_BX)); + } else { + ins2(c, A_MOVQ, + amem(D_BP, off), + areg(D_BX)); + } + } else { + cgexpr(c, base, locals); + ins2(c, A_MOVQ, areg(D_AX), areg(D_BX)); + } + ins1(c, A_POPQ, areg(D_AX)); + ins2(c, A_ADDQ, areg(D_AX), areg(D_BX)); + int load_op = fldloadop(esub, esz); + ins2(c, load_op, amem(D_BX, 0), areg(D_AX)); + ins1(c, A_POPQ, areg(D_CX)); + int unsignd_c = esub && type_isunsigned(esub); + 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_c) + ins2(c, A_MOVQ, aimm(0), + areg(D_DX)); + else + ins0(c, A_CQO); + ins1(c, unsignd_c ? A_DIVQ : A_IDIVQ, + areg(D_CX)); + break; + case TK_PERCENTEQ: + if (unsignd_c) + ins2(c, A_MOVQ, aimm(0), + areg(D_DX)); + else + ins0(c, A_CQO); + ins1(c, unsignd_c ? 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, A_SHRQ, areg(D_CX), + areg(D_AX)); + break; + default: + fatal("indexed-lvalue compound: " + "unknown op tk=%d (#133/rule-7)", + n->op); + } + int store_op_c = fldstoreop(esub, esz); + ins2(c, store_op_c, areg(D_AX), amem(D_BX, 0)); + break; + } } /* Plain `r = expr;` where r is a tagged-union local. * Delegates to cg_widen_tagged_store: covers nullable fold, diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 9b6a5fea..16e79b52 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -18639,6 +18639,178 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\tAX, (BX)\n"); return; }; + // Compound assign on an indexed scalar element + // (`arr[i] OP= v`). Pre-#133 the outer `if (n.op == + // TK_ASSIGN)` had no else and non-ASSIGN ops fell off + // the cgassign function emitting NOTHING — silent + // no-op. Mirror the chained-pointer-field compound + // template at cmd/w6c/cgen.c:3281-3317: same address + // computation as the ASSIGN arm above, then + // tnodeloadop(BX)→AX, POP rhs→CX, combine, tnodestoreop. + // Float / str / slice / tagged element compound stays + // unwired — cstage's compound template never carried + // those payload kinds. Same shape gate as the cstage + // branch (cgen.c #133). + if (n.op != tkind.TK_ASSIGN) { + let base: *node = lhs.lhs; + let idx: *node = lhs.rhs; + let esz: i32 = 8; + let baselocal: *local = nil; + let isglobalarr: bool = false; + let isglobalptr: bool = false; + let globalname: str; + globalname.ptr = nil; globalname.len = 0; + let elemtn: *node = nil; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bn: str = base.str; + baselocal = localfindnode(c, bn); + if (baselocal != nil) { + esz = elemsizeofc(c, baselocal.tnode); + let btn: *node = baselocal.tnode; + if (btn != nil) { + let bk: nkind = btn.kind; + if (bk == nkind.N_TARRAY) { elemtn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { elemtn = btn.lhs; }; + if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; + }; + } else { + let tn: *node = letvartnode(c, bn); + if (tn != nil) { + if (tn.kind == nkind.N_TARRAY) { + isglobalarr = true; + globalname = bn; + esz = elemsizeofc(c, tn); + elemtn = tn.lhs; + }; + if (tn.kind == nkind.N_TPTR) { + isglobalptr = true; + globalname = bn; + esz = elemsizeofc(c, tn); + elemtn = tn.lhs; + }; + }; + }; + } else { if (base.kind == nkind.N_DOT) { + let dt: *tinfo = lhs.type_: *tinfo; + if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; + } else { if (base.kind == nkind.N_INDEX) { + let et: *tinfo = lhs.type_: *tinfo; + if (et != nil) { + esz = et.size: i32; + elemtn = lhs; + }; + };};}; + }; + // #133-expanded: hard-error unwired payload kinds + // LOUD (rule-7) — replaces prior silent skip. + if (elemtn != nil) { + if (istaggedtype(c, elemtn)) { + let msg: str = "indexed-lvalue compound on tagged element not wired (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + if (isstrtype(c, elemtn)) { + let msg: str = "indexed-lvalue compound on str element not wired (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + if (isslicetype(c, elemtn)) { + let msg: str = "indexed-lvalue compound on slice element not wired (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + if (isfloattype(c, elemtn)) { + let msg: str = "indexed-lvalue compound on float element not wired (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + }; + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tAX\n"); + 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 (isglobalarr) { + emitline("\tLEAQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (isglobalptr) { + emitline("\tMOVQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (baselocal != nil) { + let tn: *node = baselocal.tnode; + let isarray: bool = false; + if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarray = true; }; }; + if (isarray) { + emitline("\tLEAQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + }; + } else { + cgexpr(c, base); + emitline("\tMOVQ\tAX, BX\n"); + };};}; + emitline("\tPOPQ\tAX\n"); + emitline("\tADDQ\tAX, BX\n"); + let lop: str = tnodeloadop(c, elemtn, esz); + emitline("\t"); + emitline(lop); + emitline("\t(BX), AX\n"); + emitline("\tPOPQ\tCX\n"); + // #133-expanded: all 10 integer compound ops wired. + // SLASHEQ/PERCENTEQ: CQO+IDIVQ (signed) or zero-DX+ + // DIVQ (unsigned). LSHIFTEQ/RSHIFTEQ: SHLQ/SHRQ via + // CX. Signedness from elemtn.type_; signed RSHIFTEQ + // uses SHRQ (cstage parity — A_SARQ not in w6a, + // pre-existing signed-RSHIFT concern out of scope). + let unsignd_c: bool = false; + if (elemtn != nil) { + if (elemtn.type_ != nil) { + unsignd_c = typeisunsigned(elemtn.type_: *tinfo); + }; + }; + 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_c) { 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_c) { 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) { emitline("\tSHRQ\tCX, AX\n"); wired = true; }; + if (!wired) { + let msg: str = "indexed-lvalue compound: unknown compound op (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + let sop: str = tnodestoreop(c, elemtn, esz); + emitline("\t"); + emitline(sop); + emitline("\tAX, (BX)\n"); + return; + }; }; }; // `arr[i].field = v`: N_DOT lhs whose lhs is N_INDEX. Symmetric @@ -19704,6 +19876,87 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\n"); return; }; + // #133-expanded site 3: chained-pointer- + // field compound. Pre-#133-expanded the + // wwstage chained-DOT-spine branch only + // handled TK_ASSIGN; compound ops on a + // chained-*struct.field shape (e.g. + // `d.i.v += 7`) silently emitted nothing. + // cstage cgen.c:3281-3317 handles this + // (now-expanded for the same 10 ops + + // hard-errors); this is its rule-10 twin. + // All 10 integer compound ops wired; + // float/str/slice/tagged field-type + // hard-errors LOUD. Signed RSHIFTEQ uses + // SHRQ (cstage parity, A_SARQ absent). + if (n.op != tkind.TK_ASSIGN) { + if (typeisstr(ft)) { + let m: str = "chained-ptr-field compound on str element not wired (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + if (typeisslice(ft)) { + let m: str = "chained-ptr-field compound on slice element not wired (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + if (typeisfloat(ft)) { + let m: str = "chained-ptr-field compound on float element not wired (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + if (typeistagged(ft)) { + let m: str = "chained-ptr-field compound on tagged element not wired (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tAX\n"); + cgexpr(c, base); + emitline("\tPUSHQ\tAX\n"); + let fsz: i32 = ft.slotsize: i32; + let unsignd_f: bool = typeisunsigned(ft); + let lopf: str = loadopsz(!unsignd_f, fsz); + emitline("\t"); + emitline(lopf); + emitline("\t"); + emitdispreg(tf.offset: i64, "AX"); + emitline(", AX\n"); + emitline("\tPOPQ\tBX\n"); + emitline("\tPOPQ\tCX\n"); + let wired_f: bool = false; + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_SLASHEQ) { + if (unsignd_f) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } + else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; + wired_f = true; + }; + if (n.op == tkind.TK_PERCENTEQ) { + if (unsignd_f) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } + else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; + emitline("\tMOVQ\tDX, AX\n"); + wired_f = true; + }; + if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_RSHIFTEQ) { emitline("\tSHRQ\tCX, AX\n"); wired_f = true; }; + if (!wired_f) { + let m: str = "chained-ptr-field compound: unknown op (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + let sopf: str = tnodestoreop(c, n.rhs, fsz); + emitline("\t"); + emitline(sopf); + emitline("\tAX, "); + emitdispreg(tf.offset: i64, "BX"); + emitline("\n"); + return; + }; }; tf = tf.tnext; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index c4f55d40..7243fbf2 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -4342,6 +4342,178 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\tAX, (BX)\n"); return; }; + // Compound assign on an indexed scalar element + // (`arr[i] OP= v`). Pre-#133 the outer `if (n.op == + // TK_ASSIGN)` had no else and non-ASSIGN ops fell off + // the cgassign function emitting NOTHING — silent + // no-op. Mirror the chained-pointer-field compound + // template at cmd/w6c/cgen.c:3281-3317: same address + // computation as the ASSIGN arm above, then + // tnodeloadop(BX)→AX, POP rhs→CX, combine, tnodestoreop. + // Float / str / slice / tagged element compound stays + // unwired — cstage's compound template never carried + // those payload kinds. Same shape gate as the cstage + // branch (cgen.c #133). + if (n.op != tkind.TK_ASSIGN) { + let base: *node = lhs.lhs; + let idx: *node = lhs.rhs; + let esz: i32 = 8; + let baselocal: *local = nil; + let isglobalarr: bool = false; + let isglobalptr: bool = false; + let globalname: str; + globalname.ptr = nil; globalname.len = 0; + let elemtn: *node = nil; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bn: str = base.str; + baselocal = localfindnode(c, bn); + if (baselocal != nil) { + esz = elemsizeofc(c, baselocal.tnode); + let btn: *node = baselocal.tnode; + if (btn != nil) { + let bk: nkind = btn.kind; + if (bk == nkind.N_TARRAY) { elemtn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { elemtn = btn.lhs; }; + if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; + }; + } else { + let tn: *node = letvartnode(c, bn); + if (tn != nil) { + if (tn.kind == nkind.N_TARRAY) { + isglobalarr = true; + globalname = bn; + esz = elemsizeofc(c, tn); + elemtn = tn.lhs; + }; + if (tn.kind == nkind.N_TPTR) { + isglobalptr = true; + globalname = bn; + esz = elemsizeofc(c, tn); + elemtn = tn.lhs; + }; + }; + }; + } else { if (base.kind == nkind.N_DOT) { + let dt: *tinfo = lhs.type_: *tinfo; + if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; + } else { if (base.kind == nkind.N_INDEX) { + let et: *tinfo = lhs.type_: *tinfo; + if (et != nil) { + esz = et.size: i32; + elemtn = lhs; + }; + };};}; + }; + // #133-expanded: hard-error unwired payload kinds + // LOUD (rule-7) — replaces prior silent skip. + if (elemtn != nil) { + if (istaggedtype(c, elemtn)) { + let msg: str = "indexed-lvalue compound on tagged element not wired (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + if (isstrtype(c, elemtn)) { + let msg: str = "indexed-lvalue compound on str element not wired (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + if (isslicetype(c, elemtn)) { + let msg: str = "indexed-lvalue compound on slice element not wired (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + if (isfloattype(c, elemtn)) { + let msg: str = "indexed-lvalue compound on float element not wired (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + }; + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tAX\n"); + 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 (isglobalarr) { + emitline("\tLEAQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (isglobalptr) { + emitline("\tMOVQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (baselocal != nil) { + let tn: *node = baselocal.tnode; + let isarray: bool = false; + if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarray = true; }; }; + if (isarray) { + emitline("\tLEAQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + }; + } else { + cgexpr(c, base); + emitline("\tMOVQ\tAX, BX\n"); + };};}; + emitline("\tPOPQ\tAX\n"); + emitline("\tADDQ\tAX, BX\n"); + let lop: str = tnodeloadop(c, elemtn, esz); + emitline("\t"); + emitline(lop); + emitline("\t(BX), AX\n"); + emitline("\tPOPQ\tCX\n"); + // #133-expanded: all 10 integer compound ops wired. + // SLASHEQ/PERCENTEQ: CQO+IDIVQ (signed) or zero-DX+ + // DIVQ (unsigned). LSHIFTEQ/RSHIFTEQ: SHLQ/SHRQ via + // CX. Signedness from elemtn.type_; signed RSHIFTEQ + // uses SHRQ (cstage parity — A_SARQ not in w6a, + // pre-existing signed-RSHIFT concern out of scope). + let unsignd_c: bool = false; + if (elemtn != nil) { + if (elemtn.type_ != nil) { + unsignd_c = typeisunsigned(elemtn.type_: *tinfo); + }; + }; + 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_c) { 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_c) { 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) { emitline("\tSHRQ\tCX, AX\n"); wired = true; }; + if (!wired) { + let msg: str = "indexed-lvalue compound: unknown compound op (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + let sop: str = tnodestoreop(c, elemtn, esz); + emitline("\t"); + emitline(sop); + emitline("\tAX, (BX)\n"); + return; + }; }; }; // `arr[i].field = v`: N_DOT lhs whose lhs is N_INDEX. Symmetric @@ -5407,6 +5579,87 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\n"); return; }; + // #133-expanded site 3: chained-pointer- + // field compound. Pre-#133-expanded the + // wwstage chained-DOT-spine branch only + // handled TK_ASSIGN; compound ops on a + // chained-*struct.field shape (e.g. + // `d.i.v += 7`) silently emitted nothing. + // cstage cgen.c:3281-3317 handles this + // (now-expanded for the same 10 ops + + // hard-errors); this is its rule-10 twin. + // All 10 integer compound ops wired; + // float/str/slice/tagged field-type + // hard-errors LOUD. Signed RSHIFTEQ uses + // SHRQ (cstage parity, A_SARQ absent). + if (n.op != tkind.TK_ASSIGN) { + if (typeisstr(ft)) { + let m: str = "chained-ptr-field compound on str element not wired (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + if (typeisslice(ft)) { + let m: str = "chained-ptr-field compound on slice element not wired (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + if (typeisfloat(ft)) { + let m: str = "chained-ptr-field compound on float element not wired (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + if (typeistagged(ft)) { + let m: str = "chained-ptr-field compound on tagged element not wired (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tAX\n"); + cgexpr(c, base); + emitline("\tPUSHQ\tAX\n"); + let fsz: i32 = ft.slotsize: i32; + let unsignd_f: bool = typeisunsigned(ft); + let lopf: str = loadopsz(!unsignd_f, fsz); + emitline("\t"); + emitline(lopf); + emitline("\t"); + emitdispreg(tf.offset: i64, "AX"); + emitline(", AX\n"); + emitline("\tPOPQ\tBX\n"); + emitline("\tPOPQ\tCX\n"); + let wired_f: bool = false; + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_SLASHEQ) { + if (unsignd_f) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } + else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; + wired_f = true; + }; + if (n.op == tkind.TK_PERCENTEQ) { + if (unsignd_f) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } + else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; + emitline("\tMOVQ\tDX, AX\n"); + wired_f = true; + }; + if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_RSHIFTEQ) { emitline("\tSHRQ\tCX, AX\n"); wired_f = true; }; + if (!wired_f) { + let m: str = "chained-ptr-field compound: unknown op (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + let sopf: str = tnodestoreop(c, n.rhs, fsz); + emitline("\t"); + emitline(sopf); + emitline("\tAX, "); + emitdispreg(tf.offset: i64, "BX"); + emitline("\n"); + return; + }; }; tf = tf.tnext; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 2fdd2de6..bd04e7b6 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -18639,6 +18639,178 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\tAX, (BX)\n"); return; }; + // Compound assign on an indexed scalar element + // (`arr[i] OP= v`). Pre-#133 the outer `if (n.op == + // TK_ASSIGN)` had no else and non-ASSIGN ops fell off + // the cgassign function emitting NOTHING — silent + // no-op. Mirror the chained-pointer-field compound + // template at cmd/w6c/cgen.c:3281-3317: same address + // computation as the ASSIGN arm above, then + // tnodeloadop(BX)→AX, POP rhs→CX, combine, tnodestoreop. + // Float / str / slice / tagged element compound stays + // unwired — cstage's compound template never carried + // those payload kinds. Same shape gate as the cstage + // branch (cgen.c #133). + if (n.op != tkind.TK_ASSIGN) { + let base: *node = lhs.lhs; + let idx: *node = lhs.rhs; + let esz: i32 = 8; + let baselocal: *local = nil; + let isglobalarr: bool = false; + let isglobalptr: bool = false; + let globalname: str; + globalname.ptr = nil; globalname.len = 0; + let elemtn: *node = nil; + if (base != nil) { + if (base.kind == nkind.N_IDENT) { + let bn: str = base.str; + baselocal = localfindnode(c, bn); + if (baselocal != nil) { + esz = elemsizeofc(c, baselocal.tnode); + let btn: *node = baselocal.tnode; + if (btn != nil) { + let bk: nkind = btn.kind; + if (bk == nkind.N_TARRAY) { elemtn = btn.lhs; }; + if (bk == nkind.N_TSLICE) { elemtn = btn.lhs; }; + if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; + }; + } else { + let tn: *node = letvartnode(c, bn); + if (tn != nil) { + if (tn.kind == nkind.N_TARRAY) { + isglobalarr = true; + globalname = bn; + esz = elemsizeofc(c, tn); + elemtn = tn.lhs; + }; + if (tn.kind == nkind.N_TPTR) { + isglobalptr = true; + globalname = bn; + esz = elemsizeofc(c, tn); + elemtn = tn.lhs; + }; + }; + }; + } else { if (base.kind == nkind.N_DOT) { + let dt: *tinfo = lhs.type_: *tinfo; + if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; + } else { if (base.kind == nkind.N_INDEX) { + let et: *tinfo = lhs.type_: *tinfo; + if (et != nil) { + esz = et.size: i32; + elemtn = lhs; + }; + };};}; + }; + // #133-expanded: hard-error unwired payload kinds + // LOUD (rule-7) — replaces prior silent skip. + if (elemtn != nil) { + if (istaggedtype(c, elemtn)) { + let msg: str = "indexed-lvalue compound on tagged element not wired (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + if (isstrtype(c, elemtn)) { + let msg: str = "indexed-lvalue compound on str element not wired (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + if (isslicetype(c, elemtn)) { + let msg: str = "indexed-lvalue compound on slice element not wired (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + if (isfloattype(c, elemtn)) { + let msg: str = "indexed-lvalue compound on float element not wired (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + }; + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tAX\n"); + 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 (isglobalarr) { + emitline("\tLEAQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (isglobalptr) { + emitline("\tMOVQ\t"); + emitsymname(c, globalname); + emitline("(SB), BX\n"); + } else { if (baselocal != nil) { + let tn: *node = baselocal.tnode; + let isarray: bool = false; + if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarray = true; }; }; + if (isarray) { + emitline("\tLEAQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + } else { + emitline("\tMOVQ\t"); + emitoff(baselocal.off: i64); + emitline("(BP), BX\n"); + }; + } else { + cgexpr(c, base); + emitline("\tMOVQ\tAX, BX\n"); + };};}; + emitline("\tPOPQ\tAX\n"); + emitline("\tADDQ\tAX, BX\n"); + let lop: str = tnodeloadop(c, elemtn, esz); + emitline("\t"); + emitline(lop); + emitline("\t(BX), AX\n"); + emitline("\tPOPQ\tCX\n"); + // #133-expanded: all 10 integer compound ops wired. + // SLASHEQ/PERCENTEQ: CQO+IDIVQ (signed) or zero-DX+ + // DIVQ (unsigned). LSHIFTEQ/RSHIFTEQ: SHLQ/SHRQ via + // CX. Signedness from elemtn.type_; signed RSHIFTEQ + // uses SHRQ (cstage parity — A_SARQ not in w6a, + // pre-existing signed-RSHIFT concern out of scope). + let unsignd_c: bool = false; + if (elemtn != nil) { + if (elemtn.type_ != nil) { + unsignd_c = typeisunsigned(elemtn.type_: *tinfo); + }; + }; + 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_c) { 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_c) { 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) { emitline("\tSHRQ\tCX, AX\n"); wired = true; }; + if (!wired) { + let msg: str = "indexed-lvalue compound: unknown compound op (#133/rule-7)\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + let sop: str = tnodestoreop(c, elemtn, esz); + emitline("\t"); + emitline(sop); + emitline("\tAX, (BX)\n"); + return; + }; }; }; // `arr[i].field = v`: N_DOT lhs whose lhs is N_INDEX. Symmetric @@ -19704,6 +19876,87 @@ fn cgassign(c: *cgen, n: *node) void = { emitline("\n"); return; }; + // #133-expanded site 3: chained-pointer- + // field compound. Pre-#133-expanded the + // wwstage chained-DOT-spine branch only + // handled TK_ASSIGN; compound ops on a + // chained-*struct.field shape (e.g. + // `d.i.v += 7`) silently emitted nothing. + // cstage cgen.c:3281-3317 handles this + // (now-expanded for the same 10 ops + + // hard-errors); this is its rule-10 twin. + // All 10 integer compound ops wired; + // float/str/slice/tagged field-type + // hard-errors LOUD. Signed RSHIFTEQ uses + // SHRQ (cstage parity, A_SARQ absent). + if (n.op != tkind.TK_ASSIGN) { + if (typeisstr(ft)) { + let m: str = "chained-ptr-field compound on str element not wired (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + if (typeisslice(ft)) { + let m: str = "chained-ptr-field compound on slice element not wired (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + if (typeisfloat(ft)) { + let m: str = "chained-ptr-field compound on float element not wired (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + if (typeistagged(ft)) { + let m: str = "chained-ptr-field compound on tagged element not wired (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + cgexpr(c, n.rhs); + emitline("\tPUSHQ\tAX\n"); + cgexpr(c, base); + emitline("\tPUSHQ\tAX\n"); + let fsz: i32 = ft.slotsize: i32; + let unsignd_f: bool = typeisunsigned(ft); + let lopf: str = loadopsz(!unsignd_f, fsz); + emitline("\t"); + emitline(lopf); + emitline("\t"); + emitdispreg(tf.offset: i64, "AX"); + emitline(", AX\n"); + emitline("\tPOPQ\tBX\n"); + emitline("\tPOPQ\tCX\n"); + let wired_f: bool = false; + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_SLASHEQ) { + if (unsignd_f) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } + else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; + wired_f = true; + }; + if (n.op == tkind.TK_PERCENTEQ) { + if (unsignd_f) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } + else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; + emitline("\tMOVQ\tDX, AX\n"); + wired_f = true; + }; + if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired_f = true; }; + if (n.op == tkind.TK_RSHIFTEQ) { emitline("\tSHRQ\tCX, AX\n"); wired_f = true; }; + if (!wired_f) { + let m: str = "chained-ptr-field compound: unknown op (#133/rule-7)\n"; + os.write(2, m.ptr, m.len: u64); + os.exit(1); + }; + let sopf: str = tnodestoreop(c, n.rhs, fsz); + emitline("\t"); + emitline(sopf); + emitline("\tAX, "); + emitdispreg(tf.offset: i64, "BX"); + emitline("\n"); + return; + }; }; tf = tf.tnext; }; diff --git a/test/wcc/948_idx_compound_run.c b/test/wcc/948_idx_compound_run.c new file mode 100644 index 00000000..177f7756 --- /dev/null +++ b/test/wcc/948_idx_compound_run.c @@ -0,0 +1,421 @@ +/* + * 948_idx_compound_run — runtime + byte-id net for #133: a compound + * assign on an indexed lvalue (`arr[i] OP= v`) must do a real + * LOAD-OP-STORE, not silently demote `OP=` to `=` (cstage's prior + * behaviour) and not silently emit NOTHING (wwstage's prior behaviour). + * + * Pre-fix: + * - cstage cgen.c N_ASSIGN N_INDEX-lhs branch (3699-3851) did NOT gate + * on `n->op == TK_ASSIGN`; every compound op fell through and + * emitted a plain store of the RHS — silent demotion of `arr[i] += + * 1` to `arr[i] = 1`. + * - wwstage cgenexpr.ww cgassign N_INDEX-lhs branch (4117-4344) had an + * explicit `if (n.op == tkind.TK_ASSIGN)` outer gate with no else — + * non-ASSIGN ops emitted nothing at all (silent no-op). + * + * Both stages were runtime-WRONG; the fix adds a real load-op-store arm + * to both stages, mirroring the chained-pointer-field compound template + * at cgen.c:3281-3317. + * + * Each row carries (a) a cstage `ww build` + run asserting the exit + * code and (b) a w6c vs w6c_ww `.s` cmp (rule-10 byte-id). The wide + * matrix exercises every compound op the template wires (+= -= *= &= + * |= ^=) on the narrow/wide element widths the helpers must dispatch + * (u8, i8, u32, i32, i64), with both bare-array and slice bases. + * float/str/slice/tagged element compound stays explicitly unwired (the + * cstage template never wired them either — separate follow-up). A + * plain-assign control row asserts the byte-id surface for the ASSIGN + * path is unchanged by the fix. + */ +#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; +} + +/* builderr + experr: when builderr != 0 the cstage build MUST FAIL and + * stderr MUST contain experr. wwstage builderr is verified via direct + * w6c_ww invocation on the same source. Mirrors 945_tuple_nary's pattern + * for loud-stop verification (rule 7 — never silent). */ +struct row { const char *label; const char *src; int want_exit; + int builderr; const char *experr; }; + +static const struct row rows[] = { + /* u8 += : was silently demoted to `=` in cstage (exit 1, the + * post-init value), silently dropped in wwstage (exit 10, the + * init value untouched). Post-fix: 10 + 1 = 11. */ + { "u8_pluseq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u8 = [10u8, 20u8, 30u8, 40u8];\n" + " a[0] += 1u8;\n" + " return a[0]: i32;\n" + "};\n", 11, 0, NULL }, + /* u8 -= : 30 - 5 = 25. */ + { "u8_minuseq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u8 = [10u8, 20u8, 30u8, 40u8];\n" + " a[2] -= 5u8;\n" + " return a[2]: i32;\n" + "};\n", 25, 0, NULL }, + /* u8 *= : 7 * 3 = 21. */ + { "u8_stareq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u8 = [7u8, 0u8, 0u8, 0u8];\n" + " a[0] *= 3u8;\n" + " return a[0]: i32;\n" + "};\n", 21, 0, NULL }, + /* u8 &= : 0xF3 & 0x0F = 0x03. Bitwise on the narrow width matters + * for the high-bit-set case the silent demote would clobber. */ + { "u8_ampeq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u8 = [0xF3u8, 0u8, 0u8, 0u8];\n" + " a[0] &= 0x0Fu8;\n" + " return a[0]: i32;\n" + "};\n", 3, 0, NULL }, + /* u8 |= : 0x10 | 0x07 = 0x17. */ + { "u8_pipeeq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u8 = [0x10u8, 0u8, 0u8, 0u8];\n" + " a[0] |= 0x07u8;\n" + " return a[0]: i32;\n" + "};\n", 23, 0, NULL }, + /* u8 ^= : 0xAA ^ 0xFF = 0x55. */ + { "u8_careteq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u8 = [0xAAu8, 0u8, 0u8, 0u8];\n" + " a[0] ^= 0xFFu8;\n" + " return a[0]: i32;\n" + "};\n", 85, 0, NULL }, + /* i32 += : exercises the MOVSXD load + MOVL store the localloadop + * dispatch picks for narrow signed elements (load extends, store + * truncates). 100 + 50 = 150. */ + { "i32_pluseq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]i32 = [100, 200, 300, 400];\n" + " a[1] += 50;\n" + " return a[1];\n" + "};\n", 250, 0, NULL }, + /* i64 += : full-width control. 100 + 34 = 134 (kept <256 since + * Unix exit codes are 8-bit). */ + { "i64_pluseq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]i64 = [100i64, 200i64, 300i64, 400i64];\n" + " a[0] += 34i64;\n" + " return a[0]: i32;\n" + "};\n", 134, 0, NULL }, + /* Non-zero index: a[3] += 7 — exercises the scaled-index path + * (esz*idx) for u32 element. 100 + 7 = 107. */ + { "u32_idx3_pluseq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u32 = [10u32, 20u32, 50u32, 100u32];\n" + " a[3] += 7u32;\n" + " return a[3]: i32;\n" + "};\n", 107, 0, NULL }, + /* Slice base: same shape through a []u8 view. 22 + 8 = 30. */ + { "slice_pluseq", + "package main;\n" + "export fn main() i32 = {\n" + " let buf: [4]u8 = [11u8, 22u8, 33u8, 44u8];\n" + " let s: []u8 = buf[0:4];\n" + " s[1] += 8u8;\n" + " return s[1]: i32;\n" + "};\n", 30, 0, NULL }, + /* Plain `arr[i] = v` control: the ASSIGN path must be unchanged by + * the fix (byte-id preserved on the legacy surface). */ + { "plain_assign_ctrl", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u8 = [10u8, 20u8, 30u8, 40u8];\n" + " a[1] = 99u8;\n" + " return a[1]: i32;\n" + "};\n", 99, 0, NULL }, + /* #133-expanded: SLASHEQ/PERCENTEQ/LSHIFTEQ/RSHIFTEQ on indexed + * scalar elements. Pre-expansion both stages silently identity- + * stored (load, pop RHS, store ORIGINAL — RHS discarded). Width- + * dispatch via fldloadop/fldstoreop; signedness via type_isunsigned + * picks IDIV/SAR (signed) vs DIV/SHR (unsigned). Note: signed + * RSHIFTEQ uses SHRQ in parity with cgen.c:4034 (A_SARQ not in w6a; + * pre-existing concern). */ + /* signed i32 /=: 100 / 4 = 25 (positive, no signed/unsigned diff). */ + { "i32_slasheq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]i32 = [100, 200, 300, 400];\n" + " a[0] /= 4;\n" + " return a[0];\n" + "};\n", 25, 0, NULL }, + /* unsigned u32 /=: 100u32 / 4u32 = 25u32. Routes through DIVQ + + * zero-DX (NOT CQO+IDIVQ — signed-divide of a high-bit-set u32 + * narrow-load would mis-treat the operand as negative). */ + { "u32_slasheq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u32 = [100u32, 200u32, 300u32, 400u32];\n" + " a[0] /= 4u32;\n" + " return a[0]: i32;\n" + "};\n", 25, 0, NULL }, + /* signed i32 %=: 17 % 5 = 2; result rides DX, MOVQ DX,AX to land + * in AX for the store. */ + { "i32_percenteq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]i32 = [17, 0, 0, 0];\n" + " a[0] %= 5;\n" + " return a[0];\n" + "};\n", 2, 0, NULL }, + /* unsigned u32 %=: 100u32 % 7u32 = 2u32. */ + { "u32_percenteq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u32 = [100u32, 0u32, 0u32, 0u32];\n" + " a[0] %= 7u32;\n" + " return a[0]: i32;\n" + "};\n", 2, 0, NULL }, + /* i32 <<=: 3 << 4 = 48. SHLQ via CX. */ + { "i32_lshifteq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]i32 = [3, 0, 0, 0];\n" + " a[0] <<= 4;\n" + " return a[0];\n" + "};\n", 48, 0, NULL }, + /* unsigned u32 >>=: 200u32 >> 2 = 50u32. SHRQ via CX. */ + { "u32_rshifteq", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]u32 = [200u32, 0u32, 0u32, 0u32];\n" + " a[0] >>= 2u32;\n" + " return a[0]: i32;\n" + "};\n", 50, 0, NULL }, + /* signed i32 >>=: 200 >> 2 = 50 (positive, parity with SHRQ — + * A_SARQ unimplemented in w6a; pre-existing signed-RSHIFT on + * negatives subtle, out of scope for #133). */ + { "i32_rshifteq_pos", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]i32 = [200, 0, 0, 0];\n" + " a[0] >>= 2;\n" + " return a[0];\n" + "};\n", 50, 0, NULL }, + /* HARD-ERROR rows (#133-expanded rule-7): float / str / slice / + * tagged element compound builds MUST FAIL LOUD on both stages + * with the cited diagnostic substring. Pre-expansion these were + * silent fall-through-to-default-break → silent miscompile. + * Verification: cstage build returns non-zero AND stderr carries + * the substring; wwstage same via w6c_ww direct invocation. */ + { "he_float_indexed", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]f64 = [1.0, 2.0, 3.0, 4.0];\n" + " a[0] += 1.5;\n" + " return 0;\n" + "};\n", 0, 1, "indexed-lvalue compound on float element" }, + { "he_str_indexed", + "package main;\n" + "export fn main() i32 = {\n" + " let a: [4]str = [\"a\", \"b\", \"c\", \"d\"];\n" + " a[0] += \"x\";\n" + " return 0;\n" + "};\n", 0, 1, "indexed-lvalue compound on" }, + { "he_float_chained_ptr", + "package main;\n" + "type t = struct { v: f64 };\n" + "type w = struct { i: *t };\n" + "fn bad(d: *w) void = { d.i.v += 1.5; };\n" + "export fn main() i32 = { return 0; };\n", + 0, 1, "chained-ptr-field compound on float element" }, + { NULL, NULL, 0, 0, NULL } +}; + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa); + int cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char w6c[1100], w6c_ww[1100]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + if (access(w6c_ww, X_OK) != 0) { + fprintf(stderr, "idxcompound: w6c_ww missing — cannot run the " + "cs==ww byte-id gate (the whole point of this test)\n"); + return 1; + } + + int n = 0, fail = 0; + for (int i = 0; rows[i].src; i++, n++) { + char src[64]; + snprintf(src, sizeof src, "/tmp/wwidx_%d_%d.ww", getpid(), i); + FILE *f = fopen(src, "wb"); + if (f == NULL) { fail++; continue; } + fputs(rows[i].src, f); + fclose(f); + + /* (a) cstage build + run, OR build-must-fail (builderr rows). + * For builderr: stderr captured to a temp file; pass iff + * exit-nonzero AND stderr contains experr substring. */ + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/wwidx_%d_d_%d", + getpid(), i); + mkdir(tmpdir, 0755); + + char cmd[2048]; + if (rows[i].builderr) { + char errf[96]; + snprintf(errf, sizeof errf, "/tmp/wwidx_%d_e_%d", + getpid(), i); + snprintf(cmd, sizeof cmd, + "cd %s && %s/ww build %s >/dev/null 2>%s", + tmpdir, bin, src, errf); + int brc = runwait(cmd); + FILE *ef = fopen(errf, "rb"); + char ebuf[4096]; + size_t en = 0; + if (ef) { en = fread(ebuf, 1, sizeof ebuf - 1, ef); + fclose(ef); } + ebuf[en] = '\0'; + int ok = (brc != 0) + && (rows[i].experr == NULL + || strstr(ebuf, rows[i].experr) != NULL); + if (!ok) { + fprintf(stderr, "row[%s]: cstage expected " + "builderr+'%s' (brc=%d, stderr='%s')\n", + rows[i].label, + rows[i].experr ? rows[i].experr : "(any)", + brc, ebuf); + fail++; + } + + /* Also verify wwstage hard-errors with the SAME message + * (rule-10 — both stages identical diagnostic). Invoke + * w6c_ww directly on the .ww source. */ + snprintf(cmd, sizeof cmd, + "%s -o /dev/null %s >/dev/null 2>%s", + w6c_ww, src, errf); + int wrc = runwait(cmd); + ef = fopen(errf, "rb"); en = 0; + if (ef) { en = fread(ebuf, 1, sizeof ebuf - 1, ef); + fclose(ef); } + ebuf[en] = '\0'; + int wok = (wrc != 0) + && (rows[i].experr == NULL + || strstr(ebuf, rows[i].experr) != NULL); + if (!wok) { + fprintf(stderr, "row[%s]: wwstage expected " + "builderr+'%s' (wrc=%d, stderr='%s')\n", + rows[i].label, + rows[i].experr ? rows[i].experr : "(any)", + wrc, ebuf); + fail++; + } + unlink(errf); + unlink(src); rmdir(tmpdir); + continue; + } + + snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s", + tmpdir, bin, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: cstage build failed\n", + rows[i].label); + fail++; + unlink(src); rmdir(tmpdir); + continue; + } + + char outbin[128]; + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + + int got = runwait(outbin); + if (got != rows[i].want_exit) { + fprintf(stderr, "row[%s]: cstage exit %d, want %d\n", + rows[i].label, got, rows[i].want_exit); + fail++; + } + unlink(outbin); rmdir(tmpdir); + + /* (b) cs==ww byte-id gate. */ + char cs_s[64], ws_s[64]; + snprintf(cs_s, sizeof cs_s, "/tmp/wwidx_%d_%d_cs.s", + getpid(), i); + snprintf(ws_s, sizeof ws_s, "/tmp/wwidx_%d_%d_ww.s", + getpid(), i); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", + w6c, cs_s, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label); + fail++; unlink(src); continue; + } + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", + w6c_ww, ws_s, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww failed\n", + rows[i].label); + fail++; unlink(src); unlink(cs_s); continue; + } + if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, + "row[%s]: cstage/wwstage .s DIFFER (rule-10 " + "byte-id violation)\n", rows[i].label); + fail++; + } + unlink(src); unlink(cs_s); unlink(ws_s); + } + + if (fail) { + fprintf(stderr, "%d/%d idx-compound tests failed\n", + fail, n); + return 1; + } + printf("idxcompound: %d/%d ok (cstage run + cs==ww byte-id)\n", + n, n); + return 0; +}