Files
ww/test/wcc/978_intdiv_signed.c
Hojun-Cho ce3a25a0b4 test: contain sepwork scratch per-driver tmpdir, fix /tmp+in-repo leak (#8)
The wcc test drivers ran `ww build <bare-/tmp src>` with no -o, so the
compiler's <stem>.sepwork scratch landed beside the source and was never
cleaned: unbounded /tmp growth (2195 stale dirs observed) that fills tmpfs
and fabricates phantom test failures + silent harness aborts, and for
in-repo fixture builds leaked .sepwork into the tracked tree.

Each leaking build now writes its source + output inside a per-invocation
tmpdir, passes -o <tmpdir>/<stem> so the .sepwork lands inside it, and
rm -rf's the tmpdir on every exit path -- including fopen-fail and the
expected-fail reject builds (scratch is mkdir'd before the build can fail).
`ww run` and explicit-`-o`/byte-id helpers are left as-is; the 990/993
byte-id comparison logic is byte-for-byte unchanged.

Two items filed separately (this commit holds the no-Makefile / no-main.c
rail):
- #13: a stale <src>.s byte-id readback (749) silently no-ops since
  separate-compile emits .s to <ostem>.sepwork/__root.s; documented inline.
- #14: build-system Makefile recipes build selfhost/cmd/*/main.ww with no
  -o and leak main.sepwork in-tree (bounded, gitignored; own commit).

One concern -- sepwork leak hygiene -- across 228 drivers; uniform
transform applied per-file and two-round reviewed. make test: all 402
passed, zero net-new /tmp scratch, zero test-driven in-repo .sepwork.
2026-06-22 23:29:39 +09:00

458 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 <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[128], tmpdir[64], outbin[128], rmcmd[160], cmd[1024];
snprintf(tmpdir, sizeof tmpdir, "/tmp/wwid_%d_d_%d", getpid(), i);
mkdir(tmpdir, 0755);
snprintf(src, sizeof src, "%s/wwid_%d_%d.ww", tmpdir, getpid(), i);
snprintf(outbin, sizeof outbin, "%s/wwid_%d_%d", tmpdir, getpid(), i);
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir);
FILE *f = fopen(src, "wb");
if (!f) { runwait(rmcmd); return -1; }
fputs(r->src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s build -o %s %s",
driver, outbin, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
runwait(rmcmd);
return -1;
}
int got = runwait(outbin);
runwait(rmcmd);
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;
}