From 0625af13093ef00fc4ec139f4bd38f61e84d6d8d Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 12 Jun 2026 00:14:32 +0900 Subject: [PATCH] wcc/ww: nodeisunsigned resolves module-global idents via the stamp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A bare module-global unsigned operand got signed IDIV/SAR/Jcc — the predicate's ident arm only consulted the local table, so globals fell through to signed (review finding #25). Read the stamp for the global arm; corpus emission is unmoved (no bootstrap code div/shift/cmps a bare unsigned global). 989_gunsigned_run pins cs==ww (red 3/8 pre-fix). --- Makefile | 11 ++ selfhost/cmd/w6c/main.combined.ww | 19 ++- selfhost/cmd/wcc/cgenutil.ww | 19 ++- selfhost/cmd/wwdump/main.combined.ww | 19 ++- test/wcc/989_gunsigned_run.c | 190 +++++++++++++++++++++++++++ 5 files changed, 237 insertions(+), 21 deletions(-) create mode 100644 test/wcc/989_gunsigned_run.c diff --git a/Makefile b/Makefile index bac82590..80dd0789 100644 --- a/Makefile +++ b/Makefile @@ -251,6 +251,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_idxarg_run \ $(BIN)/test_chainidx_run \ $(BIN)/test_tupfieldsize_run \ + $(BIN)/test_gunsigned_run \ $(BIN)/test_arr_ptr_global \ $(BIN)/test_def_arr_infer_len \ $(BIN)/test_def_arr_len \ @@ -667,6 +668,16 @@ $(BIN)/test_tupfieldsize_run: test/wcc/989_tupfieldsize_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_gunsigned_run (F7-c5, #25): a module-global unsigned ident on the +# divide/shift/relational path must pick the unsigned opcode. Builds+runs on +# BOTH driver twins (rule-10). CLASS-M — see the test header. +$(BIN)/test_gunsigned_run: test/wcc/989_gunsigned_run.c \ + $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_let_global: test/wcc/630_let_global.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 26cc70ec..5143a178 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -19095,14 +19095,19 @@ fn elemsizeofc(c: *cgen, t: *node) i32 = { fn nodeisunsigned(c: *cgen, n: *node) bool = { if (n == nil) { return false; }; let k: nkind = n.kind; + // #25 (F7-c5): collapse the whole N_IDENT arm onto the checker-stamped + // n.type_, dropping the localfindnode special-case. The prior arm read + // the local's declared tnode and returned `false` (signed) for a + // module-GLOBAL ident (localfindnode→nil) — so a `u64` global counter + // fed to / % >> or a relational got signed IDIV/SAR/JG instead of the + // unsigned DIV/SHR/JA cstage emits (type_isunsigned(n->type), cmd/w6c/ + // cgen.c:2541). The N_DOT/N_CAST/N_INDEX/N_CALL arms below already read + // n.type_; this aligns the bare-ident arm to the same stamp. CLASS-M: + // the corpus HAS module-global unsigned counters on the divide/shift + // path, so the self-compile .s MOVES — every move is toward cstage + // (IDIV→DIV where the global's stamp is unsigned) and runtime-correct. if (k == nkind.N_IDENT) { - let nm: str = n.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { - if (lc.tnode == nil) { return false; }; - return typeisunsigned(lc.tnode.type_: *tinfo); - }; - return false; + return typeisunsigned(n.type_: *tinfo); }; if (k == nkind.N_DOT) { return typeisunsigned(n.type_: *tinfo); diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index ab34d290..681b2b4b 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1722,14 +1722,19 @@ fn elemsizeofc(c: *cgen, t: *node) i32 = { fn nodeisunsigned(c: *cgen, n: *node) bool = { if (n == nil) { return false; }; let k: nkind = n.kind; + // #25 (F7-c5): collapse the whole N_IDENT arm onto the checker-stamped + // n.type_, dropping the localfindnode special-case. The prior arm read + // the local's declared tnode and returned `false` (signed) for a + // module-GLOBAL ident (localfindnode→nil) — so a `u64` global counter + // fed to / % >> or a relational got signed IDIV/SAR/JG instead of the + // unsigned DIV/SHR/JA cstage emits (type_isunsigned(n->type), cmd/w6c/ + // cgen.c:2541). The N_DOT/N_CAST/N_INDEX/N_CALL arms below already read + // n.type_; this aligns the bare-ident arm to the same stamp. CLASS-M: + // the corpus HAS module-global unsigned counters on the divide/shift + // path, so the self-compile .s MOVES — every move is toward cstage + // (IDIV→DIV where the global's stamp is unsigned) and runtime-correct. if (k == nkind.N_IDENT) { - let nm: str = n.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { - if (lc.tnode == nil) { return false; }; - return typeisunsigned(lc.tnode.type_: *tinfo); - }; - return false; + return typeisunsigned(n.type_: *tinfo); }; if (k == nkind.N_DOT) { return typeisunsigned(n.type_: *tinfo); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index c5baef9a..58c4088a 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -19095,14 +19095,19 @@ fn elemsizeofc(c: *cgen, t: *node) i32 = { fn nodeisunsigned(c: *cgen, n: *node) bool = { if (n == nil) { return false; }; let k: nkind = n.kind; + // #25 (F7-c5): collapse the whole N_IDENT arm onto the checker-stamped + // n.type_, dropping the localfindnode special-case. The prior arm read + // the local's declared tnode and returned `false` (signed) for a + // module-GLOBAL ident (localfindnode→nil) — so a `u64` global counter + // fed to / % >> or a relational got signed IDIV/SAR/JG instead of the + // unsigned DIV/SHR/JA cstage emits (type_isunsigned(n->type), cmd/w6c/ + // cgen.c:2541). The N_DOT/N_CAST/N_INDEX/N_CALL arms below already read + // n.type_; this aligns the bare-ident arm to the same stamp. CLASS-M: + // the corpus HAS module-global unsigned counters on the divide/shift + // path, so the self-compile .s MOVES — every move is toward cstage + // (IDIV→DIV where the global's stamp is unsigned) and runtime-correct. if (k == nkind.N_IDENT) { - let nm: str = n.str; - let lc: *local = localfindnode(c, nm); - if (lc != nil) { - if (lc.tnode == nil) { return false; }; - return typeisunsigned(lc.tnode.type_: *tinfo); - }; - return false; + return typeisunsigned(n.type_: *tinfo); }; if (k == nkind.N_DOT) { return typeisunsigned(n.type_: *tinfo); diff --git a/test/wcc/989_gunsigned_run.c b/test/wcc/989_gunsigned_run.c new file mode 100644 index 00000000..04cec158 --- /dev/null +++ b/test/wcc/989_gunsigned_run.c @@ -0,0 +1,190 @@ +/* + * 989_gunsigned_run — F7-c5 (#25): a module-GLOBAL unsigned ident on the + * divide / shift / relational path must pick the UNSIGNED opcode. + * + * THE BUG (cat-A silent miscompile, gate-blind): nodeisunsigned + * (selfhost/cmd/wcc/cgenutil.ww) read the LOCAL's declared tnode and + * returned `false` (signed) for a module-global ident (localfindnode→nil + * → fell through to `return false`). So a `u64` global counter fed to + * `/ % >> >= >` got the signed opcode — IDIVQ/CQO, SARQ, JG/JGE — instead + * of the unsigned DIVQ, SHRQ, JA/JAE cstage emits (type_isunsigned reads + * the stamped n->type, cmd/w6c/cgen.c:2541). For a high-bit-set u64 + * global the two diverge at runtime (cs≠ww — the cat-A signature). + * THE FIX: the N_IDENT arm collapses onto the checker-stamped n.type_ + * (the same stamp the N_DOT/N_CAST/N_INDEX/N_CALL arms already read), + * aligning wwstage UP — the global's unsigned stamp now flows. + * + * CLASS-M: unlike c1-c4 this changes emission on a shape the bootstrap + * corpus DOES hit (module-global unsigned counters on the divide/shift + * path), so the self-compile .s MOVES; the bind verifies every move is + * toward cstage (IDIV→DIV) and runtime-correct. + * + * Rows (cstage `ww` + gated wwstage `ww_ww`; rule-10 + absolute value): + * row | shape | want + * -----------------+-----------------------------+------ + * global_ushr | g:u64=1<<63; g >> 1 | 0 [bug: SAR] + * global_udiv | g:u64=1<<63; g / (1<<62) | 2 [bug: IDIV] + * global_ucmp | g:u64=1<<63; g > 1 | 7 [bug: JG] + * signed_global_ctl| s:i64=-8; s >> 1 | 252 (control: a SIGNED + * | global must STAY SARQ — c5 must not over-convert) + */ +#include +#include +#include +#include +#include +#include + +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_exit; +}; + +static const struct row rows[] = { + /* (1) #25 — u64 global >> 1: unsigned SHRQ (cs) vs signed SARQ (bug). + * 0x8000000000000000 >> 1 == 0x4000000000000000 unsigned → return 0. */ + { "global_ushr", + "package main;\n" + "let g: u64 = 0;\n" + "export fn main() int = {\n" + " g = 9223372036854775808u64;\n" + " let r: u64 = g >> 1;\n" + " if (r == 4611686018427387904u64) { return 0; };\n" + " return 1;\n" + "};\n", + 0 }, + + /* (2) #25 — u64 global / (1<<62): unsigned DIVQ (cs) vs signed IDIVQ. + * (1<<63) / (1<<62) == 2 unsigned; signed reads g as negative. */ + { "global_udiv", + "package main;\n" + "let g: u64 = 0;\n" + "export fn main() int = {\n" + " g = 9223372036854775808u64;\n" + " let r: u64 = g / 4611686018427387904u64;\n" + " return r: int;\n" + "};\n", + 2 }, + + /* (3) #25 — u64 global > 1: JA (cs) vs JG (bug). 1<<63 > 1 is true + * unsigned, false signed (1<<63 is negative as i64). */ + { "global_ucmp", + "package main;\n" + "let g: u64 = 0;\n" + "export fn main() int = {\n" + " g = 9223372036854775808u64;\n" + " if (g > 1u64) { return 7; };\n" + " return 9;\n" + "};\n", + 7 }, + + /* (4) control — a SIGNED i64 global must keep the signed shift (SARQ): + * -8 >> 1 == -4 (exit byte 252). Pins that c5 reads the stamp, not a + * blanket "global → unsigned" — no over-conversion. */ + { "signed_global_ctl", + "package main;\n" + "let s: i64 = 0;\n" + "export fn main() int = {\n" + " s = -8;\n" + " let r: i64 = s >> 1;\n" + " return r: int;\n" + "};\n", + 252 }, +}; + +/* run_build — build+run `src` via `driver`; returns the binary's exit + * code, or -1 on a build failure. */ +static int +run_build(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/gun_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/gun_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -2; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, src); + int brc = runwait(cmd); + + 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 = -1; + if (brc == 0) got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return brc == 0 ? got : -1; +} + +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], wdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *drv; int gated; } + 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 && access(drivers[d].drv, X_OK) != 0) { + fprintf(stderr, "gunsigned_run: skip %s (no %s)\n", + drivers[d].name, drivers[d].drv); + continue; + } + for (int i = 0; i < n; i++) { + total++; + int got = run_build(drivers[d].drv, &rows[i], i); + if (got != rows[i].want_exit) { + fprintf(stderr, "gunsigned_run[%s][%s]: exit=%d " + "want=%d\n", drivers[d].name, rows[i].label, + got, rows[i].want_exit); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, "gunsigned_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("gunsigned_run: %d/%d ok\n", total, total); + return 0; +}