diff --git a/lib/path/path.ww b/lib/path/path.ww index 4fdeec50..d6cf306c 100644 --- a/lib/path/path.ww +++ b/lib/path/path.ww @@ -49,11 +49,24 @@ export type buffer = struct { 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; +// ref/hare/path/buffer.ha:34-41. str-arm = str-prefix (vs const global, +// correct only post-#154); buffer-arm = byte. Hare `0 < buf.end` order kept. +export fn abs(path: (*buffer | str)) bool = { + match (path) { + case let p: str => { return strings.hasprefix(p, sepstr); }; + case let b: *buffer => { return 0 < b.end && b.buf[0] == SEP; }; + }; +}; + +// ref/hare/path/buffer.ha:44-51. WIDENED from the c2 *buffer-only body to +// the (*buffer | str) union (str-arm = str==, post-#154 correct). The +// appendnorm/appendlit callers pass a concrete *buffer that ptr-ident +// arg-widens into the union (#55 leg). +export fn isroot(path: (*buffer | str)) bool = { + match (path) { + case let p: str => { return p == sepstr; }; + case let b: *buffer => { return b.end == 1 && b.buf[0] == SEP; }; + }; }; // ref/hare/path/stack.ha:63-74. @@ -139,6 +152,79 @@ export fn set(buf: *buffer, items: str...) (str | error) = { return push(buf, items...); }; +// ---- c3 buffer-ops (ref/hare/path/buffer.ha, stack.ha) ---------------- + +// lifts Hare's fn-`static let buf` (buffer.ha:53-56): statically allocated, +// overwritten on the next local() call. strconv *tos-buf precedent. +let localbuf: [MAX]u8 = [0...]; + +// ref/hare/path/buffer.ha:55-77. Div: Hare `_local` returns (str|nomem) — +// ww has no nomem axis, so the MAX-guard IS the loud bound (→ too_long); +// `_local` folded inline (local is its only caller). fromutf8→frombytes +// (rule-9 carve-out); `..` reslice→colon; `0z`→`0i32` (div #1,#8). +export fn local(path: str) (str | too_long) = { + let bs: []u8 = strings.toutf8(path); + if (MAX < bs.len) { let e: too_long = too_long; return e; }; + let n: i32 = 0; + for (let b .. bs) { + // no ww static-append → index-write (regex.ww:1177 precedent: + // static append→plain; index-write keeps the static-view contract) + if (b == '/') { localbuf[n] = SEP; } else { localbuf[n] = b; }; + n += 1; + }; + return strings.frombytes(localbuf[0:n]); +}; + +// ref/hare/path/stack.ha:133-145. Helper for pop/peek: returns (new end, +// result). Over-cap tuple SRET (i32 .0 + (str|void) .1 = 40B > 32B, #22b). +// Div: (size,…)→(i32,…) (div #1); `0z`/`1z`→`0i32`/`1`; `..`→`:`; +// fromutf8→frombytes; Hare if-expr → if-statement (div #7,#8,#4,#2). +fn split(buf: *buffer) (i32, (str | void)) = { + if (buf.end == 0 || isroot(buf)) { + let e: i32 = buf.end; + let r: (str | void) = void; // #22b: bind-then-return + return (e, r); + }; + match (bytes.rindex(buf.buf[0:buf.end], SEP)) { + case void => { + let e: i32 = 0i32; + let r: (str | void) = strings.frombytes(buf.buf[0:buf.end]); // #22b: bind-then-return + return (e, r); + }; + case let i: i32 => { + let e: i32 = i; + if (i == 0) { e = 1; }; + let r: (str | void) = strings.frombytes(buf.buf[i+1:buf.end]); // #22b: bind-then-return + return (e, r); + }; + }; +}; + +// ref/hare/path/stack.ha:125-131. Remove and return the final segment; +// void if the path is empty or the root dir. +export fn pop(buf: *buffer) (str | void) = { + const (end, res) = split(buf); + buf.end = end; + return res; +}; + +// ref/hare/path/stack.ha:121-123. Examine the final segment without +// mutating the buffer; void if empty or root. +export fn peek(buf: *buffer) (str | void) = { + let t = split(buf); // #48: bind split to local (not .1 off a CALL) + return t.1; +}; + +// ref/hare/path/stack.ha:172-180. Parent directory; root → root. Hare's +// doc says "without modifying the buffer" but the BODY mutates via +// appendnorm — ported verbatim, not "fixed". Div: drop `const` ptr-qual; +// fromutf8→frombytes; `..`→`:` (div #2,#4). +export fn parent(buf: *buffer) (str | error) = { + let newend: i32 = appendnorm(buf, dotdot)?; + if (newend == 0) { return "."; }; + return strings.frombytes(buf.buf[0:newend]); +}; + // ---- POSIX jail (ref/hare/path/posix.ha) ------------------------------ // POSIX-compliant dirname/basename. They do NOT normalize the input and // operate on plain str (no buffer), so they sit apart from the stack diff --git a/lib/path/pathtest.ww b/lib/path/pathtest.ww index 905d6bfc..53565a30 100644 --- a/lib/path/pathtest.ww +++ b/lib/path/pathtest.ww @@ -79,9 +79,40 @@ fn fail() void = { os.exit(signalled + 10); }; if (push(&buf, "/foo")! != "/foo") { fail(); }; }; +// ---- abs() ------------------------------------------------------------ +// ref/hare/path/buffer.ha:34-41. str literals widen into the (*buffer|str) +// union arg (str-arm = hasprefix vs sepstr); buffer-arm = byte. Parallel +// [N]str inputs + [N]i32 wantbool (no [N]bool in the stack; cgen-field gap). + +@test fn abs_cases() void = { + let ins: [4]str; + let want: [4]i32; + ins[0]="/"; want[0]=1; + ins[1]="/foo"; want[1]=1; + ins[2]="foo"; want[2]=0; + ins[3]=""; want[3]=0; + let i: i32 = 0; + for (i < 4) { + let got: i32 = 0; + if (abs(ins[i])) { got = 1; }; + if (got != want[i]) { fail(); }; + i += 1; + }; + + // buffer-arm: &buf ptr-ident-widens into the union. + let buf = buffer { ... }; + if (abs(&buf)) { fail(); }; // empty → false + if (push(&buf, "/foo")! != "/foo") { fail(); }; + if (!abs(&buf)) { fail(); }; // "/foo" → true + buf.end = 0; + if (push(&buf, "foo")! != "foo") { fail(); }; + if (abs(&buf)) { fail(); }; // "foo" → false +}; + // ---- isroot() --------------------------------------------------------- -// ref/hare/path/buffer.ha:44-51 (*buffer arm). Heterogeneous seeds -// (manual root seed vs push) → inline rather than a uniform table. +// ref/hare/path/buffer.ha:44-51. Both arms: str (== sepstr) and *buffer +// (byte+int). Heterogeneous buffer seeds (manual root seed vs push) → +// inline; str-arm rows table-driven. @test fn isroot_cases() void = { let buf = buffer { ... }; @@ -102,6 +133,21 @@ fn fail() void = { os.exit(signalled + 10); }; buf.buf[0] = SEP; buf.end = 1; if (push(&buf, "foo")! != "/foo") { fail(); }; if (isroot(&buf)) { fail(); }; + + // str-arm (buffer.ha:47): only the bare separator is root. + let ins: [4]str; + let want: [4]i32; + ins[0]="/"; want[0]=1; + ins[1]="/foo"; want[1]=0; + ins[2]="foo"; want[2]=0; + ins[3]=""; want[3]=0; + let i: i32 = 0; + for (i < 4) { + let got: i32 = 0; + if (isroot(ins[i])) { got = 1; }; + if (got != want[i]) { fail(); }; + i += 1; + }; }; // ---- string() --------------------------------------------------------- @@ -148,10 +194,92 @@ fn fail() void = { os.exit(signalled + 10); }; }; }; +// ---- local() ---------------------------------------------------------- +// ref/hare/path/buffer.ha:55-77. Rewrites '/'→SEP into a static buffer; +// on Linux (SEP=='/') that is identity. The returned view borrows the +// module-private localbuf, so compare each row before the next call. + +@test fn local_cases() void = { + let ins: [4]str; + let want: [4]str; + ins[0]="a/b"; want[0]="a/b"; + ins[1]="/"; want[1]="/"; + ins[2]="foo"; want[2]="foo"; + ins[3]=""; want[3]=""; + let i: i32 = 0; + for (i < 4) { + if (local(ins[i])! != want[i]) { fail(); }; + i += 1; + }; +}; + +// ---- parent() --------------------------------------------------------- +// ref/hare/path/stack.ha:172-180. Parent dir; root → root; empty → "..". +// Each row reseeds a reset buffer (parent goes through appendnorm). push's +// normalized echo is dropped so the empty-buffer row ("" pushes to ".", +// not "") can share the table. + +@test fn parent_cases() void = { + let buf = buffer { ... }; + let ins: [5]str; + let want: [5]str; + ins[0]="/usr/lib"; want[0]="/usr"; + ins[1]="foo"; want[1]="."; + ins[2]="/"; want[2]="/"; + ins[3]="a/b/c"; want[3]="a/b"; + ins[4]=""; want[4]=".."; // empty buffer: appendnorm(dotdot) → ".." + let i: i32 = 0; + for (i < 5) { + buf.end = 0; + push(&buf, ins[i])!; + if (parent(&buf)! != want[i]) { fail(); }; + i += 1; + }; +}; + +// ---- split() / pop() / peek() ----------------------------------------- +// ref/hare/path/stack.ha:147-170 (@test pop). split is the helper, covered +// through pop/peek. Sequential off ONE buffer, reset buf.end=0 per case. +// The peek-then-string-unchanged assert is load-bearing: it pins that the +// #48 respelling didn't smuggle in pop's buf.end mutation. + +@test fn popsplit_cases() void = { + let buf = buffer { ... }; + + // empty + if (!(pop(&buf) is void)) { fail(); }; + if (string(&buf) != ".") { fail(); }; + + // root dir + buf.end = 0; + if (push(&buf, "/")! != "/") { fail(); }; + if (!(pop(&buf) is void)) { fail(); }; + if (string(&buf) != "/") { fail(); }; + + // relative file — peek is NON-mutating + buf.end = 0; + if (push(&buf, "foo")! != "foo") { fail(); }; + if (peek(&buf) as str != "foo") { fail(); }; + if (string(&buf) != "foo") { fail(); }; + if (pop(&buf) as str != "foo") { fail(); }; + if (string(&buf) != ".") { fail(); }; + + // absolute file + buf.end = 0; + if (push(&buf, "/foo")! != "/foo") { fail(); }; + if (peek(&buf) as str != "foo") { fail(); }; + if (pop(&buf) as str != "foo") { fail(); }; + if (string(&buf) != "/") { fail(); }; +}; + export fn main() i32 = { signalled = 1; push_cases(); signalled = 2; isroot_cases(); signalled = 3; string_cases(); signalled = 4; dirname_basename_cases(); + signalled = 5; abs_cases(); + signalled = 6; local_cases(); + signalled = 7; parent_cases(); + signalled = 8; popsplit_cases(); return 0; };