cstage+selfhost+test: revert compound-assign div/mod workarounds (post-#16)

B1 (63332fe) landed CQO in both stages' assemblers and switched the
binary `/` and `%` paths to it. The compound-assign sisters (`/=`,
`%=`) were six explicit workarounds across both stages, all calling
out either "fallback for TK_SLASHEQ" or just falling through with no
case at all. With CQO available, every site mechanically ports to the
same "park rhs in CX, slot value into AX, CQO/IDIVQ CX, ferry result
back" sequence.

wwstage cgenexpr.ww:5381-5403 silently no-op'd IDENT-local signed
compound div/mod — `x /= y` and `x %= y` produced no IDIV emit at
all, just a load-bearing `MOVQ BX, off(BP)` that wrote the freshly
loaded slot value back unchanged. Bootstrap byte-id passed because
no selfhost-corpus path exercises signed compound. Latent miscompile
retired alongside the workaround revert.

cstage cgen.c:3735 (top-level-let global compound) was NOT in the
initial five-site bundle and surfaced via worker probing the
wwstage:5147 fix — `let gs: i32 = 100; gs /= 7;` returned 7 (divisor)
on cstage but 14 (correct quotient) on wwstage. Rule 10 caught the
would-be Class A divergence; the sixth site bundles in.

Six sites, one family:
  cmd/w6c/cgen.c:3549              deref-compound  `*p OP= v`
  cmd/w6c/cgen.c:3735              top-level-let   `gs OP= v`
  cmd/w6c/cgen.c:3765              IDENT-local     `x  OP= v`
  selfhost/cmd/wcc/cgenexpr.ww:3338  deref-compound
  selfhost/cmd/wcc/cgenexpr.ww:5147  top-level-let
  selfhost/cmd/wcc/cgenexpr.ww:5381  IDENT-local (silent-no-op)

test/wcc/978_intdiv_signed.c adds 7 compound rows × 2 drivers = 14
fixtures (now 68/68): IDENT-local /= /=- /=u, deref *p /= *p %= *p
/=u, with negative-dividend, negative-divisor, and unsigned-high-
bit-set coverage. Top-level-let compound coverage is deferred per
task #18 — single-file inline drivers hit a pre-existing linker
`undefined reference to '<file>.gs'` for LEAQ name(SB) targets.
cstage:3735 and wwstage:5147 are code-review-verified for rule-10
symmetry until #18 lands.

Grep-sweep (`if (n < 0) { neg = true; n = -n; }`) returned two sites
in lib/fmt/fmt.ww i64dec and lib/strconv/strconv.ww — both mirror
ref/hare/strconv/itos.ha's pre-negate idiom for INT64_MIN safety.
Per the Hare-faithful filter, both stay.
This commit is contained in:
2026-05-17 02:40:40 +09:00
parent 63332fef50
commit 4fa4bcf34e
5 changed files with 412 additions and 20 deletions

View File

@@ -3554,9 +3554,22 @@ cgexpr(Cg *c, Node *n, Local *locals)
case TK_CARETEQ: ins2(c, A_XORQ, areg(D_CX), areg(D_AX)); break; case TK_CARETEQ: ins2(c, A_XORQ, areg(D_CX), areg(D_AX)); break;
case TK_LSHIFTEQ: ins2(c, A_SHLQ, areg(D_CX), 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; case TK_RSHIFTEQ: ins2(c, A_SHRQ, areg(D_CX), areg(D_AX)); break;
case TK_SLASHEQ:
case TK_PERCENTEQ: {
int unsignd = (vt && type_isunsigned(vt))
|| (n->rhs && type_isunsigned(n->rhs->type));
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));
if (n->op == TK_PERCENTEQ)
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
break;
}
default: default:
/* TK_SLASHEQ / unknown: store rhs only, /* unknown compound: legacy fallback —
* matching the IDENT-compound fallback. */ * store rhs only. */
ins2(c, A_MOVQ, areg(D_CX), areg(D_AX)); ins2(c, A_MOVQ, areg(D_CX), areg(D_AX));
break; break;
} }
@@ -3719,10 +3732,33 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX)); ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
ins2(c, A_SHRQ, areg(D_CX), areg(D_BX)); ins2(c, A_SHRQ, areg(D_CX), areg(D_BX));
break; break;
case TK_SLASHEQ:
case TK_PERCENTEQ: {
/* Sister site of the IDENT-local path
* below. Park rhs (AX) in CX, slot value
* (BX) into AX, CQO sign-extend (or
* MOVQ $0, DX zero-extend), IDIVQ (or
* DIVQ) CX, ferry AX (quotient) or DX
* (remainder) back to BX for the shared
* store-BX tail. */
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|| (n->rhs && type_isunsigned(n->rhs->type));
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
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));
if (n->op == TK_SLASHEQ)
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
else
ins2(c, A_MOVQ, areg(D_DX), areg(D_BX));
break;
}
default: default:
/* Unsupported compound op: store rhs /* unknown compound: legacy fallback —
* directly. Mirrors the local path's * store rhs only. */
* fallback for TK_SLASHEQ etc. */
did_compound = 0; did_compound = 0;
ins2(c, A_MOVQ, areg(D_AX), ins2(c, A_MOVQ, areg(D_AX),
masym(c, n->lhs->str)); masym(c, n->lhs->str));
@@ -3772,12 +3808,29 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_SHRQ, areg(D_CX), areg(D_BX)); ins2(c, A_SHRQ, areg(D_CX), areg(D_BX));
break; break;
case TK_SLASHEQ: case TK_SLASHEQ:
/* BX = BX / AX. IDIV uses DX:AX/RAX. Move case TK_PERCENTEQ: {
* BX→AX first, sign-extend via CQO would be /* IDIV/DIV needs dividend in RDX:RAX, divisor
* cleanest; skip for now and fall back to * in a GPR. Park rhs (currently AX) in CX, move
* MOVQ to avoid emitting wrong code. */ * slot value (BX) into AX, sign- or zero-extend
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off)); * into RDX:RAX, divide, then ferry the quotient
goto skip_assign_store; * (AX) or remainder (DX) back into BX for the
* shared store-BX-to-slot tail below. Post-#16:
* CQO is now in the assembler. */
int unsignd = (n->lhs && type_isunsigned(n->lhs->type))
|| (n->rhs && type_isunsigned(n->rhs->type));
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
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));
if (n->op == TK_SLASHEQ)
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
else
ins2(c, A_MOVQ, areg(D_DX), areg(D_BX));
break;
}
default: default:
/* unknown: just store rhs (legacy fallback) */ /* unknown: just store rhs (legacy fallback) */
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off)); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off));

