// The body of each call lands either in libwwrt.a (rt_syscall // trampoline) or libc bindings, depending on how the program was linked. package os; import time; // [[args]] allocates the []str view via the `alloc` builtin, whose malloc // lowers to rt_malloc only when the rt binding is in the bundle (mirror // lib/strings/strings.ww:30 — every alloc-using module imports rt). os is // bundled by ~every program, so without this a plain `ww build` of any // os-importing program links bare libc `malloc` (undefined). Task #17. import rt; @symbol("rt_syscall") fn syscall0(num: nr) i64; @symbol("rt_syscall") fn syscall1(num: nr, a: i64) i64; @symbol("rt_syscall") fn syscall2(num: nr, a: i64, b: i64) i64; @symbol("rt_syscall") fn syscall3(num: nr, a: i64, b: i64, c: i64) i64; @symbol("rt_syscall") fn syscall4(num: nr, a: i64, b: i64, c: i64, d: i64) i64; @symbol("rt_free") export fn free(p: *void, n: u64) void; // Linux amd64 syscall numbers. Internal to this module — passed as // the first arg of syscall0..4 via libwwrt's rt_syscall trampoline. // `nr` is the type so the call sites can't accidentally pass an // arbitrary i64 (`syscall1(0i64, ...)` no longer typechecks). type nr = enum i64 { READ = 0, WRITE = 1, OPEN = 2, CLOSE = 3, LSEEK = 8, RTSIGPROCMASK = 14, ACCESS = 21, PIPE = 22, DUP2 = 33, GETPID = 39, FORK = 57, EXECVE = 59, EXIT = 60, WAIT4 = 61, KILL = 62, GETCWD = 79, CHDIR = 80, RENAME = 82, MKDIR = 83, RMDIR = 84, UNLINK = 87, SYMLINK = 88, SETPGID = 109, GETDENTS64 = 217, NEWFSTATAT = 262, SIGNALFD4 = 289, PIPE2 = 293, }; // open(2) flags. Linux values, matching . Hare names them // `fs::flag::RDONLY` etc; we use the same leaf names so callers say // `os.flag.RDONLY` and `os.flag.WRONLY | os.flag.CREATE`. export type flag = enum i32 { RDONLY = 0, WRONLY = 1, RDWR = 2, CREATE = 64, // 0x40 EXCL = 128, // 0x80 — pair with CREATE to fail on existing path TRUNC = 512, // 0x200 }; // lseek(2) whence. Hare names it `io::whence`. export type whence = enum i32 { SET = 0, CUR = 1, END = 2, }; export fn exit(code: i32) void = { syscall1(nr.EXIT, code: i64); }; // PATH_MAX / pathbuf / kpath — port of Hare's ref/hare/sys/+linux/ // syscalls.ha:25,27,29-55. Hare's `path` accepts a sum `(str | // []u8 | *const u8)`; ww's lib/os public surface narrows to `str` // (the Hare-faithful surface at ref/hare/os/os.ha:37,47,50 etc). // Internally, [[kpath]] copies the `str` bytes into a single // module-level [[pathbuf]] scratch slot and NUL-terminates so the // raw Linux syscalls (which require C strings) see a valid // terminator. Same precedent as Hare's static `pathbuf`. // // Non-reentrant: one buffer, every [[stat]] / [[open]] / etc. // rewrites it. Same caveat as strconv's `*tos` family (overwritten // on next call). Caller must NOT hold a kpath-returned pointer // across another lib/os path call. Graduates when ww grows a // thread story. // // `nil`-as-overflow over `(*u8 | oserror)`: wwstage over-allocates // 1-word-payload tagged returns to 24B (cstage emits 16B). // Task #9; revert at task #10 when fixed. Repro at // .ai/probe_tagged_return_pointer_payload.ww. export def PATH_MAX: i32 = 4096; let pathbuf: [4096]u8; // Second path slot: [[rename]] needs both old+new NUL-terminated at once, // which the single [[pathbuf]] kpath slot can't hold (see kpath's // non-reentrancy note). let pathbuf2: [4096]u8; // ref/hare/sys/+linux/types.ha:886-888. ww folds `sys` into `os`, so the // std fd NUMBERS live here (the sys role). Typed i32, NOT io.file as in // Hare's os::stdout_file (ref/hare/os/+linux/stdfd.ha:28): Hare's `os` // imports `io`, but ww's `os` is the import floor and must never import // io (lib/CLAUDE.md) — so the io.file/io.handle binding can't live here. // Consumers (lib/fmt's stdio wrappers) cast i32→io.file at the use site, // where the handle layer is already in scope. export def STDIN_FILENO: i32 = 0; export def STDOUT_FILENO: i32 = 1; export def STDERR_FILENO: i32 = 2; // ref/hare/sys/+linux/types.ha:82,87. Narrow pipe2 flags used by the native // process coordinators; the decimal values are Linux O_CLOEXEC and O_NONBLOCK. export def O_CLOEXEC: i32 = 524288; export def O_NONBLOCK: i32 = 2048; // ref/hare/sys/+linux/types.ha:156,162,305,452-454. ww folds Hare's sys role // into os; these are the narrow process-control subset required by the // native test coordinators. export def SIGKILL: i32 = 9; export def SIGINT: i32 = 2; export def SIGTERM: i32 = 15; export def WNOHANG: i32 = 1; export def SIG_BLOCK: i32 = 0; export def SIG_UNBLOCK: i32 = 1; export def SIG_SETMASK: i32 = 2; export def SFD_NONBLOCK: i32 = O_NONBLOCK; export def SFD_CLOEXEC: i32 = O_CLOEXEC; fn kpath(p: str) *u8 = { if (p.len + 1 >= PATH_MAX) { return nil: *u8; }; // ENAMETOOLONG let i: i32 = 0; for (i < p.len) { pathbuf[i] = p[i]; i += 1; }; pathbuf[p.len] = 0u8; return &pathbuf[0]; }; // Raw, non-fallible primitives. These return Linux's int conventions // (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a // Hare-style fallible API use the wrappers below. export fn write(fd: i32, buf: *u8, n: u64) i64 = { return syscall3(nr.WRITE, fd: i64, buf: i64, n: i64); }; export fn read(fd: i32, buf: *u8, n: u64) i64 = { return syscall3(nr.READ, fd: i64, buf: i64, n: i64); }; export fn close(fd: i32) i32 = { return syscall1(nr.CLOSE, fd: i64): i32; }; // Used by w6c_ww to redirect stdout into an output file without // changing the cgen emit path. export fn dup2(oldfd: i32, newfd: i32) i32 = { return syscall2(nr.DUP2, oldfd: i64, newfd: i64): i32; }; // Linux amd64 pipe(2), ref/hare/sys/+linux/syscallno+x86_64.ha:26. // Hare's public syscall layer prefers pipe2 (syscalls.ha:367), but the // completion-token path needs no flags and os is ww's raw syscall floor. export fn pipe(fds: *[2]i32) i32 = { return syscall1(nr.PIPE, fds: i64): i32; }; // Linux amd64 pipe2(2), ref/hare/sys/+linux/syscalls.ha:367-369 and // syscallno+x86_64.ha:297. O_CLOEXEC makes launch-status writers disappear // atomically at exec rather than leaking into the executed process tree. export fn pipe2(fds: *[2]i32, flags: i32) i32 = { return syscall2(nr.PIPE2, fds: i64, flags: i64): i32; }; // Fallible wrappers. The error variant is `oserror` (an i64 carrying // -errno). The sum type makes success/failure explicit and lets // callers `?` the result up the stack. export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { let r: i64 = read(fd, buf, n); if (r < 0) { return r: oserror; }; return r; }; export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { let r: i64 = write(fd, buf, n); if (r < 0) { return r: oserror; }; return r; }; // open — Linux open(2). Returns -errno on failure, fd otherwise. // Higher-level callers prefer `tryopen`. Mirrors Hare's os::open // (ref/hare/os/os.ha:117); kpath lands the bytes in pathbuf. // Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. export fn open(path: str, flags: flag, mode: i32) i32 = { let p: *u8 = kpath(path); if (p == nil: *u8) { return -36i32; }; // ENAMETOOLONG return syscall3(nr.OPEN, p: i64, (flags as i32): i64, mode: i64): i32; }; export fn tryopen(path: str, flags: flag, mode: i32) (i32 | oserror) = { let fd: i32 = open(path, flags, mode); if (fd < 0) { return fd: i64: oserror; }; return fd; }; // Used for fstat-free file-size discovery (open ⇒ lseek to end ⇒ // lseek back). export fn lseek(fd: i32, off: i64, w: whence) i64 = { return syscall3(nr.LSEEK, fd: i64, off, (w as i32): i64); }; // oserror — the underlying errno from a failed syscall, as a // negative i64 (Linux's int convention; e.g. -2 = ENOENT). The // `!`-flagged alias makes ?-propagation pick this variant as the // error half of any (T | oserror) shape. Hare's analogue is // errors::errno carried inside io::error. export type oserror = !i64; // errno — the raw Linux errno as a positive code (ref/hare/sys/+linux/ // errno.ha:5, `errno = !int`). ww folds Hare's `sys` role into os // (lib/CLAUDE.md), so the sys::errno machinery lands here. Spelled i32 // rather than int: Linux errnos are kernel ints (32-bit), keeping os's // kernel-facing surface uniformly i32. Distinct from [[oserror]] (!i64, // the syscall's *negative* raw return) — the two model different // things, so they are not unified; the negative→positive normalization // lives at the oserror→errors.error boundary in those callers. export type errno = !i32; // Mapped errno values, ref/hare/sys/+linux/errno.ha:559-682. Positive, // matching Hare's defs (the kernel returns -N; the wrap-to-positive is // the caller's concern). Subset: exactly the errnos [[errors.errno]] // maps to a named condition; grow as callers surface more. export def ENOENT: errno = 2; export def EINTR: errno = 4; export def EAGAIN: errno = 11; export def EACCES: errno = 13; export def EBUSY: errno = 16; export def EEXIST: errno = 17; export def EINVAL: errno = 22; export def EOVERFLOW: errno = 75; export def ENETUNREACH: errno = 101; export def ETIMEDOUT: errno = 110; export def ECONNREFUSED: errno = 111; export def ECANCELED: errno = 125; // strerror — human-readable text for an [[errno]] (Hare's // sys::strerror, ref/hare/sys/+linux/errno.ha:18). FAITHFUL MINIMAL // SUBSET: the mapped errnos above plus a generic fallback; grow the // switch as callers surface more (lib/CLAUDE.md documented-subset, not // a workaround). Messages verbatim from the reference. Hare's // unknown_errno formats the numeric value; that is deferred. export fn strerror(err: errno) str = { switch (err) { case ENOENT: return "No such file or directory"; case EINTR: return "Interrupted system call"; case EAGAIN: return "Resource temporarily unavailable"; case EACCES: return "Permission denied"; case EBUSY: return "Device or resource busy"; case EEXIST: return "File exists"; case EINVAL: return "Invalid argument"; case EOVERFLOW: return "Value too large for defined data type"; case ENETUNREACH: return "Network is unreachable"; case ETIMEDOUT: return "Connection timed out"; case ECONNREFUSED: return "Connection refused"; case ECANCELED: return "Operation canceled"; }; return "Unknown error"; }; export fn filesize(fd: i32) (i64 | oserror) = { let end: i64 = lseek(fd, 0i64, whence.END); if (end < 0) { return end: oserror; }; let r: i64 = lseek(fd, 0i64, whence.SET); if (r < 0) { return r: oserror; }; return end; }; // Hare name (io::readall); the buffer is caller-supplied, matching // the Plan 9 subset convention. export fn readall(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { let got: u64 = 0u64; for (got < n) { let r: i64 = read(fd, buf + got, n - got); if (r < 0) { return r: oserror; }; if (r == 0) { return got: i64; }; // short read: caller decides got += r: u64; }; return got: i64; }; // Hare name (io::writeall). export fn writeall(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { let sent: u64 = 0u64; for (sent < n) { let r: i64 = write(fd, buf + sent, n - sent); if (r < 0) { return r: oserror; }; if (r == 0) { return sent: i64; }; sent += r: u64; }; return sent: i64; }; // access(2): returns 0 if the file is reachable, negative errno // otherwise. mode is the bitset described in (F_OK=0). // Mirrors Hare's os::access (ref/hare/os/+linux/fs.ha:access). // Returns -ENAMETOOLONG (-36) if the path overflows PATH_MAX. export fn access(path: str, mode: i32) i32 = { let p: *u8 = kpath(path); if (p == nil: *u8) { return -36i32; }; return syscall2(nr.ACCESS, p: i64, mode: i64): i32; }; // remove — unlink(2). Mirrors Hare's os::remove // (ref/hare/os/os.ha:12). export fn remove(path: str) i32 = { let p: *u8 = kpath(path); if (p == nil: *u8) { return -36i32; }; return syscall1(nr.UNLINK, p: i64): i32; }; // mkdir — mkdir(2). Mode is the unix permission bitset (e.g. 0o700). // Returns 0 on success, negative errno otherwise. Mirrors Hare's // os::mkdir (ref/hare/os/os.ha:50). export fn mkdir(path: str, mode: i32) i32 = { let p: *u8 = kpath(path); if (p == nil: *u8) { return -36i32; }; return syscall2(nr.MKDIR, p: i64, mode: i64): i32; }; // rmdir — rmdir(2). Mirrors Hare's os::rmdir // (ref/hare/os/os.ha:58). export fn rmdir(path: str) i32 = { let p: *u8 = kpath(path); if (p == nil: *u8) { return -36i32; }; return syscall1(nr.RMDIR, p: i64): i32; }; // rename — rename(2). Atomic when oldpath and newpath are on the same // filesystem; cross-fs is not. Returns 0 on success, negative errno // otherwise. Mirrors Hare's os::rename (ref/hare/os/os.ha:17), but // returns the raw i32 errno like sibling remove/mkdir/rmdir rather than // Hare's (void | fs::error): ww's os is the flat syscall floor, with no // fs:: error layer. newpath lands in the second [[pathbuf2]] slot since // kpath's single [[pathbuf]] can't hold both paths at once. export fn rename(oldpath: str, newpath: str) i32 = { let p: *u8 = kpath(oldpath); if (p == nil: *u8) { return -36i32; }; if (newpath.len + 1 >= PATH_MAX) { return -36i32; }; let i: i32 = 0; for (i < newpath.len) { pathbuf2[i] = newpath[i]; i += 1; }; pathbuf2[newpath.len] = 0u8; return syscall2(nr.RENAME, p: i64, (&pathbuf2[0]): i64): i32; }; // symlink — symlink(2). Mirrors Hare's os::symlink // (ref/hare/os/os.ha:106, `symlink(target, path)`), but returns the // raw i32 errno like sibling remove/mkdir/rename rather than Hare's // (void | fs::error): ww's os is the flat syscall floor. `target` is // stored verbatim (it may be relative and is not resolved); `path` // lands in the second [[pathbuf2]] slot since kpath's single // [[pathbuf]] can't hold both paths at once. export fn symlink(target: str, path: str) i32 = { let p: *u8 = kpath(target); if (p == nil: *u8) { return -36i32; }; if (path.len + 1 >= PATH_MAX) { return -36i32; }; let i: i32 = 0; for (i < path.len) { pathbuf2[i] = path[i]; i += 1; }; pathbuf2[path.len] = 0u8; return syscall2(nr.SYMLINK, p: i64, (&pathbuf2[0]): i64): i32; }; // mkdirs — recursive mkdir. Creates `path` and any non-existent // parent directories with the given mode. EEXIST is silently // accepted (matches Hare's `errors::exists` skip in os::mkdirs); // any other syscall failure surfaces as `oserror`. // // Mirrors Hare's os::mkdirs (ref/hare/os/os.ha:54). The in-place // '/' → NUL splice walks the kpath-loaded [[pathbuf]] directly // instead of recursing through [[mkdir]] — re-entering kpath would // clobber the buffer mid-walk (single static slot, see kpath's // non-reentrancy note above). export fn mkdirs(path: str, mode: i32) (void | oserror) = { let cp: *u8 = kpath(path); if (cp == nil: *u8) { return -36i64: oserror; }; let n: i32 = path.len; if (n == 0) { return; }; // Walk forward; at each '/' boundary, NUL-terminate the prefix, // raw MKDIR syscall on pathbuf, restore the slash, continue. // Skip index 0 so a leading '/' on absolute paths doesn't // trigger an empty mkdir. let i: i32 = 1; for (i < n) { if (pathbuf[i] == '/') { pathbuf[i] = 0u8; let r: i32 = syscall2(nr.MKDIR, (&pathbuf[0]): i64, mode: i64): i32; pathbuf[i] = 47u8; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; }; i += 1; }; let r: i32 = syscall2(nr.MKDIR, (&pathbuf[0]): i64, mode: i64): i32; if (r < 0) { if (r != -17) { return r: i64: oserror; }; }; return; }; // getpid(2). Used by the driver to mint unique scratch paths. export fn getpid() i32 = { return syscall0(nr.GETPID): i32; }; // fork(2): 0 in the child, child pid in the parent, negative errno // on failure. export fn fork() i32 = { return syscall0(nr.FORK): i32; }; // Raw Linux process-group and signal primitives. Signatures follow the // pinned Hare syscall order (ref/hare/sys/+linux/syscalls.ha:323,363), // while returning the raw negative errno used throughout this module. export fn setpgid(pid: i32, pgid: i32) i32 = { return syscall2(nr.SETPGID, pid: i64, pgid: i64): i32; }; export fn kill(pid: i32, signal: i32) i32 = { return syscall2(nr.KILL, pid: i64, signal: i64): i32; }; // ref/hare/sys/+linux/syscalls.ha:642-650 and types.ha:452-454. // Linux amd64's kernel sigset is one u64; size(u64) keeps the syscall // argument tied to that declared representation. export fn sigprocmask(how: i32, set: *u64, oldset: *u64) i32 = { return syscall4(nr.RTSIGPROCMASK, how: i64, set: i64, oldset: i64, size(u64): i64): i32; }; // Native coordinators cannot install an rt_sigreturn restorer, so interruption // delivery follows Hare's signalfd shape instead; ref/hare/sys/+linux/ // syscalls.ha:636-639 and types.ha:577-578. export fn signalfd(fd: i32, mask: *u64, flags: i32) i32 = { return syscall4(nr.SIGNALFD4, fd: i64, mask: i64, size(u64): i64, flags: i64): i32; }; // execve(2): on success, does not return. Mirrors Hare's // os::exec::exec path arg (str). argv/envp stay `**u8` — the // kernel takes a NUL-pointer-terminated table of NUL-terminated // C strings, a different shape from a path. export fn execve(path: str, argv: **u8, envp: **u8) i32 = { let p: *u8 = kpath(path); if (p == nil: *u8) { return -36i32; }; return syscall3(nr.EXECVE, p: i64, argv: i64, envp: i64): i32; }; // wait4(2): wait for `pid` (or any child if -1), store status in // `*status`, return the pid that ended (or negative errno). export fn wait4(pid: i32, status: *i32, options: i32, rusage: *void) i32 = { return syscall4(nr.WAIT4, pid: i64, status: i64, options: i64, rusage: i64): i32; }; // Linux wait-status decoding, ref/hare/sys/+linux/types.ha:312-319. // Keeping exit and signal as separate predicates prevents an expected // numeric exit from accepting a signal with the same collapsed status. export fn wexitstatus(status: i32) i32 = { return (status >> 8i32) & 255i32; }; export fn wtermsig(status: i32) i32 = { return status & 127i32; }; export fn wifexited(status: i32) bool = { return wtermsig(status) == 0; }; export fn wifsignaled(status: i32) bool = { let sig: i32 = wtermsig(status); return sig != 0 && sig != 127i32; }; // getcwd(2) — Linux flavour. Writes the NUL-terminated cwd into `buf` // and returns the number of bytes written (including the NUL), or a // negative errno. The driver uses it to expand `.` to the cwd's // basename for `ww build` / `ww test`. export fn getcwd(buf: *u8, n: u64) i64 = { return syscall2(nr.GETCWD, buf: i64, n: i64); }; // Child setup must change cwd without importing the higher fs layer; // ref/hare/sys/+linux/syscalls.ha:251-254. export fn chdir(path: str) i32 = { let p: *u8 = kpath(path); if (p == nil: *u8) { return -36i32; }; return syscall1(nr.CHDIR, p: i64): i32; }; // getdents64(2) — Linux directory enumeration. The fd must be opened // with O_RDONLY on a directory. `buf` receives a packed sequence of // linux_dirent64 records: // // struct linux_dirent64 { // u64 d_ino; // 0..7 // i64 d_off; // 8..15 // u16 d_reclen; // 16..17 — total bytes for this record // u8 d_type; // 18 — DT_REG/DT_DIR/... // u8 d_name[]; // 19.. — NUL-terminated name + padding // }; // // Returns bytes written into `buf` (advance by d_reclen to walk), // 0 at end-of-directory, or a negative errno. export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = { return syscall3(nr.GETDENTS64, fd: i64, buf: i64, n: i64); }; // rt_envp — runtime-side getter. rt/start.s captures envp into a DATAW // slot before calling main; this binding lifts the captured pointer // into ww. Same FFI shape as rt_syscall / rt_malloc / rt_abort: a TEXT // symbol the linker resolves. The returned `**u8` is a NUL-terminated // table of `*u8` entries, each pointing at a NUL-terminated // "NAME=VALUE" byte sequence. // // We don't expose `rtenvp` directly; [[getenv]] is the only consumer. @symbol("rt_envp") fn rtenvp() **u8; // rt_argc / rt_argv — runtime-side getters for the argc/argv captured by // rt/start.s at process entry (same DATAW-slot + TEXT-getter shape as // rt_envp). [[args]] is the only consumer. @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 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::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. // // 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 env: []str = getenvs(); let i: i32 = 0; for (i < env.len) { let ent: str = env[i]; let matched: bool = true; let j: i32 = 0; for (j < name.len) { if (j >= ent.len) { matched = false; break; }; if (ent[j] != name[j]) { matched = false; break; }; j += 1; }; if (matched) { 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; }; return; }; // argsbuilt / argscache — build-once cache for [[args]]. drew ruling // (task #17): an explicit `built` sentinel, NOT len==0 overloading (a // real argv always has argv[0], but the sentinel keeps the contract // honest and decoupled from content). args() is loop-callable; under // ww's no-free model a per-call rebuild would leak the slice each call, // so the slice is materialised once and reused. let argsbuilt: bool = false; let argscache: []str; // args — the process arguments as a borrowed []str. args[0] is the // program name; args[1..] are the invocation arguments. Each str views // the NUL-terminated argv bytes in place (valid for the process // lifetime), so the slice must not be mutated or freed by the caller. // // DIVERGENCE (Hare-fidelity, task #26): Hare's `os::args` is a `[]str` // GLOBAL populated by an @init that walks rt's argv // (ref/hare/os/+linux/start.ha). ww has no @init mechanism, so the // faithful global is not expressible; this is the fn-shaped equivalent // (Hare NAME kept, shape diverged). Revisit if @init lands (#26). export fn args() []str = { if (argsbuilt) { return argscache; }; let argc: i32 = rtargc(): i32; let argv: **u8 = rtargv(); let r: []str = alloc([], argc: u64)!; let i: i32 = 0; for (i < argc) { let c: *u8 = argv[i]; let n: i32 = 0; for (c[n] != 0u8) { n += 1; }; let s: str; s.ptr = c; s.len = n; append(r, s); i += 1; }; argscache = r; argsbuilt = true; return r; }; // Ports of Hare's stat family (ref/hare/fs/fs.ha:172,196 + // ref/hare/sys/+linux/stat.ha:24-58). The Hare surface returns // `filestat` by value; ww's cgreturn ABI tops out at 24B today (see // STATUS task #21) and filestat is 80B, so [[stat]] / [[lstat]] / // [[fstat]] take an out-parameter and return `(void | oserror)`. // Re-evaluate the by-value shape when full sret lands. // // `filestat`, `mode`, and `stat_mask` live in lib/os because ww has // no lib/fs yet; Hare puts them in `fs::`. These types graduate to // lib/fs when that module ships — callers should expect a future // re-export. // // Underlying syscall is SYS_newfstatat (262), which unifies // stat/lstat/fstat through the `dirfd + flags` triple: // stat = newfstatat(AT_FDCWD, path, 0) // lstat = newfstatat(AT_FDCWD, path, AT_SYMLINK_NOFOLLOW) // fstat = newfstatat(fd, "", AT_EMPTY_PATH) // Avoiding SYS_statx — its 256B variable layout would buy btime, // but Hare's filestat doesn't expose btime either, so we stay on // the simpler 144B kernel struct. // fstatat(2) flag values. Linux constants from . // Names mirror Hare's ref/hare/sys/+linux/types.ha:45-51 (capital- // AT_ prefix, top-level `def`s). export def AT_FDCWD: i32 = -100; export def AT_SYMLINK_NOFOLLOW: i32 = 256; // 0x100 export def AT_EMPTY_PATH: i32 = 4096; // 0x1000 // mode — file-mode bits. Mirrors Hare's fs::mode (ref/hare/fs/ // types.ha:63). Permission bits are the standard Unix octal subset; // type bits live in the S_IFMT = 0o170000 region. Type-bit test: // // let t: u32 = (fi.mode as u32) & 61440u32; // 0o170000 mask // if (t == os.mode.DIR as u32) { /* directory */ }; // // Numeric values are octal in Hare's source; ww has no octal // literals so they're written as decimal with the octal in a // trailing comment. export type mode = enum u32 { // permission bits USER_RWX = 448u32, // 0o700 USER_RW = 384u32, // 0o600 USER_RX = 320u32, // 0o500 USER_R = 256u32, // 0o400 USER_W = 128u32, // 0o200 USER_X = 64u32, // 0o100 GROUP_RWX = 56u32, // 0o070 GROUP_RW = 48u32, // 0o060 GROUP_RX = 40u32, // 0o050 GROUP_R = 32u32, // 0o040 GROUP_W = 16u32, // 0o020 GROUP_X = 8u32, // 0o010 OTHER_RWX = 7u32, // 0o007 OTHER_RW = 6u32, // 0o006 OTHER_RX = 5u32, // 0o005 OTHER_R = 4u32, // 0o004 OTHER_W = 2u32, // 0o002 OTHER_X = 1u32, // 0o001 SETUID = 2048u32, // 0o4000 SETGID = 1024u32, // 0o2000 STICKY = 512u32, // 0o1000 // file-type bits (S_IFMT mask = 0o170000 = 61440) UNKNOWN = 0u32, FIFO = 4096u32, // 0o010000 CHR = 8192u32, // 0o020000 DIR = 16384u32, // 0o040000 BLK = 24576u32, // 0o060000 REG = 32768u32, // 0o100000 LINK = 40960u32, // 0o120000 SOCK = 49152u32, // 0o140000 }; // stat_mask — which filestat fields the call populated. Mirrors // Hare's fs::stat_mask (ref/hare/fs/types.ha:129). newfstatat fills // every field, so [[stat]] / [[lstat]] / [[fstat]] always set all // seven bits OR-folded (see [[fillfilestat]]); per-bit testing is // the documented sparse-backend pattern (cf. Hare's fs::fs network // backends that only populate mtime+size). export type stat_mask = enum u32 { UID = 1u32, GID = 2u32, SIZE = 4u32, INODE = 8u32, ATIME = 16u32, MTIME = 32u32, CTIME = 64u32, }; // filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80 // bytes. Times are time.instant (ref/hare/time/instant.ha:9) — the // canonical Hare shape. See module-header note re: graduation to // lib/fs. export type filestat = struct { mask: stat_mask, // 0 (4) mode: mode, // 4 (4) uid: u32, // 8 (4) gid: u32, // 12 (4) sz: u64, // 16 (8) inode: u64, // 24 (8) atime: time.instant, // 32 (16) mtime: time.instant, // 48 (16) ctime: time.instant, // 64 (16) — ends at 80 }; // kstat — x86_64 kernel `struct stat` layout. Mirrors // arch/x86/include/uapi/asm/stat.h (`__kernel_ulong_t`-keyed // fields). 144 bytes. Module-internal; SYS_newfstatat writes into // this buffer and the public stat fns then copy the bits into the // Hare-shaped [[filestat]]. type kstat = struct { dev: u64, // 0 ino: u64, // 8 nlink: u64, // 16 mode: u32, // 24 uid: u32, // 28 gid: u32, // 32 pad0: u32, // 36 rdev: u64, // 40 sz: i64, // 48 blksize: i64, // 56 blocks: i64, // 64 atime_sec: i64, // 72 atime_nsec: i64, // 80 mtime_sec: i64, // 88 mtime_nsec: i64, // 96 ctime_sec: i64, // 104 ctime_nsec: i64, // 112 unused0: i64, // 120 unused1: i64, // 128 unused2: i64, // 136 — ends at 144 }; // emptypath — single-NUL byte used as the `pathname` arg to // newfstatat with AT_EMPTY_PATH. The kernel requires a non-NULL // pointer to a zero-length C string, NOT a null pointer. Bytes are // read-only from the kernel's view; ww has no module-level const so // this is a writable `let`. let emptypath: [1]u8 = [0u8]; // fillfilestat — copy a 144B kstat into the 80B Hare-shaped // filestat. Mirrors Hare's st_to_filestat (ref/hare/os/+linux/dirfdfs.ha:259): // newfstatat populates every field, so the mask is the OR-fold of // all seven Hare stat_mask bits. fn fillfilestat(out: *filestat, k: *kstat) void = { out.mask = stat_mask.UID | stat_mask.GID | stat_mask.SIZE | stat_mask.INODE | stat_mask.ATIME | stat_mask.MTIME | stat_mask.CTIME; out.mode = k.mode: mode; out.uid = k.uid; out.gid = k.gid; out.sz = k.sz: u64; out.inode = k.ino; out.atime.sec = k.atime_sec; out.atime.nsec = k.atime_nsec; out.mtime.sec = k.mtime_sec; out.mtime.nsec = k.mtime_nsec; out.ctime.sec = k.ctime_sec; out.ctime.nsec = k.ctime_nsec; }; // stat — fill *out with metadata for `path`. Follows symlinks. // Returns ENAMETOOLONG (-36) as `oserror` if the path overflows // PATH_MAX. // // Mirrors Hare's sys::stat (ref/hare/sys/+linux/stat.ha:51) modulo // the out-param shape forced by the cgreturn 24B cap. Note: Hare's // higher-level fs::stat (ref/hare/fs/fs.ha:172) instead has lstat // semantics — we follow sys::stat's POSIX-stat behavior here. export fn stat(out: *filestat, path: str) (void | oserror) = { let cp: *u8 = kpath(path); if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); }; // lstat — like [[stat]] but does NOT follow a terminal symlink. // Mirrors Hare's sys::lstat (ref/hare/sys/+linux/stat.ha:57). export fn lstat(out: *filestat, path: str) (void | oserror) = { let cp: *u8 = kpath(path); if (cp == nil: *u8) { return -36i64: oserror; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, AT_FDCWD: i64, cp: i64, (&k): i64, AT_SYMLINK_NOFOLLOW: i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); }; // fstat — like [[stat]] but addresses the file by fd. Uses // newfstatat(fd, "", AT_EMPTY_PATH); the kernel resolves the fd // directly. Mirrors Hare's sys::fstat (ref/hare/sys/+linux/stat.ha:54). export fn fstat(out: *filestat, fd: i32) (void | oserror) = { let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, fd: i64, (&emptypath[0]): i64, (&k): i64, AT_EMPTY_PATH: i64); if (r < 0) { return r: oserror; }; fillfilestat(out, &k); }; // exists — true if `path` resolves to anything (regular file, // directory, symlink, ...). Stat-shaped (Hare's `fs::exists`, // ref/hare/fs/fs.ha:196) — no separate syscall. Symlinks are // followed; a dangling symlink is `false`. ENAMETOOLONG is // swallowed as `false` — Hare's os::exists doc says "true if a // node exists at the given path, or false if not." // // Race warning: prefer "open and handle the error" over "exists // then open" in real code (Hare's docstring carries the same // note). The race is unavoidable in this shape. // // Goes through SYS_newfstatat directly rather than match'ing on // [[stat]]'s `(void | oserror)` return. Functionally identical; // the direct shape sidesteps a cstage/wwstage cgen disagreement // on the slot size of `(void | oserror)` (cstage 16B, wwstage 24B // — same class as STATUS #22, surfaced first time a match on this // shape combined with an 80B local-struct local frame). Use the // match shape once #22 lands. export fn exists(path: str) bool = { let cp: *u8 = kpath(path); if (cp == nil: *u8) { return false; }; let k: kstat; let r: i64 = syscall4(nr.NEWFSTATAT, AT_FDCWD: i64, cp: i64, (&k): i64, 0i64); return r >= 0i64; };