From 74767c70cc54b98243541038737bb0e7814c806b Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 21:53:44 +0900 Subject: [PATCH] =?UTF-8?q?wcc/check+wcc=5Fww/check:=20reject=20overlong?= =?UTF-8?q?=20array=20literal=20=E2=80=94=20frame-smash=20class=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An array literal with more elements than the declared [N] passed the per-element accept-if-fits checks in both stages and cgen then stored every element at its natural offset, writing past the slot: local frames smashed silently (the repeat form [1,2,3...] into [2]int wrote at the saved BP), module DATA corrupted neighbours. All four declaration contexts (local let, module let, def, struct-field literal) funnel through one choke point per stage — arrlit_init_fits (check.c) / checkarrlitfits (check.ww) — which now pre-counts the literal (skipping the ... marker) and rejects count > N naming both counts. cstage clet's blanket has_arr_repeat bypass is narrowed to non-array declared targets: repeat literals into arrays now run the same overlong + #130 range checks wwstage's checkletassign always ran (the bypass let [2]u8 = [999...] dodge the range check cstage-only). checkarrlitfits also recurses into NESTED array-literal elements (declared elem node N_TARRAY): cstage catches the nested shape through its typed-literal assignability net, which wwstage's untyped elements have no analog of — [2][2]int = [[1,2,3],[4,5]] at module scope silently emitted corrupted DATA (1,2,4,5) and the struct-field twin likewise. Recursion through the one choke point closes any depth; a named-alias element type still bypasses — task #16. alen==0/nil-length stays exempt ([0]/[_] sentinel conflation and un-inferred [_] in def/struct-field — task #11); a non-INTLIT length child (def-named [N]) is exempt in wwstage — task #13; under-long literals keep their current accept (Hare rejects — task #10); wwstage's overlong accept at assign/call-arg/return position (cstage already rejects) is task #12; exact-fit bare-int nested cs-reject/ ww-accept divergence is pre-existing — task #17. --- Makefile | 12 + cmd/wcc/check.c | 37 ++- selfhost/cmd/w6c/main.combined.ww | 48 +++ selfhost/cmd/wcc/check.ww | 48 +++ selfhost/cmd/wwdump/main.combined.ww | 48 +++ test/wcc/808_arrlit_overlong.c | 429 +++++++++++++++++++++++++++ 6 files changed, 621 insertions(+), 1 deletion(-) create mode 100644 test/wcc/808_arrlit_overlong.c diff --git a/Makefile b/Makefile index c065cfcd..8e15ffaf 100644 --- a/Makefile +++ b/Makefile @@ -394,6 +394,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_append_wide_elem \ $(BIN)/test_delete_elem \ $(BIN)/test_insert_elem \ + $(BIN)/test_arrlit_overlong \ $(BIN)/test_placeaddr_store \ $(BIN)/test_tryprop_multisuccess \ $(BIN)/test_append_place \ @@ -1039,6 +1040,17 @@ $(BIN)/test_insert_elem: test/wcc/807_insert_elem.c \ # cgplaceaddr resolver — runtime rows across widths/signedness/str + # compound ops + loud-tail reject rows w/ exact diagnostic text + cs==ww # asm byte-id. +# #71: overlong array literal (more elements than the declared [N]) is +# a loud checker reject in BOTH stages — local/module/def/struct-field + +# the repeat-marker form (which used to clobber the saved BP) — plus +# accept guards (exact-N, repeat-fill, [_] inference) w/ runtime + asm +# byte-id on both drivers. +$(BIN)/test_arrlit_overlong: test/wcc/808_arrlit_overlong.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_placeaddr_store: test/wcc/805_placeaddr_store.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww \ diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index ac23e23e..5841ed43 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -411,6 +411,27 @@ arrlit_init_fits(Checker *c, Type *dt, Node *rhs) && strcmp(e->str, "...") == 0) continue; count++; + } + /* #71: more elements than the declared [N] passed every per-element + * 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 + && count > u->alen) { + err(c, rhs->pos, "array literal has %llu elements " + "but declared array holds %llu", + (unsigned long long)count, (unsigned long long)u->alen); + return 0; + } + for (Node *e = rhs->list; e; e = e->next) { + if (e->kind == N_FIELD && e->str + && strcmp(e->str, "...") == 0) + continue; Node *ev = e; while (ev && ev->kind == N_CAST) ev = ev->lhs; u64 v; @@ -2086,7 +2107,15 @@ clet(Checker *c, Node *n) * "flexible" length — the last value fills the remaining slots. * The literal's type carries the explicit-element count, which * may not match the declared length. Trust the declared type - * when the marker is present. */ + * when the marker is present. + * + * #71: NOT when the declared type is an array — there the repeat + * only relaxes the length upward (explicit count <= N, fill the + * rest); arrlit_init_fits skips the marker and runs the overlong + * reject + per-element range checks. The blanket bypass let + * `[2]int = [1,2,3...]` write past the slot (saved-BP clobber) + * and `[2]u8 = [999...]` skip the #130 range check — wwstage's + * checkletassign already runs both unconditionally. */ int has_arr_repeat = 0; if (n->rhs && n->rhs->kind == N_ARRLIT) { for (Node *e = n->rhs->list; e; e = e->next) @@ -2096,6 +2125,12 @@ clet(Checker *c, Node *n) break; } } + if (has_arr_repeat && declared) { + Type *du = (declared->kind == TY_NAMED) ? declared->under + : declared; + if (du && du->kind == TY_ARRAY) + has_arr_repeat = 0; + } /* #45: alloc([], n) defers element type to the let-init context * (Hare-style). cexpr's alloc-slice branch synthesizes * ([]u8 | nomem) with no LHS context; when the let declares []T, diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 52c6af03..e6f75c7b 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14325,6 +14325,39 @@ fn checkarrlitfits(c: *checker, arrtn: *node, rhs: *node) void = { if (rhs == nil) { return; }; if (arrtn.kind != nkind.N_TARRAY) { return; }; if (rhs.kind != nkind.N_ARRLIT) { return; }; + // #71: more elements than the declared [N] passed every per-element + // 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. + // A nil or zero length child stays exempt: nil is the un-inferred + // [_] sentinel in def/struct-field contexts and 0 doubles as both + // [0] and the cstage [_] sentinel (conflation: task #11); the let + // paths stamp the real length before reaching here. A non-INTLIT + // length child (def-named [N]) is also exempt — task #13. + // Under-long (count < N, no `...`) stays accepted as before; Hare + // rejects it — task #10. + if (arrtn.rhs != nil && arrtn.rhs.kind == nkind.N_INTLIT + && arrtn.rhs.uval > 0u64) { + let cnt: u64 = 0u64; + let ce: *node = rhs.list; + for (ce != nil) { + let cskip: bool = false; + if (ce.kind == nkind.N_FIELD) { + if (streq(ce.str, "...")) { cskip = true; }; + }; + if (!cskip) { cnt += 1u64; }; + ce = ce.next; + }; + if (cnt > arrtn.rhs.uval) { + cerr("array literal has "); + cerr(strconv.u64tos(cnt, strconv.base.DEC)); + cerr(" elements but declared array holds "); + cerr(strconv.u64tos(arrtn.rhs.uval, strconv.base.DEC)); + cerr("\n"); + c.errs += 1; + return; + }; + }; let elemtn: *node = arrtn.lhs; let at: *tinfo = tinfofornode(c, arrtn); let et: *tinfo = nil; @@ -14339,6 +14372,21 @@ fn checkarrlitfits(c: *checker, arrtn: *node, rhs: *node) void = { if (!skip) { let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; + // #71: a NESTED array-literal element must run the same + // count check against the inner [N] — cstage catches the + // nested shape through its typed-literal assignability net + // (the literal's stamped [2][3]T fails type_assignable), + // which wwstage's untyped elements have no analog of; the + // silent accept emitted corrupted DATA / smashed frames. + // Recursion through the one choke point closes any depth. + // A named-alias element type ([2]row) still bypasses — the + // elemtn node is N_IDENT, not N_TARRAY — task #16. + if (elemtn != nil && elemtn.kind == nkind.N_TARRAY + && ev != nil && ev.kind == nkind.N_ARRLIT) { + checkarrlitfits(c, elemtn, ev); + e = e.next; + continue; + }; let v: u64 = 0u64; let folded: bool = false; if (et != nil) { diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index b22b8b5a..48bc908e 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -4008,6 +4008,39 @@ fn checkarrlitfits(c: *checker, arrtn: *node, rhs: *node) void = { if (rhs == nil) { return; }; if (arrtn.kind != nkind.N_TARRAY) { return; }; if (rhs.kind != nkind.N_ARRLIT) { return; }; + // #71: more elements than the declared [N] passed every per-element + // 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. + // A nil or zero length child stays exempt: nil is the un-inferred + // [_] sentinel in def/struct-field contexts and 0 doubles as both + // [0] and the cstage [_] sentinel (conflation: task #11); the let + // paths stamp the real length before reaching here. A non-INTLIT + // length child (def-named [N]) is also exempt — task #13. + // Under-long (count < N, no `...`) stays accepted as before; Hare + // rejects it — task #10. + if (arrtn.rhs != nil && arrtn.rhs.kind == nkind.N_INTLIT + && arrtn.rhs.uval > 0u64) { + let cnt: u64 = 0u64; + let ce: *node = rhs.list; + for (ce != nil) { + let cskip: bool = false; + if (ce.kind == nkind.N_FIELD) { + if (streq(ce.str, "...")) { cskip = true; }; + }; + if (!cskip) { cnt += 1u64; }; + ce = ce.next; + }; + if (cnt > arrtn.rhs.uval) { + cerr("array literal has "); + cerr(strconv.u64tos(cnt, strconv.base.DEC)); + cerr(" elements but declared array holds "); + cerr(strconv.u64tos(arrtn.rhs.uval, strconv.base.DEC)); + cerr("\n"); + c.errs += 1; + return; + }; + }; let elemtn: *node = arrtn.lhs; let at: *tinfo = tinfofornode(c, arrtn); let et: *tinfo = nil; @@ -4022,6 +4055,21 @@ fn checkarrlitfits(c: *checker, arrtn: *node, rhs: *node) void = { if (!skip) { let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; + // #71: a NESTED array-literal element must run the same + // count check against the inner [N] — cstage catches the + // nested shape through its typed-literal assignability net + // (the literal's stamped [2][3]T fails type_assignable), + // which wwstage's untyped elements have no analog of; the + // silent accept emitted corrupted DATA / smashed frames. + // Recursion through the one choke point closes any depth. + // A named-alias element type ([2]row) still bypasses — the + // elemtn node is N_IDENT, not N_TARRAY — task #16. + if (elemtn != nil && elemtn.kind == nkind.N_TARRAY + && ev != nil && ev.kind == nkind.N_ARRLIT) { + checkarrlitfits(c, elemtn, ev); + e = e.next; + continue; + }; let v: u64 = 0u64; let folded: bool = false; if (et != nil) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index cdadf451..c1f2485c 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14325,6 +14325,39 @@ fn checkarrlitfits(c: *checker, arrtn: *node, rhs: *node) void = { if (rhs == nil) { return; }; if (arrtn.kind != nkind.N_TARRAY) { return; }; if (rhs.kind != nkind.N_ARRLIT) { return; }; + // #71: more elements than the declared [N] passed every per-element + // 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. + // A nil or zero length child stays exempt: nil is the un-inferred + // [_] sentinel in def/struct-field contexts and 0 doubles as both + // [0] and the cstage [_] sentinel (conflation: task #11); the let + // paths stamp the real length before reaching here. A non-INTLIT + // length child (def-named [N]) is also exempt — task #13. + // Under-long (count < N, no `...`) stays accepted as before; Hare + // rejects it — task #10. + if (arrtn.rhs != nil && arrtn.rhs.kind == nkind.N_INTLIT + && arrtn.rhs.uval > 0u64) { + let cnt: u64 = 0u64; + let ce: *node = rhs.list; + for (ce != nil) { + let cskip: bool = false; + if (ce.kind == nkind.N_FIELD) { + if (streq(ce.str, "...")) { cskip = true; }; + }; + if (!cskip) { cnt += 1u64; }; + ce = ce.next; + }; + if (cnt > arrtn.rhs.uval) { + cerr("array literal has "); + cerr(strconv.u64tos(cnt, strconv.base.DEC)); + cerr(" elements but declared array holds "); + cerr(strconv.u64tos(arrtn.rhs.uval, strconv.base.DEC)); + cerr("\n"); + c.errs += 1; + return; + }; + }; let elemtn: *node = arrtn.lhs; let at: *tinfo = tinfofornode(c, arrtn); let et: *tinfo = nil; @@ -14339,6 +14372,21 @@ fn checkarrlitfits(c: *checker, arrtn: *node, rhs: *node) void = { if (!skip) { let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; + // #71: a NESTED array-literal element must run the same + // count check against the inner [N] — cstage catches the + // nested shape through its typed-literal assignability net + // (the literal's stamped [2][3]T fails type_assignable), + // which wwstage's untyped elements have no analog of; the + // silent accept emitted corrupted DATA / smashed frames. + // Recursion through the one choke point closes any depth. + // A named-alias element type ([2]row) still bypasses — the + // elemtn node is N_IDENT, not N_TARRAY — task #16. + if (elemtn != nil && elemtn.kind == nkind.N_TARRAY + && ev != nil && ev.kind == nkind.N_ARRLIT) { + checkarrlitfits(c, elemtn, ev); + e = e.next; + continue; + }; let v: u64 = 0u64; let folded: bool = false; if (et != nil) { diff --git a/test/wcc/808_arrlit_overlong.c b/test/wcc/808_arrlit_overlong.c new file mode 100644 index 00000000..d2dfdff4 --- /dev/null +++ b/test/wcc/808_arrlit_overlong.c @@ -0,0 +1,429 @@ +/* + * 808_arrlit_overlong — an array literal with MORE elements than the + * declared [N] must be a loud checker reject in BOTH stages (#71). + * + * The bug: `let a: [2]u8 = [1u8, 2u8, 3u8];` was silently accepted by + * both checkers — type_assignable fails on the length mismatch, but + * arrlit_init_fits (#130 accept-if-fits) only range-checked the + * elements and never compared the literal's count against the declared + * length. cgen then stored every element at its natural offset, writing + * past the slot: stack-frame smash for locals (the `[1,2,3...]`-into- + * `[2]int` repeat form clobbered the saved BP outright), silent + * neighbour corruption for module-level DATA. The repeat-marker form + * was worse on cstage: clet's has_arr_repeat bypass skipped ALL checks + * for any `...` literal, so `[2]u8 = [999...]` also dodged the #130 + * range check that wwstage already enforced. + * + * The fix (BOTH stages, one choke point each): the shared accept-if- + * fits helper (cmd/wcc/check.c arrlit_init_fits; selfhost/cmd/wcc/ + * check.ww checkarrlitfits) pre-counts the literal's elements (skipping + * the `...` marker) and rejects count > N naming both counts. All four + * declaration contexts (local let / module let / def / struct-field + * literal) funnel through that helper. cstage clet's repeat bypass is + * narrowed to non-array targets so repeat literals into arrays run the + * same checks wwstage always ran. + * + * Out of scope, probed + filed separately: UNDER-long literals (no + * `...`) stay accepted in both stages (Hare rejects); `[0]`/`[_]` + * alen==0 sentinel conflation; wwstage assign/call-arg overlong + * acceptance (cstage already rejects those positions). + * + * accept rows | want + * ----------------------------------+------ + * exact_local [2]int = [1,2] | runs, 0 + * exact_module module-level [2] | runs, 0 + * exact_def def [3] = [1,2,3] | runs, 0 + * exact_field struct f=[2 elems] | runs, 0 + * repeat_fill [4]int = [9...] | runs, 0 + * repeat_partial [4]int = [1,2...] | runs, 0 + * infer_len [_]int = [1,2,3] | runs, 0 + * + * reject rows (build must FAIL on both drivers, and stderr must name + * both counts — per-stage expected substring: cstage catches the + * NESTED rows through its typed-literal assignability net instead of + * the choke-point diag, so those carry a different cstage substring) + * ---------------------------------------------- + * overlong local / module / def(5-vs-3) / struct-field + * overlong repeat `[2]int = [1,2,3...]` + * overlong narrow `[2]u8 = [1u8,2u8,3u8]` + * nested module `[2][2]int = [[1,2,3],[4,5]]` (inner overlong — + * pre-fix wwstage emitted corrupted DATA: 1,2,4,5) + * nested field struct{f:[2][2]int} inner overlong + */ +#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_local", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [2]int = [1, 2];\n" + "\treturn a[1]: i32 - 2;\n" + "};\n", + 0 }, + + { "exact_module", + "package main;\n" + "let g: [2]int = [4, 5];\n" + "export fn main() i32 = {\n" + "\treturn g[1]: i32 - 5;\n" + "};\n", + 0 }, + + { "exact_def", + "package main;\n" + "def TAB: [3]int = [1, 2, 3];\n" + "export fn main() i32 = {\n" + "\treturn TAB[2]: i32 - 3;\n" + "};\n", + 0 }, + + { "exact_field", + "package main;\n" + "type s = struct { f: [2]int, g: int };\n" + "export fn main() i32 = {\n" + "\tlet v: s = s{ f = [6, 7], g = 8 };\n" + "\treturn v.f[1]: i32 + v.g: i32 - 15;\n" + "};\n", + 0 }, + + { "repeat_fill", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [4]int = [9...];\n" + "\treturn a[3]: i32 - 9;\n" + "};\n", + 0 }, + + { "repeat_partial", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [4]int = [1, 2...];\n" + "\treturn a[3]: i32 - 2;\n" + "};\n", + 0 }, + + { "infer_len", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [_]int = [10, 20, 30];\n" + "\treturn a.len: i32 - 3;\n" + "};\n", + 0 }, +}; + +/* Overlong literals — both stages must FAIL the build (the old accept + * stored every element at its natural offset: frame smash) AND the + * diagnostic must carry the expected substring (the choke-point reject + * names BOTH counts; the nested rows reach cstage's pre-existing + * assignability net instead, hence per-stage substrings). */ +struct negrow { + const char *label; + const char *src; + const char *diag_c; /* expected stderr substring, cstage */ + const char *diag_w; /* expected stderr substring, wwstage */ +}; + +#define OVERLONG_3V2 "array literal has 3 elements but declared array holds 2" + +static const struct negrow neg[] = { + { "overlong_local", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [2]int = [1, 2, 3];\n" + "\treturn 0;\n" + "};\n", + OVERLONG_3V2, OVERLONG_3V2 }, + + { "overlong_module", + "package main;\n" + "let g: [2]int = [1, 2, 3];\n" + "export fn main() i32 = { return 0; };\n", + OVERLONG_3V2, OVERLONG_3V2 }, + + /* counts deliberately differ from the other rows so a diag that + * hardcodes 3/2 instead of naming the real counts trips here */ + { "overlong_def", + "package main;\n" + "def TAB: [3]int = [1, 2, 3, 4, 5];\n" + "export fn main() i32 = { return 0; };\n", + "array literal has 5 elements but declared array holds 3", + "array literal has 5 elements but declared array holds 3" }, + + { "overlong_field", + "package main;\n" + "type s = struct { f: [2]int };\n" + "export fn main() i32 = {\n" + "\tlet v: s = s{ f = [1, 2, 3] };\n" + "\treturn 0;\n" + "};\n", + OVERLONG_3V2, OVERLONG_3V2 }, + + /* repeat marker with too many explicit elements — the worst + * pre-fix case (wrote at the saved BP) */ + { "overlong_repeat", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [2]int = [1, 2, 3...];\n" + "\treturn 0;\n" + "};\n", + OVERLONG_3V2, OVERLONG_3V2 }, + + /* narrow element width (sub-8B store path) */ + { "overlong_narrow", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet a: [2]u8 = [1u8, 2u8, 3u8];\n" + "\treturn 0;\n" + "};\n", + OVERLONG_3V2, OVERLONG_3V2 }, + + /* nested inner overlong, module DATA — pre-fix wwstage silently + * emitted 1,2,4,5; cstage rejects via assignability, wwstage via + * the recursive choke point */ + { "nested_module", + "package main;\n" + "let g: [2][2]int = [[1, 2, 3], [4, 5]];\n" + "export fn main() i32 = { return 0; };\n", + "not assignable", OVERLONG_3V2 }, + + { "nested_field", + "package main;\n" + "type s = struct { f: [2][2]int };\n" + "export fn main() i32 = {\n" + "\tlet v: s = s{ f = [[1, 2, 3], [4, 5]] };\n" + "\treturn 0;\n" + "};\n", + "not assignable", OVERLONG_3V2 }, +}; + +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/aol_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/aol_%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 literal must error on `driver` AND + * the diagnostic must contain `diag`; returns 0 when the build + * correctly FAILS with the expected text, non-zero otherwise. */ +static int +build_should_fail(const char *dname, const char *driver, + const struct negrow *r, const char *diag, int i) +{ + char s[64], tmpdir[64], errf[64], cmd[1024]; + snprintf(s, sizeof s, "/tmp/aoln_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/aoln_%d_d_%d", getpid(), i); + snprintf(errf, sizeof errf, "/tmp/aoln_%d_%d.err", getpid(), i); + + FILE *f = fopen(s, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>%s", + tmpdir, driver, s, errf); + 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); + + int bad = 0; + if (rc == 0) { + fprintf(stderr, "arrlit_overlong[%s][%s]: built ok, " + "expected a loud error\n", dname, r->label); + bad = 1; + } else { + char ebuf[4096]; + size_t n = 0; + FILE *ef = fopen(errf, "rb"); + if (ef) { + n = fread(ebuf, 1, sizeof ebuf - 1, ef); + fclose(ef); + } + ebuf[n] = '\0'; + if (strstr(ebuf, diag) == NULL) { + fprintf(stderr, "arrlit_overlong[%s][%s]: rejected " + "but diagnostic lacks \"%s\"; got: %s\n", + dname, r->label, diag, ebuf); + bad = 1; + } + } + unlink(errf); + return bad; +} + +/* 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/aol_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/aol_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/aol_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, "arrlit_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, + "arrlit_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++; + const char *diag = + strcmp(drivers[d].name, "cstage") == 0 + ? neg[i].diag_c : neg[i].diag_w; + if (build_should_fail(drivers[d].name, + drivers[d].path, &neg[i], diag, 100 + i) != 0) + 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, + "arrlit_overlong: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("arrlit_overlong: %d/%d ok\n", total, total); + return 0; +}