View File

@@ -13124,6 +13124,10 @@ fn cgassign(c: *cgen, n: *node) void = {
let inner: *node = lhs.lhs; let inner: *node = lhs.lhs;
let loadop: str = "MOVQ"; let loadop: str = "MOVQ";
let storeop: str = "MOVQ"; let storeop: str = "MOVQ";
// Pointee node for the lhs-sign side of the /=
// and %= dispatch. Mirror of cstage's `vt` at
// cmd/w6c/cgen.c's TK_STAR-compound branch.
let pe: *node = nil;
if (inner != nil) { if (inner != nil) {
if (inner.kind == nkind.N_IDENT) { if (inner.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, inner.str); let lc: *local = localfindnode(c, inner.str);
@@ -13131,7 +13135,7 @@ fn cgassign(c: *cgen, n: *node) void = {
let tn: *node = lc.tnode; let tn: *node = lc.tnode;
if (tn != nil) { if (tn != nil) {
if (tn.kind == nkind.N_TPTR) { if (tn.kind == nkind.N_TPTR) {
let pe: *node = tn.lhs; pe = tn.lhs;
if (pe != nil) { if (pe != nil) {
let ps: i32 = fieldsize(c, pe); let ps: i32 = fieldsize(c, pe);
if (ps == 1 || ps == 2 || ps == 4) { if (ps == 1 || ps == 2 || ps == 4) {
@@ -13152,6 +13156,30 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline(loadop); emitline(loadop);
emitline("\t(BX), AX\n"); emitline("\t(BX), AX\n");
emitline("\tPOPQ\tCX\n"); emitline("\tPOPQ\tCX\n");
// Post-63332fe: /= and %= via CQO/IDIVQ on the
// signed arm and MOVQ-zero/DIVQ on the unsigned
// arm. Pre-fix the default branch silently stored
// rhs into *p (combineop = MOVQ shape).
if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = typenodeisunsignedc(c, pe);
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_PERCENTEQ) {
emitline("\tMOVQ\tDX, AX\n");
};
emitline("\t");
emitline(storeop);
emitline("\tAX, (BX)\n");
return;
};
let combineop: str = "MOVQ"; let combineop: str = "MOVQ";
if (n.op == tkind.TK_PLUSEQ) { combineop = "ADDQ"; } if (n.op == tkind.TK_PLUSEQ) { combineop = "ADDQ"; }
else { if (n.op == tkind.TK_MINUSEQ) { combineop = "SUBQ"; } else { if (n.op == tkind.TK_MINUSEQ) { combineop = "SUBQ"; }
@@ -14976,15 +15004,41 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tAX, CX\n"); emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHRQ\tCX, BX\n"); emitline("\tSHRQ\tCX, BX\n");
} }
// Post-63332fe: /= and %= for a top-level
// let. Same shape as the IDENT-local path:
// park rhs in CX, slot value (BX) into AX,
// CQO (or zero DX), IDIVQ (or DIVQ) CX,
// ferry AX or DX back to BX for the shared
// store-BX tail below.
else { if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = typenodeisunsignedc(c, lvftn);
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
emitline("\tMOVQ\tAX, CX\n");
emitline("\tMOVQ\tBX, AX\n");
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_SLASHEQ) {
emitline("\tMOVQ\tAX, BX\n");
} else {
emitline("\tMOVQ\tDX, BX\n");
};
}
else { else {
// Unsupported compound: store rhs // Unsupported compound: store rhs
// directly. Mirrors the local path's // directly. Mirrors the local path's
// fallback for TK_SLASHEQ etc. // legacy fallback for unknown ops.
didcompound = false; didcompound = false;
emitline("\tMOVQ\tAX, "); emitline("\tMOVQ\tAX, ");
emitsymname(c, nm); emitsymname(c, nm);
emitline("(SB)\n"); emitline("(SB)\n");
};};};};};};};}; };};};};};};};};};
if (didcompound) { if (didcompound) {
emitline("\tMOVQ\tBX, "); emitline("\tMOVQ\tBX, ");
emitsymname(c, nm); emitsymname(c, nm);
@@ -15215,6 +15269,36 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tAX, CX\n"); emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHRQ\tCX, BX\n"); emitline("\tSHRQ\tCX, BX\n");
}; };
// Post-63332fe: /= and %= for an IDENT local. Pre-fix
// fell through with no case, so BX (still holding the
// freshly loaded slot value) was stored back unchanged
// — a silent no-op rather than the natural rhs-only
// shape the global/deref siblings took. Park rhs in
// CX, slot value (BX) into AX, CQO/IDIVQ, ferry AX
// (quotient) or DX (remainder) back to BX.
if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = false;
if (lcn != nil) {
unsignd = typenodeisunsignedc(c, lcn.tnode);
};
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
emitline("\tMOVQ\tAX, CX\n");
emitline("\tMOVQ\tBX, AX\n");
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_SLASHEQ) {
emitline("\tMOVQ\tAX, BX\n");
} else {
emitline("\tMOVQ\tDX, BX\n");
};
};
emitline("\tMOVQ\tBX, "); emitline("\tMOVQ\tBX, ");
emitoff(off: i64); emitoff(off: i64);
emitline("(BP)\n"); emitline("(BP)\n");

