w6a, wcc: add A_SARQ for signed arithmetic right-shift (#136)

Add SAR/SARQ to both assemblers' opcode tables (cstage cmd/w6a +
wwstage selfhost/cmd/w6a) — REX.W + D3 /7, parallel to SHR's D3 /5.
Encoding is the unary-on-CL form (SAR r/m64, CL), the only variant
the cgen emits today. cstage cgen + wwstage cgen sweep all 12 SHRQ
emission sites (6 per stage) so signed RSHIFT and signed RSHIFTEQ
route through SARQ (arithmetic, sign-extends MSB) instead of SHRQ
(logical, zero-fill). Pre-fix `let i: i32 = -200; i >>= 2;`
produced 0x3FFFFFCE (1073741774) instead of -50; cs==ww held because
BOTH stages emitted SHRQ, so the 990-997 byte-id gates were
gate-blind to this silent miscompile.

Sites covered (per stage 6, same shape in both):
  - plain TK_RSHIFT (cgbin / N_BIN ordered binop) — derives unsignd
    from operand types via type_isunsigned / nodeisunsigned, picks
    SHRQ vs SARQ at emit
  - chained-ptr-field compound RSHIFTEQ (cgen.c:3281-3317 area)
  - N_INDEX-lhs compound RSHIFTEQ (#133-expanded N_INDEX site)
  - deref-target compound RSHIFTEQ
  - top-level let compound RSHIFTEQ
  - IDENT-local compound RSHIFTEQ
All sites reuse the in-scope unsignd variable from the surrounding
SLASHEQ block (or derive one locally when not available). LSHIFTEQ
unchanged — SHL == SAL at the encoder, no signedness dispatch needed.

912_sar_shr_run: 5 rows. i32_neg_rshifteq (lead's repro, was wrong
1073741774 → now -50), i64_neg_rshifteq (wider type), i32_pos_
rshifteq (positive control, SARQ ≡ SHRQ on positives, no regression),
u32_rshifteq (unsigned control, still SHRQ), i32_neg_rshift_binop
(plain >> not compound, cgbin TK_RSHIFT site). Exit codes use small
absolute values with u8 wrap (-50 = 206) per Unix 8-bit exit.

Bootstrap-NEUTRAL — `grep -rE '>>=|>>\b'` in lib/+selfhost/ (excl.
combined.ww) returned zero callers of signed RSHIFT today; the only
asm shifts are on previously-broken paths. 990-997 + combined_ww_
fresh stay green. Closes the silent-misbehavior class on signed
right-shift across all 12 cgen emission paths in one fold per
rule-11. Foundation for Eisel-Lemire (strconv fold-4) big-int signed
shifts.
This commit is contained in:
2026-05-27 01:29:24 +09:00
parent 90d31c5b41
commit 13441c5e2e
14 changed files with 471 additions and 81 deletions

View File

@@ -3174,8 +3174,12 @@ fn cgbin(c: *cgen, n: *node) void = {
return;
};
if (n.op == tkind.TK_RSHIFT) {
// #136: signed RSHIFT → SAR (arithmetic, sign-extends MSB);
// unsigned → SHR (logical, zero-fill). `unsignd` derived above
// at cgbin head from nodeisunsigned(lhs) || nodeisunsigned(rhs).
emitline("\tMOVQ\tBX, CX\n");
emitline("\tSHRQ\tCX, AX\n");
if (unsignd) { emitline("\tSHRQ\tCX, AX\n"); }
else { emitline("\tSARQ\tCX, AX\n"); };
return;
};
// TK_AND / TK_OR handled with short-circuit codegen at the top of
@@ -4151,14 +4155,16 @@ fn cgassign(c: *cgen, n: *node) void = {
// signed arm and MOVQ-zero/DIVQ on the unsigned
// arm. Pre-fix the default branch silently stored
// rhs into *p (combineop = MOVQ shape).
// #136: lift unsignd above the SLASHEQ block so
// RSHIFTEQ can route SHRQ vs SARQ on the same key.
let unsignd: bool = false;
if (pe != nil) {
unsignd = typeisunsigned(pe.type_: *tinfo);
};
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = false;
if (pe != nil) {
unsignd = typeisunsigned(pe.type_: *tinfo);
};
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
@@ -4182,7 +4188,11 @@ fn cgassign(c: *cgen, n: *node) void = {
else { if (n.op == tkind.TK_PIPEEQ) { combineop = "ORQ"; }
else { if (n.op == tkind.TK_CARETEQ) { combineop = "XORQ"; }
else { if (n.op == tkind.TK_LSHIFTEQ) { combineop = "SHLQ"; }
else { if (n.op == tkind.TK_RSHIFTEQ) { combineop = "SHRQ"; };
else { if (n.op == tkind.TK_RSHIFTEQ) {
// #136: signed RSHIFTEQ → SARQ.
if (unsignd) { combineop = "SHRQ"; }
else { combineop = "SARQ"; };
};
}; }; }; }; }; }; };
emitline("\t");
emitline(combineop);
@@ -4561,10 +4571,9 @@ fn cgassign(c: *cgen, n: *node) void = {
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).
// DIVQ (unsigned). LSHIFTEQ via SHLQ; RSHIFTEQ via
// SARQ (signed) or SHRQ (unsigned) per #136.
// Signedness from elemtn.type_.
let unsignd_c: bool = false;
if (elemtn != nil) {
if (elemtn.type_ != nil) {
@@ -4590,7 +4599,11 @@ fn cgassign(c: *cgen, n: *node) void = {
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 (n.op == tkind.TK_RSHIFTEQ) {
if (unsignd_c) { emitline("\tSHRQ\tCX, AX\n"); }
else { emitline("\tSARQ\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);
@@ -5679,7 +5692,7 @@ fn cgassign(c: *cgen, n: *node) void = {
// All 10 integer compound ops wired;
// float/str/slice/tagged field-type
// hard-errors LOUD. Signed RSHIFTEQ uses
// SHRQ (cstage parity, A_SARQ absent).
// SARQ (signed) or SHRQ (unsigned) per #136.
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";
@@ -5734,7 +5747,11 @@ fn cgassign(c: *cgen, n: *node) void = {
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 (n.op == tkind.TK_RSHIFTEQ) {
if (unsignd_f) { emitline("\tSHRQ\tCX, AX\n"); }
else { emitline("\tSARQ\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);
@@ -6373,8 +6390,19 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\tSHLQ\tCX, BX\n");
}
else { if (n.op == tkind.TK_RSHIFTEQ) {
// #136: signed RSHIFTEQ → SARQ.
let unsignd_r: bool = false;
if (lvftn != nil) {
if (lvftn.type_ != nil) {
unsignd_r = typeisunsigned(lvftn.type_: *tinfo);
};
};
if (!unsignd_r) {
unsignd_r = nodeisunsigned(c, n.rhs);
};
emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHRQ\tCX, BX\n");
if (unsignd_r) { emitline("\tSHRQ\tCX, BX\n"); }
else { emitline("\tSARQ\tCX, BX\n"); };
}
// Post-63332fe: /= and %= for a top-level
// let. Same shape as the IDENT-local path:
@@ -6658,8 +6686,21 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\tSHLQ\tCX, BX\n");
};
if (n.op == tkind.TK_RSHIFTEQ) {
// #136: signed RSHIFTEQ → SARQ.
let unsignd_r: bool = false;
if (lcn != nil) {
if (lcn.tnode != nil) {
if (lcn.tnode.type_ != nil) {
unsignd_r = typeisunsigned(lcn.tnode.type_: *tinfo);
};
};
};
if (!unsignd_r) {
unsignd_r = nodeisunsigned(c, n.rhs);
};
emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHRQ\tCX, BX\n");
if (unsignd_r) { emitline("\tSHRQ\tCX, BX\n"); }
else { emitline("\tSARQ\tCX, BX\n"); };
};
// Post-63332fe: /= and %= for an IDENT local. Pre-fix
// fell through with no case, so BX (still holding the