diff --git a/Makefile b/Makefile index 40717e95..879f418a 100644 --- a/Makefile +++ b/Makefile @@ -247,6 +247,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_arr_strslice_elem \ $(BIN)/test_arr_tagged_elem \ $(BIN)/test_dup_main_reject \ + $(BIN)/test_size_untyped_int \ $(BIN)/test_tagged_staticinit \ $(BIN)/test_arr_infer_len \ $(BIN)/test_arr_cap_reject \ @@ -1445,6 +1446,12 @@ $(BIN)/test_tagged_staticinit: test/wcc/843_tagged_staticinit.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_size_untyped_int: test/wcc/844_size_untyped_int.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_slice_str_global_zero: test/wcc/686_slice_str_global_zero.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 8d2eee96..02b6fc5c 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -13551,11 +13551,10 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { }; // Post-fold the node IS an N_INTLIT-shaped // untyped_int constant. Mirrors cstage - // cmd/wcc/check.c:926/958 which stamps - // ty_untyped_int after the fold. The return - // tnode mktname("i32") is the assignability - // target for callers, not the constant's - // own type. + // cmd/wcc/check.c:1570/1602 which stamps + // ty_untyped_int after the fold AND returns it + // to the caller (the Hare-correct type), so a + // `let x: size = size(T)` binding is assignable. let utn: *node = mktname(c, "untyped_int"); if (issize) { // #108(b): rule-10 twin of the cstage @@ -13566,7 +13565,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { let v: i64 = astsize(c, e.list); foldtointlit(c, e, v); e.type_ = tinfofornode(c, utn): *void; - return mktname(c, "i32"); + return mktname(c, "untyped_int"); }; if (isalign) { if (astunsized(c, e.list)) { @@ -13575,7 +13574,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { let v: i64 = astalign(c, e.list); foldtointlit(c, e, v); e.type_ = tinfofornode(c, utn): *void; - return mktname(c, "i32"); + return mktname(c, "untyped_int"); }; // offset(e.f): the arg is a value expression // (N_DOT), parsed via parsearglist — not a @@ -13592,7 +13591,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { }; foldtointlit(c, e, off); e.type_ = tinfofornode(c, utn): *void; - return mktname(c, "i32"); + return mktname(c, "untyped_int"); }; }; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 40e57452..e422cba9 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -3081,11 +3081,10 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { }; // Post-fold the node IS an N_INTLIT-shaped // untyped_int constant. Mirrors cstage - // cmd/wcc/check.c:926/958 which stamps - // ty_untyped_int after the fold. The return - // tnode mktname("i32") is the assignability - // target for callers, not the constant's - // own type. + // cmd/wcc/check.c:1570/1602 which stamps + // ty_untyped_int after the fold AND returns it + // to the caller (the Hare-correct type), so a + // `let x: size = size(T)` binding is assignable. let utn: *node = mktname(c, "untyped_int"); if (issize) { // #108(b): rule-10 twin of the cstage @@ -3096,7 +3095,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { let v: i64 = astsize(c, e.list); foldtointlit(c, e, v); e.type_ = tinfofornode(c, utn): *void; - return mktname(c, "i32"); + return mktname(c, "untyped_int"); }; if (isalign) { if (astunsized(c, e.list)) { @@ -3105,7 +3104,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { let v: i64 = astalign(c, e.list); foldtointlit(c, e, v); e.type_ = tinfofornode(c, utn): *void; - return mktname(c, "i32"); + return mktname(c, "untyped_int"); }; // offset(e.f): the arg is a value expression // (N_DOT), parsed via parsearglist — not a @@ -3122,7 +3121,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { }; foldtointlit(c, e, off); e.type_ = tinfofornode(c, utn): *void; - return mktname(c, "i32"); + return mktname(c, "untyped_int"); }; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 0d6a2e6d..ba68f0a2 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -13551,11 +13551,10 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { }; // Post-fold the node IS an N_INTLIT-shaped // untyped_int constant. Mirrors cstage - // cmd/wcc/check.c:926/958 which stamps - // ty_untyped_int after the fold. The return - // tnode mktname("i32") is the assignability - // target for callers, not the constant's - // own type. + // cmd/wcc/check.c:1570/1602 which stamps + // ty_untyped_int after the fold AND returns it + // to the caller (the Hare-correct type), so a + // `let x: size = size(T)` binding is assignable. let utn: *node = mktname(c, "untyped_int"); if (issize) { // #108(b): rule-10 twin of the cstage @@ -13566,7 +13565,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { let v: i64 = astsize(c, e.list); foldtointlit(c, e, v); e.type_ = tinfofornode(c, utn): *void; - return mktname(c, "i32"); + return mktname(c, "untyped_int"); }; if (isalign) { if (astunsized(c, e.list)) { @@ -13575,7 +13574,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { let v: i64 = astalign(c, e.list); foldtointlit(c, e, v); e.type_ = tinfofornode(c, utn): *void; - return mktname(c, "i32"); + return mktname(c, "untyped_int"); }; // offset(e.f): the arg is a value expression // (N_DOT), parsed via parsearglist — not a @@ -13592,7 +13591,7 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { }; foldtointlit(c, e, off); e.type_ = tinfofornode(c, utn): *void; - return mktname(c, "i32"); + return mktname(c, "untyped_int"); }; }; }; diff --git a/test/wcc/844_size_untyped_int.c b/test/wcc/844_size_untyped_int.c new file mode 100644 index 00000000..ebb5012c --- /dev/null +++ b/test/wcc/844_size_untyped_int.c @@ -0,0 +1,244 @@ +/* + * 844_size_untyped_int — the size/align/offset builtins fold to an + * untyped_int constant and RETURN untyped_int to the caller (the + * Hare-correct type), so `let x: size = size(T)` is assignable + * (catB-9; 2026-06-14). + * + * The fold stamps the node `e.type_ = untyped_int` in BOTH stages + * (correct), but pre-fix wwstage's check.ww RETURNED `i32` to the + * caller while cstage returned `untyped_int` (check.c:1570/1602). + * untyped_int is assignable to any integer type (size, u64, ...); + * i32 is NOT assignable to `size`. So `let lenbytes: size = + * size(u64);` — the canonical Hare idiom (sha256.ww:189) — was + * FALSE-REJECTED by wwstage (`let: not assignable (i32 -> size)`) + * while cstage accepted. The fix aligns wwstage UP: check.ww:3099/ + * 3108/3125 now return mktname("untyped_int"). cstage is unchanged. + * + * DISCRIMINATION (verified against the pre-fix master out/bin/ww_ww): + * p1 built rc=1 with `let: not assignable (i32 -> size)` on wwstage + * and rc=0 on cstage; post-fix both accept. (A live discrimination + * row can't run here — the post-fix binary accepts p1 — so the + * pre-fix reject is pinned in this comment, not as a runtime row.) + * + * row | shape | want + * --------+----------------------------------------+----- + * p1 | let lenbytes: size = size(u64); | 8 + * p2 | let x: u64 = size(int); | 8 + * p3 | let pad: size = size(u64) - n; (n=1) | 7 + * p7_ctrl | let x = size(u64); (UNANNOTATED) | 8 + * + * p7_ctrl is drew's de-risk control: an unannotated `let` defaults + * the untyped_int to int and behaves IDENTICALLY in both stages, + * pre AND post fix (a both-ACCEPT, run 8). It proves the return-type + * flip did not perturb the default-int path. + * + * BYTE-ID (rule 10): the bid program mixes a `size`-typed local with + * `size(u64)` in a comparison — the context where the fold's new + * untyped_int could converge a type and shift CMPL->CMPQ width. Post- + * fix cstage's w6c and wwstage's w6c_ww emit byte-identical asm (both + * CMPQ); the row diffs the two .s files. + * + * Table-driven, BOTH stages (cstage `ww`, wwstage `ww_ww` gated on + * existence), mirrors 682_arr_enum_elem / 842_dup_main_reject. + * Intermediates land in tmpdirs, never the source tree (rule 14). + */ +#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; }; + +static const struct row rows[] = { + { "p1_let_size", + "package main;\n" + "export fn main() i32 = {\n" + " let lenbytes: size = size(u64);\n" + " return lenbytes: i32;\n" + "};\n", 8 }, + + { "p2_let_u64", + "package main;\n" + "export fn main() i32 = {\n" + " let x: u64 = size(int);\n" + " return x: i32;\n" + "};\n", 8 }, + + { "p3_size_arith", + "package main;\n" + "export fn main() i32 = {\n" + " let n: size = 1;\n" + " let pad: size = size(u64) - n;\n" + " return pad: i32;\n" + "};\n", 7 }, + + { "p7_ctrl_unannotated", + "package main;\n" + "export fn main() i32 = {\n" + " let x = size(u64);\n" + " return x: i32;\n" + "};\n", 8 }, +}; + +/* the byte-id program: a size()-mixing comparison (the CMPL->CMPQ + * width context). Both stages must emit identical asm. */ +static const char *bid_src = + "package main;\n" + "export fn main() i32 = {\n" + " let m: size = 16;\n" + " if (m >= size(u64)) { return 1; };\n" + " return 0;\n" + "};\n"; + +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/szui_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/szui_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %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; +} + +static int +asm_byte_identical(const char *bin, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/szui_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/szui_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/szui_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(bid_src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "bid: w6c errored\n"); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "bid: w6c_ww errored\n"); + unlink(src); 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, "bid: cstage vs wwstage asm differs\n"); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +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 *path; 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].path, X_OK) != 0) { + fprintf(stderr, "size_untyped_int: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + total++; + int got = run_driver(drivers[d].path, &rows[i], i); + if (got != rows[i].want) { + fprintf(stderr, + "size_untyped_int[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (access(wdrv, X_OK) == 0) { + total++; + if (asm_byte_identical(bin, 0) != 0) + fail++; + } + + if (fail) { + fprintf(stderr, + "size_untyped_int: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("size_untyped_int: %d/%d ok\n", total, total); + return 0; +}