View File

@@ -3307,6 +3307,10 @@ fn cgassign(c: *cgen, n: *node) void = {
let inner: *node = lhs.lhs; let inner: *node = lhs.lhs;
let loadop: str = "MOVQ"; let loadop: str = "MOVQ";
let storeop: str = "MOVQ"; let storeop: str = "MOVQ";
// Pointee node for the lhs-sign side of the /=
// and %= dispatch. Mirror of cstage's `vt` at
// cmd/w6c/cgen.c's TK_STAR-compound branch.
let pe: *node = nil;
if (inner != nil) { if (inner != nil) {
if (inner.kind == nkind.N_IDENT) { if (inner.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, inner.str); let lc: *local = localfindnode(c, inner.str);
@@ -3314,7 +3318,7 @@ fn cgassign(c: *cgen, n: *node) void = {
let tn: *node = lc.tnode; let tn: *node = lc.tnode;
if (tn != nil) { if (tn != nil) {
if (tn.kind == nkind.N_TPTR) { if (tn.kind == nkind.N_TPTR) {
let pe: *node = tn.lhs; pe = tn.lhs;
if (pe != nil) { if (pe != nil) {
let ps: i32 = fieldsize(c, pe); let ps: i32 = fieldsize(c, pe);
if (ps == 1 || ps == 2 || ps == 4) { if (ps == 1 || ps == 2 || ps == 4) {
@@ -3335,6 +3339,30 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline(loadop); emitline(loadop);
emitline("\t(BX), AX\n"); emitline("\t(BX), AX\n");
emitline("\tPOPQ\tCX\n"); emitline("\tPOPQ\tCX\n");
// Post-63332fe: /= and %= via CQO/IDIVQ on the
// signed arm and MOVQ-zero/DIVQ on the unsigned
// arm. Pre-fix the default branch silently stored
// rhs into *p (combineop = MOVQ shape).
if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = typenodeisunsignedc(c, pe);
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_PERCENTEQ) {
emitline("\tMOVQ\tDX, AX\n");
};
emitline("\t");
emitline(storeop);
emitline("\tAX, (BX)\n");
return;
};
let combineop: str = "MOVQ"; let combineop: str = "MOVQ";
if (n.op == tkind.TK_PLUSEQ) { combineop = "ADDQ"; } if (n.op == tkind.TK_PLUSEQ) { combineop = "ADDQ"; }
else { if (n.op == tkind.TK_MINUSEQ) { combineop = "SUBQ"; } else { if (n.op == tkind.TK_MINUSEQ) { combineop = "SUBQ"; }
@@ -5159,15 +5187,41 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tAX, CX\n"); emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHRQ\tCX, BX\n"); emitline("\tSHRQ\tCX, BX\n");
} }
// Post-63332fe: /= and %= for a top-level
// let. Same shape as the IDENT-local path:
// park rhs in CX, slot value (BX) into AX,
// CQO (or zero DX), IDIVQ (or DIVQ) CX,
// ferry AX or DX back to BX for the shared
// store-BX tail below.
else { if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = typenodeisunsignedc(c, lvftn);
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
emitline("\tMOVQ\tAX, CX\n");
emitline("\tMOVQ\tBX, AX\n");
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_SLASHEQ) {
emitline("\tMOVQ\tAX, BX\n");
} else {
emitline("\tMOVQ\tDX, BX\n");
};
}
else { else {
// Unsupported compound: store rhs // Unsupported compound: store rhs
// directly. Mirrors the local path's // directly. Mirrors the local path's
// fallback for TK_SLASHEQ etc. // legacy fallback for unknown ops.
didcompound = false; didcompound = false;
emitline("\tMOVQ\tAX, "); emitline("\tMOVQ\tAX, ");
emitsymname(c, nm); emitsymname(c, nm);
emitline("(SB)\n"); emitline("(SB)\n");
};};};};};};};}; };};};};};};};};};
if (didcompound) { if (didcompound) {
emitline("\tMOVQ\tBX, "); emitline("\tMOVQ\tBX, ");
emitsymname(c, nm); emitsymname(c, nm);
@@ -5398,6 +5452,36 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tAX, CX\n"); emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHRQ\tCX, BX\n"); emitline("\tSHRQ\tCX, BX\n");
}; };
// Post-63332fe: /= and %= for an IDENT local. Pre-fix
// fell through with no case, so BX (still holding the
// freshly loaded slot value) was stored back unchanged
// — a silent no-op rather than the natural rhs-only
// shape the global/deref siblings took. Park rhs in
// CX, slot value (BX) into AX, CQO/IDIVQ, ferry AX
// (quotient) or DX (remainder) back to BX.
if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = false;
if (lcn != nil) {
unsignd = typenodeisunsignedc(c, lcn.tnode);
};
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
emitline("\tMOVQ\tAX, CX\n");
emitline("\tMOVQ\tBX, AX\n");
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_SLASHEQ) {
emitline("\tMOVQ\tAX, BX\n");
} else {
emitline("\tMOVQ\tDX, BX\n");
};
};
emitline("\tMOVQ\tBX, "); emitline("\tMOVQ\tBX, ");
emitoff(off: i64); emitoff(off: i64);
emitline("(BP)\n"); emitline("(BP)\n");

