Files
ww/lib/path/path.ww
Hojun-Cho 3c7f1aa027 lib/path: #138 c3-posix dirname/basename — faithful posix.ha port, table-driven @test (cstage)
dirname/basename ported verbatim from ref/hare/path/posix.ha:17,37,
re-routing Hare's byte scanning to bytes.rtrim/rindex. Additive to the
c2-stack buffer core; no harness/Makefile/module-const changes.

Work-var renamed path->p to avoid Hare's param-shadowing
`let path = toutf8(path)`: ww's cgen N_LET prepends the new local into the
name-keyed localfind chain BEFORE the init is emitted, so the shadow-init
reads the fresh uninit slot — a silent miscompile, both-wrong-identical,
filed #152 and fixed independently next. drew ruled path->p a permanent
acceptable spelling (internal work-var, not API surface; avoids a
param-shadow footgun; not a rule-7 workaround). WHY-comment + #152 pointer
at both sites.

Tests: 10-row dirname/basename table (posix.ha:51-71 verbatim) — root,
no-sep, empty, trailing-sep, multi-sep all covered, expecteds traced.

C-first: cstage @test green; the str-view-vs-literal rows are #146-deferred
(wwstage byte-id, rides #125). Gate: all 324 passed, byte-id 990-997 green,
w6c/w6c_ww unchanged (lib-only additive).
2026-06-08 11:47:43 +09:00

189 lines
6.3 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: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/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...);
};
// ---- 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);
};