/* * 732_def_const_fold — top-level `def` rhs const-fold extended to * sibling/imported def references, casts, and arithmetic (PROJECT #88). * * Predecessor 631_def_neg_global lifted the LITERAL fold (INTLIT/RUNELIT/ * TRUE/FALSE/NIL + unary +/-/~) into the shared fold_int_literal / * foldintliteral helper; it explicitly left N_CAST, sibling-ident, and * N_BIN OUT OF SCOPE (see 631's header, "negative-u32-cast"). #88 brings * them in: a Hare-faithful `def SCHAR_MAX = types::I8_MAX;` or * `def MASK: i32 = (1 << 7) - 1;` was previously inexpressible — the rhs * folded to nothing, no DATA row was emitted, and any reference failed * to link. * * The fix folds at CHECK time (both stages' pass-2 N_DEF arm) via a new * eval_def_const / evaldefconst that reuses the shared fold_binop arith * core (factored out of eval_enum_value / enumvalfold) and the checker's * EXISTING scope lookup (scope_lookup_prefer for a sibling ref, * scope_lookup_in_module for a `mod.NAME` qualified ref). On success the * rhs is stamped to an N_INTLIT in place, so cgen's literal-only * emit_defs / emitdefconstants lays down the DATA row with no codegen * change. The fold is GATED on the plain literal fold missing first, so * every pre-#88 def keeps its node and the emitted bytes stay * byte-identical (the bootstrap has zero def-ref-def, so 990-997 never * touch this latent path — hence this targeted fixture). * * Coverage: * 1. EXEC (same-module, via the `ww` driver, + `ww_ww` if built): * sibling-ref, sibling+arith, def->def->def chain, shift+arith, * and a widening cast — 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. * 3. CROSS-MODULE (#88 a2): a combined multi-`package` source (the * driver's internal concat form, fed straight to w6c like * 728_match_4arm_cross_module) where `def K = a.J + 1;` resolves * the imported `a.J`. Assert the folded DATA row value AND byte-id. * 4. FAIL-LOUD (rule 7): a same-module cycle (def A=B; def B=A), a * cross-module cycle, and a narrowing cast whose value overflows * the target must each fail the build on BOTH stages (never a * silent missing row / silent truncation). */ #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; } /* ---- 1. same-module EXEC rows (compile+link+run) -------------------- */ struct row { const char *label; const char *src; int want; }; static const struct row exec_rows[] = { /* Bare sibling reference: `def B = A;`. Pre-#88 the rhs (N_IDENT) * folded to nothing. */ { "sibling-ref", "def A: i32 = 7;\n" "def B: i32 = A;\n" "fn main() i32 = {\n" "\tlet x: i32 = B;\n" "\tif (x == 7) { return 42; };\n" "\treturn 1;\n" "};\n", 42 }, /* Sibling + arithmetic: the headline `def B = A + 1;`. */ { "sibling-add", "def A: i32 = 5;\n" "def B: i32 = A + 1;\n" "fn main() i32 = {\n" "\tlet x: i32 = B;\n" "\tif (x == 6) { return 42; };\n" "\treturn 1;\n" "};\n", 42 }, /* def -> def -> def chain: C resolves through B through A. Pins * the recursive resolve (and that the depth guard doesn't trip on * a legitimate short chain). */ { "def-chain", "def A: i32 = 5;\n" "def B: i32 = A + 1;\n" "def C: i32 = B * A;\n" "fn main() i32 = {\n" "\tlet x: i32 = C;\n" "\tif (x == 30) { return 42; };\n" "\treturn 1;\n" "};\n", 42 }, /* Pure arithmetic with a shift: `(1 << 7) - 1` == 127. Exercises * the fold_binop shift + subtract path off any sibling ref. */ { "shift-arith", "def MASK: i32 = (1 << 7) - 1;\n" "fn main() i32 = {\n" "\tlet x: i32 = MASK;\n" "\tif (x == 127) { return 42; };\n" "\treturn 1;\n" "};\n", 42 }, /* Widening cast over a sibling ref: `A: i64` where A: i32 = 5. * The N_CAST is stripped (i32->i64 widens, value fits), so W folds * to 5 in an 8-byte slot. */ { "widening-cast", "def A: i32 = 5;\n" "def W: i64 = A: i64;\n" "fn main() i32 = {\n" "\tlet x: i64 = W;\n" "\tif (x == 5i64) { 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/dcf_%d_d_%d", getpid(), i); mkdir(tmpdir, 0755); snprintf(src, sizeof src, "%s/dcf_%d_%d.ww", tmpdir, getpid(), i); snprintf(outbin, sizeof outbin, "%s/dcf_%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 (990-997); 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 + slurp ----------------------------------------- */ 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); return runwait(cmd); } static int slurp(const char *path, char *buf, size_t cap) { FILE *f = fopen(path, "rb"); if (!f) return -1; size_t n = fread(buf, 1, cap - 1, f); fclose(f); buf[n] = '\0'; return (int)n; } /* ---- 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/dcf_bi_%d_%d.ww", getpid(), i); snprintf(cs, sizeof cs, "/tmp/dcf_bi_%d_%d_c.s", getpid(), i); snprintf(ws, sizeof ws, "/tmp/dcf_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. cross-module fold: a combined multi-package source ---------- * Fed straight to w6c (the driver's internal concat form — `ww build` * can't expand an already-concatenated source). `def K = a.J + 1;` * resolves the imported `a.J` (41) through scope_lookup_in_module and * folds to 42, whose DATA row first byte is '*' (0x2a). */ static const char xmod_src[] = "package a;\n" "export def J: i32 = 41;\n" "package b;\n" "import a;\n" "def K: i32 = a.J + 1;\n" "export fn main() i32 = {\n" "\tlet k: i32 = K;\n" "\tif (k == 42) { return 42; };\n" "\treturn 1;\n" "};\n"; /* Assert the folded DATA row for symbol `b.K` is present with value 42 * (first byte '*'). Returns 0 on success. */ static int xmod_fold_present(const char *w6c, const char *stage) { char src[64], s[64], buf[1 << 15]; snprintf(src, sizeof src, "/tmp/dcf_xm_%d_%s.ww", getpid(), stage); snprintf(s, sizeof s, "/tmp/dcf_xm_%d_%s.s", getpid(), stage); FILE *f = fopen(src, "wb"); if (!f) return -1; fputs(xmod_src, f); fclose(f); if (emit_s(w6c, src, s, sizeof s) != 0) { fprintf(stderr, "xmod[%s]: w6c emit failed\n", stage); unlink(src); return -1; } int rc = 0; if (slurp(s, buf, sizeof buf) < 0) { rc = -1; } else if (strstr(buf, "b.K(SB),\"*") == NULL) { fprintf(stderr, "xmod[%s]: no folded `DATA b.K(SB),\"*` (value 42) row\n", stage); rc = -1; } unlink(src); unlink(s); return rc; } /* ---- 4. fail-loud rows: must fail the build on BOTH stages ---------- */ struct failrow { const char *label; const char *src; }; static const struct failrow fail_rows[] = { /* Same-module cycle: the depth guard must trip and fail loud, not * hang and not silently drop the row. */ { "same-module-cycle", "def A: i32 = B;\n" "def B: i32 = A;\n" "export fn main() i32 = { return A; };\n" }, /* Cross-module cycle (combined source): a.J -> b.K -> a.J. */ { "cross-module-cycle", "package a;\n" "import b;\n" "export def J: i32 = b.K;\n" "package b;\n" "import a;\n" "export def K: i32 = a.J;\n" "export fn main() i32 = { return 0; };\n" }, /* Narrowing cast that loses the value: 0x1FF (511) does not fit a * u8, so the cast must fail loud rather than silently truncate. */ { "narrowing-cast", "def Z: u8 = 0x1FF: u8;\n" "export fn main() i32 = { let z: u8 = Z; return z: i32; };\n" }, }; /* Compile via w6c only (no link/run); success means the build FAILED as * required (nonzero w6c exit). */ static int compile_fails(const char *w6c, const struct failrow *r, const char *stage, int i) { char src[64], s[64], cmd[1024]; snprintf(src, sizeof src, "/tmp/dcf_fl_%d_%d.ww", getpid(), i); snprintf(s, sizeof s, "/tmp/dcf_fl_%d_%d.s", getpid(), i); FILE *f = fopen(src, "wb"); if (!f) return -1; fputs(r->src, f); fclose(f); /* A regressed depth guard would HANG the cycle rows rather than fail * loud (rule 7). timeout's 124 is distinct from a clean guard failure * -- fold it back to a TEST failure so a hang can't pass as fail-loud. */ 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, "failrow[%s][%s]: w6c exited 0 (expected loud failure)\n", r->label, stage); return -1; } if (rc == 124) { fprintf(stderr, "failrow[%s][%s]: w6c timed out (depth guard hung?)\n", r->label, stage); 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. same-module 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_const_fold: 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_const_fold[%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. cross-module fold: DATA row value on cstage, + byte-id */ total++; if (xmod_fold_present(w6c, "cstage") != 0) fail++; if (have_ww) { total++; if (xmod_fold_present(w6c_ww, "wwstage") != 0) fail++; total++; if (asm_byte_identical(bin, xmod_src, "xmod-fold", 900) != 0) fail++; } /* 4. fail-loud on BOTH stages */ int nfail = (int)(sizeof fail_rows / sizeof fail_rows[0]); for (int i = 0; i < nfail; i++) { total++; if (compile_fails(w6c, &fail_rows[i], "cstage", i) != 0) fail++; if (have_ww) { total++; if (compile_fails(w6c_ww, &fail_rows[i], "wwstage", 1000 + i) != 0) fail++; } } if (fail) { fprintf(stderr, "def_const_fold: %d/%d fixtures failed\n", fail, total); return 1; } printf("def_const_fold: %d/%d ok\n", total, total); return 0; }