cstage+selfhost+test: sign-aware codegen for signed int div/mod (#16)

Shared miscompile in both stages — not a divergence. Bootstrap byte-id
passed throughout because both stages emitted the same wrong asm. Both
the C cgen (cmd/w6c/cgen.c TK_SLASH/TK_PERCENT) and the ww cgen
(selfhost/cmd/wcc/cgenexpr.ww) prepped IDIVQ with `MOVQ $0, DX`, which
is the unsigned 128-bit dividend shape. For a negative RAX, the CPU
then divides 2^64 + (-RAX) by the divisor — unsigned wraparound, not
signed division. Surfaced via lib/time/add() needing the verbatim Hare
signed-%-normalisation in ref/hare/time/arithm.ha.

Fix: emit CQO (sign-extend RAX into RDX:RAX, REX.W 99) on the signed
arm; keep MOVQ $0, DX on the unsigned arm where the DIVQ-vs-IDIVQ
dispatch was already correct. Since both stages always emit 64-bit
IDIVQ regardless of source width, a single CQO suffices for
i64/i32/i16/i8 — the dividend already lives in RAX sign-extended. No
CDQ/CWTL/CBTW needed.

Symmetric stages (rule 10): both stages were broken identically; both
get the same surgical fix. Adds A_CQO to each assembler's opcode set:
cstage in cmd/w6c/6.out.h + cmd/w6c/txt.c + cmd/w6a/{parse,asm}.c;
wwstage in selfhost/cmd/w6a/{types,parse,asm}.ww.

Class B (shared miscompile) — new in the session's polarity catalog.
Bootstrap byte-id is useless for catching it; semantic 9xx runtime
tests are the right shape. test/wcc/978_intdiv_signed.c covers 27 rows
× 2 drivers = 54 fixtures across {i8,i16,i32,i64,u8,u16,u32,u64} ×
{/, %} with width-boundary minima (INT8_MIN, INT16_MIN, INT32_MIN,
INT64_MIN/2) and high-bit-set unsigned anchors. INT64_MIN is spelled
(-INT64_MAX) - 1 per task #17 (wwstage NEGQ-over-imm drops digits on
-9223372036854775808i64); that literal-cgen bug is unrelated to this
fix.

Two known compound-assign workarounds at cmd/w6c/cgen.c:3765
(TK_SLASHEQ IDENT-local) and :3549 (TK_SLASHEQ/TK_PERCENTEQ
deref-compound) remain in tree; both depend on the assembler having
CQO, so they revert in a follow-up commit citing this one.
This commit is contained in:
2026-05-17 02:12:29 +09:00
parent b401cced05
commit 63332fef50
14 changed files with 478 additions and 22 deletions

View File

