/* * 760_def_widen_const — const-scoped cross-prim-width integer widening * in a `def` initializer (PROJECT #113). * * The gap: a def-reference types as the referent's DECLARED type, so * `def INT_MIN: int = I32_MIN;` saw the rhs as i32 and the def-init * assignability check (cmd/wcc/check.c, type_assignable) rejected i32 → * int. The fix: in a def initializer the rhs is a flexible constant, so * a const integer that folds to a concrete primitive width widens to * the declared integer type exactly as an UNTYPED_INT literal would, * when the value fits the target (def_cast_fits). This is the * const-scoped subset of Hare's general integer-widening; non-const * widening stays stricter in ww (PROJECT #115). It rides the #88 * eval_def_const fold + stamp, so cgen's literal-only emit lays the * 8-byte DATA row with no codegen change. * * The fix is cstage-only: the wwstage checker (selfhost/cmd/wcc/check.ww, * "let init / return assignability") intentionally never checks def-init * assignability (it stays quiet, leaving full inference to the C side), * so it never rejected the widening — the #88 stamp already laid the * correct row. Relaxing the cstage aligns the richer side DOWN to the * leaner side (CLAUDE.md rule 10), and both stages stamp the identical * folded value, so the emitted asm is byte-identical. * * Coverage: * 1. EXEC (cstage `ww`, + wwstage `ww_ww` if built): i32 → int (a * negative value, to pin sign-extension across the widen) and * u64 → size (a value above the u32 range, to pin the full 8-byte * slot) — each read back at a use site to prove the DATA row links * and carries the right value. * 2. BYTE-ID: cstage w6c vs wwstage w6c_ww `.s` for every exec row * (proves the const-fold stamp is bit-identical across stages). * 3. FAIL-LOUD (rule 7), cstage only: an out-of-range narrowing * def-ref (`def S: i16 = BIG;` where BIG overflows i16) must still * fail the cstage build — def_cast_fits keeps the widening from * becoming a silent truncation. Asserted on the cstage alone * because the wwstage has no def-init assignability check at all * (pre-existing leanness, unchanged by #113). */ #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; } /* ---- 1. EXEC rows (compile+link+run) -------------------------------- */ struct row { const char *label; const char *src; int want; }; static const struct row exec_rows[] = { /* i32 → int widening of a negative value: `def INT_MIN`-shaped. * I32_MIN = -2147483648; read back as int it must sign-extend to * the full 8-byte slot. main returns 42 iff the value round-trips. */ { "i32-to-int-neg", "def A_I32: i32 = -2147483648;\n" "def A_INT: int = A_I32;\n" "fn main() i32 = {\n" "\tlet x: int = A_INT;\n" "\tif (x == -2147483648) { return 42; };\n" "\treturn 1;\n" "};\n", 42 }, /* small negative i32 → int, to pin -7 specifically (the task's * worked example). */ { "i32-to-int-small", "def A_I32: i32 = -7;\n" "def A_INT: int = A_I32;\n" "fn main() i32 = {\n" "\tlet x: int = A_INT;\n" "\tif (x == -7) { return 42; };\n" "\treturn 1;\n" "};\n", 42 }, /* u64 → size widening of a value above the u32 range: pins the * full 8-byte slot survives the widen (0x1_0000_0003 = 4294967299). */ { "u64-to-size", "def BIG_U64: u64 = 4294967299;\n" "def S_SIZE: size = BIG_U64;\n" "fn main() i32 = {\n" "\tlet x: size = S_SIZE;\n" "\tif (x == 4294967299) { return 42; };\n" "\treturn 1;\n" "};\n", 42 }, }; static int run_driver(const char *driver, const struct row *r, int i) { char tmpdir[64], src[128], outbin[128], rmcmd[160], cmd[1024]; snprintf(tmpdir, sizeof tmpdir, "/tmp/dwc_%d_d_%d", getpid(), i); mkdir(tmpdir, 0755); snprintf(src, sizeof src, "%s/dwc_%d_%d.ww", tmpdir, getpid(), i); snprintf(outbin, sizeof outbin, "%s/dwc_%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); /* timeout 180 per repo convention; test/run does not bound * individual binaries, so an unguarded hang would stall make test. */ snprintf(cmd, sizeof cmd, "timeout 180 %s build -o %s %s 2>/dev/null", 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; } /* ---- shared .s emit -------------------------------------------------- */ static int emit_s(const char *w6c, const char *src, char *out_s, size_t cap) { char cmd[1024]; snprintf(cmd, sizeof cmd, "timeout 180 %s -o %s %s 2>/dev/null", w6c, out_s, src); (void)cap; return runwait(cmd); } /* ---- 2. byte-identity of cstage vs wwstage .s ----------------------- */ static int asm_byte_identical(const char *bin, const char *src, const char *label, int i) { char wwsrc[64], cs[64], ws[64]; snprintf(wwsrc, sizeof wwsrc, "/tmp/dwc_bi_%d_%d.ww", getpid(), i); snprintf(cs, sizeof cs, "/tmp/dwc_bi_%d_%d_c.s", getpid(), i); snprintf(ws, sizeof ws, "/tmp/dwc_bi_%d_%d_w.s", getpid(), i); FILE *f = fopen(wwsrc, "wb"); if (!f) return -1; fputs(src, f); fclose(f); char w6c[640], w6c_ww[640]; snprintf(w6c, sizeof w6c, "%s/w6c", bin); snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); if (emit_s(w6c, wwsrc, cs, sizeof cs) != 0) { fprintf(stderr, "row[%s]: w6c errored\n", label); unlink(wwsrc); return -1; } if (emit_s(w6c_ww, wwsrc, ws, sizeof ws) != 0) { fprintf(stderr, "row[%s]: w6c_ww errored\n", label); unlink(wwsrc); unlink(cs); return -1; } FILE *fc = fopen(cs, "rb"); FILE *fw = fopen(ws, "rb"); int rc = 0; if (!fc || !fw) { rc = -1; } else { for (;;) { int a = fgetc(fc); int b = fgetc(fw); if (a != b) { rc = -1; break; } if (a == EOF) break; } } if (fc) fclose(fc); if (fw) fclose(fw); if (rc != 0) fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", label); unlink(wwsrc); unlink(cs); unlink(ws); return rc; } /* ---- 3. fail-loud (cstage only): out-of-range narrowing def-ref ----- */ /* `def BIG: i32 = 70000; def S: i16 = BIG;` — 70000 overflows i16, so * def_cast_fits says the value does not fit and the const-scoped widen * does NOT fire; the def-init assignability check then rejects i32 → * i16. Compile via w6c only; success means the build FAILED as required * (nonzero w6c exit), never a silent truncation. */ static const char narrow_src[] = "def BIG_I32: i32 = 70000;\n" "def S_I16: i16 = BIG_I32;\n" "export fn main() i32 = { let s: i16 = S_I16; return s: i32; };\n"; static int cstage_narrow_fails(const char *w6c) { char src[64], s[64], cmd[1024]; snprintf(src, sizeof src, "/tmp/dwc_nl_%d.ww", getpid()); snprintf(s, sizeof s, "/tmp/dwc_nl_%d.s", getpid()); FILE *f = fopen(src, "wb"); if (!f) return -1; fputs(narrow_src, f); fclose(f); snprintf(cmd, sizeof cmd, "timeout 180 %s -o %s %s 2>/dev/null", w6c, s, src); int rc = runwait(cmd); unlink(src); unlink(s); if (rc == 0) { fprintf(stderr, "narrow[cstage]: w6c exited 0 (expected loud failure, " "no silent truncation)\n"); return -1; } if (rc == 124) { fprintf(stderr, "narrow[cstage]: w6c timed out\n"); return -1; } return 0; } 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], w6c[1024], w6c_ww[1024]; snprintf(cdrv, sizeof cdrv, "%s/ww", bin); snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); snprintf(w6c, sizeof w6c, "%s/w6c", bin); snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); int have_ww = (access(w6c_ww, X_OK) == 0); int total = 0, fail = 0; /* 1. exec via cstage `ww` (+ wwstage `ww_ww` if built) */ struct { const char *name; const char *path; int gated; } drivers[] = { { "cstage", cdrv, 0 }, { "wwstage", wdrv, 1 }, { NULL, NULL, 0 }, }; int nexec = (int)(sizeof exec_rows / sizeof exec_rows[0]); for (int d = 0; drivers[d].name; d++) { if (drivers[d].gated && access(drivers[d].path, X_OK) != 0) { fprintf(stderr, "def_widen_const: skip %s (no %s)\n", drivers[d].name, drivers[d].path); continue; } for (int i = 0; i < nexec; i++) { int got = run_driver(drivers[d].path, &exec_rows[i], i); total++; if (got != exec_rows[i].want) { fprintf(stderr, "def_widen_const[%s][%s]: exit=%d want=%d\n", drivers[d].name, exec_rows[i].label, got, exec_rows[i].want); fail++; } } } /* 2. byte-id on every exec row (only when wwstage is built) */ if (have_ww) { for (int i = 0; i < nexec; i++) { total++; if (asm_byte_identical(bin, exec_rows[i].src, exec_rows[i].label, i) != 0) fail++; } } /* 3. fail-loud (cstage only) */ total++; if (cstage_narrow_fails(w6c) != 0) fail++; if (fail) { fprintf(stderr, "def_widen_const: %d/%d fixtures failed\n", fail, total); return 1; } printf("def_widen_const: %d/%d ok\n", total, total); return 0; }