From 5e4d67d90a8b097cb11f87e67b8c0b14a764e812 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 26 May 2026 02:39:35 +0900 Subject: [PATCH] check: widen const def-ref to declared int type in def init (#113) A def initializer whose rhs references another def -- `def INT_MIN: int = I32_MIN;`, `def SIZE_MAX: size = U64_MAX;` -- failed to compile: an N_IDENT->SK_DEF types as the referent's DECLARED type (i32, u64), so the def-init assignability check (type_assignable) rejected i32 -> int / u64 -> size, even though the value is a compile-time constant that fits. This blocked faithful types/types::c limit defs (no cast in the Hare source). In a def initializer the rhs is a flexible constant. When it folds to a compile-time integer (the #88 eval_def_const path: sibling/imported def refs, casts, arithmetic) and the value fits the declared integer target, re-flexibilize it to UNTYPED_INT so the existing untyped-int->typed assignability path accepts it. This emulates Hare's flexible-constant promotion (ICONST -> promote_flexible/lower_flexible, ref/harec/src/types.c:860); def_cast_fits is the range check that keeps a genuine out-of-range narrowing a loud "not assignable" error, never a silent truncation (rule 7). It is strictly the const subset: the general CONCRETE (non-const) integer widening Hare does at types.c:1021-1037 is intentionally stricter in ww -- #115. 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 DATA row. Relaxing the cstage aligns the richer side DOWN to the leaner side (rule 10); both stages stamp the identical folded value, so emitted asm is byte-identical. The bootstrap corpus has zero cross-prim-width def-ref defs, so the new path is dead there and 990-997 are unperturbed. Coverage: test/wcc/760_def_widen_const (i32->int neg, u64->size, byte-id on each, cstage-only out-of-range narrowing fail-loud). --- Makefile | 7 + cmd/wcc/check.c | 49 ++++-- test/wcc/760_def_widen_const.c | 310 +++++++++++++++++++++++++++++++++ 3 files changed, 355 insertions(+), 11 deletions(-) create mode 100644 test/wcc/760_def_widen_const.c diff --git a/Makefile b/Makefile index f38d3010..cf551678 100644 --- a/Makefile +++ b/Makefile @@ -310,6 +310,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ $(BIN)/test_check_enum_fold \ + $(BIN)/test_def_widen_const \ $(BIN)/test_use_promote_alias \ $(BIN)/test_field_signed $(BIN)/test_frame_argcount \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ @@ -546,6 +547,12 @@ $(BIN)/test_check_enum_fold: test/wcc/759_check_enum_fold.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_def_widen_const: test/wcc/760_def_widen_const.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_arrlit_str_full: test/wcc/711_arrlit_str_full.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 40ebef79..0bacfd69 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -2160,21 +2160,48 @@ check_file(Checker *c, Node *file) case N_DEF: { if (d->rhs) { Type *rt = cexpr(c, d->rhs); + /* #88: fold sibling/imported def refs, casts, and + * arithmetic to a constant. litfold (plain literal + * leaf) is already typed UNTYPED_INT by cexpr; + * constfold (#88, gated on litfold missing) covers + * the richer shapes and is the one we stamp, so + * existing literal/unary defs keep their rhs node + * and the emitted bytes stay byte-identical. */ + u64 dv; + int litfold = rt != ty_err + && fold_int_literal(d->rhs, &dv); + int constfold = rt != ty_err && !litfold + && eval_def_const(c, d->rhs, &dv, 0); + /* #113: a def-ref that folds to a compile-time + * constant (a def ref like `def INT_MIN: int = + * I32_MIN`) carries its referent's concrete declared + * type (i32), not UNTYPED_INT, so the assignability + * check below rejected i32 -> int. In a def + * initializer the rhs is a flexible constant, so + * re-flexibilize the folded value to UNTYPED_INT here + * when it fits the declared integer target — emulating + * Hare's flexible-constant promotion (ICONST -> + * promote_flexible/lower_flexible, range-checked: + * ref/harec/src/types.c:860, reached via the + * STORAGE_ICONST assignability case at :1012/:1019). + * ww has no ICONST flexible-range type; def_cast_fits + * is the range check that keeps a genuine out-of-range + * value a loud "not assignable" error, never a silent + * truncation (rule 7). This is strictly the const + * subset: the general CONCRETE (non-const) integer + * widening Hare does at ref/harec/src/types.c:1021-1037 + * is intentionally stricter in ww, see #115. */ + Type *art = rt; + if (constfold && d->type && type_isint(d->type) + && type_isint(rt) && !type_isuntyped(rt) + && def_cast_fits(d->type, dv)) + art = ty_untyped_int; if (d->type && rt != ty_err && d->type != ty_err - && !type_assignable(d->type, rt)) + && !type_assignable(d->type, art)) err(c, d->pos, "def %s init %s not assignable to %s", d->str, type_name(c->a, rt), type_name(c->a, d->type)); - /* #88: const-fold sibling/imported def refs, - * casts, and arithmetic so cgen's literal-only - * emit can lay down the DATA row. GATED on the - * plain literal fold missing first, so existing - * literal/unary defs keep their rhs node and the - * emitted bytes stay byte-identical. */ - u64 dv; - if (rt != ty_err - && !fold_int_literal(d->rhs, &dv) - && eval_def_const(c, d->rhs, &dv, 0)) + if (constfold) stamp_intlit(c, d->rhs, dv); } break; diff --git a/test/wcc/760_def_widen_const.c b/test/wcc/760_def_widen_const.c new file mode 100644 index 00000000..2d4369d0 --- /dev/null +++ b/test/wcc/760_def_widen_const.c @@ -0,0 +1,310 @@ +/* + * 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 +#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 src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/dwc_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/dwc_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + /* timeout 180 per repo convention; test/run does not bound + * individual binaries, so an unguarded hang would stall make test. */ + snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s build %s 2>/dev/null", + 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; +} + +/* ---- 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; +}