View File

@@ -13124,6 +13124,10 @@ fn cgassign(c: *cgen, n: *node) void = {
let inner: *node = lhs.lhs; let inner: *node = lhs.lhs;
let loadop: str = "MOVQ"; let loadop: str = "MOVQ";
let storeop: str = "MOVQ"; let storeop: str = "MOVQ";
// Pointee node for the lhs-sign side of the /=
// and %= dispatch. Mirror of cstage's `vt` at
// cmd/w6c/cgen.c's TK_STAR-compound branch.
let pe: *node = nil;
if (inner != nil) { if (inner != nil) {
if (inner.kind == nkind.N_IDENT) { if (inner.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, inner.str); let lc: *local = localfindnode(c, inner.str);
@@ -13131,7 +13135,7 @@ fn cgassign(c: *cgen, n: *node) void = {
let tn: *node = lc.tnode; let tn: *node = lc.tnode;
if (tn != nil) { if (tn != nil) {
if (tn.kind == nkind.N_TPTR) { if (tn.kind == nkind.N_TPTR) {
let pe: *node = tn.lhs; pe = tn.lhs;
if (pe != nil) { if (pe != nil) {
let ps: i32 = fieldsize(c, pe); let ps: i32 = fieldsize(c, pe);
if (ps == 1 || ps == 2 || ps == 4) { if (ps == 1 || ps == 2 || ps == 4) {
@@ -13152,6 +13156,30 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline(loadop); emitline(loadop);
emitline("\t(BX), AX\n"); emitline("\t(BX), AX\n");
emitline("\tPOPQ\tCX\n"); emitline("\tPOPQ\tCX\n");
// Post-63332fe: /= and %= via CQO/IDIVQ on the
// signed arm and MOVQ-zero/DIVQ on the unsigned
// arm. Pre-fix the default branch silently stored
// rhs into *p (combineop = MOVQ shape).
if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = typenodeisunsignedc(c, pe);
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_PERCENTEQ) {
emitline("\tMOVQ\tDX, AX\n");
};
emitline("\t");
emitline(storeop);
emitline("\tAX, (BX)\n");
return;
};
let combineop: str = "MOVQ"; let combineop: str = "MOVQ";
if (n.op == tkind.TK_PLUSEQ) { combineop = "ADDQ"; } if (n.op == tkind.TK_PLUSEQ) { combineop = "ADDQ"; }
else { if (n.op == tkind.TK_MINUSEQ) { combineop = "SUBQ"; } else { if (n.op == tkind.TK_MINUSEQ) { combineop = "SUBQ"; }
@@ -14976,15 +15004,41 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tAX, CX\n"); emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHRQ\tCX, BX\n"); emitline("\tSHRQ\tCX, BX\n");
} }
// Post-63332fe: /= and %= for a top-level
// let. Same shape as the IDENT-local path:
// park rhs in CX, slot value (BX) into AX,
// CQO (or zero DX), IDIVQ (or DIVQ) CX,
// ferry AX or DX back to BX for the shared
// store-BX tail below.
else { if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = typenodeisunsignedc(c, lvftn);
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
emitline("\tMOVQ\tAX, CX\n");
emitline("\tMOVQ\tBX, AX\n");
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_SLASHEQ) {
emitline("\tMOVQ\tAX, BX\n");
} else {
emitline("\tMOVQ\tDX, BX\n");
};
}
else { else {
// Unsupported compound: store rhs // Unsupported compound: store rhs
// directly. Mirrors the local path's // directly. Mirrors the local path's
// fallback for TK_SLASHEQ etc. // legacy fallback for unknown ops.
didcompound = false; didcompound = false;
emitline("\tMOVQ\tAX, "); emitline("\tMOVQ\tAX, ");
emitsymname(c, nm); emitsymname(c, nm);
emitline("(SB)\n"); emitline("(SB)\n");
};};};};};};};}; };};};};};};};};};
if (didcompound) { if (didcompound) {
emitline("\tMOVQ\tBX, "); emitline("\tMOVQ\tBX, ");
emitsymname(c, nm); emitsymname(c, nm);
@@ -15215,6 +15269,36 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tAX, CX\n"); emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHRQ\tCX, BX\n"); emitline("\tSHRQ\tCX, BX\n");
}; };
// Post-63332fe: /= and %= for an IDENT local. Pre-fix
// fell through with no case, so BX (still holding the
// freshly loaded slot value) was stored back unchanged
// — a silent no-op rather than the natural rhs-only
// shape the global/deref siblings took. Park rhs in
// CX, slot value (BX) into AX, CQO/IDIVQ, ferry AX
// (quotient) or DX (remainder) back to BX.
if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = false;
if (lcn != nil) {
unsignd = typenodeisunsignedc(c, lcn.tnode);
};
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
emitline("\tMOVQ\tAX, CX\n");
emitline("\tMOVQ\tBX, AX\n");
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_SLASHEQ) {
emitline("\tMOVQ\tAX, BX\n");
} else {
emitline("\tMOVQ\tDX, BX\n");
};
};
emitline("\tMOVQ\tBX, "); emitline("\tMOVQ\tBX, ");
emitoff(off: i64); emitoff(off: i64);
emitline("(BP)\n"); emitline("(BP)\n");

