ww test: @test name-filter via fnmatch (both stages)

`ww test <file> <pattern>` 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)
This commit is contained in:
2026-06-11 04:51:17 +09:00
parent 64f0ddf01b
commit b9c4562135
16 changed files with 853 additions and 165 deletions

View File

@@ -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 <file> [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 $@ $<

View File

@@ -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;

View File

@@ -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

View File

@@ -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 +

View File

@@ -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
// <file> [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

View File

@@ -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"

View File

@@ -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::.

View File

@@ -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::.

View File

@@ -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::.

View File

@@ -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 <stem>; 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);
};

View File

@@ -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 <stem>; 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);
};

View File

@@ -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/.
//

View File

@@ -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/.
//

View File

@@ -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 "<bin>/<drv> <args>" capturing rc + stderr text into err (NUL-

142
test/wcc/989_test_filter.c Normal file
View File

@@ -0,0 +1,142 @@
/*
* 989_test_filter — the `ww test <file> [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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
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 "<bin>/<drv> test <fixture> [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;
}

View File

@@ -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 <this> [pattern]` and
// asserts which "<name> ... 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 = { };