From 1f1efb273af6b9168bbc7949388f30635d27e2ff Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sun, 21 Jun 2026 12:14:31 +0900 Subject: [PATCH] check: gate C-style ... to bodiless decls, both stages (#11) A bodied fn with a bare C-style `...` was silently accepted by cstage and SEGFAULTED wwstage (resolvefnbody walked a typeless `...` param). Gate it: bare C-`...` is allowed only on bodiless decls (extern / @symbol prototypes), the real FFI path; Hare-style `T...` is unaffected. ww restricts C-`...` to bodiless decls pending vastart/vaarg/vaend builtins (#16); harec permits bodied C-variadic fns (check.c:3656) -- a documented divergence, reopened when #16 lands. Test 852 runs both stages; its reject rows require the gate's diagnostic (not merely a nonzero exit), so a crash can't pass them vacuously. --- Makefile | 7 + cmd/wcc/check.c | 6 + selfhost/cmd/wcc/check.ww | 25 ++++ test/wcc/852_variadic_body_reject.c | 225 ++++++++++++++++++++++++++++ 4 files changed, 263 insertions(+) create mode 100644 test/wcc/852_variadic_body_reject.c diff --git a/Makefile b/Makefile index 67d16566..19137d30 100644 --- a/Makefile +++ b/Makefile @@ -249,6 +249,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_const_reassign_reject \ $(BIN)/test_dupfield_reject \ $(BIN)/test_enum_reject \ + $(BIN)/test_variadic_body_reject \ $(BIN)/test_index_int_reject \ $(BIN)/test_size_untyped_int \ $(BIN)/test_tagged_staticinit \ @@ -1581,6 +1582,12 @@ $(BIN)/test_enum_reject: test/wcc/850_enum_reject.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_variadic_body_reject: test/wcc/852_variadic_body_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_index_int_reject: test/wcc/851_index_int_reject.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 4393c10c..ed7099d0 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -3332,6 +3332,12 @@ check_file(Checker *c, Node *file) break; } case N_FNDECL: { + /* ww restricts C-style ... to bodiless decls pending + * vastart/vaarg/vaend builtins (#16); harec permits bodied + * C-variadic fns (check.c:3656). */ + if (d->body != NULL && d->type->variadic) + err(c, d->pos, "C-style variadic '...' " + "requires a bodiless declaration"); if (d->body == NULL) break; /* extern decl */ Scope *saved = c->cur; c->cur = newscope(c->a, saved); diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 602f6ca0..7ea92f06 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -6306,6 +6306,20 @@ fn checktryprop(c: *checker, n: *syntax.node) void = { }; }; +// hascvariadic — true iff the param list ends in a bare C-style `...` +// (the param whose str=="...", set by the parser; Hare-style `T...` +// carries a name + op==TK_ELLIPSIS instead). Mirrors cstage +// build_fn_type's `p->str && strcmp(p->str,"...")==0` test +// (check.c:2646). +fn hascvariadic(params: *syntax.node) bool = { + let p: *syntax.node = params; + for (p != nil) { + if (syntax.streq(p.str, "...")) { return true; }; + p = p.next; + }; + return false; +}; + // install_param — when entering a fn body, define its params in a // fresh local scope. // @@ -6781,6 +6795,17 @@ export fn checkfile(c: *checker, file: *syntax.node) void = { let k: syntax.nkind = d.kind; switch (k) { case syntax.nkind.N_FNDECL: + // ww restricts C-style ... to bodiless decls pending + // vastart/vaarg/vaend builtins (#16); harec permits bodied + // C-variadic fns (check.c:3656). The bare C-style param is + // the one with str=="..." (Hare-style `T...` carries a name + // + op==TK_ELLIPSIS instead), mirroring cstage build_fn_type + // check.c:2646. + if (d.body != nil && hascvariadic(d.list)) { + cerr(d.file); + cerr(": error: C-style variadic '...' requires a bodiless declaration\n"); + c.errs += 1; + }; if (d.lhs != nil) { resolvewalk(c, d.lhs); }; // return type resolvefnbody(c, d); case syntax.nkind.N_DEF: diff --git a/test/wcc/852_variadic_body_reject.c b/test/wcc/852_variadic_body_reject.c new file mode 100644 index 00000000..ee47dc66 --- /dev/null +++ b/test/wcc/852_variadic_body_reject.c @@ -0,0 +1,225 @@ +/* + * 852_variadic_body_reject (C3) — cstage and wwstage LOUD-REJECT a + * BODIED function whose param list ends in a bare C-style `...`, while + * still ACCEPTING the bodiless extern/@symbol prototype (the real FFI + * path) and the bodied Hare-style `T...` typed variadic. + * + * DIVERGENCE (rule 7/8): harec places NO bodiless restriction on C + * variadism — a bodied C-variadic fn is legal in Hare because that is + * where vastart/vaarg/vaend live (ref/harec/src/check.c:3656). ww ships + * no va* builtins (task #16), so a bodied bare-`...` body could never + * read its varargs — the construct is inexpressible. Gating it to + * bodiless decls is therefore a sound ww-pragmatic restriction, NOT + * "what Hare does"; when #16's builtins land this gate reopens. + * + * Pre-fix: cstage silently ACCEPTED a bodied bare-`...` fn (cmd/wcc/ + * check.c N_FNDECL pass-2 only guarded body==NULL); wwstage SEGFAULTED + * (resolvefnbody walked a `...` param with no type). The fix adds, at + * the N_FNDECL body-check site of each stage, a guard rejecting + * (body != NULL && C-style variadic). The bare C-style param is the one + * with str=="..."; Hare-style `T...` carries a name + op==TK_ELLIPSIS, + * so it is unaffected. Bodiless decls keep flowing the FFI path. + * + * row | shape | expect + * -------------+----------------------------------------+-------- + * bodied_cvar | fn f(x: i64, ...) void = {...} | REJECT + * bodied_bare | fn f(...) void = {...} | REJECT + * extern_cvar | @symbol(..) fn ext(x: i64, ...) i64; | BUILD, run 7 + * hare_var | fn sum(xs: i64...) i64 = {...} | BUILD, run 7 + */ +#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; +} + +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'; +} + +/* + * The reject must be the gate's clean diagnostic, not any nonzero exit: + * the wwstage gate's absence reverts to a w6c SEGFAULT, which the driver + * also wraps as a nonzero "w6c failed". Matching this shared diagnostic + * substring distinguishes a loud reject from a crash, so the wwstage + * reject rows are non-vacuous. + */ +static const char rejmsg[] = + "C-style variadic '...' requires a bodiless declaration"; + +static int +filehas(const char *path, const char *needle) +{ + char buf[8192]; + FILE *f = fopen(path, "rb"); + if (!f) return 0; + size_t n = fread(buf, 1, sizeof buf - 1, f); + fclose(f); + buf[n] = '\0'; + return strstr(buf, needle) != NULL; +} + +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], errf[64]; + snprintf(s, sizeof s, "/tmp/vbr_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/vbr_%d_d_%d", getpid(), i); + snprintf(errf, sizeof errf, "/tmp/vbr_%d_e_%d.txt", 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 >/dev/null 2>%s", + tmpdir, driver, s, errf); + int rc = runwait(cmd); + int hasmsg = filehas(errf, rejmsg); + outbin(bin, sizeof bin, tmpdir, s); + unlink(s); + unlink(bin); + unlink(errf); + rmdir(tmpdir); + if (rc == 0) { + fprintf(stderr, "vbr[%s][%s]: built ok, expected a reject\n", + driver, label); + return -1; + } + if (!hasmsg) { + fprintf(stderr, "vbr[%s][%s]: nonzero exit but missing gate " + "diagnostic (a crash, not a clean reject)\n", driver, label); + return -1; + } + return 0; +} + +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/vbr_ok_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/vbr_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, "vbr[%s][%s]: build failed (valid variadic " + "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[] = { + { "bodied_cvar", + "package main;\n" + "fn f(x: i64, ...) void = { return; };\n" + "fn main() i32 = { return 0; };\n" }, + { "bodied_bare", + "package main;\n" + "fn f(...) void = { return; };\n" + "fn main() i32 = { return 0; };\n" }, +}; + +struct okrow { const char *label; const char *src; int want; }; + +static const struct okrow ok_rows[] = { + { "extern_cvar", + "package main;\n" + "@symbol(\"extfix\") fn extfix(x: i64, ...) i64;\n" + "fn main() i32 = { return 7; };\n", + 7 }, + { "hare_var", + "package main;\n" + "fn sum(xs: i64...) i64 = { return 0; };\n" + "fn main() i32 = { return 7; };\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, "vbr: 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, "vbr[%s][%s]: exit=%d want=%d\n", + drivers[d].name, ok_rows[i].label, + got, ok_rows[i].want); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, "vbr: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("vbr: %d/%d ok\n", total, total); + return 0; +}