@@ -357,6 +357,11 @@ export fn encode(a: *asm_) i32 = {
if (op == A_NOTQ) { encodeunary(a, 247u8, 2, p.to.atype); p = p.link; continue; };
if (op == A_IDIVQ) { encodeunary(a, 247u8, 7, p.to.atype); p = p.link; continue; };
if (op == A_DIVQ) { encodeunary(a, 247u8, 6, p.to.atype); p = p.link; continue; };
if (op == A_CQO) {
emitbyte(a, 72u8); // REX.W (0x48)
emitbyte(a, 153u8); // 0x99
p = p.link; continue;
};
if (op == A_MOVQ) {
let ft: i32 = p.from.atype;

View File

@@ -904,6 +904,11 @@ def A_SYSCALL: i32 = 59;
def A_DATAW: i32 = 60;
def A_DATAR: i32 = 61;
// REX.W 99 — sign-extend RAX into RDX:RAX. Pairs with IDIVQ for
// signed division; pendant to the MOVQ $0, DX zero-fill that pairs
// with DIVQ.
def A_CQO: i32 = 66;
// ---- structs (mirror cmd/w6a/a.h) --------------------------------------
type aoperand = struct {
@@ -1116,6 +1121,7 @@ fn opcodelookup(p: *u8, n: u64) i32 = {
if (streqlit(p, n, "IMULQ")) { return A_IMULQ; };
if (streqlit(p, n, "IDIVQ")) { return A_IDIVQ; };
if (streqlit(p, n, "DIVQ")) { return A_DIVQ; };
if (streqlit(p, n, "CQO")) { return A_CQO; };
if (streqlit(p, n, "NEGQ")) { return A_NEGQ; };
if (streqlit(p, n, "NOTQ")) { return A_NOTQ; };
if (streqlit(p, n, "ANDQ")) { return A_ANDQ; };
@@ -2003,6 +2009,11 @@ export fn encode(a: *asm_) i32 = {
if (op == A_NOTQ) { encodeunary(a, 247u8, 2, p.to.atype); p = p.link; continue; };
if (op == A_IDIVQ) { encodeunary(a, 247u8, 7, p.to.atype); p = p.link; continue; };
if (op == A_DIVQ) { encodeunary(a, 247u8, 6, p.to.atype); p = p.link; continue; };
if (op == A_CQO) {
emitbyte(a, 72u8); // REX.W (0x48)
emitbyte(a, 153u8); // 0x99
p = p.link; continue;
};
if (op == A_MOVQ) {
let ft: i32 = p.from.atype;

View File

@@ -61,6 +61,7 @@ fn opcodelookup(p: *u8, n: u64) i32 = {
if (streqlit(p, n, "IMULQ")) { return A_IMULQ; };
if (streqlit(p, n, "IDIVQ")) { return A_IDIVQ; };
if (streqlit(p, n, "DIVQ")) { return A_DIVQ; };
if (streqlit(p, n, "CQO")) { return A_CQO; };
if (streqlit(p, n, "NEGQ")) { return A_NEGQ; };
if (streqlit(p, n, "NOTQ")) { return A_NOTQ; };
if (streqlit(p, n, "ANDQ")) { return A_ANDQ; };

View File

@@ -132,6 +132,11 @@ def A_SYSCALL: i32 = 59;
def A_DATAW: i32 = 60;
def A_DATAR: i32 = 61;
// REX.W 99 — sign-extend RAX into RDX:RAX. Pairs with IDIVQ for
// signed division; pendant to the MOVQ $0, DX zero-fill that pairs
// with DIVQ.
def A_CQO: i32 = 66;
// ---- structs (mirror cmd/w6a/a.h) --------------------------------------
type aoperand = struct {

View File

@@ -12353,15 +12353,26 @@ fn cgbin(c: *cgen, n: *node) void = {
if (n.op == tkind.TK_MINUS) { emitline("\tSUBQ\tBX, AX\n"); return; };
if (n.op == tkind.TK_STAR) { emitline("\tIMULQ\tBX, AX\n"); return; };
if (n.op == tkind.TK_SLASH) {
emitline("\tMOVQ\t$0, DX\n");
if (unsignd) { emitline("\tDIVQ\tBX\n"); }
else { emitline("\tIDIVQ\tBX\n"); };
// Signed IDIV reads dividend from RDX:RAX; CQO sign-extends
// RAX. Zero-filling DX would treat a negative RAX as a huge
// positive 128-bit value. Unsigned DIV needs RDX zero.
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tBX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tBX\n");
};
return;
};
if (n.op == tkind.TK_PERCENT) {
emitline("\tMOVQ\t$0, DX\n");
if (unsignd) { emitline("\tDIVQ\tBX\n"); }
else { emitline("\tIDIVQ\tBX\n"); };
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tBX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tBX\n");
};
emitline("\tMOVQ\tDX, AX\n");
return;
};

View File

@@ -2536,15 +2536,26 @@ fn cgbin(c: *cgen, n: *node) void = {
if (n.op == tkind.TK_MINUS) { emitline("\tSUBQ\tBX, AX\n"); return; };
if (n.op == tkind.TK_STAR) { emitline("\tIMULQ\tBX, AX\n"); return; };
if (n.op == tkind.TK_SLASH) {
emitline("\tMOVQ\t$0, DX\n");
if (unsignd) { emitline("\tDIVQ\tBX\n"); }
else { emitline("\tIDIVQ\tBX\n"); };
// Signed IDIV reads dividend from RDX:RAX; CQO sign-extends
// RAX. Zero-filling DX would treat a negative RAX as a huge
// positive 128-bit value. Unsigned DIV needs RDX zero.
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tBX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tBX\n");
};
return;
};
if (n.op == tkind.TK_PERCENT) {
emitline("\tMOVQ\t$0, DX\n");
if (unsignd) { emitline("\tDIVQ\tBX\n"); }
else { emitline("\tIDIVQ\tBX\n"); };
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tBX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tBX\n");
};
emitline("\tMOVQ\tDX, AX\n");
return;
};

View File

@@ -12353,15 +12353,26 @@ fn cgbin(c: *cgen, n: *node) void = {
if (n.op == tkind.TK_MINUS) { emitline("\tSUBQ\tBX, AX\n"); return; };
if (n.op == tkind.TK_STAR) { emitline("\tIMULQ\tBX, AX\n"); return; };
if (n.op == tkind.TK_SLASH) {
emitline("\tMOVQ\t$0, DX\n");
if (unsignd) { emitline("\tDIVQ\tBX\n"); }
else { emitline("\tIDIVQ\tBX\n"); };
// Signed IDIV reads dividend from RDX:RAX; CQO sign-extends
// RAX. Zero-filling DX would treat a negative RAX as a huge
// positive 128-bit value. Unsigned DIV needs RDX zero.
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tBX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tBX\n");
};
return;
};
if (n.op == tkind.TK_PERCENT) {
emitline("\tMOVQ\t$0, DX\n");
if (unsignd) { emitline("\tDIVQ\tBX\n"); }
else { emitline("\tIDIVQ\tBX\n"); };
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tBX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tBX\n");
};
emitline("\tMOVQ\tDX, AX\n");
return;
};