diff --git a/Makefile b/Makefile index 9cd715f5..0d8212b2 100644 --- a/Makefile +++ b/Makefile @@ -212,6 +212,7 @@ $(BIN) $(LIB) $(OBJ)/wcc $(OBJ)/ww $(OBJ)/wwdump $(OBJ)/w6c $(OBJ)/w6a $(OBJ)/w6 TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_w6c $(BIN)/test_w6a $(BIN)/test_w6l $(BIN)/test_arch \ $(BIN)/test_e2e $(BIN)/test_ffi $(BIN)/test_dyn $(BIN)/test_stdlib \ + $(BIN)/test_at_test \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ $(BIN)/test_dyn_ww @@ -255,6 +256,10 @@ $(BIN)/test_dyn: test/wcc/810_dyn.c $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/test_stdlib: test/wcc/900_stdlib.c $(BIN)/w6c | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_at_test: test/wcc/910_at_test.c $(BIN)/ww $(BIN)/w6c $(BIN)/w6a \ + $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_selfhost: test/wcc/990_selfhost.c $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww $(BIN)/wwdump $(BIN)/wwdump_ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index 156c8f31..28eaf4b8 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -1241,9 +1241,13 @@ parseattrs(Parser *p) advance(p); Node *a = newnode(p->a, N_ATTR, pp); a->str = expectident(p); - expect(p, TK_LPAREN); - a->list = parsearglist(p, TK_RPAREN); - expect(p, TK_RPAREN); + /* `@name(args...)` for FFI-style attributes; + * `@name` (no parens) for marker-only attributes like + * `@test`. */ + if (accept(p, TK_LPAREN)) { + a->list = parsearglist(p, TK_RPAREN); + expect(p, TK_RPAREN); + } if (head == NULL) head = a; else tail->next = a; tail = a; diff --git a/lib/ww/parse/decl.ww b/lib/ww/parse/decl.ww index 7357fe12..8452a57a 100644 --- a/lib/ww/parse/decl.ww +++ b/lib/ww/parse/decl.ww @@ -74,11 +74,14 @@ fn parseattrs(p: *parser) *node = { let id: str; expectident(p, &id); a.str = id; - expecttok(p, TK_LPAREN, "expected '(' after attribute name"); - let arghead: *node = nil; - parsearglist(p, TK_RPAREN, &arghead); - a.list = arghead; - expecttok(p, TK_RPAREN, "expected ')' after attribute args"); + // `@name(args...)` for FFI-style attrs; `@name` for marker- + // only attrs like @test (no parens). + if (accepttok(p, TK_LPAREN)) { + let arghead: *node = nil; + parsearglist(p, TK_RPAREN, &arghead); + a.list = arghead; + expecttok(p, TK_RPAREN, "expected ')' after attribute args"); + }; if (head == nil) { head = a; tail = a; } else { tail.next = a; tail = a; }; }; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index b564f4e4..23a89952 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -2793,11 +2793,14 @@ fn parseattrs(p: *parser) *node = { let id: str; expectident(p, &id); a.str = id; - expecttok(p, TK_LPAREN, "expected '(' after attribute name"); - let arghead: *node = nil; - parsearglist(p, TK_RPAREN, &arghead); - a.list = arghead; - expecttok(p, TK_RPAREN, "expected ')' after attribute args"); + // `@name(args...)` for FFI-style attrs; `@name` for marker- + // only attrs like @test (no parens). + if (accepttok(p, TK_LPAREN)) { + let arghead: *node = nil; + parsearglist(p, TK_RPAREN, &arghead); + a.list = arghead; + expecttok(p, TK_RPAREN, "expected ')' after attribute args"); + }; if (head == nil) { head = a; tail = a; } else { tail.next = a; tail = a; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index a051cd69..26ce53f6 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -2793,11 +2793,14 @@ fn parseattrs(p: *parser) *node = { let id: str; expectident(p, &id); a.str = id; - expecttok(p, TK_LPAREN, "expected '(' after attribute name"); - let arghead: *node = nil; - parsearglist(p, TK_RPAREN, &arghead); - a.list = arghead; - expecttok(p, TK_RPAREN, "expected ')' after attribute args"); + // `@name(args...)` for FFI-style attrs; `@name` for marker- + // only attrs like @test (no parens). + if (accepttok(p, TK_LPAREN)) { + let arghead: *node = nil; + parsearglist(p, TK_RPAREN, &arghead); + a.list = arghead; + expecttok(p, TK_RPAREN, "expected ')' after attribute args"); + }; if (head == nil) { head = a; tail = a; } else { tail.next = a; tail = a; }; }; diff --git a/test/wcc/910_at_test.c b/test/wcc/910_at_test.c new file mode 100644 index 00000000..912ef3f1 --- /dev/null +++ b/test/wcc/910_at_test.c @@ -0,0 +1,171 @@ +/* + * 910_at_test — runs @test fn blocks in a source file by + * generating a synthetic main() that calls each one. A test + * passes iff the generated binary exits 0; if any @test calls + * abort/exit(1), the run fails. + * + * Scanning is regex-free, matching the literal sequence + * `\n@test\s+fn\s+(IDENT)`. Same parser shape as ww accepts. + */ +#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 int +isws(int c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} + +static int +isident(int c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '_'; +} + +/* find_attest — scan src for `@test\s+fn\s+(ident)`; on a match, + * write the identifier into `out` (NUL-terminated) and return the + * scan position past the matched ident. Returns -1 when no more. */ +static long +find_attest(const char *src, long pos, char *out, size_t outsz) +{ + long n = (long)strlen(src); + while (pos < n) { + const char *q = strstr(src + pos, "@test"); + if (q == NULL) return -1; + long off = q - src; + /* must be a top-level marker: previous non-space char is + * '\n' or we're at the file start. */ + long pre = off - 1; + while (pre >= 0 && (src[pre] == ' ' || src[pre] == '\t')) pre--; + if (pre >= 0 && src[pre] != '\n') { + pos = off + 1; + continue; + } + long c = off + 5; /* past "@test" */ + while (c < n && isws(src[c])) c++; + if (c + 2 > n || strncmp(src + c, "fn", 2) != 0) { + pos = off + 1; + continue; + } + c += 2; + if (c < n && isident(src[c])) { + pos = off + 1; + continue; + } + while (c < n && isws(src[c])) c++; + long ids = c; + while (c < n && isident(src[c])) c++; + if (c == ids) { + pos = off + 1; + continue; + } + size_t len = (size_t)(c - ids); + if (len + 1 > outsz) return -1; + memcpy(out, src + ids, len); + out[len] = '\0'; + return c; + } + return -1; +} + +static char * +slurp(const char *path) +{ + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + char *b = malloc((size_t)n + 1); + if (!b) { fclose(f); return NULL; } + if (fread(b, 1, (size_t)n, f) != (size_t)n) { + free(b); fclose(f); return NULL; + } + b[n] = '\0'; + fclose(f); + return b; +} + +static int +runtests(const char *path, const char *bin) +{ + char *src = slurp(path); + if (!src) { + fprintf(stderr, "910 FAIL: cannot read %s\n", path); + return -1; + } + + /* Build a synthetic test driver: original source + a main() + * that calls each @test fn. Each test that runs to completion + * (no abort/exit) counts as a pass. */ + char tmpsrc[256]; + snprintf(tmpsrc, sizeof tmpsrc, "/tmp/wwd_attest_%d.ww", getpid()); + FILE *out = fopen(tmpsrc, "wb"); + if (!out) { free(src); return -1; } + fputs(src, out); + fputs("\nexport fn main() i32 = {\n", out); + + long pos = 0; + int count = 0; + for (;;) { + char name[128]; + long next = find_attest(src, pos, name, sizeof name); + if (next < 0) break; + fprintf(out, "\t%s();\n", name); + pos = next; + count++; + } + fputs("\treturn 0;\n};\n", out); + fclose(out); + free(src); + + if (count == 0) { + fprintf(stderr, "910 FAIL: no @test fns found in %s\n", path); + unlink(tmpsrc); + return -1; + } + + char cmd[2048]; + snprintf(cmd, sizeof cmd, "%s/ww run %s 2>/dev/null", bin, tmpsrc); + int rc = runwait(cmd); + unlink(tmpsrc); + if (rc != 0) { + fprintf(stderr, "910 FAIL: %s: %d @test fn(s), driver exited %d\n", + path, count, rc); + return -1; + } + return count; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + const char *files[] = { + "test/wcc/data/attest_pass.ww", + NULL, + }; + int total = 0; + for (int i = 0; files[i]; i++) { + int n = runtests(files[i], bin); + if (n < 0) return 1; + total += n; + } + printf("@test: %d test(s) ran ok\n", total); + return 0; +} diff --git a/test/wcc/data/attest_pass.ww b/test/wcc/data/attest_pass.ww new file mode 100644 index 00000000..41c8992f --- /dev/null +++ b/test/wcc/data/attest_pass.ww @@ -0,0 +1,21 @@ +// @test fixture: every test passes (exits without aborting). + +@test fn check_add() void = { + let a: i32 = 2; + let b: i32 = 3; + let c: i32 = a + b; + if (c != 5) { + let _: i32 = 1 / 0; // abort via div-by-zero would also work + }; +}; + +@test fn check_match() void = { + let r: (i32 | str) = 7; + let v: i32 = match (r) { + case let n: i32 => yield n; + case let s: str => yield 0; + }; + if (v != 7) { + let _: i32 = 1 / 0; + }; +};