From 9cf156039201c38c1265fd1477b5e2d61a3b899c Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 1 Jun 2026 03:52:06 +0900 Subject: [PATCH] wwstage: load-combine-store local-field compound assign (#227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A compound assign (`-=`/`+=`) on a local field silently dropped the operator in wwstage, storing the bare rhs. Two same-class sites in cgenexpr.ww lacked the `n.op != TK_ASSIGN` load-combine-store guard that the pointer-to-struct path already had: the local str/slice pseudo-field fall-through (`view.len -= 1` stored 1) and the direct struct-local scalar field (`p.x -= 4` stored 4). Both now load the field, push, eval rhs, pop, combine (ADDQ/SUBQ), and store — mirroring cstage cmd/w6c/cgen.c:3235-3264 and :3477-3502. cstage was already correct; this aligns wwstage up. PLUSEQ/MINUSEQ only, matching cstage's switch. This is the missing-SUBQ half of fmt's cs/ww divergence (fmt's view.len-=1). The remaining match-label-counter offset is separate, so 777/780/781 stay STAGE_CS until that lands. test/wcc/data/attest_pass.ww: @test check_local_field_compound covers both sites (str pseudo-field + struct scalar), run by 910_at_test (cstage) and 997_at_test_ww (wwstage); pre-fix the dropped op aborts via the 1/0 idiom. --- selfhost/cmd/w6c/main.combined.ww | 43 ++++++++++++++++++++++++++++ selfhost/cmd/wcc/cgenexpr.ww | 43 ++++++++++++++++++++++++++++ selfhost/cmd/wwdump/main.combined.ww | 43 ++++++++++++++++++++++++++++ test/wcc/data/attest_pass.ww | 23 +++++++++++++++ 4 files changed, 152 insertions(+) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index b7287d89..4dbea470 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -24299,7 +24299,30 @@ fn cgassign(c: *cgen, n: *node) void = { return; };}; }; + if (n.op != tkind.TK_ASSIGN) { + // Compound on direct struct-local + // scalar field: load current → push + // → eval rhs → combine → store + // (mirror cgen.c:3477 local arm). + let lop: str = fieldloadop(c, fi); + emitline("\t"); + emitline(lop); + emitline("\t"); + emitoff((lc.off + fi.foff): i64); + emitline("(BP), BX\n"); + emitline("\tPUSHQ\tBX\n"); + }; cgexpr(c, n.rhs); + if (n.op != tkind.TK_ASSIGN) { + emitline("\tPOPQ\tBX\n"); + // PLUSEQ commutes; MINUSEQ needs + // lhs-rhs (BX old lhs, AX rhs). + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; + if (n.op == tkind.TK_MINUSEQ) { + emitline("\tSUBQ\tAX, BX\n"); + emitline("\tMOVQ\tBX, AX\n"); + }; + }; // str/slice field direct: str IS []u8, so both store the // full 3-word {ptr,len,cap} from (AX,BX,CX) at +0/+8/+16. // BP base, no scratch reload needed; the generic fldstoreop @@ -24392,6 +24415,26 @@ fn cgassign(c: *cgen, n: *node) void = { return; }; }; + if (n.op != tkind.TK_ASSIGN) { + // Compound on local `str|slice` pseudo-field: + // load current → push → eval rhs → combine → + // store (mirror cgen.c:3235 local arm). + emitline("\tMOVQ\t"); + emitoff((lc.off + delta): i64); + emitline("(BP), BX\n"); + emitline("\tPUSHQ\tBX\n"); + cgexpr(c, n.rhs); + emitline("\tPOPQ\tBX\n"); + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; + if (n.op == tkind.TK_MINUSEQ) { + emitline("\tSUBQ\tAX, BX\n"); + emitline("\tMOVQ\tBX, AX\n"); + }; + emitline("\tMOVQ\tAX, "); + emitoff((lc.off + delta): i64); + emitline("(BP)\n"); + return; + }; cgexpr(c, n.rhs); emitline("\tMOVQ\tAX, "); emitoff((lc.off + delta): i64); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 47defe5f..1682e146 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -5658,7 +5658,30 @@ fn cgassign(c: *cgen, n: *node) void = { return; };}; }; + if (n.op != tkind.TK_ASSIGN) { + // Compound on direct struct-local + // scalar field: load current → push + // → eval rhs → combine → store + // (mirror cgen.c:3477 local arm). + let lop: str = fieldloadop(c, fi); + emitline("\t"); + emitline(lop); + emitline("\t"); + emitoff((lc.off + fi.foff): i64); + emitline("(BP), BX\n"); + emitline("\tPUSHQ\tBX\n"); + }; cgexpr(c, n.rhs); + if (n.op != tkind.TK_ASSIGN) { + emitline("\tPOPQ\tBX\n"); + // PLUSEQ commutes; MINUSEQ needs + // lhs-rhs (BX old lhs, AX rhs). + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; + if (n.op == tkind.TK_MINUSEQ) { + emitline("\tSUBQ\tAX, BX\n"); + emitline("\tMOVQ\tBX, AX\n"); + }; + }; // str/slice field direct: str IS []u8, so both store the // full 3-word {ptr,len,cap} from (AX,BX,CX) at +0/+8/+16. // BP base, no scratch reload needed; the generic fldstoreop @@ -5751,6 +5774,26 @@ fn cgassign(c: *cgen, n: *node) void = { return; }; }; + if (n.op != tkind.TK_ASSIGN) { + // Compound on local `str|slice` pseudo-field: + // load current → push → eval rhs → combine → + // store (mirror cgen.c:3235 local arm). + emitline("\tMOVQ\t"); + emitoff((lc.off + delta): i64); + emitline("(BP), BX\n"); + emitline("\tPUSHQ\tBX\n"); + cgexpr(c, n.rhs); + emitline("\tPOPQ\tBX\n"); + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; + if (n.op == tkind.TK_MINUSEQ) { + emitline("\tSUBQ\tAX, BX\n"); + emitline("\tMOVQ\tBX, AX\n"); + }; + emitline("\tMOVQ\tAX, "); + emitoff((lc.off + delta): i64); + emitline("(BP)\n"); + return; + }; cgexpr(c, n.rhs); emitline("\tMOVQ\tAX, "); emitoff((lc.off + delta): i64); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 51450350..21ff61ac 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -24299,7 +24299,30 @@ fn cgassign(c: *cgen, n: *node) void = { return; };}; }; + if (n.op != tkind.TK_ASSIGN) { + // Compound on direct struct-local + // scalar field: load current → push + // → eval rhs → combine → store + // (mirror cgen.c:3477 local arm). + let lop: str = fieldloadop(c, fi); + emitline("\t"); + emitline(lop); + emitline("\t"); + emitoff((lc.off + fi.foff): i64); + emitline("(BP), BX\n"); + emitline("\tPUSHQ\tBX\n"); + }; cgexpr(c, n.rhs); + if (n.op != tkind.TK_ASSIGN) { + emitline("\tPOPQ\tBX\n"); + // PLUSEQ commutes; MINUSEQ needs + // lhs-rhs (BX old lhs, AX rhs). + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; + if (n.op == tkind.TK_MINUSEQ) { + emitline("\tSUBQ\tAX, BX\n"); + emitline("\tMOVQ\tBX, AX\n"); + }; + }; // str/slice field direct: str IS []u8, so both store the // full 3-word {ptr,len,cap} from (AX,BX,CX) at +0/+8/+16. // BP base, no scratch reload needed; the generic fldstoreop @@ -24392,6 +24415,26 @@ fn cgassign(c: *cgen, n: *node) void = { return; }; }; + if (n.op != tkind.TK_ASSIGN) { + // Compound on local `str|slice` pseudo-field: + // load current → push → eval rhs → combine → + // store (mirror cgen.c:3235 local arm). + emitline("\tMOVQ\t"); + emitoff((lc.off + delta): i64); + emitline("(BP), BX\n"); + emitline("\tPUSHQ\tBX\n"); + cgexpr(c, n.rhs); + emitline("\tPOPQ\tBX\n"); + if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; + if (n.op == tkind.TK_MINUSEQ) { + emitline("\tSUBQ\tAX, BX\n"); + emitline("\tMOVQ\tBX, AX\n"); + }; + emitline("\tMOVQ\tAX, "); + emitoff((lc.off + delta): i64); + emitline("(BP)\n"); + return; + }; cgexpr(c, n.rhs); emitline("\tMOVQ\tAX, "); emitoff((lc.off + delta): i64); diff --git a/test/wcc/data/attest_pass.ww b/test/wcc/data/attest_pass.ww index ac70e6ce..40119e2a 100644 --- a/test/wcc/data/attest_pass.ww +++ b/test/wcc/data/attest_pass.ww @@ -2,6 +2,8 @@ package data; +type point = struct { x: i32, y: i32 }; + @test fn check_add() void = { let a: i32 = 2; let b: i32 = 3; @@ -21,3 +23,24 @@ package data; let _: i32 = 1 / 0; }; }; + +// #227: compound assign (`-=`/`+=`) on a local field must +// load-combine-store, not drop the op. Pre-fix wwstage stored the bare +// rhs (p.x->4, p.y->5, view.len->1), so each mismatch aborts via 1/0. +@test fn check_local_field_compound() void = { + let p: point = point { x = 10i32, y = 3i32 }; + p.x -= 4i32; + p.y += 5i32; + if (p.x != 6) { + let _: i32 = 1 / 0; + }; + if (p.y != 8) { + let _: i32 = 1 / 0; + }; + let view: str = "hello"; + view.len -= 1; + let n: i32 = view.len: i32; + if (n != 4) { + let _: i32 = 1 / 0; + }; +};