diff --git a/Makefile b/Makefile index 7ba83eb6..add8e2d3 100644 --- a/Makefile +++ b/Makefile @@ -447,6 +447,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_intdiv_signed \ $(BIN)/test_strings_run \ $(BIN)/test_hex_run $(BIN)/test_utf8_run $(BIN)/test_bytes_run \ + $(BIN)/test_path_run \ $(BIN)/test_decimal_run $(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 \ @@ -1992,6 +1993,10 @@ $(BIN)/test_bytes_run: test/wcc/967_bytes_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_path_run: test/wcc/989_path_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_ascii_run: test/wcc/904_ascii_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/lib/path/path.ww b/lib/path/path.ww index 5422e38b..b1f1c2fe 100644 --- a/lib/path/path.ww +++ b/lib/path/path.ww @@ -1,134 +1,139 @@ // path — filesystem path manipulation. UTF-8 paths, '/' separator. -// Mirrors Hare's path:: surface. ww doesn't yet ship the stack-buffer -// path::buffer; the functions here all take a `str` and return a -// borrowed view (basename / dirname) or a fresh owned str (join). +// Mirrors Hare's path:: surface (ref/hare/path/). This is the +// path::buffer stack-buffer port (c2-stack subset: error/posix defs + +// buffer + isroot + appendlit + appendnorm + push + string + set). +// init/abs/dirname/basename and the rest land in later folds. package path; +import bytes; import strings; +import os; -def SEP: u8 = '/'; +// ref/hare/path/error.ha:6-12. The 3 !void singletons are +// structurally identical; cstage discriminates them correctly +// (#142 is a wwstage-only flatvariantidxt ambiguity, rides #125). +export type cant_extend = !void; +export type too_long = !void; +export type not_prefix = !void; +export type error = !(cant_extend | too_long | not_prefix); -// abs — `p` is an absolute path (starts with '/'). -export fn abs(p: str) bool = { - if (p.len == 0) { return false; }; - return p[0] == SEP; +// ref/hare/path/error.ha:15-24. +export fn strerror(e: error) str = { + match (e) { + case cant_extend => return "Can't add extension (filename is root or all dots)"; + case too_long => return "Path buffer overflow"; + case not_prefix => return "Prefix not present"; + }; }; -// dirname — directory component of `p`, POSIX-style. Returns a -// borrowed view of `p` (or the static "." / "/" strings). Mirrors -// Hare's path::dirname. -export fn dirname(p: str) str = { - if (p.len == 0) { return "."; }; - // Strip trailing separators. - let n: i32 = p.len; - for (n > 0) { - if (p[n - 1] != SEP) { break; }; - n -= 1; - }; - if (n == 0) { return "/"; }; - // Last separator in the trimmed prefix. - let i: i32 = n - 1; - for (i >= 0) { - if (p[i] == SEP) { break; }; - i -= 1; - }; - if (i < 0) { return "."; }; - // Strip trailing separators on the directory part too. - for (i > 0) { - if (p[i - 1] != SEP) { break; }; - i -= 1; - }; - if (i == 0) { return "/"; }; - let r: str; - r.ptr = p.ptr; - r.len = i; - return r; +// ref/hare/path/+linux.ha:7. +export def SEP: u8 = '/'; +// ref/hare/path/+linux.ha:9. No c2 use; c3 abs/isroot-str consume it. +const sepstr: str = "/"; +// ref/hare/path/+linux.ha:18 (sys::PATH_MAX-1). Div: route through +// os.PATH_MAX (lib/os/os.ww:84, i32=4096) → MAX=4095; const-folds as +// the buffer array dim (c1 #141). No hardcoded literal (rule-13). +export def MAX: i32 = os.PATH_MAX - 1; + +// ref/hare/path/buffer.ha:6-9. Div: end size→i32 (lib/CLAUDE.md +// str.len:i32; bytes.index→(i32|void)). +export type buffer = struct { + buf: [MAX]u8, + end: i32, }; -// basename — final path component of `p`, POSIX-style. Returns a -// borrowed view of `p` (or "." / "/" sentinel strings). Mirrors -// Hare's path::basename. -export fn basename(p: str) str = { - if (p.len == 0) { return "."; }; - let n: i32 = p.len; - for (n > 0) { - if (p[n - 1] != SEP) { break; }; - n -= 1; - }; - if (n == 0) { return "/"; }; - let i: i32 = n - 1; - for (i >= 0) { - if (p[i] == SEP) { break; }; - i -= 1; - }; - let r: str; - r.ptr = p.ptr + ((i + 1): u64); - r.len = n - (i + 1); - return r; +// ref/hare/path/stack.ha:30-31. Faithful module-global []u8 (D2 #148 +// fixes the by-value-arg garbage header) — no [N]u8, no copy-loop. +const dot: []u8 = ['.']; +const dotdot: []u8 = ['.', '.']; + +// ref/hare/path/buffer.ha:44-51. c3 WIDENS to isroot(path:(*buffer|str)) +// alongside abs; c2 needs only the *buffer arm (appendnorm/appendlit +// callers). +export fn isroot(buf: *buffer) bool = { + return buf.end == 1 && buf.buf[0] == SEP; }; -// extension — the final ".ext" suffix of `basename(p)`, including -// the dot, or "" if none. Returns a borrowed view of `p`. Mirrors -// Hare's path::extension. -export fn extension(p: str) str = { - let b: str = basename(p); - let i: i32 = b.len - 1; - for (i > 0) { - if (b[i] == '.') { - let r: str; - r.ptr = b.ptr + (i: u64); - r.len = b.len - i; - return r; +// ref/hare/path/stack.ha:63-74. +fn appendlit(buf: *buffer, bs: []u8) (i32 | error) = { + let newend: i32 = buf.end; + if (buf.end == 0 || isroot(buf)) { + if (MAX < buf.end + bs.len) { + // cstage rejects bare `return too_long` into the nested + // (i32|error); two-step widen (io precedent stream.ww:56-58). + let e: error = too_long; return e; }; - i -= 1; + } else { + if (MAX < buf.end + bs.len + 1) { + let e: error = too_long; return e; + }; + buf.buf[buf.end] = SEP; + newend += 1; }; - let empty: str; - empty.ptr = nil; - empty.len = 0; - return empty; + // Div: Hare `buf.buf[newend..newend+len(bs)] = bs` in colon + // spelling — the #145 slice-copy-assign LHS arm (stack.ha:72). + buf.buf[newend:newend+bs.len] = bs; + return newend + bs.len; }; -// join — concatenate two path components with a single '/' separator. -// Returns owned str (release via os.free). If `b` is absolute, the -// result is `b`. Mirrors Hare's path::buffer init+push, narrowed to -// two-arg join (no variadic). -export fn join(a: str, b: str) str = { - if (abs(b)) { - let buf: []u8 = alloc([], b.len: u64)!; - let i: i32 = 0; - for (i < b.len) { buf[i] = b[i]; i += 1; }; - buf.len = b.len; - return strings.frombytes(buf); +// ref/hare/path/stack.ha:43-59. Div: `..` reslice→colon; Hare if-expr +// → if-statement; `0z` yield → `0i32` (match arms must agree, bare 0 +// is untyped_int). +fn appendnorm(buf: *buffer, seg: []u8) (i32 | error) = { + if (seg.len == 0 || bytes.equal(dot, seg)) { return buf.end; }; + if (bytes.equal(dotdot, seg)) { + if (isroot(buf)) { return buf.end; }; + let isep: i32 = match (bytes.rindex(buf.buf[0:buf.end], SEP)) { + case void => yield 0i32; + case let i: i32 => yield i + 1; + }; + if (buf.end == 0 || bytes.equal(buf.buf[isep:buf.end], dotdot)) { + return appendlit(buf, dotdot)?; + } else { + if (isep <= 1) { return isep; }; + return isep - 1; + }; + } else { + return appendlit(buf, seg)?; }; - if (a.len == 0) { - let buf: []u8 = alloc([], b.len: u64)!; - let i: i32 = 0; - for (i < b.len) { buf[i] = b[i]; i += 1; }; - buf.len = b.len; - return strings.frombytes(buf); - }; - if (b.len == 0) { - let buf: []u8 = alloc([], a.len: u64)!; - let i: i32 = 0; - for (i < a.len) { buf[i] = a[i]; i += 1; }; - buf.len = a.len; - return strings.frombytes(buf); - }; - // Trim trailing '/' from a; b never starts with '/' here (checked - // above via abs(b)). - let an: i32 = a.len; - for (an > 0) { - if (a[an - 1] != SEP) { break; }; - an -= 1; - }; - let total: i32 = an + 1 + b.len; - let buf: []u8 = alloc([], total: u64)!; - let i: i32 = 0; - for (i < an) { buf[i] = a[i]; i += 1; }; - buf[an] = SEP; - let j: i32 = 0; - for (j < b.len) { buf[an + 1 + j] = b[j]; j += 1; }; - buf.len = total; - return strings.frombytes(buf); +}; + +// ref/hare/path/buffer.ha:28-31. Div: fromutf8_unsafe→strings.frombytes +// (rule-9 carve-out); `..` reslice→colon. +export fn string(buf: *buffer) str = { + if (buf.end == 0) { return "."; }; + return strings.frombytes(buf.buf[0:buf.end]); +}; + +// ref/hare/path/stack.ha:9-28. Div: `..` reslice→colon; len(x)→x.len. +export fn push(buf: *buffer, items: str...) (str | error) = { + for (let item .. items) { + let elem: []u8 = strings.toutf8(item); + // Hare's `for(true) match{...}` (stack.ha:13-25) breaks out of + // the void arm and re-loops the j-arm; ww spells the loop step + // as explicit `continue` (j-arm) + `break` (after match). + for (true) { + match (bytes.index(elem, SEP)) { + case void => { buf.end = appendnorm(buf, elem)?; }; + case let j: i32 => { + if (j == 0 && buf.end == 0) { + buf.buf[0] = SEP; buf.end = 1; + } else { + buf.end = appendnorm(buf, elem[0:j])?; + }; + elem = elem[j+1:]; + continue; + }; + }; + break; + }; + }; + return string(buf); +}; + +// ref/hare/path/buffer.ha:20-23. (str|error) ≤32B return → in c2-stack. +export fn set(buf: *buffer, items: str...) (str | error) = { + buf.end = 0; + return push(buf, items...); }; diff --git a/lib/path/pathtest.ww b/lib/path/pathtest.ww new file mode 100644 index 00000000..91d338c1 --- /dev/null +++ b/lib/path/pathtest.ww @@ -0,0 +1,123 @@ +// pathtest — exercises lib/path (c2-stack subset). Run with +// `out/bin/ww run lib/path/pathtest.ww`. Same signalled-then-fail() +// -with-+10 pattern as bytes / getopt tests: a non-zero exit code +// pinpoints the failing scenario. +// +// Vectors mirror Hare's @test fns in ref/hare/path/stack.ha:77-119 +// and buffer.ha. The absolute rows (Hare's `local("/")`-seeded) DEFER +// to c3 (abs/local unimplemented); only the relative rows run here. +// +// Parallel `[N]str` row arrays (rather than `[N]struct{...}`) sidestep +// the cstage cgen chained `arr[i].field` store gap (getopttest.ww:6). +// push is stateful, so each row is applied in sequence to one buffer; +// the table holds {segment, expected-string} pairs (stack.ha:36-42 is +// the normalization spec these rows encode). + +package path; + +import path; +import os; + +let signalled: i32 = 0; +fn fail() void = { os.exit(signalled + 10); }; + +// ---- push() + appendnorm normalization -------------------------------- +// ref/hare/path/stack.ha:77-119 (relative rows only; absolute local() +// rows defer to c3). The dot/dotdot handling exercised here IS the +// appendnorm normalization spec (stack.ha:36-42), so no separate +// appendnorm @test is needed. + +@test fn push_cases() void = { + let buf = buffer { ... }; + if (string(&buf) != ".") { fail(); }; + + // current-dir + parent-dir invariants (stack.ha:82-88). Single + // segment per row → table-driven sequential apply. + let segs: [5]str; + let wants: [5]str; + segs[0]=""; wants[0]="."; + segs[1]="."; wants[1]="."; + segs[2]=".."; wants[2]=".."; + segs[3]=""; wants[3]=".."; + segs[4]="."; wants[4]=".."; + let i: i32 = 0; + for (i < 5) { + if (push(&buf, segs[i])! != wants[i]) { fail(); }; + i += 1; + }; + + // set(&buf) resets to "." (stack.ha:91). set forwards an EMPTY + // variadic into push (KEN-oracle flag 1: zero-arg gather+forward). + if (set(&buf)! != ".") { fail(); }; + + // regular path + parent (stack.ha:101-104, minus the local() row). + let segs2: [3]str; + let wants2: [3]str; + segs2[0]="foo"; wants2[0]="foo"; + segs2[1]="."; wants2[1]="foo"; + segs2[2]=".."; wants2[2]="."; + let k: i32 = 0; + for (k < 3) { + if (push(&buf, segs2[k])! != wants2[k]) { fail(); }; + k += 1; + }; + + // multi-segment (stack.ha:107-111). Variadic arity varies (1 or 2) + // → inline. + if (push(&buf, "a", "b")! != "a/b") { fail(); }; + if (push(&buf, "..", "c")! != "a/c") { fail(); }; + if (push(&buf, "..")! != "a") { fail(); }; + // stack.ha:110; local()→literal, SEP='/' on Linux, proper local() rides c3 + if (push(&buf, "/d")! != "a/d") { fail(); }; + if (push(&buf, "..", "..")! != ".") { fail(); }; // stack.ha:111 + + // leading-SEP segment into an EMPTY buffer exercises push's + // `j==0 && buf.end==0` root-seed arm (stack.ha:18-20), unreached + // by the relative rows above. Hare covers it via local("/")-seeded + // rows that defer to c3; literal "/foo" stands in (SEP='/' on Linux). + if (set(&buf)! != ".") { fail(); }; + if (push(&buf, "/foo")! != "/foo") { fail(); }; +}; + +// ---- isroot() --------------------------------------------------------- +// ref/hare/path/buffer.ha:44-51 (*buffer arm). Heterogeneous seeds +// (manual root seed vs push) → inline rather than a uniform table. + +@test fn isroot_cases() void = { + let buf = buffer { ... }; + // empty buffer (end 0) + if (isroot(&buf)) { fail(); }; + + // "/" — root. local() is c3, so seed the SEP byte directly. + buf.buf[0] = SEP; buf.end = 1; + if (!isroot(&buf)) { fail(); }; + if (string(&buf) != "/") { fail(); }; + + // "foo" — relative, not root. + buf.end = 0; + if (push(&buf, "foo")! != "foo") { fail(); }; + if (isroot(&buf)) { fail(); }; + + // "/foo" — absolute but not root. + buf.buf[0] = SEP; buf.end = 1; + if (push(&buf, "foo")! != "/foo") { fail(); }; + if (isroot(&buf)) { fail(); }; +}; + +// ---- string() --------------------------------------------------------- +// ref/hare/path/buffer.ha:28-31. Empty buffer views as "."; a filled +// buffer views the byte prefix. + +@test fn string_cases() void = { + let buf = buffer { ... }; + if (string(&buf) != ".") { fail(); }; + if (push(&buf, "foo")! != "foo") { fail(); }; + if (string(&buf) != "foo") { fail(); }; +}; + +export fn main() i32 = { + signalled = 1; push_cases(); + signalled = 2; isroot_cases(); + signalled = 3; string_cases(); + return 0; +}; diff --git a/test/wcc/900_stdlib.c b/test/wcc/900_stdlib.c index 97c2e8eb..8a9dd951 100644 --- a/test/wcc/900_stdlib.c +++ b/test/wcc/900_stdlib.c @@ -15,7 +15,6 @@ static const char *modules[] = { "lib/io/io.ww", "lib/strconv/strconv.ww", "lib/sort/sort.ww", - "lib/path/path.ww", "lib/encoding/utf8/utf8.ww", "lib/encoding/base32/base32.ww", "lib/hash/fnv/fnv.ww", @@ -41,13 +40,17 @@ static const char *modules[] = { * lib/encoding/base64 graduated to Hare's io-streaming surface * (hex references io.handle / fmt.fprint / memio.dynamic / strconv / * errors.invalid; base64 references io.handle / memio.dynamic / - * bytes.zero / strings.frombytes / errors.invalid). Coverage lives at + * bytes.zero / strings.frombytes / errors.invalid); lib/path.path + * is import-dependent post-c2-stack realignment ([MAX]u8 via + * os.PATH_MAX def-dim + match over imported path error types). + * Coverage lives at * lib/bufio/bufiotest.ww + lib/bytes/bytestest.ww + * lib/errors/errnotest.ww + lib/fmt/fmttest.ww + lib/os/stattest.ww * + lib/strings/stringstest.ww + lib/encoding/hex/hextest.ww + * lib/encoding/base64/base64_test.ww (wired at 998_bufio_run.c, * 967_bytes_run.c, 902_errno_run.c, 970_fmt_run.c, 976_stat_run.c, - * 966_strings_run.c, 979_hex_run.c, 984_base64_run.c), plus the + * 966_strings_run.c, 979_hex_run.c, 984_base64_run.c) + path at + * lib/path/pathtest.ww (989_path_run.c), plus the * bufio.scanline / fmt.println e2e rows in test/wcc/700_e2e.c. */ "lib/net/net.ww", NULL diff --git a/test/wcc/989_lib_byteid.c b/test/wcc/989_lib_byteid.c index cc46a5e2..2a7e00a5 100644 --- a/test/wcc/989_lib_byteid.c +++ b/test/wcc/989_lib_byteid.c @@ -107,8 +107,14 @@ static const struct ent ents[] = { /* fixtureless modules, import-probe shape */ { .probe = "package main;\nimport sort;\nfn main() i32 = { return 0; };\n", .mode = M_ID, .sentinel = "package sort;", .moddir = "lib/sort" }, + /* c2-stack path::buffer realignment: w6c compiles, w6c_ww rejects + * path's module-global slice consts (const dot/dotdot: []u8) = the + * #120/#29 acceptance divergence (module-level composite globals); + * + the #148 twin #151 (global []u8 by-value arg). Both → #125 + * batch. cstage runtime is covered by 989_path_run. M_WWREJECT + * self-graduates: flips RED to M_ID the day wwstage accepts this. */ { .probe = "package main;\nimport path;\nfn main() i32 = { return 0; };\n", - .mode = M_ID, .sentinel = "package path;", .moddir = "lib/path" }, + .mode = M_WWREJECT, .cite = "#120/#29", .sentinel = "package path;", .moddir = "lib/path" }, { .probe = "package main;\nimport endian;\nfn main() i32 = { return 0; };\n", .mode = M_ID, .sentinel = "package endian;", .moddir = "lib/endian" }, { .probe = "package main;\nimport net;\nfn main() i32 = { return 0; };\n", diff --git a/test/wcc/989_path_run.c b/test/wcc/989_path_run.c new file mode 100644 index 00000000..3e9f2a49 --- /dev/null +++ b/test/wcc/989_path_run.c @@ -0,0 +1,49 @@ +/* + * 989_path_run — execute the lib/path @test fixture under the + * C-side `ww run` driver and assert exit 0. + * + * Same thin-wrapper shape as 979_hex_run / 967_bytes_run: + * pathtest.ww carries its own `export fn main()` that drives the + * @test fns and signals which case failed via the exit code. + */ +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + const char *src = "lib/path/pathtest.ww"; + char path[1024], cmd[2048]; + snprintf(path, sizeof path, "%s/%s", cwd, src); + snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path); + int rc = runwait(cmd); + if (rc != 0) { + fprintf(stderr, "path_run FAIL: %s exited %d\n", src, rc); + return 1; + } + printf("path_run: %s ok\n", src); + return 0; +}