Files
ww/test/wcc/978_intdiv_signed.c
Hojun-Cho 63332fef50 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.
2026-05-17 02:12:29 +09:00

376 lines
10 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 978_intdiv_signed — runtime semantics of integer `/` and `%` across
* the {i8,i16,i32,i64,u8,u16,u32,u64} × {/, %} matrix, plus the
* width-boundary minima.
*
* Class B (shared miscompile, not divergence): both cstage and
* wwstage previously emitted `MOVQ $0, DX + IDIVQ` on the signed
* arm, treating a negative dividend as a huge unsigned 128-bit
* value. Bootstrap byte-id passed throughout — both stages stomped
* the same way — so only a semantic runtime test catches it. The
* fix swaps the prep to `CQO` (sign-extend RAX into RDX:RAX) on
* the signed arm of TK_SLASH/TK_PERCENT; unsigned stays MOVQ-zero +
* DIVQ. See cmd/w6c/cgen.c TK_SLASH/TK_PERCENT and the matching
* selfhost/cmd/wcc/cgenexpr.ww branches.
*
* Unsigned rows pin the DIVQ arm against future regression — the
* cgen.c comment from #41 (high-bit-set u64 / 2) is exactly the
* shape that the unsigned-vs-signed dispatch protects.
*
* Same dual-driver runner as 640_int_cast_signed.c: each row gets
* compiled by both cstage `ww` and wwstage `ww_ww` (when built),
* with the binary exit code carrying the per-case verdict (42 = ok).
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return -1;
}
struct row { const char *label; const char *src; int want; };
static const struct row rows[] = {
/* ---- i64 signed: negative dividend, positive divisor ---------- */
{ "i64_div_neg_dividend",
"fn main() i32 = {\n"
" let a: i64 = -100i64;\n"
" let b: i64 = 1000000000i64;\n"
" if (a / b == 0i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "i64_mod_neg_dividend",
"fn main() i32 = {\n"
" let a: i64 = -100i64;\n"
" let b: i64 = 1000000000i64;\n"
" if (a % b == -100i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* ---- i64 signed: positive dividend, negative divisor ---------- */
{ "i64_div_neg_divisor",
"fn main() i32 = {\n"
" let a: i64 = 50i64;\n"
" let b: i64 = -3i64;\n"
" if (a / b == -16i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "i64_mod_neg_divisor",
"fn main() i32 = {\n"
" let a: i64 = 50i64;\n"
" let b: i64 = -3i64;\n"
" if (a % b == 2i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* ---- i64 signed: both negative -------------------------------- */
{ "i64_div_both_negative",
"fn main() i32 = {\n"
" let a: i64 = -50i64;\n"
" let b: i64 = -3i64;\n"
" if (a / b == 16i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "i64_mod_both_negative",
"fn main() i32 = {\n"
" let a: i64 = -50i64;\n"
" let b: i64 = -3i64;\n"
" if (a % b == -2i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* ---- i64 signed: both positive (sanity / regression anchor) --- */
{ "i64_div_both_positive",
"fn main() i32 = {\n"
" let a: i64 = 50i64;\n"
" let b: i64 = 3i64;\n"
" if (a / b == 16i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "i64_mod_both_positive",
"fn main() i32 = {\n"
" let a: i64 = 50i64;\n"
" let b: i64 = 3i64;\n"
" if (a % b == 2i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* ---- i64 boundary: INT64_MIN ---------------------------------- *
* INT64_MIN / -1 would SIGFPE (signed overflow); /2 instead.
* The literal `-9223372036854775808i64` triggers task #17
* (wwstage NEGQ-over-imm drops digits → `MOVQ $-, AX`), so
* we spell it `(-INT64_MAX) - 1` to sidestep that orthogonal
* bug until #17 lands. */
{ "i64_div_INT64_MIN_by_two",
"fn main() i32 = {\n"
" let a: i64 = (-9223372036854775807i64) - 1i64;\n"
" let b: i64 = 2i64;\n"
" if (a / b == -4611686018427387904i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "i64_mod_INT64_MIN_by_two",
"fn main() i32 = {\n"
" let a: i64 = (-9223372036854775807i64) - 1i64;\n"
" let b: i64 = 2i64;\n"
" if (a % b == 0i64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* ---- i32 signed ----------------------------------------------- */
{ "i32_div_neg_dividend",
"fn main() i32 = {\n"
" let x: i32 = -100i32;\n"
" let y: i32 = 7i32;\n"
" if (x / y == -14i32) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "i32_mod_neg_dividend",
"fn main() i32 = {\n"
" let x: i32 = -100i32;\n"
" let y: i32 = 7i32;\n"
" if (x % y == -2i32) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "i32_div_INT32_MIN_by_two",
"fn main() i32 = {\n"
" let x: i32 = -2147483648i32;\n"
" let y: i32 = 2i32;\n"
" if (x / y == -1073741824i32) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "i32_mod_INT32_MIN_by_two",
"fn main() i32 = {\n"
" let x: i32 = -2147483648i32;\n"
" let y: i32 = 2i32;\n"
" if (x % y == 0i32) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* ---- i16 signed ----------------------------------------------- */
{ "i16_div_neg_dividend",
"fn main() i32 = {\n"
" let a: i16 = -100i16;\n"
" let b: i16 = 7i16;\n"
" if (a / b == -14i16) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "i16_mod_neg_dividend",
"fn main() i32 = {\n"
" let a: i16 = -100i16;\n"
" let b: i16 = 7i16;\n"
" if (a % b == -2i16) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "i16_div_INT16_MIN_by_two",
"fn main() i32 = {\n"
" let a: i16 = -32768i16;\n"
" let b: i16 = 2i16;\n"
" if (a / b == -16384i16) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* ---- i8 signed ------------------------------------------------ */
{ "i8_div_neg_dividend",
"fn main() i32 = {\n"
" let a: i8 = -100i8;\n"
" let b: i8 = 7i8;\n"
" if (a / b == -14i8) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "i8_mod_neg_dividend",
"fn main() i32 = {\n"
" let a: i8 = -100i8;\n"
" let b: i8 = 7i8;\n"
" if (a % b == -2i8) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "i8_div_INT8_MIN_by_two",
"fn main() i32 = {\n"
" let a: i8 = -128i8;\n"
" let b: i8 = 2i8;\n"
" if (a / b == -64i8) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* ---- u64 unsigned: high-bit-set / 2 must not sign-extend ----- */
{ "u64_div_high_bit_set",
"fn main() i32 = {\n"
" let a: u64 = 0x8000000000000001u64;\n"
" let b: u64 = 2u64;\n"
" if (a / b == 0x4000000000000000u64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "u64_mod_high_bit_set",
"fn main() i32 = {\n"
" let a: u64 = 0x8000000000000001u64;\n"
" let b: u64 = 2u64;\n"
" if (a % b == 1u64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* ---- u32 unsigned: high-bit-set / 2 -------------------------- */
{ "u32_div_high_bit_set",
"fn main() i32 = {\n"
" let a: u32 = 0x80000001u32;\n"
" let b: u32 = 2u32;\n"
" if (a / b == 0x40000000u32) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "u32_mod_high_bit_set",
"fn main() i32 = {\n"
" let a: u32 = 0x80000001u32;\n"
" let b: u32 = 2u32;\n"
" if (a % b == 1u32) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* ---- u16 unsigned: high-bit-set / 2 -------------------------- */
{ "u16_div_high_bit_set",
"fn main() i32 = {\n"
" let a: u16 = 0xFFFFu16;\n"
" let b: u16 = 2u16;\n"
" if (a / b == 0x7FFFu16) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* ---- u8 unsigned: high-bit-set / 2 --------------------------- */
{ "u8_div_high_bit_set",
"fn main() i32 = {\n"
" let a: u8 = 0xFFu8;\n"
" let b: u8 = 2u8;\n"
" if (a / b == 0x7Fu8) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* ---- mixed-type-flag row: i64 cast to u64 forces unsigned arm.
* `unsignd` flag fires if either operand is unsigned — pin it. */
{ "mixed_unsigned_rhs_picks_unsigned",
"fn main() i32 = {\n"
" let a: i64 = 100i64;\n"
" let b: u64 = 7u64;\n"
" if ((a: u64) / b == 14u64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
};
static int
run_driver(const char *driver, const struct row *r, int i)
{
char src[64], tmpdir[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/wwid_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwid_%d_d_%d", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s",
tmpdir, driver, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
unlink(src); rmdir(tmpdir);
return -1;
}
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = runwait(outbin);
unlink(src); unlink(outbin); rmdir(tmpdir);
return got;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cdrv[1024];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
char wdrv[1024];
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
struct { const char *name; const char *path; int gated_on_existence; }
drivers[] = {
{ "cstage", cdrv, 0 },
{ "wwstage", wdrv, 1 },
{ NULL, NULL, 0 },
};
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int d = 0; drivers[d].name; d++) {
if (drivers[d].gated_on_existence
&& access(drivers[d].path, X_OK) != 0) {
fprintf(stderr, "intdiv_signed: skip %s (no %s)\n",
drivers[d].name, drivers[d].path);
continue;
}
for (int i = 0; i < n; i++) {
int got = run_driver(drivers[d].path, &rows[i], i);
total++;
if (got != rows[i].want) {
fprintf(stderr,
"intdiv_signed[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
}
if (fail) {
fprintf(stderr,
"intdiv_signed: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("intdiv_signed: %d/%d ok\n", total, total);
return 0;
}