View File

@@ -283,6 +283,93 @@ static const struct row rows[] = {
" return 0;\n" " return 0;\n"
"};\n", "};\n",
42 }, 42 },
/* ---- COMPOUND ASSIGN rows ------------------------------------- *
*
* The siblings of the binary `/` and `%` paths above: `x /= v`,
* `x %= v`, and `*p /= v`. Pre-#16's-B2 these emitted the worst
* shape catalog:
* - cstage cgen.c:3765 IDENT-local: silent no-op (BX held
* loaded slot value, the trailing store wrote it back
* unchanged — no IDIV emit at all).
* - wwstage cgenexpr.ww:5381-5403 IDENT-local: identical silent
* no-op shape; the load-bearing sentinel — a future regression
* here passes byte-id (no asm = no divergence) AND passes the
* selfhost corpus (no consumer of signed compound /=). The
* semantic test below is the only catch.
* - cstage cgen.c:3549 / wwstage cgenexpr.ww:3338 deref-compound
* `*p OP= v`: silent rhs-only store, value of *p clobbered with
* the divisor instead of computed quotient.
*
* Top-level-let compound coverage (cstage cgen.c:3735 / wwstage
* cgenexpr.ww:5147) is deferred per task #18 — the inline-driver
* 978 fixture can't currently exercise it because of a pre-existing
* linker `undefined reference to '<file>.gs'` for single-file
* top-level lets that flow through `LEAQ name(SB)`. The cstage:3735
* and wwstage:5147 sites are code-review-verified for rule-10
* symmetry; runtime coverage follows once #18 lands. */
{ "compound_ident_local_div_neg_dividend",
"fn main() i32 = {\n"
" let a: i64 = -100i64;\n"
" a /= 7i64;\n"
" if (a == -14i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "compound_ident_local_mod_neg_dividend",
"fn main() i32 = {\n"
" let a: i64 = -100i64;\n"
" a %= 7i64;\n"
" if (a == -2i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "compound_ident_local_div_neg_divisor",
"fn main() i32 = {\n"
" let a: i64 = 100i64;\n"
" a /= -7i64;\n"
" if (a == -14i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "compound_ident_local_div_unsigned",
"fn main() i32 = {\n"
" let a: u64 = 0x8000000000000001u64;\n"
" a /= 2u64;\n"
" if (a == 0x4000000000000000u64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "compound_deref_div_neg_dividend",
"fn main() i32 = {\n"
" let arr: [1]i64;\n"
" arr[0] = -50i64;\n"
" let p: *i64 = &arr[0];\n"
" *p /= 3i64;\n"
" if (arr[0] == -16i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "compound_deref_mod_neg_dividend",
"fn main() i32 = {\n"
" let arr: [1]i64;\n"
" arr[0] = -50i64;\n"
" let p: *i64 = &arr[0];\n"
" *p %= 3i64;\n"
" if (arr[0] == -2i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "compound_deref_div_unsigned",
"fn main() i32 = {\n"
" let arr: [1]u64;\n"
" arr[0] = 0x8000000000000001u64;\n"
" let p: *u64 = &arr[0];\n"
" *p /= 2u64;\n"
" if (arr[0] == 0x4000000000000000u64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
}; };
static int static int