From b9c45621354d55498434ed59a452a00de1aaf784 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 11 Jun 2026 04:51:17 +0900 Subject: [PATCH] ww test: @test name-filter via fnmatch (both stages) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ww test ` runs only the @test fns whose names match the fnmatch glob; no pattern runs all (byte-for-byte the pre-filter path); zero matches prints "No tests run" and exits 0 (Hare ground truth ref/hare/test/+test.ha:114-117). A pattern in directory mode is rejected "ww test: pattern needs a single test file" (rc 2), identical wording in both twins (cmd/ww/main.c do_test + selfhost/cmd/ww/main.ww dotest). Mechanism (a): rt/start.s stashes argc/argv into rt_argc/rt_argv getters (rt_envp twin shape, -T synth untouched so 990-997 byte-id holds); lib/os.args() rebuilds the []str view, build-once-cached; lib/test/run.ww imports fnmatch and filters av[1..] (argv[0] is the binary path). The driver forwards the 2nd positional as argv[1] via fork/execv (cstage) / procrun (wwstage) so glob metachars aren't shell-expanded. os.args() is the first `alloc`-caller in the base os module, so os.ww now imports rt — the `alloc` builtin's malloc lowers to rt_malloc only when the rt binding is bundled (mirror lib/strings/strings.ww:30); without it a plain `ww build` of any os-importing program links bare libc `malloc` (undefined). os is bundled by ~every program, so this is load-bearing. The lib/test floor rises os-only -> os+fnmatch+ascii+strings in every -T build; the bundled `ascii` module vs a `@test fn ascii` collision that exposed is closed by the preceding #30 promote commit. 989_test_filter pins the full matrix on both twins byte-identically; 949 gains the dir-mode reject row. (#17) --- Makefile | 10 ++ cmd/ww/main.c | 42 +++++++- lib/getopt/getopt.ww | 2 +- lib/os/os.ww | 52 ++++++++++ lib/test/run.ww | 74 +++++++++++--- rt/start.s | 23 +++++ selfhost/cmd/w6a/main.combined.ww | 98 +++++++++++++----- selfhost/cmd/w6c/main.combined.ww | 98 +++++++++++++----- selfhost/cmd/w6l/main.combined.ww | 98 +++++++++++++----- selfhost/cmd/ww/main.combined.ww | 130 ++++++++++++++++++------ selfhost/cmd/ww/main.ww | 32 ++++-- selfhost/cmd/wwdump/main.combined.ww | 98 +++++++++++++----- selfhost/test/smoke.combined.ww | 98 +++++++++++++----- test/wcc/949_driver_flagargs.c | 8 +- test/wcc/989_test_filter.c | 142 +++++++++++++++++++++++++++ test/wcc/data/filter_fixture.ww | 13 +++ 16 files changed, 853 insertions(+), 165 deletions(-) create mode 100644 test/wcc/989_test_filter.c create mode 100644 test/wcc/data/filter_fixture.ww diff --git a/Makefile b/Makefile index 59e54e1b..203f6b36 100644 --- a/Makefile +++ b/Makefile @@ -479,6 +479,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_path_run \ $(BIN)/test_letshadow_run \ $(BIN)/test_strglobeq_run \ + $(BIN)/test_test_filter \ $(BIN)/test_strconv_int_run \ $(BIN)/test_stof_run $(BIN)/test_ftos_run \ $(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \ @@ -610,6 +611,15 @@ $(BIN)/test_driver_flagargs: test/wcc/949_driver_flagargs.c $(BIN)/ww \ $(BIN)/ww_ww | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_test_filter drives `ww test [pattern]` end-to-end on BOTH +# driver twins, so it needs the full cstage + wwstage tool sets (ww_ww +# shells to w6c_ww/w6a_ww/w6l_ww) plus libwwrt for the link. #17. +$(BIN)/test_test_filter: test/wcc/989_test_filter.c $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_let_global: test/wcc/630_let_global.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 56e28be7..a22af885 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -54,6 +54,32 @@ run(const char *cmd) return 1; } +/* run_test_bin — exec the built test binary with an optional name-filter + * pattern as argv[1] (lib/test run() reads it via os.args). fork+execv + * (not system()) so glob metacharacters in the pattern reach the binary + * verbatim instead of being expanded by the shell. Mirrors the wwstage + * twin (selfhost/cmd/ww/main.ww runsingletest, which always builds an + * execargv for procrun). #17 fnmatch filter. */ +static int +run_test_bin(const char *bin, const char *pattern) +{ + pid_t pid = fork(); + if (pid < 0) { perror("ww: fork"); return -1; } + if (pid == 0) { + char *xargv[3]; + xargv[0] = (char *)bin; + if (pattern) { xargv[1] = (char *)pattern; xargv[2] = NULL; } + else { xargv[1] = NULL; } + execv(bin, xargv); + perror("ww: exec"); + _exit(127); + } + int status = 0; + waitpid(pid, &status, 0); + if (WIFEXITED(status)) return WEXITSTATUS(status); + return 1; +} + /* Set of imported module paths, kept on the heap. Used to break * cycles in `use` resolution. Linear because typical imports are * a handful per build. */ @@ -815,6 +841,10 @@ do_test(int argc, char **argv) * wwstage twin (selfhost/cmd/ww/main.ww dotest). */ int compileonly = 0; char outstem[1024] = {0}; + /* #17: an optional second positional after the target is a fnmatch + * name-filter pattern, forwarded to the test binary as argv[1]. Only + * meaningful for a single test file/module — rejected in dir mode. */ + const char *pattern = NULL; for (int i = 0; i < argc; i++) { if (argv[i][0] == '-') { if (argv[i][1] == 'I') { @@ -849,6 +879,8 @@ do_test(int argc, char **argv) } } else if (src == NULL) { src = argv[i]; + } else if (pattern == NULL) { + pattern = argv[i]; } } const char *target = src ? src : "."; @@ -870,7 +902,7 @@ do_test(int argc, char **argv) if (build_one(resolved, is_dir, outp, outstem[0] ? outstem : NULL, incs, "", "", 1) != 0) return 1; if (compileonly) return 0; - int rc = run(outp); + int rc = run_test_bin(outp, pattern); if (!outstem[0]) unlink(outp); return rc; } @@ -883,7 +915,7 @@ do_test(int argc, char **argv) if (build_one(target, 0, outp, outstem[0] ? outstem : NULL, incs, "", "", 1) != 0) return 1; if (compileonly) return 0; - int rc = run(outp); + int rc = run_test_bin(outp, pattern); if (!outstem[0]) unlink(outp); return rc; } @@ -895,6 +927,12 @@ do_test(int argc, char **argv) fprintf(stderr, "ww test: -c/-o need a single test file\n"); return 2; } + /* #17: a name-filter pattern is per-binary; directory mode builds one + * binary per *_test.ww, so a single pattern can't sensibly route. */ + if (pattern) { + fprintf(stderr, "ww test: pattern needs a single test file\n"); + return 2; + } /* directory — run every *_test.ww inside. */ char **files = NULL; int n = 0; diff --git a/lib/getopt/getopt.ww b/lib/getopt/getopt.ww index 336b2bfa..45406ed0 100644 --- a/lib/getopt/getopt.ww +++ b/lib/getopt/getopt.ww @@ -229,7 +229,7 @@ fn findflag(hs: []help, r: rune) (helpkind | void) = { // tryparse — parse `argv` against `help`, filling `*out` on success. // `argv` must include the program name in argv[0]; matches Hare and -// matches the slice [[os.args]] would hand back. +// matches the slice [[os.args]] hands back. // // Recognised forms: // -X FLAG-kind option diff --git a/lib/os/os.ww b/lib/os/os.ww index b0c8facb..7a70f36c 100644 --- a/lib/os/os.ww +++ b/lib/os/os.ww @@ -5,6 +5,12 @@ package os; import time; +// [[args]] allocates the []str view via the `alloc` builtin, whose malloc +// lowers to rt_malloc only when the rt binding is in the bundle (mirror +// lib/strings/strings.ww:30 — every alloc-using module imports rt). os is +// bundled by ~every program, so without this a plain `ww build` of any +// os-importing program links bare libc `malloc` (undefined). Task #17. +import rt; @symbol("rt_syscall") fn syscall0(num: nr) i64; @symbol("rt_syscall") fn syscall1(num: nr, a: i64) i64; @@ -403,6 +409,12 @@ export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = { // We don't expose `rtenvp` directly; [[getenv]] is the only consumer. @symbol("rt_envp") fn rtenvp() **u8; +// rt_argc / rt_argv — runtime-side getters for the argc/argv captured by +// rt/start.s at process entry (same DATAW-slot + TEXT-getter shape as +// rt_envp). [[args]] is the only consumer. +@symbol("rt_argc") fn rtargc() i64; +@symbol("rt_argv") fn rtargv() **u8; + // getenv — POSIX getenv. Returns a borrowed `str` view over the value // bytes of the named environment variable, or void if the name is not // present. The view is valid for the process lifetime — the bytes @@ -451,6 +463,46 @@ export fn getenv(name: str) (str | void) = { return; }; +// argsbuilt / argscache — build-once cache for [[args]]. drew ruling +// (task #17): an explicit `built` sentinel, NOT len==0 overloading (a +// real argv always has argv[0], but the sentinel keeps the contract +// honest and decoupled from content). args() is loop-callable; under +// ww's no-free model a per-call rebuild would leak the slice each call, +// so the slice is materialised once and reused. +let argsbuilt: bool = false; +let argscache: []str; + +// args — the process arguments as a borrowed []str. args[0] is the +// program name; args[1..] are the invocation arguments. Each str views +// the NUL-terminated argv bytes in place (valid for the process +// lifetime), so the slice must not be mutated or freed by the caller. +// +// DIVERGENCE (Hare-fidelity, task #26): Hare's `os::args` is a `[]str` +// GLOBAL populated by an @init that walks rt's argv +// (ref/hare/os/+linux/start.ha). ww has no @init mechanism, so the +// faithful global is not expressible; this is the fn-shaped equivalent +// (Hare NAME kept, shape diverged). Revisit if @init lands (#26). +export fn args() []str = { + if (argsbuilt) { return argscache; }; + let argc: i32 = rtargc(): i32; + let argv: **u8 = rtargv(); + let r: []str = alloc([], argc: u64)!; + let i: i32 = 0; + for (i < argc) { + let c: *u8 = argv[i]; + let n: i32 = 0; + for (c[n] != 0u8) { n += 1; }; + let s: str; + s.ptr = c; + s.len = n; + append(r, s); + i += 1; + }; + argscache = r; + argsbuilt = true; + return r; +}; + // ---- stat / lstat / fstat / exists ----------------------------------- // // Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 + diff --git a/lib/test/run.ww b/lib/test/run.ww index a5dc1cf9..ac7fee6a 100644 --- a/lib/test/run.ww +++ b/lib/test/run.ww @@ -16,9 +16,29 @@ package test; // // D1/D4/D5 reductions also retained from the -T synth era: no // __test_array linker section (the synth hands us a value table), no -// sort, no file:line reflection. fnmatch name-filtering is task #17 -// commit-3 (drew spec §d), not here. +// sort, no file:line reflection. +// +// fnmatch NAME-FILTER (task #17 commit-3, drew spec §d): when the test +// binary is invoked with trailing argv (the driver passes `ww test +// [pattern]` through as argv[1..]), each pattern is a shell glob +// matched against the test name via [[fnmatch.fnmatch]]; a test runs iff +// some pattern matches. No pattern (argv.len == 1) runs ALL tests — the +// no-filter path is byte-for-byte the pre-filter behavior. Zero matches +// prints "No tests run" and exits 0, mirroring Hare's __test_main +// (ref/hare/test/+test.ha:104-117). The synth (`run(__wwtests)`) and the +// value table are UNCHANGED — the pattern reaches us via os.args, so +// 990-997 byte-id and the no-pattern path hold. +// +// BUNDLE-FLOOR CONSEQUENCE (sanctioned, task #17): importing fnmatch +// raises lib/test's auto-bundle floor from os-only to +// os+fnmatch+ascii+strings (+their transitive bytes/encoding.utf8/types/ +// rt) in EVERY -T build. fnmatch's module-private symbols co-bundle into +// the same flat "" scope as the test's modules (pre-#8, no package +// isolation); a same-named module-private fn in a tested module would +// collide. Accepted as the cost of name-filtering; #8 (real packages) +// retires it. +import fnmatch; import os; // run — execute each test in `tests` in a forked subprocess, report a @@ -41,30 +61,62 @@ import os; // rejects a duplicate fn, wwstage does not), so the collision is loud on // the reference stage but silent on wwstage until #23 lands. export fn run(tests: [](str, *fn() void)) i32 = { + let av: []str = os.args(); let nfail: i32 = 0; + let nrun: i32 = 0; let i: i32 = 0; for (i < tests.len) { let row: (str, *fn() void) = tests[i]; let name: str = row.0; let f: *fn() void = row.1; - tstputs(name); - tstputs(" ... "); - let st: i32 = tstrunone(f); - if (st == 0) { - tstputs("ok\n"); - } else { - tstputs("FAIL\n"); - nfail += 1; + if (tstenabled(name, av)) { + tstputs(name); + tstputs(" ... "); + let st: i32 = tstrunone(f); + if (st == 0) { + tstputs("ok\n"); + } else { + tstputs("FAIL\n"); + nfail += 1; + }; + nrun += 1; }; i += 1; }; - tstputuint(tests.len - nfail); + // Zero tests selected (an over-narrow pattern, or none matched): Hare + // prints "No tests run" and returns 0 regardless of whether a pattern + // was given (ref/hare/test/+test.ha:114-117). + if (nrun == 0) { + tstputs("No tests run\n"); + return 0; + }; + tstputuint(nrun - nfail); tstputs(" passed, "); tstputuint(nfail); tstputs(" failed\n"); return nfail; }; +// tstenabled — does `name` pass the name-filter? `av` is os.args(): +// av[0] is the program name, av[1..] the glob patterns the driver +// forwarded. No patterns (av.len <= 1) enables every test; otherwise a +// test is enabled iff SOME pattern fnmatch-matches (Hare's OR-over-args, +// ref/hare/test/+test.ha:106-113). DIVERGENCE: Hare iterates all of +// os::args including [0]; ww skips [0] — the test binary path is never a +// test-name pattern (the driver passes the pattern as the first +// post-program arg, never argv[0]). +fn tstenabled(name: str, av: []str) bool = { + if (av.len <= 1) { return true; }; + let j: i32 = 1; + for (j < av.len) { + if (fnmatch.fnmatch(av[j], name, fnmatch.flag.NONE)) { + return true; + }; + j += 1; + }; + return false; +}; + // runone — fork, run `f` in the child, decode the child's wait-status. // Byte-for-byte the procrun (main.ww:154-177) status decode: low 7 bits // are the killing signal (abort=SIGABRT, div0/SIGSEGV), 0 if the child diff --git a/rt/start.s b/rt/start.s index 6eb58c4b..9d16d094 100644 --- a/rt/start.s +++ b/rt/start.s @@ -25,6 +25,11 @@ TEXT _start,$0 ADDQ SP, AX // AX = SP + argc*8 ADDQ $16, AX // AX = SP + argc*8 + 16 = &argv[argc+1] = envp MOVQ AX, rt_envp_slot(SB) + // argc/argv survive the envp calc (only AX was clobbered): stash both + // so lib/os.args can rebuild the []str view after entry. Same DATAW- + // slot + TEXT-getter shape as rt_envp below (task #17 fnmatch filter). + MOVQ DI, rt_argc_slot(SB) + MOVQ SI, rt_argv_slot(SB) CALL main(SB) MOVQ AX, DI MOVQ $60, AX @@ -46,3 +51,21 @@ TEXT rt_envp,$0 // slot (w6a has no GLOBL; same shape as lib/log's silent/default/ // global cells in lib/log/log.s). DATAW rt_envp_slot(SB),"\x00\x00\x00\x00\x00\x00\x00\x00" + +// rt_argc — getter returning the process argc captured at entry. Bound +// from lib/os as `@symbol("rt_argc") fn rtargc() i64;` (rt_envp twin). +TEXT rt_argc,$0 + MOVQ rt_argc_slot(SB), AX + RET + +// rt_argv — getter returning &argv[0], a NUL-terminated table of *u8 +// argument strings. Bound from lib/os as +// `@symbol("rt_argv") fn rtargv() **u8;`. +TEXT rt_argv,$0 + MOVQ rt_argv_slot(SB), AX + RET + +// rt_argc_slot / rt_argv_slot — 8-byte writable cells, zero in .data, +// overwritten by _start before CALL main. Same shape as rt_envp_slot. +DATAW rt_argc_slot(SB),"\x00\x00\x00\x00\x00\x00\x00\x00" +DATAW rt_argv_slot(SB),"\x00\x00\x00\x00\x00\x00\x00\x00" diff --git a/selfhost/cmd/w6a/main.combined.ww b/selfhost/cmd/w6a/main.combined.ww index a37e7ab2..5880429e 100644 --- a/selfhost/cmd/w6a/main.combined.ww +++ b/selfhost/cmd/w6a/main.combined.ww @@ -95,6 +95,29 @@ export fn compare(a: instant, b: instant) i8 = { return 0i8; }; +// rt — runtime primitives exposed to ww programs. +// Mirrors Hare's rt:: module placement (ref/hare/rt/). + +package rt; + +// malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a +// `*void`; callers cast to the target type. Diverges from Hare: Hare +// exposes `alloc` / `free` as typed language builtins that the +// compiler lowers to rt::malloc/rt::free; ww has no such builtins, +// so the rt-symbol surface is exposed directly. Stdlib callers that +// need a typed allocation pattern wrap this with a cast plus a stored +// capacity (see [[strings.dup]], [[memio.dynamic]]). +// +// OOM: rt_malloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with no +// error path. The raw Linux mmap syscall returns a negative errno cast +// to `*void` on failure (e.g. `(void*)-12` for ENOMEM); the +// `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention that +// rt_malloc doesn't apply. Neither `== nil` nor `== (void*)-1` catches +// it; any deref of such a return faults. Today the stdlib does not +// check; OOM faults on first dereference. A typed fallible variant is +// a future task (task #39). ref/hare/rt/malloc.ha:27. +@symbol("rt_malloc") export fn malloc(n: u64) *void; + // os — process and filesystem facade. The body of each call lands // either in libwwrt.a (rt_syscall trampoline) or libc bindings, // depending on how the program was linked. @@ -102,6 +125,12 @@ export fn compare(a: instant, b: instant) i8 = { package os; import time; +// [[args]] allocates the []str view via the `alloc` builtin, whose malloc +// lowers to rt_malloc only when the rt binding is in the bundle (mirror +// lib/strings/strings.ww:30 — every alloc-using module imports rt). os is +// bundled by ~every program, so without this a plain `ww build` of any +// os-importing program links bare libc `malloc` (undefined). Task #17. +import rt; @symbol("rt_syscall") fn syscall0(num: nr) i64; @symbol("rt_syscall") fn syscall1(num: nr, a: i64) i64; @@ -500,6 +529,12 @@ export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = { // We don't expose `rtenvp` directly; [[getenv]] is the only consumer. @symbol("rt_envp") fn rtenvp() **u8; +// rt_argc / rt_argv — runtime-side getters for the argc/argv captured by +// rt/start.s at process entry (same DATAW-slot + TEXT-getter shape as +// rt_envp). [[args]] is the only consumer. +@symbol("rt_argc") fn rtargc() i64; +@symbol("rt_argv") fn rtargv() **u8; + // getenv — POSIX getenv. Returns a borrowed `str` view over the value // bytes of the named environment variable, or void if the name is not // present. The view is valid for the process lifetime — the bytes @@ -548,6 +583,46 @@ export fn getenv(name: str) (str | void) = { return; }; +// argsbuilt / argscache — build-once cache for [[args]]. drew ruling +// (task #17): an explicit `built` sentinel, NOT len==0 overloading (a +// real argv always has argv[0], but the sentinel keeps the contract +// honest and decoupled from content). args() is loop-callable; under +// ww's no-free model a per-call rebuild would leak the slice each call, +// so the slice is materialised once and reused. +let argsbuilt: bool = false; +let argscache: []str; + +// args — the process arguments as a borrowed []str. args[0] is the +// program name; args[1..] are the invocation arguments. Each str views +// the NUL-terminated argv bytes in place (valid for the process +// lifetime), so the slice must not be mutated or freed by the caller. +// +// DIVERGENCE (Hare-fidelity, task #26): Hare's `os::args` is a `[]str` +// GLOBAL populated by an @init that walks rt's argv +// (ref/hare/os/+linux/start.ha). ww has no @init mechanism, so the +// faithful global is not expressible; this is the fn-shaped equivalent +// (Hare NAME kept, shape diverged). Revisit if @init lands (#26). +export fn args() []str = { + if (argsbuilt) { return argscache; }; + let argc: i32 = rtargc(): i32; + let argv: **u8 = rtargv(); + let r: []str = alloc([], argc: u64)!; + let i: i32 = 0; + for (i < argc) { + let c: *u8 = argv[i]; + let n: i32 = 0; + for (c[n] != 0u8) { n += 1; }; + let s: str; + s.ptr = c; + s.len = n; + append(r, s); + i += 1; + }; + argscache = r; + argsbuilt = true; + return r; +}; + // ---- stat / lstat / fstat / exists ----------------------------------- // // Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 + @@ -781,29 +856,6 @@ export fn exists(path: str) bool = { return r >= 0i64; }; -// rt — runtime primitives exposed to ww programs. -// Mirrors Hare's rt:: module placement (ref/hare/rt/). - -package rt; - -// malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a -// `*void`; callers cast to the target type. Diverges from Hare: Hare -// exposes `alloc` / `free` as typed language builtins that the -// compiler lowers to rt::malloc/rt::free; ww has no such builtins, -// so the rt-symbol surface is exposed directly. Stdlib callers that -// need a typed allocation pattern wrap this with a cast plus a stored -// capacity (see [[strings.dup]], [[memio.dynamic]]). -// -// OOM: rt_malloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with no -// error path. The raw Linux mmap syscall returns a negative errno cast -// to `*void` on failure (e.g. `(void*)-12` for ENOMEM); the -// `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention that -// rt_malloc doesn't apply. Neither `== nil` nor `== (void*)-1` catches -// it; any deref of such a return faults. Today the stdlib does not -// check; OOM faults on first dereference. A typed fallible variant is -// a future task (task #39). ref/hare/rt/malloc.ha:27. -@symbol("rt_malloc") export fn malloc(n: u64) *void; - // types — integer limits. Mirrors Hare's types::limits (I8_MAX, …) // platform-fixed for amd64. Numeric helpers live in lib/math, matching // Hare's split between types::limits and math::. diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index fd6ce548..3c5e41fa 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -95,6 +95,29 @@ export fn compare(a: instant, b: instant) i8 = { return 0i8; }; +// rt — runtime primitives exposed to ww programs. +// Mirrors Hare's rt:: module placement (ref/hare/rt/). + +package rt; + +// malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a +// `*void`; callers cast to the target type. Diverges from Hare: Hare +// exposes `alloc` / `free` as typed language builtins that the +// compiler lowers to rt::malloc/rt::free; ww has no such builtins, +// so the rt-symbol surface is exposed directly. Stdlib callers that +// need a typed allocation pattern wrap this with a cast plus a stored +// capacity (see [[strings.dup]], [[memio.dynamic]]). +// +// OOM: rt_malloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with no +// error path. The raw Linux mmap syscall returns a negative errno cast +// to `*void` on failure (e.g. `(void*)-12` for ENOMEM); the +// `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention that +// rt_malloc doesn't apply. Neither `== nil` nor `== (void*)-1` catches +// it; any deref of such a return faults. Today the stdlib does not +// check; OOM faults on first dereference. A typed fallible variant is +// a future task (task #39). ref/hare/rt/malloc.ha:27. +@symbol("rt_malloc") export fn malloc(n: u64) *void; + // os — process and filesystem facade. The body of each call lands // either in libwwrt.a (rt_syscall trampoline) or libc bindings, // depending on how the program was linked. @@ -102,6 +125,12 @@ export fn compare(a: instant, b: instant) i8 = { package os; import time; +// [[args]] allocates the []str view via the `alloc` builtin, whose malloc +// lowers to rt_malloc only when the rt binding is in the bundle (mirror +// lib/strings/strings.ww:30 — every alloc-using module imports rt). os is +// bundled by ~every program, so without this a plain `ww build` of any +// os-importing program links bare libc `malloc` (undefined). Task #17. +import rt; @symbol("rt_syscall") fn syscall0(num: nr) i64; @symbol("rt_syscall") fn syscall1(num: nr, a: i64) i64; @@ -500,6 +529,12 @@ export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = { // We don't expose `rtenvp` directly; [[getenv]] is the only consumer. @symbol("rt_envp") fn rtenvp() **u8; +// rt_argc / rt_argv — runtime-side getters for the argc/argv captured by +// rt/start.s at process entry (same DATAW-slot + TEXT-getter shape as +// rt_envp). [[args]] is the only consumer. +@symbol("rt_argc") fn rtargc() i64; +@symbol("rt_argv") fn rtargv() **u8; + // getenv — POSIX getenv. Returns a borrowed `str` view over the value // bytes of the named environment variable, or void if the name is not // present. The view is valid for the process lifetime — the bytes @@ -548,6 +583,46 @@ export fn getenv(name: str) (str | void) = { return; }; +// argsbuilt / argscache — build-once cache for [[args]]. drew ruling +// (task #17): an explicit `built` sentinel, NOT len==0 overloading (a +// real argv always has argv[0], but the sentinel keeps the contract +// honest and decoupled from content). args() is loop-callable; under +// ww's no-free model a per-call rebuild would leak the slice each call, +// so the slice is materialised once and reused. +let argsbuilt: bool = false; +let argscache: []str; + +// args — the process arguments as a borrowed []str. args[0] is the +// program name; args[1..] are the invocation arguments. Each str views +// the NUL-terminated argv bytes in place (valid for the process +// lifetime), so the slice must not be mutated or freed by the caller. +// +// DIVERGENCE (Hare-fidelity, task #26): Hare's `os::args` is a `[]str` +// GLOBAL populated by an @init that walks rt's argv +// (ref/hare/os/+linux/start.ha). ww has no @init mechanism, so the +// faithful global is not expressible; this is the fn-shaped equivalent +// (Hare NAME kept, shape diverged). Revisit if @init lands (#26). +export fn args() []str = { + if (argsbuilt) { return argscache; }; + let argc: i32 = rtargc(): i32; + let argv: **u8 = rtargv(); + let r: []str = alloc([], argc: u64)!; + let i: i32 = 0; + for (i < argc) { + let c: *u8 = argv[i]; + let n: i32 = 0; + for (c[n] != 0u8) { n += 1; }; + let s: str; + s.ptr = c; + s.len = n; + append(r, s); + i += 1; + }; + argscache = r; + argsbuilt = true; + return r; +}; + // ---- stat / lstat / fstat / exists ----------------------------------- // // Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 + @@ -781,29 +856,6 @@ export fn exists(path: str) bool = { return r >= 0i64; }; -// rt — runtime primitives exposed to ww programs. -// Mirrors Hare's rt:: module placement (ref/hare/rt/). - -package rt; - -// malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a -// `*void`; callers cast to the target type. Diverges from Hare: Hare -// exposes `alloc` / `free` as typed language builtins that the -// compiler lowers to rt::malloc/rt::free; ww has no such builtins, -// so the rt-symbol surface is exposed directly. Stdlib callers that -// need a typed allocation pattern wrap this with a cast plus a stored -// capacity (see [[strings.dup]], [[memio.dynamic]]). -// -// OOM: rt_malloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with no -// error path. The raw Linux mmap syscall returns a negative errno cast -// to `*void` on failure (e.g. `(void*)-12` for ENOMEM); the -// `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention that -// rt_malloc doesn't apply. Neither `== nil` nor `== (void*)-1` catches -// it; any deref of such a return faults. Today the stdlib does not -// check; OOM faults on first dereference. A typed fallible variant is -// a future task (task #39). ref/hare/rt/malloc.ha:27. -@symbol("rt_malloc") export fn malloc(n: u64) *void; - // types — integer limits. Mirrors Hare's types::limits (I8_MAX, …) // platform-fixed for amd64. Numeric helpers live in lib/math, matching // Hare's split between types::limits and math::. diff --git a/selfhost/cmd/w6l/main.combined.ww b/selfhost/cmd/w6l/main.combined.ww index b6942475..2e049e2d 100644 --- a/selfhost/cmd/w6l/main.combined.ww +++ b/selfhost/cmd/w6l/main.combined.ww @@ -95,6 +95,29 @@ export fn compare(a: instant, b: instant) i8 = { return 0i8; }; +// rt — runtime primitives exposed to ww programs. +// Mirrors Hare's rt:: module placement (ref/hare/rt/). + +package rt; + +// malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a +// `*void`; callers cast to the target type. Diverges from Hare: Hare +// exposes `alloc` / `free` as typed language builtins that the +// compiler lowers to rt::malloc/rt::free; ww has no such builtins, +// so the rt-symbol surface is exposed directly. Stdlib callers that +// need a typed allocation pattern wrap this with a cast plus a stored +// capacity (see [[strings.dup]], [[memio.dynamic]]). +// +// OOM: rt_malloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with no +// error path. The raw Linux mmap syscall returns a negative errno cast +// to `*void` on failure (e.g. `(void*)-12` for ENOMEM); the +// `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention that +// rt_malloc doesn't apply. Neither `== nil` nor `== (void*)-1` catches +// it; any deref of such a return faults. Today the stdlib does not +// check; OOM faults on first dereference. A typed fallible variant is +// a future task (task #39). ref/hare/rt/malloc.ha:27. +@symbol("rt_malloc") export fn malloc(n: u64) *void; + // os — process and filesystem facade. The body of each call lands // either in libwwrt.a (rt_syscall trampoline) or libc bindings, // depending on how the program was linked. @@ -102,6 +125,12 @@ export fn compare(a: instant, b: instant) i8 = { package os; import time; +// [[args]] allocates the []str view via the `alloc` builtin, whose malloc +// lowers to rt_malloc only when the rt binding is in the bundle (mirror +// lib/strings/strings.ww:30 — every alloc-using module imports rt). os is +// bundled by ~every program, so without this a plain `ww build` of any +// os-importing program links bare libc `malloc` (undefined). Task #17. +import rt; @symbol("rt_syscall") fn syscall0(num: nr) i64; @symbol("rt_syscall") fn syscall1(num: nr, a: i64) i64; @@ -500,6 +529,12 @@ export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = { // We don't expose `rtenvp` directly; [[getenv]] is the only consumer. @symbol("rt_envp") fn rtenvp() **u8; +// rt_argc / rt_argv — runtime-side getters for the argc/argv captured by +// rt/start.s at process entry (same DATAW-slot + TEXT-getter shape as +// rt_envp). [[args]] is the only consumer. +@symbol("rt_argc") fn rtargc() i64; +@symbol("rt_argv") fn rtargv() **u8; + // getenv — POSIX getenv. Returns a borrowed `str` view over the value // bytes of the named environment variable, or void if the name is not // present. The view is valid for the process lifetime — the bytes @@ -548,6 +583,46 @@ export fn getenv(name: str) (str | void) = { return; }; +// argsbuilt / argscache — build-once cache for [[args]]. drew ruling +// (task #17): an explicit `built` sentinel, NOT len==0 overloading (a +// real argv always has argv[0], but the sentinel keeps the contract +// honest and decoupled from content). args() is loop-callable; under +// ww's no-free model a per-call rebuild would leak the slice each call, +// so the slice is materialised once and reused. +let argsbuilt: bool = false; +let argscache: []str; + +// args — the process arguments as a borrowed []str. args[0] is the +// program name; args[1..] are the invocation arguments. Each str views +// the NUL-terminated argv bytes in place (valid for the process +// lifetime), so the slice must not be mutated or freed by the caller. +// +// DIVERGENCE (Hare-fidelity, task #26): Hare's `os::args` is a `[]str` +// GLOBAL populated by an @init that walks rt's argv +// (ref/hare/os/+linux/start.ha). ww has no @init mechanism, so the +// faithful global is not expressible; this is the fn-shaped equivalent +// (Hare NAME kept, shape diverged). Revisit if @init lands (#26). +export fn args() []str = { + if (argsbuilt) { return argscache; }; + let argc: i32 = rtargc(): i32; + let argv: **u8 = rtargv(); + let r: []str = alloc([], argc: u64)!; + let i: i32 = 0; + for (i < argc) { + let c: *u8 = argv[i]; + let n: i32 = 0; + for (c[n] != 0u8) { n += 1; }; + let s: str; + s.ptr = c; + s.len = n; + append(r, s); + i += 1; + }; + argscache = r; + argsbuilt = true; + return r; +}; + // ---- stat / lstat / fstat / exists ----------------------------------- // // Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 + @@ -781,29 +856,6 @@ export fn exists(path: str) bool = { return r >= 0i64; }; -// rt — runtime primitives exposed to ww programs. -// Mirrors Hare's rt:: module placement (ref/hare/rt/). - -package rt; - -// malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a -// `*void`; callers cast to the target type. Diverges from Hare: Hare -// exposes `alloc` / `free` as typed language builtins that the -// compiler lowers to rt::malloc/rt::free; ww has no such builtins, -// so the rt-symbol surface is exposed directly. Stdlib callers that -// need a typed allocation pattern wrap this with a cast plus a stored -// capacity (see [[strings.dup]], [[memio.dynamic]]). -// -// OOM: rt_malloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with no -// error path. The raw Linux mmap syscall returns a negative errno cast -// to `*void` on failure (e.g. `(void*)-12` for ENOMEM); the -// `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention that -// rt_malloc doesn't apply. Neither `== nil` nor `== (void*)-1` catches -// it; any deref of such a return faults. Today the stdlib does not -// check; OOM faults on first dereference. A typed fallible variant is -// a future task (task #39). ref/hare/rt/malloc.ha:27. -@symbol("rt_malloc") export fn malloc(n: u64) *void; - // types — integer limits. Mirrors Hare's types::limits (I8_MAX, …) // platform-fixed for amd64. Numeric helpers live in lib/math, matching // Hare's split between types::limits and math::. diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww index 638cfda5..0ae09dc9 100644 --- a/selfhost/cmd/ww/main.combined.ww +++ b/selfhost/cmd/ww/main.combined.ww @@ -95,6 +95,29 @@ export fn compare(a: instant, b: instant) i8 = { return 0i8; }; +// rt — runtime primitives exposed to ww programs. +// Mirrors Hare's rt:: module placement (ref/hare/rt/). + +package rt; + +// malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a +// `*void`; callers cast to the target type. Diverges from Hare: Hare +// exposes `alloc` / `free` as typed language builtins that the +// compiler lowers to rt::malloc/rt::free; ww has no such builtins, +// so the rt-symbol surface is exposed directly. Stdlib callers that +// need a typed allocation pattern wrap this with a cast plus a stored +// capacity (see [[strings.dup]], [[memio.dynamic]]). +// +// OOM: rt_malloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with no +// error path. The raw Linux mmap syscall returns a negative errno cast +// to `*void` on failure (e.g. `(void*)-12` for ENOMEM); the +// `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention that +// rt_malloc doesn't apply. Neither `== nil` nor `== (void*)-1` catches +// it; any deref of such a return faults. Today the stdlib does not +// check; OOM faults on first dereference. A typed fallible variant is +// a future task (task #39). ref/hare/rt/malloc.ha:27. +@symbol("rt_malloc") export fn malloc(n: u64) *void; + // os — process and filesystem facade. The body of each call lands // either in libwwrt.a (rt_syscall trampoline) or libc bindings, // depending on how the program was linked. @@ -102,6 +125,12 @@ export fn compare(a: instant, b: instant) i8 = { package os; import time; +// [[args]] allocates the []str view via the `alloc` builtin, whose malloc +// lowers to rt_malloc only when the rt binding is in the bundle (mirror +// lib/strings/strings.ww:30 — every alloc-using module imports rt). os is +// bundled by ~every program, so without this a plain `ww build` of any +// os-importing program links bare libc `malloc` (undefined). Task #17. +import rt; @symbol("rt_syscall") fn syscall0(num: nr) i64; @symbol("rt_syscall") fn syscall1(num: nr, a: i64) i64; @@ -500,6 +529,12 @@ export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = { // We don't expose `rtenvp` directly; [[getenv]] is the only consumer. @symbol("rt_envp") fn rtenvp() **u8; +// rt_argc / rt_argv — runtime-side getters for the argc/argv captured by +// rt/start.s at process entry (same DATAW-slot + TEXT-getter shape as +// rt_envp). [[args]] is the only consumer. +@symbol("rt_argc") fn rtargc() i64; +@symbol("rt_argv") fn rtargv() **u8; + // getenv — POSIX getenv. Returns a borrowed `str` view over the value // bytes of the named environment variable, or void if the name is not // present. The view is valid for the process lifetime — the bytes @@ -548,6 +583,46 @@ export fn getenv(name: str) (str | void) = { return; }; +// argsbuilt / argscache — build-once cache for [[args]]. drew ruling +// (task #17): an explicit `built` sentinel, NOT len==0 overloading (a +// real argv always has argv[0], but the sentinel keeps the contract +// honest and decoupled from content). args() is loop-callable; under +// ww's no-free model a per-call rebuild would leak the slice each call, +// so the slice is materialised once and reused. +let argsbuilt: bool = false; +let argscache: []str; + +// args — the process arguments as a borrowed []str. args[0] is the +// program name; args[1..] are the invocation arguments. Each str views +// the NUL-terminated argv bytes in place (valid for the process +// lifetime), so the slice must not be mutated or freed by the caller. +// +// DIVERGENCE (Hare-fidelity, task #26): Hare's `os::args` is a `[]str` +// GLOBAL populated by an @init that walks rt's argv +// (ref/hare/os/+linux/start.ha). ww has no @init mechanism, so the +// faithful global is not expressible; this is the fn-shaped equivalent +// (Hare NAME kept, shape diverged). Revisit if @init lands (#26). +export fn args() []str = { + if (argsbuilt) { return argscache; }; + let argc: i32 = rtargc(): i32; + let argv: **u8 = rtargv(); + let r: []str = alloc([], argc: u64)!; + let i: i32 = 0; + for (i < argc) { + let c: *u8 = argv[i]; + let n: i32 = 0; + for (c[n] != 0u8) { n += 1; }; + let s: str; + s.ptr = c; + s.len = n; + append(r, s); + i += 1; + }; + argscache = r; + argsbuilt = true; + return r; +}; + // ---- stat / lstat / fstat / exists ----------------------------------- // // Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 + @@ -781,29 +856,6 @@ export fn exists(path: str) bool = { return r >= 0i64; }; -// rt — runtime primitives exposed to ww programs. -// Mirrors Hare's rt:: module placement (ref/hare/rt/). - -package rt; - -// malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a -// `*void`; callers cast to the target type. Diverges from Hare: Hare -// exposes `alloc` / `free` as typed language builtins that the -// compiler lowers to rt::malloc/rt::free; ww has no such builtins, -// so the rt-symbol surface is exposed directly. Stdlib callers that -// need a typed allocation pattern wrap this with a cast plus a stored -// capacity (see [[strings.dup]], [[memio.dynamic]]). -// -// OOM: rt_malloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with no -// error path. The raw Linux mmap syscall returns a negative errno cast -// to `*void` on failure (e.g. `(void*)-12` for ENOMEM); the -// `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention that -// rt_malloc doesn't apply. Neither `== nil` nor `== (void*)-1` catches -// it; any deref of such a return faults. Today the stdlib does not -// check; OOM faults on first dereference. A typed fallible variant is -// a future task (task #39). ref/hare/rt/malloc.ha:27. -@symbol("rt_malloc") export fn malloc(n: u64) *void; - // types — integer limits. Mirrors Hare's types::limits (I8_MAX, …) // platform-fixed for amd64. Numeric helpers live in lib/math, matching // Hare's split between types::limits and math::. @@ -4280,7 +4332,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { // directory: open the dir, getdents64, build+run each *_test.ww, // report ok/FAIL per file, return 0 iff all pass. -fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8) i32 = { +fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8) i32 = { let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; tmp.len = os.PATH_MAX; // -o redirects the binary + its combined (objstem, T3) to ; the @@ -4299,10 +4351,19 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: * return 1; }; if (compileonly != 0) { return 0; }; - let execargv: []*u8 = alloc([], 2u64)!; - execargv.len = 2; + // #17 fnmatch filter: forward `pattern` as argv[1] so lib/test run() + // reads it via os.args. procrun execve's a NUL-terminated argv, so the + // terminator (not .len) bounds the vector. cstage twin: run_test_bin. + let execargv: []*u8 = alloc([], 3u64)!; execargv[0] = outp; - execargv[1] = nil; + if (pattern != nil) { + execargv.len = 3; + execargv[1] = pattern; + execargv[2] = nil; + } else { + execargv.len = 2; + execargv[1] = nil; + }; let rc: i32 = procrun(outp, execargv.ptr); if (outstem == nil) { os.remove(pathstr(outp)); }; return rc; @@ -4390,6 +4451,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { // transitive imports (e.g. 905_nkname asttest → tok); coupled to the // -T flip (task #5/#10). let target: *u8 = nil; + // #17: optional 2nd positional = fnmatch name-filter pattern, forwarded + // to the test binary as argv[1] (single-file/module only; dir-mode + // rejects). cstage twin: do_test `pattern`. + let patarg: *u8 = nil; let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!; incs.len = ((os.PATH_MAX: u64) * 2u64): i32; let incoff: u64 = 0u64; @@ -4440,7 +4505,8 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { return 2; }; }; }; } else { - if (target == nil) { target = p; }; + if (target == nil) { target = p; } + else { if (patarg == nil) { patarg = p; }; }; }; i += 1; }; @@ -4452,7 +4518,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { // single-file mode: literal *.ww that exists if (cstrendswithlit(target, ".ww")) { if (os.access(pathstr(target), 0i32) == 0) { - return runsingletest(selfdir, target, incs.ptr, compileonly, outstem); + return runsingletest(selfdir, target, incs.ptr, compileonly, outstem, patarg); }; }; @@ -4460,6 +4526,12 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cerr("ww test: -c/-o need a single test file\n"); return 2; }; + // #17: a name-filter pattern is per-binary; dir mode builds one binary + // per *_test.ww, so a single pattern can't route. cstage twin parity. + if (patarg != nil) { + cerr("ww test: pattern needs a single test file\n"); + return 2; + }; // otherwise treat target as a directory; enumerate *_test.ww return rundirtests(selfdir, target); }; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index ea793afd..982bb11e 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -1494,7 +1494,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { // directory: open the dir, getdents64, build+run each *_test.ww, // report ok/FAIL per file, return 0 iff all pass. -fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8) i32 = { +fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8) i32 = { let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; tmp.len = os.PATH_MAX; // -o redirects the binary + its combined (objstem, T3) to ; the @@ -1513,10 +1513,19 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: * return 1; }; if (compileonly != 0) { return 0; }; - let execargv: []*u8 = alloc([], 2u64)!; - execargv.len = 2; + // #17 fnmatch filter: forward `pattern` as argv[1] so lib/test run() + // reads it via os.args. procrun execve's a NUL-terminated argv, so the + // terminator (not .len) bounds the vector. cstage twin: run_test_bin. + let execargv: []*u8 = alloc([], 3u64)!; execargv[0] = outp; - execargv[1] = nil; + if (pattern != nil) { + execargv.len = 3; + execargv[1] = pattern; + execargv[2] = nil; + } else { + execargv.len = 2; + execargv[1] = nil; + }; let rc: i32 = procrun(outp, execargv.ptr); if (outstem == nil) { os.remove(pathstr(outp)); }; return rc; @@ -1604,6 +1613,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { // transitive imports (e.g. 905_nkname asttest → tok); coupled to the // -T flip (task #5/#10). let target: *u8 = nil; + // #17: optional 2nd positional = fnmatch name-filter pattern, forwarded + // to the test binary as argv[1] (single-file/module only; dir-mode + // rejects). cstage twin: do_test `pattern`. + let patarg: *u8 = nil; let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!; incs.len = ((os.PATH_MAX: u64) * 2u64): i32; let incoff: u64 = 0u64; @@ -1654,7 +1667,8 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { return 2; }; }; }; } else { - if (target == nil) { target = p; }; + if (target == nil) { target = p; } + else { if (patarg == nil) { patarg = p; }; }; }; i += 1; }; @@ -1666,7 +1680,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { // single-file mode: literal *.ww that exists if (cstrendswithlit(target, ".ww")) { if (os.access(pathstr(target), 0i32) == 0) { - return runsingletest(selfdir, target, incs.ptr, compileonly, outstem); + return runsingletest(selfdir, target, incs.ptr, compileonly, outstem, patarg); }; }; @@ -1674,6 +1688,12 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cerr("ww test: -c/-o need a single test file\n"); return 2; }; + // #17: a name-filter pattern is per-binary; dir mode builds one binary + // per *_test.ww, so a single pattern can't route. cstage twin parity. + if (patarg != nil) { + cerr("ww test: pattern needs a single test file\n"); + return 2; + }; // otherwise treat target as a directory; enumerate *_test.ww return rundirtests(selfdir, target); }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index b5a5b2d6..06509582 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -95,6 +95,29 @@ export fn compare(a: instant, b: instant) i8 = { return 0i8; }; +// rt — runtime primitives exposed to ww programs. +// Mirrors Hare's rt:: module placement (ref/hare/rt/). + +package rt; + +// malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a +// `*void`; callers cast to the target type. Diverges from Hare: Hare +// exposes `alloc` / `free` as typed language builtins that the +// compiler lowers to rt::malloc/rt::free; ww has no such builtins, +// so the rt-symbol surface is exposed directly. Stdlib callers that +// need a typed allocation pattern wrap this with a cast plus a stored +// capacity (see [[strings.dup]], [[memio.dynamic]]). +// +// OOM: rt_malloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with no +// error path. The raw Linux mmap syscall returns a negative errno cast +// to `*void` on failure (e.g. `(void*)-12` for ENOMEM); the +// `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention that +// rt_malloc doesn't apply. Neither `== nil` nor `== (void*)-1` catches +// it; any deref of such a return faults. Today the stdlib does not +// check; OOM faults on first dereference. A typed fallible variant is +// a future task (task #39). ref/hare/rt/malloc.ha:27. +@symbol("rt_malloc") export fn malloc(n: u64) *void; + // os — process and filesystem facade. The body of each call lands // either in libwwrt.a (rt_syscall trampoline) or libc bindings, // depending on how the program was linked. @@ -102,6 +125,12 @@ export fn compare(a: instant, b: instant) i8 = { package os; import time; +// [[args]] allocates the []str view via the `alloc` builtin, whose malloc +// lowers to rt_malloc only when the rt binding is in the bundle (mirror +// lib/strings/strings.ww:30 — every alloc-using module imports rt). os is +// bundled by ~every program, so without this a plain `ww build` of any +// os-importing program links bare libc `malloc` (undefined). Task #17. +import rt; @symbol("rt_syscall") fn syscall0(num: nr) i64; @symbol("rt_syscall") fn syscall1(num: nr, a: i64) i64; @@ -500,6 +529,12 @@ export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = { // We don't expose `rtenvp` directly; [[getenv]] is the only consumer. @symbol("rt_envp") fn rtenvp() **u8; +// rt_argc / rt_argv — runtime-side getters for the argc/argv captured by +// rt/start.s at process entry (same DATAW-slot + TEXT-getter shape as +// rt_envp). [[args]] is the only consumer. +@symbol("rt_argc") fn rtargc() i64; +@symbol("rt_argv") fn rtargv() **u8; + // getenv — POSIX getenv. Returns a borrowed `str` view over the value // bytes of the named environment variable, or void if the name is not // present. The view is valid for the process lifetime — the bytes @@ -548,6 +583,46 @@ export fn getenv(name: str) (str | void) = { return; }; +// argsbuilt / argscache — build-once cache for [[args]]. drew ruling +// (task #17): an explicit `built` sentinel, NOT len==0 overloading (a +// real argv always has argv[0], but the sentinel keeps the contract +// honest and decoupled from content). args() is loop-callable; under +// ww's no-free model a per-call rebuild would leak the slice each call, +// so the slice is materialised once and reused. +let argsbuilt: bool = false; +let argscache: []str; + +// args — the process arguments as a borrowed []str. args[0] is the +// program name; args[1..] are the invocation arguments. Each str views +// the NUL-terminated argv bytes in place (valid for the process +// lifetime), so the slice must not be mutated or freed by the caller. +// +// DIVERGENCE (Hare-fidelity, task #26): Hare's `os::args` is a `[]str` +// GLOBAL populated by an @init that walks rt's argv +// (ref/hare/os/+linux/start.ha). ww has no @init mechanism, so the +// faithful global is not expressible; this is the fn-shaped equivalent +// (Hare NAME kept, shape diverged). Revisit if @init lands (#26). +export fn args() []str = { + if (argsbuilt) { return argscache; }; + let argc: i32 = rtargc(): i32; + let argv: **u8 = rtargv(); + let r: []str = alloc([], argc: u64)!; + let i: i32 = 0; + for (i < argc) { + let c: *u8 = argv[i]; + let n: i32 = 0; + for (c[n] != 0u8) { n += 1; }; + let s: str; + s.ptr = c; + s.len = n; + append(r, s); + i += 1; + }; + argscache = r; + argsbuilt = true; + return r; +}; + // ---- stat / lstat / fstat / exists ----------------------------------- // // Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 + @@ -3329,29 +3404,6 @@ export fn position(d: *decoder) i32 = { }; -// rt — runtime primitives exposed to ww programs. -// Mirrors Hare's rt:: module placement (ref/hare/rt/). - -package rt; - -// malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a -// `*void`; callers cast to the target type. Diverges from Hare: Hare -// exposes `alloc` / `free` as typed language builtins that the -// compiler lowers to rt::malloc/rt::free; ww has no such builtins, -// so the rt-symbol surface is exposed directly. Stdlib callers that -// need a typed allocation pattern wrap this with a cast plus a stored -// capacity (see [[strings.dup]], [[memio.dynamic]]). -// -// OOM: rt_malloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with no -// error path. The raw Linux mmap syscall returns a negative errno cast -// to `*void` on failure (e.g. `(void*)-12` for ENOMEM); the -// `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention that -// rt_malloc doesn't apply. Neither `== nil` nor `== (void*)-1` catches -// it; any deref of such a return faults. Today the stdlib does not -// check; OOM faults on first dereference. A typed fallible variant is -// a future task (task #39). ref/hare/rt/malloc.ha:27. -@symbol("rt_malloc") export fn malloc(n: u64) *void; - // strings — operations over str ({ptr,len}). Hare port; see // ref/hare/strings/. // diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 65cca48f..75ae68c8 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -95,6 +95,29 @@ export fn compare(a: instant, b: instant) i8 = { return 0i8; }; +// rt — runtime primitives exposed to ww programs. +// Mirrors Hare's rt:: module placement (ref/hare/rt/). + +package rt; + +// malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a +// `*void`; callers cast to the target type. Diverges from Hare: Hare +// exposes `alloc` / `free` as typed language builtins that the +// compiler lowers to rt::malloc/rt::free; ww has no such builtins, +// so the rt-symbol surface is exposed directly. Stdlib callers that +// need a typed allocation pattern wrap this with a cast plus a stored +// capacity (see [[strings.dup]], [[memio.dynamic]]). +// +// OOM: rt_malloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with no +// error path. The raw Linux mmap syscall returns a negative errno cast +// to `*void` on failure (e.g. `(void*)-12` for ENOMEM); the +// `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention that +// rt_malloc doesn't apply. Neither `== nil` nor `== (void*)-1` catches +// it; any deref of such a return faults. Today the stdlib does not +// check; OOM faults on first dereference. A typed fallible variant is +// a future task (task #39). ref/hare/rt/malloc.ha:27. +@symbol("rt_malloc") export fn malloc(n: u64) *void; + // os — process and filesystem facade. The body of each call lands // either in libwwrt.a (rt_syscall trampoline) or libc bindings, // depending on how the program was linked. @@ -102,6 +125,12 @@ export fn compare(a: instant, b: instant) i8 = { package os; import time; +// [[args]] allocates the []str view via the `alloc` builtin, whose malloc +// lowers to rt_malloc only when the rt binding is in the bundle (mirror +// lib/strings/strings.ww:30 — every alloc-using module imports rt). os is +// bundled by ~every program, so without this a plain `ww build` of any +// os-importing program links bare libc `malloc` (undefined). Task #17. +import rt; @symbol("rt_syscall") fn syscall0(num: nr) i64; @symbol("rt_syscall") fn syscall1(num: nr, a: i64) i64; @@ -500,6 +529,12 @@ export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = { // We don't expose `rtenvp` directly; [[getenv]] is the only consumer. @symbol("rt_envp") fn rtenvp() **u8; +// rt_argc / rt_argv — runtime-side getters for the argc/argv captured by +// rt/start.s at process entry (same DATAW-slot + TEXT-getter shape as +// rt_envp). [[args]] is the only consumer. +@symbol("rt_argc") fn rtargc() i64; +@symbol("rt_argv") fn rtargv() **u8; + // getenv — POSIX getenv. Returns a borrowed `str` view over the value // bytes of the named environment variable, or void if the name is not // present. The view is valid for the process lifetime — the bytes @@ -548,6 +583,46 @@ export fn getenv(name: str) (str | void) = { return; }; +// argsbuilt / argscache — build-once cache for [[args]]. drew ruling +// (task #17): an explicit `built` sentinel, NOT len==0 overloading (a +// real argv always has argv[0], but the sentinel keeps the contract +// honest and decoupled from content). args() is loop-callable; under +// ww's no-free model a per-call rebuild would leak the slice each call, +// so the slice is materialised once and reused. +let argsbuilt: bool = false; +let argscache: []str; + +// args — the process arguments as a borrowed []str. args[0] is the +// program name; args[1..] are the invocation arguments. Each str views +// the NUL-terminated argv bytes in place (valid for the process +// lifetime), so the slice must not be mutated or freed by the caller. +// +// DIVERGENCE (Hare-fidelity, task #26): Hare's `os::args` is a `[]str` +// GLOBAL populated by an @init that walks rt's argv +// (ref/hare/os/+linux/start.ha). ww has no @init mechanism, so the +// faithful global is not expressible; this is the fn-shaped equivalent +// (Hare NAME kept, shape diverged). Revisit if @init lands (#26). +export fn args() []str = { + if (argsbuilt) { return argscache; }; + let argc: i32 = rtargc(): i32; + let argv: **u8 = rtargv(); + let r: []str = alloc([], argc: u64)!; + let i: i32 = 0; + for (i < argc) { + let c: *u8 = argv[i]; + let n: i32 = 0; + for (c[n] != 0u8) { n += 1; }; + let s: str; + s.ptr = c; + s.len = n; + append(r, s); + i += 1; + }; + argscache = r; + argsbuilt = true; + return r; +}; + // ---- stat / lstat / fstat / exists ----------------------------------- // // Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 + @@ -3329,29 +3404,6 @@ export fn position(d: *decoder) i32 = { }; -// rt — runtime primitives exposed to ww programs. -// Mirrors Hare's rt:: module placement (ref/hare/rt/). - -package rt; - -// malloc — mmap-backed page allocator. Untyped: `malloc(n)` returns a -// `*void`; callers cast to the target type. Diverges from Hare: Hare -// exposes `alloc` / `free` as typed language builtins that the -// compiler lowers to rt::malloc/rt::free; ww has no such builtins, -// so the rt-symbol surface is exposed directly. Stdlib callers that -// need a typed allocation pattern wrap this with a cast plus a stored -// capacity (see [[strings.dup]], [[memio.dynamic]]). -// -// OOM: rt_malloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with no -// error path. The raw Linux mmap syscall returns a negative errno cast -// to `*void` on failure (e.g. `(void*)-12` for ENOMEM); the -// `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention that -// rt_malloc doesn't apply. Neither `== nil` nor `== (void*)-1` catches -// it; any deref of such a return faults. Today the stdlib does not -// check; OOM faults on first dereference. A typed fallible variant is -// a future task (task #39). ref/hare/rt/malloc.ha:27. -@symbol("rt_malloc") export fn malloc(n: u64) *void; - // strings — operations over str ({ptr,len}). Hare port; see // ref/hare/strings/. // diff --git a/test/wcc/949_driver_flagargs.c b/test/wcc/949_driver_flagargs.c index 8ee773af..5d9495cd 100644 --- a/test/wcc/949_driver_flagargs.c +++ b/test/wcc/949_driver_flagargs.c @@ -9,7 +9,9 @@ * rejected with "ww test: unknown flag" (rc 2); a lone -I/-o * is "ww test: -X needs an argument" (rc 2); -c/-o without a * single test file is "ww test: -c/-o need a single test file" - * (rc 2, #17). + * (rc 2, #17); a 2nd positional (fnmatch name-filter pattern) + * in directory mode is "ww test: pattern needs a single test + * file" (rc 2, #17). * Each row asserts the expected rc + stderr substring AND that the two * drivers are byte-identical (rule 10): the cstage parse_build_flags / * do_test must match the wwstage main.ww dobuild/dorun/dotest verbatim. @@ -46,6 +48,10 @@ static const struct row rows[] = { { "test -o", 2, "ww test: -o needs an argument" }, { "test -o x", 2, "ww test: -c/-o need a single test file" }, { "test -c", 2, "ww test: -c/-o need a single test file" }, + /* #17: a 2nd positional is a fnmatch name-filter pattern, valid only + * for a single test file/module; in directory mode (target ".") it + * has no single binary to route to and is rejected (rc 2). */ + { "test . zzz", 2, "ww test: pattern needs a single test file" }, }; /* Run "/ " capturing rc + stderr text into err (NUL- diff --git a/test/wcc/989_test_filter.c b/test/wcc/989_test_filter.c new file mode 100644 index 00000000..76b9908f --- /dev/null +++ b/test/wcc/989_test_filter.c @@ -0,0 +1,142 @@ +/* + * 989_test_filter — the `ww test [pattern]` fnmatch NAME-FILTER + * (task #17 commit-3, drew spec §d). Drives the full driver→binary→ + * lib/test path on BOTH driver twins (cstage `ww`, wwstage `ww_ww`): + * + * - no pattern → every @test runs (byte-for-byte the pre-filter + * behavior: "N passed, M failed"). + * - "beta" → only the matching @test runs; the others are + * absent from the per-test output. + * - "ga*" → glob selects gamma only. + * - "zzz" → zero matches → "No tests run", rc 0 (Hare ground + * truth, ref/hare/test/+test.ha:114-117). + * + * Each row asserts rc + required/forbidden stdout substrings AND that the + * two driver twins emit byte-identical stdout (rule 10). The fixture is + * test/wcc/data/filter_fixture.ww (three trivially-passing @test fns + * alpha/beta/gamma). The filter lives in lib/test run() reading os.args; + * the -T synth + value table are unchanged, so 990-997 byte-id holds. + * + * Driver-arg ERROR forms (lone/dir-mode pattern) live in + * 949_driver_flagargs. Light driver test, intermediates go to the + * driver's own /tmp temp (rule 14), any NNN. + */ +#include +#include +#include +#include +#include + +struct row { + const char *pat; /* pattern arg, or NULL for none */ + int rc; /* expected exit code */ + const char *must[4]; /* substrings required in stdout */ + const char *mustnot[4]; /* substrings forbidden in stdout */ +}; + +static const struct row rows[] = { + /* no pattern → all three, full count */ + { NULL, 0, { "alpha ... ok", "beta ... ok", "gamma ... ok", + "3 passed, 0 failed" }, { NULL } }, + /* exact name → only beta */ + { "beta", 0, { "beta ... ok", "1 passed, 0 failed", NULL }, + { "alpha", "gamma", NULL } }, + /* glob → only gamma */ + { "ga*", 0, { "gamma ... ok", "1 passed, 0 failed", NULL }, + { "alpha", "beta", NULL } }, + /* no match → "No tests run", success */ + { "zzz", 0, { "No tests run", NULL }, + { "alpha", "beta", "gamma", "passed", NULL } }, + /* star matches everything */ + { "*", 0, { "alpha ... ok", "beta ... ok", "gamma ... ok", + "3 passed, 0 failed" }, { NULL } }, +}; + +/* Run "/ test [pat]" capturing rc + stdout into out + * (NUL-terminated). Returns the child's exit code (or -1 on spawn fail). */ +static int +run_drv(const char *bin, const char *drv, const char *pat, + char *out, size_t outsz) +{ + int pid = getpid(); + char outf[64], cmd[2048]; + snprintf(outf, sizeof outf, "/tmp/tf989_%d.out", pid); + if (pat) { + snprintf(cmd, sizeof cmd, + "%s/%s test test/wcc/data/filter_fixture.ww '%s' >%s 2>/dev/null", + bin, drv, pat, outf); + } else { + snprintf(cmd, sizeof cmd, + "%s/%s test test/wcc/data/filter_fixture.ww >%s 2>/dev/null", + bin, drv, outf); + } + int rc = system(cmd); + if (rc == -1) { unlink(outf); return -1; } + rc = WIFEXITED(rc) ? WEXITSTATUS(rc) : 1; + out[0] = '\0'; + FILE *f = fopen(outf, "r"); + if (f) { + size_t got = fread(out, 1, outsz - 1, f); + out[got] = '\0'; + fclose(f); + } + unlink(outf); + 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; + } + + int fail = 0; + size_t n = sizeof rows / sizeof rows[0]; + for (size_t i = 0; i < n; i++) { + const char *pat = rows[i].pat ? rows[i].pat : "(none)"; + char cout[8192], wout[8192]; + int crc = run_drv(bin, "ww", rows[i].pat, cout, sizeof cout); + int wrc = run_drv(bin, "ww_ww", rows[i].pat, wout, sizeof wout); + + if (crc != rows[i].rc) { + fprintf(stderr, "989 FAIL: ww pat=%s rc=%d want=%d\n", + pat, crc, rows[i].rc); + fail = 1; + } + for (int k = 0; k < 4 && rows[i].must[k]; k++) + if (!strstr(cout, rows[i].must[k])) { + fprintf(stderr, "989 FAIL: ww pat=%s stdout missing " + "'%s' (got '%s')\n", pat, rows[i].must[k], cout); + fail = 1; + } + for (int k = 0; k < 4 && rows[i].mustnot[k]; k++) + if (strstr(cout, rows[i].mustnot[k])) { + fprintf(stderr, "989 FAIL: ww pat=%s stdout has " + "forbidden '%s' (got '%s')\n", + pat, rows[i].mustnot[k], cout); + fail = 1; + } + /* twin parity: ww_ww must match ww exactly (rc + stdout). */ + if (wrc != crc) { + fprintf(stderr, "989 FAIL: pat=%s ww_ww rc=%d != ww rc=%d\n", + pat, wrc, crc); + fail = 1; + } + if (strcmp(wout, cout) != 0) { + fprintf(stderr, "989 FAIL: pat=%s ww_ww stdout '%s' != ww '%s'\n", + pat, wout, cout); + fail = 1; + } + } + + if (fail) return 1; + printf("test name-filter: %zu rows, ww==ww_ww pinned\n", n); + return 0; +} diff --git a/test/wcc/data/filter_fixture.ww b/test/wcc/data/filter_fixture.ww new file mode 100644 index 00000000..13f21c4c --- /dev/null +++ b/test/wcc/data/filter_fixture.ww @@ -0,0 +1,13 @@ +// @test fixture for the fnmatch name-filter (task #17 commit-3). Three +// trivially-passing @test fns with distinct names so a glob selects a +// known subset; 989_test_filter drives `ww test [pattern]` and +// asserts which " ... ok" lines appear. Bodies are empty (clean +// return = pass); the test observes SELECTION, not assertion outcomes. + +package filterfix; + +@test fn alpha() void = { }; + +@test fn beta() void = { }; + +@test fn gamma() void = { };