From 29a2ab2a7225d82f5fc84aaf25b14978d8b08a98 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 8 Jun 2026 19:54:29 +0900 Subject: [PATCH] wcc/check: #9 reject explicit [N]=[init] over-fill incl [0] (both stages) An explicit [N]T = [init] with more initializers than N silently mis-compiled for N==0: the over-fill length-mismatch check was suppressed when alen==0, because alen==0 doubles as the [_] infer-sentinel after resolve_type collapses the two. So def/let [0]int=[1,2] silently resized (cstage exit 2) or OOB-read/segfaulted (wwstage) instead of the loud length-mismatch that [N]=[init>N] gets everywhere else. The AST keeps the distinction the Type loses: [_] leaves the N_TARRAY length-child NULL, an explicit [N] carries N_INTLIT. cstage adds an is_infer_arr() helper, drops the alen>0 exemption at the over-fill check, and gates the 4 infer-resize/no-init sites on is_infer_arr so an explicit [0] flows to the over-fill -> loud. wwstage flips the one shared count gate (checkarrlitfits) from declen>0 to arrtn.rhs!=nil, which also dissolves a wwstage local-resize/module-OOB inconsistency. [_] inference, [0]=[] empty, and [_]-no-init louding all preserved. Under-long (countlhs) instead. Mirrors wwstage's `arrtn.rhs != nil` test. */ +static int +is_infer_arr(Node *tn) +{ + return tn != NULL && tn->kind == N_TARRAY && tn->rhs == NULL; +} + /* arrlit_init_fits — #130: accept-if-fits for `let/def A: [N]T = [..]` * where the whole-array type_assignable failed (bare-int elements * synthesize [N]i32 via type_default, losing the literal flavor that @@ -430,12 +440,14 @@ arrlit_init_fits(Checker *c, Type *dt, Node *rhs) * check below and then smashed the frame at cgen (each element is * stored at its natural offset — the overflow clobbered neighbours * and even the saved BP). Reject loud before the element walk. - * alen==0 stays exempt: 0 doubles as the [_] infer sentinel ([0] - * vs [_] conflation, and [_] in def/struct-field never infers — - * task #11), and the let paths patch the real length in before - * reaching here. Under-long (count < N, no `...`) stays accepted - * as before; Hare rejects it — task #10. */ - if (u->kind == TY_ARRAY && u->alen != SIZE_UNDEFINED && u->alen > 0 + * #9: the `alen > 0` exemption is GONE. An infer `[_]` is resized to + * its real count at the decl sites (is_infer_arr-gated) BEFORE + * reaching here, so an array arriving with alen==0 is necessarily an + * EXPLICIT `[0]` — and `[0] = [1,2]` (count 2 > 0) must be loud, not + * silently resized. `[0] = []` (count 0) stays accepted. SIZE_UNDEFINED + * (opaque/unsized) still exempt. Under-long (count < N, no `...`) stays + * accepted as before; Hare rejects it — task #10. */ + if (u->kind == TY_ARRAY && u->alen != SIZE_UNDEFINED && count > u->alen) { err(c, rhs->pos, "array literal has %llu elements " "but declared array holds %llu", @@ -2199,7 +2211,8 @@ clet(Checker *c, Node *n) * with no array-literal initialiser (no init at all, or a non-array * init) can't infer its length — that is a loud error, never a * silent zero-length array (rule 7, #7). */ - if (declared && declared->kind == TY_ARRAY && declared->alen == 0) { + if (declared && declared->kind == TY_ARRAY && declared->alen == 0 + && is_infer_arr(n->lhs)) { Type *iu = type_chase_named(initt); if (iu && iu->kind == TY_ARRAY) declared = type_array(c->a, declared->sub, iu->alen); @@ -2864,7 +2877,8 @@ check_file(Checker *c, Node *file) * assignability check so arrlit_init_fits sees the * inferred length. */ if (d->type && d->type->kind == TY_ARRAY - && d->type->alen == 0) { + && d->type->alen == 0 + && is_infer_arr(d->lhs)) { Type *iu = type_chase_named(rt); if (iu && iu->kind == TY_ARRAY) { d->type = type_array(c->a, @@ -2957,7 +2971,8 @@ check_file(Checker *c, Node *file) * which both lays the full-length DATA row and reads * the right `.len`. */ if (d->type && d->type->kind == TY_ARRAY - && d->type->alen == 0) { + && d->type->alen == 0 + && is_infer_arr(d->lhs)) { Type *iu = type_chase_named(rt); if (iu && iu->kind == TY_ARRAY) { d->type = type_array(c->a, @@ -2997,9 +3012,10 @@ check_file(Checker *c, Node *file) && eval_def_const(c, d->rhs, &dv, 0)) stamp_intlit(c, d->rhs, dv); } else if (d->type && d->type->kind == TY_ARRAY - && d->type->alen == 0) { + && d->type->alen == 0 && is_infer_arr(d->lhs)) { /* `let x: [_]T;` — no initialiser, length can't be - * inferred (rule 7, #7). */ + * inferred (rule 7, #7). An explicit `[0]T;` with no + * init is a valid empty array, not this error. */ err(c, d->pos, "[_]T needs an array-literal " "initialiser"); } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 778c2bb9..2f2a8173 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14499,7 +14499,14 @@ fn checkarrlitfits(c: *checker, arrtn: *node, rhs: *node) void = { // Under-long (count < N, no `...`) stays accepted as before; Hare // rejects it — task #10. let declen: u64 = arrayelen(c, arrtn.rhs); - if (declen > 0u64) { + // #9: fire the over-fill whenever the length is EXPLICITLY declared + // (arrtn.rhs present) — incl `[0]`. `[_]` leaves arrtn.rhs nil UNTIL + // inferarraylen stamps it with the real count (runs first), so a + // resolved `[_]` arrives here with cnt == declen (no over-fill). An + // explicit `[0]=[1,2]` keeps arrtn.rhs=N_INTLIT(0) → declen 0, cnt 2 → + // loud. `[0]=[]` → cnt 0, no error. Replaces the `declen > 0` guard, + // the wwstage twin of cstage's dropped `alen > 0`. + if (arrtn.rhs != nil) { let cnt: u64 = 0u64; let ce: *node = rhs.list; for (ce != nil) { diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 0b31d347..c942bda2 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -4218,7 +4218,14 @@ fn checkarrlitfits(c: *checker, arrtn: *node, rhs: *node) void = { // Under-long (count < N, no `...`) stays accepted as before; Hare // rejects it — task #10. let declen: u64 = arrayelen(c, arrtn.rhs); - if (declen > 0u64) { + // #9: fire the over-fill whenever the length is EXPLICITLY declared + // (arrtn.rhs present) — incl `[0]`. `[_]` leaves arrtn.rhs nil UNTIL + // inferarraylen stamps it with the real count (runs first), so a + // resolved `[_]` arrives here with cnt == declen (no over-fill). An + // explicit `[0]=[1,2]` keeps arrtn.rhs=N_INTLIT(0) → declen 0, cnt 2 → + // loud. `[0]=[]` → cnt 0, no error. Replaces the `declen > 0` guard, + // the wwstage twin of cstage's dropped `alen > 0`. + if (arrtn.rhs != nil) { let cnt: u64 = 0u64; let ce: *node = rhs.list; for (ce != nil) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 7da2c9eb..e5d8939e 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14499,7 +14499,14 @@ fn checkarrlitfits(c: *checker, arrtn: *node, rhs: *node) void = { // Under-long (count < N, no `...`) stays accepted as before; Hare // rejects it — task #10. let declen: u64 = arrayelen(c, arrtn.rhs); - if (declen > 0u64) { + // #9: fire the over-fill whenever the length is EXPLICITLY declared + // (arrtn.rhs present) — incl `[0]`. `[_]` leaves arrtn.rhs nil UNTIL + // inferarraylen stamps it with the real count (runs first), so a + // resolved `[_]` arrives here with cnt == declen (no over-fill). An + // explicit `[0]=[1,2]` keeps arrtn.rhs=N_INTLIT(0) → declen 0, cnt 2 → + // loud. `[0]=[]` → cnt 0, no error. Replaces the `declen > 0` guard, + // the wwstage twin of cstage's dropped `alen > 0`. + if (arrtn.rhs != nil) { let cnt: u64 = 0u64; let ce: *node = rhs.list; for (ce != nil) { diff --git a/test/wcc/820_arr_zero_vs_infer.c b/test/wcc/820_arr_zero_vs_infer.c new file mode 100644 index 00000000..ac872d35 --- /dev/null +++ b/test/wcc/820_arr_zero_vs_infer.c @@ -0,0 +1,328 @@ +/* + * 820_arr_zero_vs_infer — an EXPLICIT zero/short fixed-size array over-filled + * by its initializer (`[0]int = [1,2]`, `[2]int = [1,2,3]`) is a LOUD length- + * mismatch on BOTH stages; an INFER `[_]` still infers its length from the + * initializer (task #9; ken oracle .ai/ken-9-oracle.md, rob spec + * .ai/rob-9-spec.md). + * + * The bug: post-resolve_type, both `[0]` and `[_]` collapse to alen==0 — the + * Type loses the distinction. The #71 over-fill diagnostic was suppressed for + * alen==0, so `[0]int = [1,2]` slipped past and each stage misbehaved + * DIFFERENTLY (byte-id-blind): + * - cstage silently RESIZED [0]→[2] (exit 2), or for the `def`/`let` cases + * resized too. + * - wwstage kept [0] and OOB-read / SEGFAULTed (exit 8 / 139), or resized + * the local (exit 2) — inconsistent across local vs module. + * + * The fix (one both-stage CHECKER commit): the AST RETAINS the distinction the + * Type loses — an infer `[_]` leaves the N_TARRAY length-child NULL, an + * explicit `[N]` (incl `[0]`) carries an N_INTLIT. cstage gates the four + * infer-resize / no-init sites on is_infer_arr() and drops the + * `alen > 0` exemption at the over-fill check; wwstage's shared count-gate + * checkarrlitfits fires whenever `arrtn.rhs != nil`. So an explicit `[N]=[init]` + * with count>N louds in EVERY context, INCLUDING N==0, before codegen. + * + * Mutation-sanity: every neg `[0]` row BUILT+RAN pre-fix (silent resize / OOB); + * it must now FAIL to build (the rows assert build-FAIL, which only holds + * post-fix). def_two_overfill (`[2]=[1,2,3]`) is the #71 N>0 regression guard. + * + * neg row | shape | gate + * -------------------+------------------------------------+---------- + * def_zero_overfill | def X:[0]int=[1,2] | build FAIL + * let_zero_overfill | let X:[0]int=[1,2] (module) | build FAIL + * local_zero_overfill| local [0]int=[1,2] | build FAIL + * def_two_overfill | def X:[2]int=[1,2,3] (#71 guard) | build FAIL + * str_zero_overfill | def X:[0]str=["a"] (elem-agnostic) | build FAIL + * + * pos row | shape | want + * ------------+--------------------------------+------ + * infer_ctl | def X:[_]int=[10,20]; X[1] | 20 (#11 infer works) + * empty_zero | let X:[0]int=[]; return 7 | 7 (legit empty array) + * infer_len | let X:[_]int=[1,2,3]; X.len | 3 (infer unaffected) + */ +#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; +} + +/* beid — include this row in the cstage-vs-wwstage byte-id sweep. The + * empty_zero row is EXCLUDED (beid=0): an empty `[0]int = []` module global + * has a PRE-EXISTING cgen divergence unrelated to #9 (a checker-only fix) — + * wwstage emits an extra zero-width `DATAW main.X(SB),""` row that cstage + * omits; both run identically (return 7). Filed as task #15 (empty-array- + * global DATA emission, NOT folded into #9). A LOCAL `[0]int = []` is no + * escape — it diverges differently (wwstage allocates a $16 frame, cstage + * $0), so there is no byte-id-clean spelling of an empty zero-length array + * to restructure toward. The infer rows DO converge and pin that the + * accept-path asm stays byte-identical. */ +struct row { const char *label; const char *src; int want; int beid; }; + +static const struct row rows[] = { + /* infer_ctl — `[_]` still infers length from the initializer (#11). */ + { "infer_ctl", + "package main;\n" + "def X: [_]int = [10, 20];\n" + "export fn main() i32 = {\n" + "\treturn X[1]: i32;\n" + "};\n", + 20, 1 }, + + /* empty_zero — a real zero-length array (`[0]int = []`) stays VALID. + * beid=0: pre-existing empty-array-global cgen divergence (see beid). */ + { "empty_zero", + "package main;\n" + "let X: [0]int = [];\n" + "export fn main() i32 = {\n" + "\treturn 7;\n" + "};\n", + 7, 0 }, + + /* infer_len — `[_]` infer is unaffected, `.len` reads the real count. */ + { "infer_len", + "package main;\n" + "let X: [_]int = [1, 2, 3];\n" + "export fn main() i32 = {\n" + "\treturn X.len: i32;\n" + "};\n", + 3, 1 }, +}; + +/* An EXPLICIT `[N]int = [init]` with init-count > N — both stages must FAIL + * the build (loud over-fill diagnostic, not silent resize / OOB). */ +static const char *neg[] = { + /* def_zero_overfill */ + "package main;\n" + "def X: [0]int = [1, 2];\n" + "export fn main() i32 = {\n" + "\treturn X[1]: i32;\n" + "};\n", + /* let_zero_overfill */ + "package main;\n" + "let X: [0]int = [1, 2];\n" + "export fn main() i32 = {\n" + "\treturn X[1]: i32;\n" + "};\n", + /* local_zero_overfill */ + "package main;\n" + "export fn main() i32 = {\n" + "\tlet X: [0]int = [1, 2];\n" + "\treturn X[1]: i32;\n" + "};\n", + /* def_two_overfill — the #71 N>0 regression guard, must stay loud */ + "package main;\n" + "def X: [2]int = [1, 2, 3];\n" + "export fn main() i32 = {\n" + "\treturn X[1]: i32;\n" + "};\n", + /* str_zero_overfill — element-type-agnostic */ + "package main;\n" + "def X: [0]str = [\"a\"];\n" + "export fn main() i32 = {\n" + "\treturn 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/azi_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/azi_%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 — the over-fill 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/azin_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/azin_%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. */ +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/azi_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/azi_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/azi_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, "arr_zero_vs_infer: 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, + "arr_zero_vs_infer[%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, + "arr_zero_vs_infer[%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++) { + if (!rows[i].beid) + continue; /* see `beid` — out-of-#9 divergence */ + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "arr_zero_vs_infer: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("arr_zero_vs_infer: %d/%d ok\n", total, total); + return 0; +}