os: getenvs() []str + single-walker getenv (Hare env surface)

Mirror Hare's os env surface: getenvs() builds an owned []str of
NAME=VALUE entries from the rt_envp table (platform_environ.ha:41),
and getenv iterates it (environ.ha:32) so there is exactly one env
walker. strings.dup is unusable here -- lib/strings imports os, so os
importing strings would cycle; the owned copy is inlined (dup.ha:7).
test_getenvs_entries pins the []str shape non-vacuously.
This commit is contained in:
2026-06-21 23:41:18 +09:00
parent b817e5d498
commit 93b671808c
2 changed files with 108 additions and 29 deletions

View File

@@ -437,47 +437,98 @@ export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = {
@symbol("rt_argc") fn rtargc() i64;
@symbol("rt_argv") fn rtargv() **u8;
// envpbuilt / envpcache — build-once cache for [[getenvs]], the single
// env walker. drew ruling (task #17, shared with [[args]]): an explicit
// `built` sentinel, NOT len==0 overloading — an empty environment is a
// legitimate len-0 state, so a len check would re-walk every call.
let envpbuilt: bool = false;
let envpcache: []str;
// getenvs — the environment as an owned `[]str` of "NAME=VALUE" entries.
// Mirrors ref/hare/os/+linux/platform_environ.ha:41: lazy-build a
// file-private cache by walking the NUL-pointer-terminated rt_envp table,
// duping each C string into an owned str. Second call returns the cache.
// [[getenv]] borrows into this slice, so there is exactly one env walker.
//
// DIVERGENCE (Hare-fidelity): Hare's getenvs uses strings::dup, but ww's
// strings imports os (os.alloc is ww's allocator — a divergence from
// Hare's rt::malloc-direct strings), so os importing strings would be an
// import cycle. The owned copy is inlined here — same alloc + byte-copy
// as ref/hare/strings/dup.ha:7.
export fn getenvs() []str = {
if (envpbuilt) { return envpcache; };
let tab: **u8 = rtenvp();
let count: i32 = 0;
for (tab[count] != nil: *u8) { count += 1; };
// alloc([], 0) is rt_malloc(0) = an mmap of 0 bytes (-EINVAL); the
// empty environment carries the {nil,0,0} shape directly (mirrors
// lib/strings dupall's empty bypass).
let r: []str;
r.ptr = nil: *str;
r.len = 0;
r.cap = 0;
if (count != 0) {
let acc: []str = alloc([], count: u64)!;
let i: i32 = 0;
for (i < count) {
let entry: *u8 = tab[i];
let n: i32 = 0;
for (entry[n] != 0u8) { n += 1; };
let buf: []u8 = alloc([], n: u64)!;
buf.len = n;
let j: i32 = 0;
for (j < n) { buf[j] = entry[j]; j += 1; };
let s: str;
s.ptr = buf.ptr;
s.len = n;
append(acc, s);
i += 1;
};
r = acc;
};
envpcache = r;
envpbuilt = true;
return r;
};
// getenv — POSIX getenv. Returns a borrowed `str` view over the value
// bytes of the named environment variable, or void if the name is not
// present. The view is valid for the process lifetime — the bytes
// live in the kernel-supplied envp table at process entry. A future
// `setenv` (separate task) that grows the table behind the scenes
// would invalidate prior views; v1 has no setenv, so callers can
// hold the view indefinitely.
// live in the owned []str built by [[getenvs]] (borrow-source change,
// task #28: was the raw rt_envp table; now the single duped walker). A
// future `setenv` that grows the table behind the scenes would
// invalidate prior views; v1 has no setenv, so callers can hold the
// view indefinitely.
//
// Mirrors Hare's os::tryenv shape (returns void rather than panicking
// on missing). Hare also ships os::getenv (`(str | void)`) and
// os::mustenv (panic-on-missing); ww collapses to the single
// `(str | void)` form for now — consumers wanting "must" semantics
// abort at the call site.
// Mirrors Hare's os::getenv (ref/hare/os/environ.ha:32): iterate
// [[getenvs]], "name=" prefix-match each entry, return the value tail.
// Hare also ships os::tryenv (default-on-missing) and os::mustenv
// (panic-on-missing); ww collapses to the single `(str | void)` form for
// now — consumers wanting "must" semantics abort at the call site.
//
// Algorithm: walk the NUL-pointer-terminated `environ` table doing a
// "name=" prefix match against each entry, byte-wise. NUL inside
// `name` would never match a real env var (env var names cannot
// contain '\0'), so we don't filter — POSIX puts that responsibility
// on the caller.
// NUL inside `name` would never match a real env var (env var names
// cannot contain '\0'), so we don't filter — POSIX puts that
// responsibility on the caller.
export fn getenv(name: str) (str | void) = {
let envp: **u8 = rtenvp();
let env: []str = getenvs();
let i: i32 = 0;
for (true) {
let entry: *u8 = envp[i];
if (entry == nil: *u8) { return; };
let j: i32 = 0;
for (i < env.len) {
let ent: str = env[i];
let matched: bool = true;
let j: i32 = 0;
for (j < name.len) {
if (entry[j] == 0u8) { matched = false; break; };
if (entry[j] != name[j]) { matched = false; break; };
if (j >= ent.len) { matched = false; break; };
if (ent[j] != name[j]) { matched = false; break; };
j += 1;
};
if (matched) {
if (entry[name.len] == '=') {
let val: *u8 = entry + ((name.len + 1): u64);
let n: i32 = 0;
for (val[n] != 0u8) { n += 1; };
let r: str;
r.ptr = val;
r.len = n;
return r;
if (name.len < ent.len) {
if (ent[name.len] == '=') {
let val: str;
val.ptr = ent.ptr + ((name.len + 1): u64);
val.len = ent.len - (name.len + 1);
return val;
};
};
};
i += 1;

View File

@@ -91,6 +91,34 @@ fn streq(a: str, b: str) bool = {
};
};
// ---- getenvs: direct non-vacuous coverage of the []str shape --------
//
// getenvs() is public API (task #28) and the single env walker getenv
// routes through. getenv's cases exercise the walk transitively, but
// none asserts the []str RETURN shape — its count or "NAME=VALUE" entry
// text. This row pins both: the slice must be non-empty, and every
// expected entry must appear verbatim (name, '=', value duped intact).
// Fails if getenvs miscounts (len 0 → the inner search finds nothing)
// or dups the wrong bytes (off-by-one truncation → no streq match).
// The "WW_TEST_EMPTY=" row also pins the empty-value entry text.
@test fn test_getenvs_entries() void = {
let env: []str = os.getenvs();
assert(!(env.len == 0));
let want: [2]str = ["WW_TEST_GETENV=hello-world", "WW_TEST_EMPTY="];
let w: i32 = 0;
for (w < 2) {
let found: bool = false;
let i: i32 = 0;
for (i < env.len) {
if (streq(env[i], want[w])) { found = true; };
i += 1;
};
assert(found);
w += 1;
};
};
// ---- alloc/free: mmap-backed runtime allocator ----------------------
//
// Direct round-trip. Write-then-read-back proves the returned page is