From 6b7de54272b8db317823b67d9c53456db4cfc5a6 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 15 Jun 2026 03:49:38 +0900 Subject: [PATCH] wcc/ww: reject (a,) single-element trailing-comma tuple (catB-92) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wwstage's tuple-parse loop checked the RPAREN-break at the top, so `(a,)` parsed as a 1-element N_TUPLE and reached cgen — a silent wrong-accept. A trailing comma is legal only after >=2 elements. Align the loop order to cstage cmd/wcc/parse.c:552-558 (parse each element before the RPAREN-break); `(a,)` now errors at the next parseexpr, `(a, b)` / `(a, b,)` are unchanged. cstage already rejected; this brings wwstage's w6c_ww parser into agreement. Regen w6c/wwdump combined.ww (parser embeds in both). Valid-program codegen unchanged → cs==ww byte-id gate stays green. --- Makefile | 7 + lib/ww/parse/expr.ww | 6 +- selfhost/cmd/w6c/main.combined.ww | 6 +- selfhost/cmd/wwdump/main.combined.ww | 6 +- test/wcc/845_tuple_trailing_comma_reject.c | 186 +++++++++++++++++++++ 5 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 test/wcc/845_tuple_trailing_comma_reject.c diff --git a/Makefile b/Makefile index f475a758..a200b10a 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_tuple_trailing_comma_reject \ $(BIN)/test_size_untyped_int \ $(BIN)/test_tagged_staticinit \ $(BIN)/test_arr_infer_len \ @@ -1449,6 +1450,12 @@ $(BIN)/test_dup_main_reject: test/wcc/842_dup_main_reject.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_tuple_trailing_comma_reject: test/wcc/845_tuple_trailing_comma_reject.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_tagged_staticinit: test/wcc/843_tagged_staticinit.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/lib/ww/parse/expr.ww b/lib/ww/parse/expr.ww index 0cc1e0ea..2db0852c 100644 --- a/lib/ww/parse/expr.ww +++ b/lib/ww/parse/expr.ww @@ -119,12 +119,16 @@ fn parseprimary(p: *parser) *node = { let t: *node = newnode(nkind.N_TUPLE, pf, pl, pc); t.list = e; let tail: *node = e; + // Parse each element BEFORE the RPAREN-break so `(a,)` + // (a single elem + trailing comma) is a loud parse error; + // a trailing comma is legal only after >=2 elems. Mirror + // cstage cmd/wcc/parse.c:552-558 loop order. for (true) { - if (p.curkind == tkind.TK_RPAREN) { break; }; let en: *node = parseexpr(p); tail.next = en; tail = en; if (!accepttok(p, tkind.TK_COMMA)) { break; }; + if (p.curkind == tkind.TK_RPAREN) { break; }; }; expecttok(p, tkind.TK_RPAREN, "expected ')' in tuple"); return t; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 9af3f42c..642ee888 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -8268,12 +8268,16 @@ fn parseprimary(p: *parser) *node = { let t: *node = newnode(nkind.N_TUPLE, pf, pl, pc); t.list = e; let tail: *node = e; + // Parse each element BEFORE the RPAREN-break so `(a,)` + // (a single elem + trailing comma) is a loud parse error; + // a trailing comma is legal only after >=2 elems. Mirror + // cstage cmd/wcc/parse.c:552-558 loop order. for (true) { - if (p.curkind == tkind.TK_RPAREN) { break; }; let en: *node = parseexpr(p); tail.next = en; tail = en; if (!accepttok(p, tkind.TK_COMMA)) { break; }; + if (p.curkind == tkind.TK_RPAREN) { break; }; }; expecttok(p, tkind.TK_RPAREN, "expected ')' in tuple"); return t; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 1ea68df9..11ec3b77 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -8268,12 +8268,16 @@ fn parseprimary(p: *parser) *node = { let t: *node = newnode(nkind.N_TUPLE, pf, pl, pc); t.list = e; let tail: *node = e; + // Parse each element BEFORE the RPAREN-break so `(a,)` + // (a single elem + trailing comma) is a loud parse error; + // a trailing comma is legal only after >=2 elems. Mirror + // cstage cmd/wcc/parse.c:552-558 loop order. for (true) { - if (p.curkind == tkind.TK_RPAREN) { break; }; let en: *node = parseexpr(p); tail.next = en; tail = en; if (!accepttok(p, tkind.TK_COMMA)) { break; }; + if (p.curkind == tkind.TK_RPAREN) { break; }; }; expecttok(p, tkind.TK_RPAREN, "expected ')' in tuple"); return t; diff --git a/test/wcc/845_tuple_trailing_comma_reject.c b/test/wcc/845_tuple_trailing_comma_reject.c new file mode 100644 index 00000000..d5d0e3d9 --- /dev/null +++ b/test/wcc/845_tuple_trailing_comma_reject.c @@ -0,0 +1,186 @@ +/* + * 845_tuple_trailing_comma_reject (catB-92) — cstage and wwstage + * LOUD-REJECT a single-element trailing-comma tuple `(a,)`. + * + * A trailing comma is legal ONLY after >=2 tuple elements; `(a,)` is a + * parse error, `(a, b)` and `(a, b,)` are 2-tuples. cstage parse.c:552- + * 558 parses each element BEFORE the RPAREN-break, so `(a,)` errors at + * the next parseexpr (it sees `)`). wwstage's lib/ww/parse/expr.ww tuple + * loop checked the RPAREN-break at the TOP, so `(a,)` parsed as a + * 1-element N_TUPLE and reached cgen — a silent wrong-accept. The fix + * mirrors cstage's loop order (break sits AFTER accept(COMMA)). + * + * The reject row is `let t = (5,);` — an INFERRED-type 1-tuple, the + * context the old wwstage built rc=0 and codegen'd (a 2-tuple-DECLARED + * `(5,)` already errored on arity downstream, masking the parse bug; + * the inferred form is the clean discriminator). Pre-fix wwstage built + * it rc=0; cstage rejected it. Post-fix both reject at parse. + * + * row | shape | expect + * -----------+-----------------+-------- + * one_comma | let t = (5,); | REJECT (parse) + * two_elem | (3, 4) | BUILD, run 7 + * two_comma | (3, 4,) | BUILD, run 7 (trailing OK after >=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; +} + +/* outbin — derive the driver's default binary name from a *.ww source + * (strip dir + the .ww suffix), placed in tmpdir. */ +static void +outbin(char *dst, size_t n, const char *tmpdir, const char *src) +{ + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + snprintf(dst, n, "%s/%s", tmpdir, base); + char *dot = strrchr(dst, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; +} + +/* build_should_fail — returns 0 when the build correctly FAILS. */ +static int +build_should_fail(const char *driver, const char *label, const char *src, + int i) +{ + char s[64], tmpdir[64], cmd[1024], bin[128]; + snprintf(s, sizeof s, "/tmp/tuptc_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/tuptc_%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); + outbin(bin, sizeof bin, tmpdir, s); + unlink(s); + unlink(bin); + rmdir(tmpdir); + if (rc == 0) + fprintf(stderr, "tuptc[%s][%s]: built ok, expected a reject\n", + driver, label); + return rc == 0 ? -1 : 0; +} + +/* run_build — build a program that MUST compile, run it, return exit. */ +static int +run_build(const char *driver, const char *label, const char *src, int i) +{ + char s[64], tmpdir[64], cmd[1024], bin[128]; + snprintf(s, sizeof s, "/tmp/tuptc_ok_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/tuptc_ok_%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); + if (runwait(cmd) != 0) { + fprintf(stderr, "tuptc[%s][%s]: build failed (a valid tuple " + "must compile)\n", driver, label); + unlink(s); rmdir(tmpdir); + return -1; + } + outbin(bin, sizeof bin, tmpdir, s); + int got = runwait(bin); + unlink(s); unlink(bin); rmdir(tmpdir); + return got; +} + +struct rejrow { const char *label; const char *src; }; + +static const struct rejrow reject_rows[] = { + { "one_comma", + "package main;\n" + "fn main() i32 = { let t = (5,); return 0; };\n" }, +}; + +struct okrow { const char *label; const char *src; int want; }; + +static const struct okrow ok_rows[] = { + { "two_elem", + "package main;\n" + "fn pair() (i32, i32) = { return (3, 4); };\n" + "fn main() i32 = { let a, b = pair(); return a + b; };\n", 7 }, + + { "two_comma", + "package main;\n" + "fn pair() (i32, i32) = { return (3, 4,); };\n" + "fn main() i32 = { let a, b = pair(); return a + b; };\n", 7 }, +}; + +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 nrej = (int)(sizeof reject_rows / sizeof reject_rows[0]); + int nok = (int)(sizeof ok_rows / sizeof ok_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, "tuptc: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < nrej; i++) { + total++; + if (build_should_fail(drivers[d].path, + reject_rows[i].label, reject_rows[i].src, + d * 100 + i) != 0) + fail++; + } + for (int i = 0; i < nok; i++) { + total++; + int got = run_build(drivers[d].path, ok_rows[i].label, + ok_rows[i].src, d * 100 + i); + if (got != ok_rows[i].want) { + fprintf(stderr, "tuptc[%s][%s]: exit=%d want=%d\n", + drivers[d].name, ok_rows[i].label, + got, ok_rows[i].want); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, "tuptc: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("tuptc: %d/%d ok\n", total, total); + return 0; +}