From dfdf99ffd814359175b2e6e6c0367c94260b58b3 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 9 Jun 2026 02:34:33 +0900 Subject: [PATCH] wcc/check: #20 reject overlong array-literal in a tuple element (wwstage) An overlong array literal as a tuple element -- let t: ([2]int, i32) = ([1,2,3], 5) -- was silently accepted by wwstage; cstage loud-rejects it. The #12+#106 over-fill coverage wired checkarrlitfits for direct-array, slice and alias lhs positions but not the tuple-element position. wwstage-only checker, reject-align: checkletassign gains an N_TTUPLE arm that walks the lhs element types (llhs.list) lockstep with the rhs values (n.rhs.list), calling the existing alias-aware checkarrlitfits per array element (no-ops scalars, recurses nested arrays). cstage unchanged (w6c md5 unchanged); reject-only, 990-997 8/8, no lib pin flips. test/wcc/832. Two sibling tuple-element positions stay open (filed, not folded -- they are reject-aligns on invalid programs, no selfhost byte-id impact): #25 tuple-RETURN overlong, #26 nested tuple-in-tuple. --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 22 +++ selfhost/cmd/wcc/check.ww | 22 +++ selfhost/cmd/wwdump/main.combined.ww | 22 +++ test/wcc/832_tuple_elem_overlong.c | 228 +++++++++++++++++++++++++++ 5 files changed, 301 insertions(+) create mode 100644 test/wcc/832_tuple_elem_overlong.c diff --git a/Makefile b/Makefile index 214e7c46..5410972b 100644 --- a/Makefile +++ b/Makefile @@ -257,6 +257,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_alias_tuple_coerce \ $(BIN)/test_str_eq \ $(BIN)/test_overlong_arrlit \ + $(BIN)/test_tuple_elem_overlong \ $(BIN)/test_slice_str_global_arg \ $(BIN)/test_slice_str_global_zero \ $(BIN)/test_slice_literal_global \ @@ -715,6 +716,12 @@ $(BIN)/test_overlong_arrlit: test/wcc/828_overlong_arrlit.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_tuple_elem_overlong: test/wcc/832_tuple_elem_overlong.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_arg: test/wcc/829_slice_str_global_arg.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 abd1cf8f..1fdfb66d 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14982,6 +14982,28 @@ fn checkletassign(c: *checker, n: *node) void = { checkarrlitfits(c, n.lhs, n.rhs); return; }; + // #20: array-typed TUPLE element with an overlong array literal — + // `let t:([2]int,i32) = ([1,2,3],5)` was silently accepted (the tuple + // position wasn't wired to checkarrlitfits, unlike the direct-array + // let above). Walk declared-tuple element types vs rhs values; over- + // fill-check each (checkarrlitfits chases aliases #106, recurses + // nested arrays #251, and no-ops on non-array elements). Mirror + // cstage's tuple element-wise reject. No early return — the rest of + // checkletassign still runs for the tuple. N_TTUPLE elements wrap + // their type on .lhs (N_TPARAM chain, stamptuplebinds:311); the + // N_TUPLE rhs values chain directly on .list. + if (llhs != nil && llhs.kind == nkind.N_TTUPLE + && n.rhs.kind == nkind.N_TUPLE) { + let dt: *node = llhs.list; + let vt: *node = n.rhs.list; + for (dt != nil && vt != nil) { + if (vt.kind == nkind.N_ARRLIT) { + checkarrlitfits(c, dt.lhs, vt); + }; + dt = dt.next; + vt = vt.next; + }; + }; // #25/#31: an array literal initialising a SLICE local. Re-stamp the // literal as [count]T (the slice element) so the #258 borrow's exact- // element typeeq holds and the cgen N_SLICE-over-N_ARRLIT arm reads the diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index a916bea1..8e37cbeb 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -4701,6 +4701,28 @@ fn checkletassign(c: *checker, n: *node) void = { checkarrlitfits(c, n.lhs, n.rhs); return; }; + // #20: array-typed TUPLE element with an overlong array literal — + // `let t:([2]int,i32) = ([1,2,3],5)` was silently accepted (the tuple + // position wasn't wired to checkarrlitfits, unlike the direct-array + // let above). Walk declared-tuple element types vs rhs values; over- + // fill-check each (checkarrlitfits chases aliases #106, recurses + // nested arrays #251, and no-ops on non-array elements). Mirror + // cstage's tuple element-wise reject. No early return — the rest of + // checkletassign still runs for the tuple. N_TTUPLE elements wrap + // their type on .lhs (N_TPARAM chain, stamptuplebinds:311); the + // N_TUPLE rhs values chain directly on .list. + if (llhs != nil && llhs.kind == nkind.N_TTUPLE + && n.rhs.kind == nkind.N_TUPLE) { + let dt: *node = llhs.list; + let vt: *node = n.rhs.list; + for (dt != nil && vt != nil) { + if (vt.kind == nkind.N_ARRLIT) { + checkarrlitfits(c, dt.lhs, vt); + }; + dt = dt.next; + vt = vt.next; + }; + }; // #25/#31: an array literal initialising a SLICE local. Re-stamp the // literal as [count]T (the slice element) so the #258 borrow's exact- // element typeeq holds and the cgen N_SLICE-over-N_ARRLIT arm reads the diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index cd2737e2..36eba55d 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14982,6 +14982,28 @@ fn checkletassign(c: *checker, n: *node) void = { checkarrlitfits(c, n.lhs, n.rhs); return; }; + // #20: array-typed TUPLE element with an overlong array literal — + // `let t:([2]int,i32) = ([1,2,3],5)` was silently accepted (the tuple + // position wasn't wired to checkarrlitfits, unlike the direct-array + // let above). Walk declared-tuple element types vs rhs values; over- + // fill-check each (checkarrlitfits chases aliases #106, recurses + // nested arrays #251, and no-ops on non-array elements). Mirror + // cstage's tuple element-wise reject. No early return — the rest of + // checkletassign still runs for the tuple. N_TTUPLE elements wrap + // their type on .lhs (N_TPARAM chain, stamptuplebinds:311); the + // N_TUPLE rhs values chain directly on .list. + if (llhs != nil && llhs.kind == nkind.N_TTUPLE + && n.rhs.kind == nkind.N_TUPLE) { + let dt: *node = llhs.list; + let vt: *node = n.rhs.list; + for (dt != nil && vt != nil) { + if (vt.kind == nkind.N_ARRLIT) { + checkarrlitfits(c, dt.lhs, vt); + }; + dt = dt.next; + vt = vt.next; + }; + }; // #25/#31: an array literal initialising a SLICE local. Re-stamp the // literal as [count]T (the slice element) so the #258 borrow's exact- // element typeeq holds and the cgen N_SLICE-over-N_ARRLIT arm reads the diff --git a/test/wcc/832_tuple_elem_overlong.c b/test/wcc/832_tuple_elem_overlong.c new file mode 100644 index 00000000..109df811 --- /dev/null +++ b/test/wcc/832_tuple_elem_overlong.c @@ -0,0 +1,228 @@ +/* + * 832_tuple_elem_overlong — an OVERLONG array literal in a TUPLE ELEMENT + * position is INVALID ww; BOTH stages must LOUDLY REJECT at check time + * (#20, the #12 + #106 follow-up; rob spec .ai/rob-20-spec.md). #12/#106 + * wired checkarrlitfits for a DIRECT array lhs (+ alias) at the decl / + * return / call-arg positions, but the tuple-element position was never + * wired — `let t: ([2]int, i32) = ([1,2,3], 5)` over-fills the [2]int slot + * with 3 initialisers. + * + * WWSTAGE-ONLY fix — cstage already rejects (tuple element-wise + * type_assignable counts elements: `[3]int` vs `[2]int`). Pre-fix wwstage + * divergence (the mutation-sanity target): + * - tuple_arr_over `let t:([2]int,i32)=([1,2,3],5)` : ww silently ACCEPTED + * - tuple_nested_arr `let t:([2][3]int,i32)=([...x3],5)` : the outer [2] + * slot over-filled by 3 sub-arrays (checkarrlitfits + * nested-array recursion under the tuple walk) + * + * The diagnostic TEXT may differ between stages ("over-fill" vs "not + * assignable") — byte-id-blind (stderr is not asm). Both REJECT and emit no + * asm; selfhost has no overlong tuple-elements, so 990-997 byte-id is + * untouched. Do NOT chase message parity. + * + * neg row | shape | gate + * -----------------+------------------------------------------------+-------- + * tuple_arr_over | let t:([2]int,i32)=([1,2,3],5) | b. FAIL + * tuple_nested_arr | let t:([2][3]int,i32)=([[..],[..],[..]],5) | b. FAIL + * + * pos row | shape | want + * -----------------+------------------------------------------------+------ + * tuple_arr_exact | let t:([2]int,i32)=([1,2],5); t.1 | 5 + * tuple_scalar | let t:(i32,i32)=(1,2); t.1 | 2 + */ +#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[] = { + /* exact-length array element in a tuple still ACCEPTS, builds + runs. + * Readout is the scalar t.1 (=5), NOT t.0[1]: indexing an array + * element THROUGH a tuple is a separate pre-existing cgen read bug + * that segfaults on BOTH stages (w6c byte-identical, so not a #20 + * regression — filed). The point of this control is that the #20 + * over-fill walk does NOT over-reject the valid exact-length tuple- + * with-array-element: the build must succeed and the program run. */ + { "tuple_arr_exact", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet t: ([2]int, i32) = ([1, 2], 5);\n" + "\treturn t.1;\n" + "};\n", + 5 }, + + /* a scalar-only tuple has no array element — the #20 walk no-ops. */ + { "tuple_scalar", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet t: (i32, i32) = (1, 2);\n" + "\treturn t.1;\n" + "};\n", + 2 }, +}; + +/* An overlong array literal in a tuple element — both stages must FAIL the + * build (loud checker diagnostic, not silent accept). */ +static const char *neg[] = { + /* tuple_arr_over (the #20 repro) — [2]int slot gets 3 inits. */ + "package main;\n" + "export fn main() i32 = {\n" + "\tlet t: ([2]int, i32) = ([1, 2, 3], 5);\n" + "\treturn t.0[1]: i32;\n" + "};\n", + /* tuple_nested_arr — outer [2] slot over-filled by 3 sub-arrays + * (checkarrlitfits recursion under the tuple walk). */ + "package main;\n" + "export fn main() i32 = {\n" + "\tlet t: ([2][3]int, i32) = " + "([[1, 2, 3], [4, 5, 6], [7, 8, 9]], 5);\n" + "\treturn t.0[0][0]: i32;\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/teo_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/teo_%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 — an overlong tuple-element array 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/teon_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/teon_%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); + 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 */ +} + +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, "tuple_elem_overlong: 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, + "tuple_elem_overlong[%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, + "tuple_elem_overlong[%s][neg%d]: built ok, " + "expected a loud error\n", + drivers[d].name, i); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, + "tuple_elem_overlong: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("tuple_elem_overlong: %d/%d ok\n", total, total); + return 0; +}