wcc: single-dot field compound assignment routes through one combine helper

s.f *= v silently became s.f = v (and the other non-+=/-= ops dropped
likewise) in BOTH stages across five lvalue sub-arms: via-ptr field,
direct local field, str/slice pseudo-field, and the two global-field
forms. Funnel all five through a shared combine dispatch
(cgdotfieldcombine / cg_dotfield_combine) emitting the load-OP-store
sequence at field width, hard-erroring the unhandled kinds — close-by-
construction so no arm stays on the old PLUSEQ-only path (the #133
BUS-routing lesson; #227 sites A/B are the closed siblings). The
refactor routes the corpus's existing +=/-= sites through the same
helper output-identically (byte-id held). Review item #34.

Both stages move in one commit: one emission contract; splitting the
halves would leave the byte-id gates red in between.
This commit is contained in:
2026-06-12 19:29:58 +09:00
parent 1be0e6b5db
commit c405e777d3
6 changed files with 762 additions and 94 deletions

View File

@@ -3905,6 +3905,89 @@ cg_arrlit_fill_bp(Cg *c, Local **locals, Type *lu, Node *arrlit, int off)
}
}
/* cg_dotfield_combine — single-dot field compound combine. The old
* field value is in BX, the rhs in AX; the result is left in AX.
* PLUSEQ/MINUSEQ preserve the pre-#34 emission (byte-id); the other 8
* ops were silently DROPPED (the arm fell through to a plain store of
* the rhs -> `s.f = rhs`, #34/#263). SLASHEQ/PERCENTEQ/LSHIFTEQ/RSHIFTEQ
* need the lhs in AX and the divisor/count in CX, so swap (rhs AX->CX,
* old BX->AX) first. Signed RSHIFTEQ uses SARQ, unsigned SHRQ (#136).
* Mirrors wwstage cgdotfieldcombine — both stages emit identical asm. */
static void
cg_dotfield_combine(Cg *c, int op, int unsignd)
{
switch (op) {
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
case TK_MINUSEQ:
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
break;
case TK_STAREQ: ins2(c, A_IMULQ, areg(D_BX), areg(D_AX)); break;
case TK_AMPEQ: ins2(c, A_ANDQ, areg(D_BX), areg(D_AX)); break;
case TK_PIPEEQ: ins2(c, A_ORQ, areg(D_BX), areg(D_AX)); break;
case TK_CARETEQ: ins2(c, A_XORQ, areg(D_BX), areg(D_AX)); break;
case TK_SLASHEQ:
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));
ins1(c, A_DIVQ, areg(D_CX));
} else {
ins0(c, A_CQO);
ins1(c, A_IDIVQ, areg(D_CX));
}
break;
case TK_PERCENTEQ:
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));
ins1(c, A_DIVQ, areg(D_CX));
} else {
ins0(c, A_CQO);
ins1(c, A_IDIVQ, areg(D_CX));
}
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
break;
case TK_LSHIFTEQ:
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
ins2(c, A_SHLQ, areg(D_CX), areg(D_AX));
break;
case TK_RSHIFTEQ:
ins2(c, A_MOVQ, areg(D_AX), areg(D_CX));
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
ins2(c, unsignd ? A_SHRQ : A_SARQ, areg(D_CX), areg(D_AX));
break;
default:
fatal("single-dot field compound: unknown op tk=%d "
"(#34/rule-7)", op);
}
}
/* cg_dotfield_hardstop — loud-stop a single-dot field compound on a
* non-integer field (float/str/slice/tagged): the combine arm only
* speaks integer ABI; pre-#34 these silently became `s.f = rhs`.
* Mirrors the wwstage cgdotfieldhardstop gate. (#34/rule-7) */
static void
cg_dotfield_hardstop(Type *ft)
{
int isf32 = 0;
Type *u = type_chase_named(ft);
if (u && u->kind == TY_TAGGED)
fatal("single-dot field compound on tagged field not wired "
"(#34/rule-7)");
if (u && u->kind == TY_STR)
fatal("single-dot field compound on str field not wired "
"(#34/rule-7)");
if (u && u->kind == TY_SLICE)
fatal("single-dot field compound on slice field not wired "
"(#34/rule-7)");
if (fld_isfloat(ft, &isf32))
fatal("single-dot field compound on float field not wired "
"(#34/rule-7)");
}
static void
cgexpr(Cg *c, Node *n, Local *locals)
{
@@ -4952,16 +5035,7 @@ cgexpr(Cg *c, Node *n, Local *locals)
cgexpr(c, n->rhs, locals);
if (n->op != TK_ASSIGN) {
ins1(c, A_POPQ, areg(D_BX));
switch (n->op) {
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
case TK_MINUSEQ:
/* old in BX, rhs in AX; want AX = old-rhs.
* SUBQ src,dst is dst -= src in Plan 9. */
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
break;
default: break;
}
cg_dotfield_combine(c, n->op, 0);
}
if (via_ptr) {
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_BX));
@@ -5226,15 +5300,8 @@ cgexpr(Cg *c, Node *n, Local *locals)
cgexpr(c, n->rhs, locals); /* AX = rhs */
if (n->op != TK_ASSIGN) {
ins1(c, A_POPQ, areg(D_BX));
switch (n->op) {
case TK_PLUSEQ: ins2(c, A_ADDQ, areg(D_BX), areg(D_AX)); break;
case TK_MINUSEQ:
/* old in BX, rhs in AX; want AX=old-rhs */
ins2(c, A_SUBQ, areg(D_AX), areg(D_BX));
ins2(c, A_MOVQ, areg(D_BX), areg(D_AX));
break;
default: break; /* others rare */
}
cg_dotfield_hardstop(f->type);
cg_dotfield_combine(c, n->op, type_isunsigned(f->type));
}
/* f64/f32 field, plain `=`: cgexpr left the value in
* X0, not AX. Route the store via MOVSD/MOVSS.