Files
ww/test/wcc/978_intdiv_signed.c
Hojun-Cho 4fa4bcf34e 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.
2026-05-17 02:40:40 +09:00

463 lines
13 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 },
/* ---- 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
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;
}