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).
This commit is contained in:
2026-06-08 11:47:43 +09:00
parent 16f47da916
commit 3c7f1aa027
2 changed files with 85 additions and 2 deletions

View File

@@ -1,8 +1,9 @@
// path — filesystem path manipulation. UTF-8 paths, '/' separator. // path — filesystem path manipulation. UTF-8 paths, '/' separator.
// Mirrors Hare's path:: surface (ref/hare/path/). This is the // Mirrors Hare's path:: surface (ref/hare/path/). This is the
// path::buffer stack-buffer port (c2-stack subset: error/posix defs + // path::buffer stack-buffer port (c2-stack subset: error/posix defs +
// buffer + isroot + appendlit + appendnorm + push + string + set). // buffer + isroot + appendlit + appendnorm + push + string + set) plus
// init/abs/dirname/basename and the rest land in later folds. // the c3-posix subset (dirname + basename, ref/hare/path/posix.ha).
// init/abs and the rest land in later folds.
package path; package path;
@@ -137,3 +138,51 @@ export fn set(buf: *buffer, items: str...) (str | error) = {
buf.end = 0; buf.end = 0;
return push(buf, items...); 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);
};

View File

@@ -115,9 +115,43 @@ fn fail() void = { os.exit(signalled + 10); };
if (string(&buf) != "foo") { fail(); }; if (string(&buf) != "foo") { fail(); };
}; };
// ---- dirname() / basename() -------------------------------------------
// ref/hare/path/posix.ha:51-71. Hare seeds each row through _local/local()
// for cross-platform SEP; on ww (Linux, SEP='/') that is identity, so the
// raw str literals stand in directly (local/_local defer to c3). POSIX
// dirname/basename do NOT normalize the input (posix.ha:14-15,35), so no
// seeding is needed. Parallel `[N]str` rows (cgen chained-field gap, as
// above). Rows whose expected is a borrowed VIEW (not a "."/"/" literal)
// compare a computed str to a literal — cstage-correct; wwstage mis-
// compares via #146 (str== over a frombytes result), and this @test is
// cstage-gated, so those rows are #146-deferred on wwstage only.
@test fn dirname_basename_cases() void = {
let inputs: [10]str;
let wantdir: [10]str;
let wantbase: [10]str;
inputs[0]="usr"; wantdir[0]="."; wantbase[0]="usr";
inputs[1]="usr/"; wantdir[1]="."; wantbase[1]="usr";
inputs[2]=""; wantdir[2]="."; wantbase[2]=".";
inputs[3]="/"; wantdir[3]="/"; wantbase[3]="/";
inputs[4]="//"; wantdir[4]="/"; wantbase[4]="/"; // impl-defined
inputs[5]="///"; wantdir[5]="/"; wantbase[5]="/";
inputs[6]="/usr/"; wantdir[6]="/"; wantbase[6]="usr";
inputs[7]="/usr/lib"; wantdir[7]="/usr"; wantbase[7]="lib";
inputs[8]="//usr//lib//"; wantdir[8]="//usr"; wantbase[8]="lib";
inputs[9]="/home//dwc//test"; wantdir[9]="/home//dwc"; wantbase[9]="test";
let i: i32 = 0;
for (i < 10) {
if (dirname(inputs[i]) != wantdir[i]) { fail(); };
if (basename(inputs[i]) != wantbase[i]) { fail(); };
i += 1;
};
};
export fn main() i32 = { export fn main() i32 = {
signalled = 1; push_cases(); signalled = 1; push_cases();
signalled = 2; isroot_cases(); signalled = 2; isroot_cases();
signalled = 3; string_cases(); signalled = 3; string_cases();
signalled = 4; dirname_basename_cases();
return 0; return 0;
}; };