Files
ww/lib/path/path.ww
Hojun-Cho ba13940b96 lib/path: #153 c3 buffer-ops — abs/isroot-widen/local/parent/split/pop/peek faithful port, table-driven @test (cstage)
Faithful ref/hare/path/buffer.ha port of the buffer-mutating ops. isroot
widened *buffer->(*buffer|str) (existing callers ptr-ident-widen); abs str-arm
strings.hasprefix(p,sepstr) (rides #154); local lifts Hare's `static let buf`
to a module-private [MAX]u8 index-write (no ww static-append, regex.ww:1177).

Three documented respells (never silent): split binds each (str|void) elem to
an ident before return (#22b); peek binds split() to a local (#48); local's
index-write. parent via appendnorm + frombytes.

Tests mirror Hare's pop() @test 1:1 (stack.ha:147) + table-driven
abs/isroot/local/parent rows off a local buffer. cstage-first; wwstage str-arms
ride deferred #146 (path stays M_WWREJECT). lib/path non-embedded (no
combined.ww regen). Completes the c3 stage of the path arc.
2026-06-08 13:37:23 +09:00

275 lines
9.6 KiB
Plaintext

// 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...);
};
// ---- 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
// 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);
};