From 0c5482fad08d36b69ec5ad18b11c2493ebd44257 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 8 Jun 2026 14:50:03 +0900 Subject: [PATCH] =?UTF-8?q?wcc/check:=20#11=20def=20[=5F]T=20length-infere?= =?UTF-8?q?nce=20=E2=80=94=20stamp=20the=20def=20decl=20path,=20the=20#7?= =?UTF-8?q?=20let-twin=20(both=20stages)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit def xs:[_]T=arrlit was sized 0 (no DATA emitted, garbage indexed reads) on BOTH stages, byte-id-identical: #7 wired [_] length-inference only on the let decl path, never def. cstage check.c N_DEF pass-2 infers the length from the initialiser and re-points both d->type and the SK_DEF Sym (an indexed read resolves the def through its Sym); wwstage check.ww runs inferarraylen before resolvewalk. Checker-only — cgen lays the DATA correctly once the length is stamped. w6c and wwdump combined.ww regen'd (both embed the wcc checker). Pin: table-driven test/wcc/814_def_arr_infer_len (index reads int/u8/2d + 1-elem edge + negative build-fail), teeth-proven against a reverted inference. Filed separately, not folded (rule-11): def-global .len GAP-A (#7 cgdot twin), def str-array element DATA GAP-B (#270), [0]T-vs-[_] alen==0 conflation (pre-existing in the #7 let path too). --- Makefile | 7 + cmd/wcc/check.c | 25 ++ selfhost/cmd/w6c/main.combined.ww | 8 + selfhost/cmd/wcc/check.ww | 8 + selfhost/cmd/wwdump/main.combined.ww | 8 + test/wcc/814_def_arr_infer_len.c | 354 +++++++++++++++++++++++++++ 6 files changed, 410 insertions(+) create mode 100644 test/wcc/814_def_arr_infer_len.c diff --git a/Makefile b/Makefile index 2faa3e42..9bee4422 100644 --- a/Makefile +++ b/Makefile @@ -243,6 +243,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_arr_infer_len \ + $(BIN)/test_def_arr_infer_len \ $(BIN)/test_slice_str_global_zero \ $(BIN)/test_slice_literal_global \ $(BIN)/test_global_arr_elem_field \ @@ -632,6 +633,12 @@ $(BIN)/test_arr_infer_len: test/wcc/684_arr_infer_len.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_def_arr_infer_len: test/wcc/814_def_arr_infer_len.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_arr_tagged_elem: test/wcc/685_arr_tagged_elem.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 a760fa13..238a5fa5 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -2838,6 +2838,31 @@ check_file(Checker *c, Node *file) case N_DEF: { if (d->rhs) { Type *rt = cexpr(c, d->rhs); + /* #11: `def xs: [_]T = arrlit;` — infer the length + * from the initialiser, the def twin of the module + * N_LET path below. pass-1 (N_DEF above) installed + * the SK_DEF Sym + d->type with the alen=0 sentinel; + * an indexed read resolves the def through its Sym, + * so re-point BOTH d->type (feeds cgen's emit_defs + * DATA row + defarray registry) and the Sym (feeds + * the N_INDEX / `.len` type read). #7 only wired the + * let decl path; the def path silently stayed length + * 0 (no DATA, garbage reads). Run before the + * assignability check so arrlit_init_fits sees the + * inferred length. */ + if (d->type && d->type->kind == TY_ARRAY + && d->type->alen == 0) { + Type *iu = type_chase_named(rt); + if (iu && iu->kind == TY_ARRAY) { + d->type = type_array(c->a, + d->type->sub, iu->alen); + Sym *s = scope_lookup_local(c->cur, + d->str); + if (s) s->type = d->type; + } else + err(c, d->pos, "[_]T needs an " + "array-literal initialiser"); + } /* #88: fold sibling/imported def refs, casts, and * arithmetic to a constant. litfold (plain literal * leaf) is already typed UNTYPED_INT by cexpr; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index d0f7e836..370afe53 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -15416,6 +15416,14 @@ export fn checkfile(c: *checker, file: *node) void = { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; // return type resolvefnbody(c, d); case nkind.N_DEF: + // #11: a module-level `def xs: [_]T = arrlit;` must infer + // its length BEFORE resolvewalk stamps d.lhs's tinfo, the + // def twin of the N_LET arm below. #7 wired only the let + // path, so the def path silently stayed length 0 (no DATA, + // garbage indexed reads). inferarraylen mutates the N_TARRAY + // length child in place (the SSoT all reads resolve through), + // so no Sym re-point is needed on this side. + inferarraylen(c, d); if (d.lhs != nil) { resolvewalk(c, d.lhs); }; if (d.rhs != nil) { resolvewalk(c, d.rhs); }; // #251: `def D: [N]T = [int/rune lits]` array-init diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index b37bf5b6..731e6473 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -5135,6 +5135,14 @@ export fn checkfile(c: *checker, file: *node) void = { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; // return type resolvefnbody(c, d); case nkind.N_DEF: + // #11: a module-level `def xs: [_]T = arrlit;` must infer + // its length BEFORE resolvewalk stamps d.lhs's tinfo, the + // def twin of the N_LET arm below. #7 wired only the let + // path, so the def path silently stayed length 0 (no DATA, + // garbage indexed reads). inferarraylen mutates the N_TARRAY + // length child in place (the SSoT all reads resolve through), + // so no Sym re-point is needed on this side. + inferarraylen(c, d); if (d.lhs != nil) { resolvewalk(c, d.lhs); }; if (d.rhs != nil) { resolvewalk(c, d.rhs); }; // #251: `def D: [N]T = [int/rune lits]` array-init diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 4d2223c2..99ddbf40 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -15416,6 +15416,14 @@ export fn checkfile(c: *checker, file: *node) void = { if (d.lhs != nil) { resolvewalk(c, d.lhs); }; // return type resolvefnbody(c, d); case nkind.N_DEF: + // #11: a module-level `def xs: [_]T = arrlit;` must infer + // its length BEFORE resolvewalk stamps d.lhs's tinfo, the + // def twin of the N_LET arm below. #7 wired only the let + // path, so the def path silently stayed length 0 (no DATA, + // garbage indexed reads). inferarraylen mutates the N_TARRAY + // length child in place (the SSoT all reads resolve through), + // so no Sym re-point is needed on this side. + inferarraylen(c, d); if (d.lhs != nil) { resolvewalk(c, d.lhs); }; if (d.rhs != nil) { resolvewalk(c, d.rhs); }; // #251: `def D: [N]T = [int/rune lits]` array-init diff --git a/test/wcc/814_def_arr_infer_len.c b/test/wcc/814_def_arr_infer_len.c new file mode 100644 index 00000000..24b7eef5 --- /dev/null +++ b/test/wcc/814_def_arr_infer_len.c @@ -0,0 +1,354 @@ +/* + * 814_def_arr_infer_len — cstage and wwstage agree, byte-for-byte and at + * runtime, that a `def NAME: [_]T = [...]` array infers its length from + * the initialiser's element count — the `def` twin of the `let` path + * pinned by 684 (task #11 / #5; #7 wired only the let decl path). + * + * The bug (BOTH stages, byte-id identical): the `[_]` infer sentinel + * (alen=0 / nil length-child) was stamped on the `let` decl path but NOT + * on the `def` decl path. A `def [_]int = [10,20,30]` stayed sized 0, + * emitted no/short DATA, and indexed reads (`TAB[1]`) / `.len` returned + * garbage with no diagnostic (rule-7 silent miscompile). + * + * The fix (CHECKER ONLY — cgen already keys stride/length/DATA off the + * stamped length, so `def [3]int` works today): + * - cstage cmd/wcc/check.c N_DEF pass-2: mirror the module N_LET path — + * if d->type is `[_]T` (TY_ARRAY, alen==0), chase the rhs type, + * rebuild type_array(.., iu->alen), and re-point BOTH d->type AND the + * installed SK_DEF Sym (an indexed read resolves the def through its + * Sym, so the Sym repoint is required). + * - wwstage selfhost/cmd/wcc/check.ww N_DEF arm: call inferarraylen(c,d) + * BEFORE resolvewalk stamps d.lhs's tinfo (the N_TARRAY AST is the + * SSoT; no Sym repoint needed). A `def [_]T` whose init is not an + * array literal can't infer → LOUD error, never a silent length-0. + * + * `def` is MODULE-SCOPE ONLY in both stages, so there is exactly one def + * site per stage and every element type rides the single inference arm + * (it is gated only on TY_ARRAY+alen==0, agnostic to the element type). + * + * SCOPE (corrected at impl-probe — the inference is complete, but two + * adjacent shapes hit SEPARATE PRE-EXISTING cgen gaps that fail the SAME + * way on an explicit-length `def [N]T`, so they are NOT #11 and are + * excluded here; each is filed as its own task): + * - `def`-array `.len`/`.ptr` on wwstage falls to the SB fallback and + * emits `MOVQ len(SB)` (w6l: undefined reference). cstage handles it + * (so cstage vs wwstage asm also DIVERGES) — the wwstage cgdot + * def-array arm is missing, the def twin of #7's let-global cgdot + * fix. Every `.len` row is dropped for this reason. + * - `def [_]str` / `def [N]str` emit no DATA symbol (w6l: undefined + * reference, BOTH stages) — the #270 / str-slice-array-element DATA + * lineage. Every str row is dropped for this reason. + * The pin therefore exercises INDEX reads across int / u8 / multi-dim, + * which is exactly the shape #11 corrupts and is mutation-sensitive + * (a collapse-to-0 def lays no DATA row, so the indexed read strikes + * garbage / segv). + * + * The NEG row (`def [_]int = 5`, non-array init) proves the loud-error + * arm fires (rule-7: never a silent zero-length array). + * + * row | shape | want + * --------------+--------------------------------------------+----- + * def_int_i1 | def [_]int=[10,20,30], TAB[1] (ken oracle)| 20 + * def_int_last | def [_]int=[10,20,30], TAB[2] (full len) | 30 + * def_one_elem | def [_]int=[7], A[0] (1-elem edge) | 7 + * def_u8_first | def [_]u8=[1..5], B[0] (narrow stride) | 1 + * def_u8_last | def [_]u8=[1..5], B[4] | 5 + * def_2d_first | def [_][2]int=[[1,2],[3,4],[5,6]], D[0][0] | 1 + * def_2d_last | def [_][2]int=[..], D[2][1] | 6 + * NEG def_noarr | def [_]int = 5; (non-array init) | BUILD FAIL + */ +#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[] = { + /* INDEX reads only — the shape #11 corrupts. Reading the LAST + * element of an N-element def proves the inference stamped the full + * length: a collapse-to-0 lays no DATA row at that offset, so the + * read strikes garbage / segv. This is mutation-sensitive without + * `.len` (whose def-array codegen is a SEPARATE pre-existing gap — + * see the header). */ + { "def_int_i1", + "package main;\n" + "def TAB: [_]int = [10, 20, 30];\n" + "export fn main() i32 = {\n" + "\treturn TAB[1]: i32;\n" + "};\n", + 20 }, + + { "def_int_last", + "package main;\n" + "def TAB: [_]int = [10, 20, 30];\n" + "export fn main() i32 = {\n" + "\treturn TAB[2]: i32;\n" + "};\n", + 30 }, + + /* 1-element edge — the minimal non-empty count. */ + { "def_one_elem", + "package main;\n" + "def A: [_]int = [7];\n" + "export fn main() i32 = {\n" + "\treturn A[0]: i32;\n" + "};\n", + 7 }, + + { "def_u8_first", + "package main;\n" + "def B: [_]u8 = [1u8, 2u8, 3u8, 4u8, 5u8];\n" + "export fn main() i32 = {\n" + "\treturn B[0]: i32;\n" + "};\n", + 1 }, + + /* narrow (1B) stride — the last element of 5 proves both the stride + * and the inferred length. */ + { "def_u8_last", + "package main;\n" + "def B: [_]u8 = [1u8, 2u8, 3u8, 4u8, 5u8];\n" + "export fn main() i32 = {\n" + "\treturn B[4]: i32;\n" + "};\n", + 5 }, + + /* multi-dim: outer `[_]` infers 3 from the row count, element type + * is the explicit `[2]int`. The def static-init DATA path lays the + * full 3x2 aggregate (the `let` runtime-store path for this shape is + * separately #270-1c-blocked, so this is def-only today). */ + { "def_2d_first", + "package main;\n" + "def D: [_][2]int = [[1, 2], [3, 4], [5, 6]];\n" + "export fn main() i32 = {\n" + "\treturn D[0][0]: i32;\n" + "};\n", + 1 }, + + { "def_2d_last", + "package main;\n" + "def D: [_][2]int = [[1, 2], [3, 4], [5, 6]];\n" + "export fn main() i32 = {\n" + "\treturn D[2][1]: i32;\n" + "};\n", + 6 }, +}; + +/* `def [_]T` whose initialiser is not an array literal can't infer its + * length — both stages must FAIL the build (loud diagnostic, not a silent + * zero-length array). `def` requires an `= value` init (parser), so the + * no-init case can't reach the checker; the non-array init is the only + * negative shape. */ +static const char *neg[] = { + /* non-array initialiser */ + "package main;\n" + "def TAB: [_]int = 5;\n" + "export fn main() i32 = { return 0; };\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/dail_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/dail_%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; +} + +/* build_should_fail — a `def [_]T` that can't infer must error on + * `driver`; returns 0 when the build correctly FAILS, non-zero when it + * wrongly succeeded. */ +static int +build_should_fail(const char *driver, const char *src, int i) +{ + char s[64], tmpdir[64], cmd[1024]; + snprintf(s, sizeof s, "/tmp/dailn_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/dailn_%d_d_%d", getpid(), i); + + FILE *f = fopen(s, "wb"); + if (!f) return -1; + fputs(src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, s); + int rc = runwait(cmd); + unlink(s); + /* clean any emitted binary */ + const char *base = strrchr(s, '/'); + base = base ? base + 1 : s; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + unlink(outbin); + rmdir(tmpdir); + return rc == 0 ? -1 : 0; /* build must NOT succeed */ +} + +/* asm_byte_identical — w6c vs w6c_ww .s for the same source must match. + * The bug is byte-id-BLIND (both stages emitted identical wrong asm), so + * this is a rule-10 convergence invariant, not a mutation detector. */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/dail_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/dail_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/dail_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->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, "row[%s]: w6c errored\n", r->label); + 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, "row[%s]: w6c_ww errored\n", r->label); + 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, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + 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]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[1024]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int nn = (int)(sizeof neg / sizeof neg[0]); + int total = 0, fail = 0; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "def_arr_infer_len: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "def_arr_infer_len[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + for (int i = 0; i < nn; i++) { + total++; + if (build_should_fail(drivers[d].path, neg[i], + 100 + i) != 0) { + fprintf(stderr, + "def_arr_infer_len[%s][neg%d]: built ok, " + "expected a loud error\n", + drivers[d].name, i); + fail++; + } + } + } + + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "def_arr_infer_len: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("def_arr_infer_len: %d/%d ok\n", total, total); + return 0; +}