// path — filesystem path manipulation. UTF-8 paths, '/' separator. // 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) plus // the c3-posix subset (dirname + basename, ref/hare/path/posix.ha). // init/abs and the rest land in later folds. package path; import bytes; import strings; import os; // 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); // 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"; }; }; // 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, }; // 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: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. 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; }; } else { if (MAX < buf.end + bs.len + 1) { let e: error = too_long; return e; }; buf.buf[buf.end] = SEP; newend += 1; }; // 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; }; // 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)?; }; }; // 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...); }; // 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-compliant dirname/basename. They do NOT normalize the input and // operate on plain str (no buffer), so they sit apart from the stack // paradigm above — same POSIX-complaint as posix.ha:7-11. // ref/hare/path/posix.ha:17. Does *not* normalize; the return is either a // literal ("."/sepstr) or a borrowed view of the input. Div: toutf8 stays // strings.toutf8; len(path)→path.len; fromutf8_unsafe→strings.frombytes // (rule-9 carve-out); rindex z:size→i32; reslice `[..z]`→`[0:z]` (div #4). // Div: Hare shadows the param (`let path = strings::toutf8(path)`); ww's // new `let` binding is in scope for its OWN initializer, so the shadow // reads the uninitialized local — work var renamed `p` (cstage self-init // scope bug, #152). export fn dirname(path: str) str = { let p: []u8 = strings.toutf8(path); if (p.len == 0) { return "."; }; p = bytes.rtrim(p, SEP); if (p.len == 0) { return sepstr; }; match (bytes.rindex(p, SEP)) { case void => { return "."; }; case let z: i32 => { p = p[0:z]; }; }; p = bytes.rtrim(p, SEP); if (p.len == 0) { return sepstr; }; return strings.frombytes(p); }; // ref/hare/path/posix.ha:37. Does *not* normalize; the return is a // borrowed view of the input. Div: as dirname (incl. param-shadow rename // to `p`, #152); reslice `[z+1..]`→`[z+1:]` (div #4); empty-match no-op // arm spelled `case void => void;` (bytes.ww:130 precedent). export fn basename(path: str) str = { let p: []u8 = strings.toutf8(path); if (p.len == 0) { return "."; }; p = bytes.rtrim(p, SEP); if (p.len == 0) { return sepstr; }; match (bytes.rindex(p, SEP)) { case void => void; case let z: i32 => { p = p[z+1:]; }; }; return strings.frombytes(p); };