diff --git a/Makefile b/Makefile index aa2b8d0d..f7cb6894 100644 --- a/Makefile +++ b/Makefile @@ -241,6 +241,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_at_test_ww \ $(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \ $(BIN)/test_shlex_run $(BIN)/test_getenv_run $(BIN)/test_dirs_run \ + $(BIN)/test_stat_run \ $(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \ $(BIN)/test_base32_run $(BIN)/test_base64_run \ $(BIN)/test_adler32_run $(BIN)/test_crc16_run \ @@ -498,6 +499,10 @@ $(BIN)/test_dirs_run: test/wcc/975_dirs_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_stat_run: test/wcc/976_stat_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_memio_run: test/wcc/980_memio_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/lib/os/os.ww b/lib/os/os.ww index 4e61a4ee..9cce6c6f 100644 --- a/lib/os/os.ww +++ b/lib/os/os.ww @@ -61,6 +61,7 @@ type nr = enum i64 { UNLINK = 87, GETCWD = 79, GETDENTS64 = 217, + NEWFSTATAT = 262, }; // open(2) flags. Linux values, matching . Hare names them @@ -365,3 +366,245 @@ export fn getenv(name: str) (str | void) = { }; return; }; + +// ---- stat / lstat / fstat / exists ----------------------------------- +// +// 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. + +// at — flags for fstatat(2). Numeric values match . +// Bundled as an enum because top-level `def` of a negative literal +// (FDCWD = -100) doesn't get a DATA slot emitted by cstage cgen — +// linker surfaces as "undefined reference". Tracked as task #24; +// enum-member resolution sidesteps it cleanly. +type at = enum i64 { + FDCWD = -100, + SYMLINK_NOFOLLOW = 256, // 0x100 + EMPTY_PATH = 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, +}; + +// timespec — {sec, nsec} pair, matching Hare's time::instant +// (ref/hare/time/types.ha). Local to lib/os; graduates to +// lib/time.instant when lib/time and lib/fs ship. Same byte layout +// (i64+i64 = 16B) so a future migration is field-rename only. +export type timespec = struct { + sec: i64, + nsec: i64, +}; + +// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80 +// bytes. 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: timespec, // 32 (16) + mtime: timespec, // 48 (16) + ctime: timespec, // 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]]. +// +// Mode is typed as the public [[mode]] enum (rather than raw u32) +// so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage +// emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that +// wwstage skips (task #25); the in-tree shape sidesteps it. +// Identical byte layout (both 4B at offset 24). +type kstat = struct { + dev: u64, // 0 + ino: u64, // 8 + nlink: u64, // 16 + mode: mode, // 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. Internal helper used by all three public entry points. +// 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; + 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. +// `path` must be NUL-terminated (lib/os convention; see task #23 +// for a planned `path: str` migration). +// +// 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: *u8) (void | oserror) = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: 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: *u8) (void | oserror) = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: 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`. +// +// 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: *u8) bool = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: i64, (&k): i64, 0i64); + return r >= 0i64; +}; diff --git a/lib/os/stattest.ww b/lib/os/stattest.ww new file mode 100644 index 00000000..5ccf29b4 --- /dev/null +++ b/lib/os/stattest.ww @@ -0,0 +1,185 @@ +// stattest — exercises [[os.stat]] / [[os.lstat]] / [[os.fstat]] / +// [[os.exists]] against a scratch tree pre-arranged by +// test/wcc/976_stat_run.c. The driver mkdtemp's a tmp dir, creates a +// regfile + symlink + subdir, exports paths through env vars, then +// exec's `ww run lib/os/stattest.ww`. We read the paths back via +// os.getenv and stat the targets. +// +// Pre-arranged scratch tree (set by 976_stat_run.c): +// +// WW_TEST_STAT_REGFILE = "/regfile" regular file, 11 bytes +// WW_TEST_STAT_SYMLINK = "/symlink" symlink → ./regfile +// WW_TEST_STAT_SUBDIR = "/subdir" directory, 0700 +// WW_TEST_STAT_NOENT = "/does-not-exist" +// +// Test exit code follows the stdlib `_run` convention: a `signalled` +// global the main fn bumps before each row, so `WEXITSTATUS = signalled +// + 10` tells the harness which row tripped. Same shape as +// ostest / temptest / shlextest. + +use os; + +let signalled: i32 = 0; + +fn fail() void = { os.exit(signalled + 10); }; + +// envpath — fetch an env var or abort; we want a clean signal if +// 976_stat_run.c didn't prime the env state. +fn envpath(name: str) str = { + match (os.getenv(name)) { + case void => { fail(); return "": str; }; + case let s: str => return s; + }; +}; + +// istype — mask the file-type bits (S_IFMT = 0o170000 = 61440) +// out of a mode and compare against the requested type bit. +fn istype(m: os.mode, t: os.mode) bool = { + return ((m as u32) & 61440u32) == (t as u32); +}; + +// ---- stat: regular file -------------------------------------------- +// +// Pinned bytes are "hello world" (11 bytes). We verify: +// - mask is fully set (newfstatat fills everything) +// - mode's type bits == REG +// - sz == 11 +// - inode is non-zero (real fs entry, not synthetic) + +@test fn test_stat_regfile() void = { + let p = envpath("WW_TEST_STAT_REGFILE"); + let fi: os.filestat; + // newfstatat populates every field, so the mask is the + // OR-fold of all 7 Hare stat_mask bits — mirrors what + // fillfilestat assigns. + let wantmask: u32 = (os.stat_mask.UID | os.stat_mask.GID + | os.stat_mask.SIZE | os.stat_mask.INODE + | os.stat_mask.ATIME | os.stat_mask.MTIME + | os.stat_mask.CTIME) as u32; + match (os.stat(&fi, p.ptr)) { + case let e: os.oserror => fail(); + case void => { + if ((fi.mask as u32) != wantmask) { fail(); }; + if (!istype(fi.mode, os.mode.REG)) { fail(); }; + if (fi.sz != 11u64) { fail(); }; + if (fi.inode == 0u64) { fail(); }; + // Permission-bit smoke test — 976_stat_run.c open(2)s with + // mode 0644 so USER_R survives any reasonable umask. Catches + // a struct-field-offset miscompile on `fi.mode` that the + // type-bit istype() check could miss if perm bits aliased a + // neighbouring u32 (uid/gid). + if (((fi.mode as u32) & (os.mode.USER_R as u32)) == 0u32) { + fail(); + }; + // atime/mtime/ctime — kernel-set at create time, all + // post-epoch (>0). Three distinct kstat offsets (72/88/104) + // so a fillfilestat field-copy miscompile or a filestat + // timespec-offset bug surfaces here, not silently. + if (fi.atime.sec <= 0i64) { fail(); }; + if (fi.mtime.sec <= 0i64) { fail(); }; + if (fi.ctime.sec <= 0i64) { fail(); }; + }; + }; +}; + +// ---- stat: directory ------------------------------------------------ + +@test fn test_stat_subdir() void = { + let p = envpath("WW_TEST_STAT_SUBDIR"); + let fi: os.filestat; + match (os.stat(&fi, p.ptr)) { + case let e: os.oserror => fail(); + case void => { + if (!istype(fi.mode, os.mode.DIR)) { fail(); }; + }; + }; +}; + +// ---- stat: missing path → oserror ENOENT --------------------------- + +@test fn test_stat_noent() void = { + let p = envpath("WW_TEST_STAT_NOENT"); + let fi: os.filestat; + match (os.stat(&fi, p.ptr)) { + case void => fail(); + case let e: os.oserror => { + // ENOENT = 2 → raw errno is -2. + if ((e: i64) != -2i64) { fail(); }; + }; + }; +}; + +// ---- stat (follow) vs lstat (no-follow) on a symlink ---------------- +// +// stat follows the link → reports the regfile (REG, 11 bytes). +// lstat does NOT follow → reports the link itself (LINK). + +@test fn test_stat_symlink_follow() void = { + let p = envpath("WW_TEST_STAT_SYMLINK"); + let fi: os.filestat; + match (os.stat(&fi, p.ptr)) { + case let e: os.oserror => fail(); + case void => { + if (!istype(fi.mode, os.mode.REG)) { fail(); }; + if (fi.sz != 11u64) { fail(); }; + }; + }; +}; + +@test fn test_lstat_symlink_nofollow() void = { + let p = envpath("WW_TEST_STAT_SYMLINK"); + let fi: os.filestat; + match (os.lstat(&fi, p.ptr)) { + case let e: os.oserror => fail(); + case void => { + if (!istype(fi.mode, os.mode.LINK)) { fail(); }; + }; + }; +}; + +// ---- fstat: open a file and stat by fd ------------------------------ + +@test fn test_fstat_regfile() void = { + let p = envpath("WW_TEST_STAT_REGFILE"); + let fd: i32 = os.open(p.ptr, os.flag.RDONLY, 0i32); + if (fd < 0) { fail(); }; + let fi: os.filestat; + match (os.fstat(&fi, fd)) { + case let e: os.oserror => { os.close(fd); fail(); }; + case void => { + if (!istype(fi.mode, os.mode.REG)) { os.close(fd); fail(); }; + if (fi.sz != 11u64) { os.close(fd); fail(); }; + }; + }; + os.close(fd); +}; + +// ---- exists: true on regfile/dir/symlink, false on noent ------------ + +@test fn test_exists_regfile() void = { + let p = envpath("WW_TEST_STAT_REGFILE"); + if (!os.exists(p.ptr)) { fail(); }; +}; + +@test fn test_exists_subdir() void = { + let p = envpath("WW_TEST_STAT_SUBDIR"); + if (!os.exists(p.ptr)) { fail(); }; +}; + +@test fn test_exists_noent() void = { + let p = envpath("WW_TEST_STAT_NOENT"); + if (os.exists(p.ptr)) { fail(); }; +}; + +export fn main() i32 = { + signalled = 1; test_stat_regfile(); + signalled = 2; test_stat_subdir(); + signalled = 3; test_stat_noent(); + signalled = 4; test_stat_symlink_follow(); + signalled = 5; test_lstat_symlink_nofollow(); + signalled = 6; test_fstat_regfile(); + signalled = 7; test_exists_regfile(); + signalled = 8; test_exists_subdir(); + signalled = 9; test_exists_noent(); + return 0; +}; diff --git a/selfhost/cmd/w6a/main.combined.ww b/selfhost/cmd/w6a/main.combined.ww index c0d266a0..06a650ca 100644 --- a/selfhost/cmd/w6a/main.combined.ww +++ b/selfhost/cmd/w6a/main.combined.ww @@ -62,6 +62,7 @@ type nr = enum i64 { UNLINK = 87, GETCWD = 79, GETDENTS64 = 217, + NEWFSTATAT = 262, }; // open(2) flags. Linux values, matching . Hare names them @@ -367,6 +368,248 @@ export fn getenv(name: str) (str | void) = { return; }; +// ---- stat / lstat / fstat / exists ----------------------------------- +// +// 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. + +// at — flags for fstatat(2). Numeric values match . +// Bundled as an enum because top-level `def` of a negative literal +// (FDCWD = -100) doesn't get a DATA slot emitted by cstage cgen — +// linker surfaces as "undefined reference". Tracked as task #24; +// enum-member resolution sidesteps it cleanly. +type at = enum i64 { + FDCWD = -100, + SYMLINK_NOFOLLOW = 256, // 0x100 + EMPTY_PATH = 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, +}; + +// timespec — {sec, nsec} pair, matching Hare's time::instant +// (ref/hare/time/types.ha). Local to lib/os; graduates to +// lib/time.instant when lib/time and lib/fs ship. Same byte layout +// (i64+i64 = 16B) so a future migration is field-rename only. +export type timespec = struct { + sec: i64, + nsec: i64, +}; + +// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80 +// bytes. 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: timespec, // 32 (16) + mtime: timespec, // 48 (16) + ctime: timespec, // 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]]. +// +// Mode is typed as the public [[mode]] enum (rather than raw u32) +// so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage +// emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that +// wwstage skips (task #25); the in-tree shape sidesteps it. +// Identical byte layout (both 4B at offset 24). +type kstat = struct { + dev: u64, // 0 + ino: u64, // 8 + nlink: u64, // 16 + mode: mode, // 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. Internal helper used by all three public entry points. +// 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; + 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. +// `path` must be NUL-terminated (lib/os convention; see task #23 +// for a planned `path: str` migration). +// +// 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: *u8) (void | oserror) = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: 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: *u8) (void | oserror) = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: 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`. +// +// 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: *u8) bool = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: i64, (&k): i64, 0i64); + return r >= 0i64; +}; + // MODULE: wcc // selfhost/cmd/wcc/mem.ww — port of cmd/wcc/mem.c. // diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index a5a4ccc4..0b2da955 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -62,6 +62,7 @@ type nr = enum i64 { UNLINK = 87, GETCWD = 79, GETDENTS64 = 217, + NEWFSTATAT = 262, }; // open(2) flags. Linux values, matching . Hare names them @@ -367,6 +368,248 @@ export fn getenv(name: str) (str | void) = { return; }; +// ---- stat / lstat / fstat / exists ----------------------------------- +// +// 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. + +// at — flags for fstatat(2). Numeric values match . +// Bundled as an enum because top-level `def` of a negative literal +// (FDCWD = -100) doesn't get a DATA slot emitted by cstage cgen — +// linker surfaces as "undefined reference". Tracked as task #24; +// enum-member resolution sidesteps it cleanly. +type at = enum i64 { + FDCWD = -100, + SYMLINK_NOFOLLOW = 256, // 0x100 + EMPTY_PATH = 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, +}; + +// timespec — {sec, nsec} pair, matching Hare's time::instant +// (ref/hare/time/types.ha). Local to lib/os; graduates to +// lib/time.instant when lib/time and lib/fs ship. Same byte layout +// (i64+i64 = 16B) so a future migration is field-rename only. +export type timespec = struct { + sec: i64, + nsec: i64, +}; + +// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80 +// bytes. 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: timespec, // 32 (16) + mtime: timespec, // 48 (16) + ctime: timespec, // 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]]. +// +// Mode is typed as the public [[mode]] enum (rather than raw u32) +// so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage +// emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that +// wwstage skips (task #25); the in-tree shape sidesteps it. +// Identical byte layout (both 4B at offset 24). +type kstat = struct { + dev: u64, // 0 + ino: u64, // 8 + nlink: u64, // 16 + mode: mode, // 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. Internal helper used by all three public entry points. +// 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; + 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. +// `path` must be NUL-terminated (lib/os convention; see task #23 +// for a planned `path: str` migration). +// +// 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: *u8) (void | oserror) = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: 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: *u8) (void | oserror) = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: 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`. +// +// 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: *u8) bool = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: i64, (&k): i64, 0i64); + return r >= 0i64; +}; + // MODULE: wcc // selfhost/cmd/wcc/mem.ww — port of cmd/wcc/mem.c. // diff --git a/selfhost/cmd/w6l/main.combined.ww b/selfhost/cmd/w6l/main.combined.ww index 2487c911..67d778d6 100644 --- a/selfhost/cmd/w6l/main.combined.ww +++ b/selfhost/cmd/w6l/main.combined.ww @@ -62,6 +62,7 @@ type nr = enum i64 { UNLINK = 87, GETCWD = 79, GETDENTS64 = 217, + NEWFSTATAT = 262, }; // open(2) flags. Linux values, matching . Hare names them @@ -367,6 +368,248 @@ export fn getenv(name: str) (str | void) = { return; }; +// ---- stat / lstat / fstat / exists ----------------------------------- +// +// 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. + +// at — flags for fstatat(2). Numeric values match . +// Bundled as an enum because top-level `def` of a negative literal +// (FDCWD = -100) doesn't get a DATA slot emitted by cstage cgen — +// linker surfaces as "undefined reference". Tracked as task #24; +// enum-member resolution sidesteps it cleanly. +type at = enum i64 { + FDCWD = -100, + SYMLINK_NOFOLLOW = 256, // 0x100 + EMPTY_PATH = 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, +}; + +// timespec — {sec, nsec} pair, matching Hare's time::instant +// (ref/hare/time/types.ha). Local to lib/os; graduates to +// lib/time.instant when lib/time and lib/fs ship. Same byte layout +// (i64+i64 = 16B) so a future migration is field-rename only. +export type timespec = struct { + sec: i64, + nsec: i64, +}; + +// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80 +// bytes. 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: timespec, // 32 (16) + mtime: timespec, // 48 (16) + ctime: timespec, // 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]]. +// +// Mode is typed as the public [[mode]] enum (rather than raw u32) +// so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage +// emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that +// wwstage skips (task #25); the in-tree shape sidesteps it. +// Identical byte layout (both 4B at offset 24). +type kstat = struct { + dev: u64, // 0 + ino: u64, // 8 + nlink: u64, // 16 + mode: mode, // 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. Internal helper used by all three public entry points. +// 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; + 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. +// `path` must be NUL-terminated (lib/os convention; see task #23 +// for a planned `path: str` migration). +// +// 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: *u8) (void | oserror) = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: 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: *u8) (void | oserror) = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: 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`. +// +// 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: *u8) bool = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: i64, (&k): i64, 0i64); + return r >= 0i64; +}; + // MODULE: wcc // selfhost/cmd/wcc/mem.ww — port of cmd/wcc/mem.c. // diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww index 8290dd53..750ef7c7 100644 --- a/selfhost/cmd/ww/main.combined.ww +++ b/selfhost/cmd/ww/main.combined.ww @@ -62,6 +62,7 @@ type nr = enum i64 { UNLINK = 87, GETCWD = 79, GETDENTS64 = 217, + NEWFSTATAT = 262, }; // open(2) flags. Linux values, matching . Hare names them @@ -367,6 +368,248 @@ export fn getenv(name: str) (str | void) = { return; }; +// ---- stat / lstat / fstat / exists ----------------------------------- +// +// 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. + +// at — flags for fstatat(2). Numeric values match . +// Bundled as an enum because top-level `def` of a negative literal +// (FDCWD = -100) doesn't get a DATA slot emitted by cstage cgen — +// linker surfaces as "undefined reference". Tracked as task #24; +// enum-member resolution sidesteps it cleanly. +type at = enum i64 { + FDCWD = -100, + SYMLINK_NOFOLLOW = 256, // 0x100 + EMPTY_PATH = 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, +}; + +// timespec — {sec, nsec} pair, matching Hare's time::instant +// (ref/hare/time/types.ha). Local to lib/os; graduates to +// lib/time.instant when lib/time and lib/fs ship. Same byte layout +// (i64+i64 = 16B) so a future migration is field-rename only. +export type timespec = struct { + sec: i64, + nsec: i64, +}; + +// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80 +// bytes. 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: timespec, // 32 (16) + mtime: timespec, // 48 (16) + ctime: timespec, // 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]]. +// +// Mode is typed as the public [[mode]] enum (rather than raw u32) +// so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage +// emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that +// wwstage skips (task #25); the in-tree shape sidesteps it. +// Identical byte layout (both 4B at offset 24). +type kstat = struct { + dev: u64, // 0 + ino: u64, // 8 + nlink: u64, // 16 + mode: mode, // 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. Internal helper used by all three public entry points. +// 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; + 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. +// `path` must be NUL-terminated (lib/os convention; see task #23 +// for a planned `path: str` migration). +// +// 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: *u8) (void | oserror) = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: 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: *u8) (void | oserror) = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: 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`. +// +// 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: *u8) bool = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: i64, (&k): i64, 0i64); + return r >= 0i64; +}; + // MODULE: wcc // selfhost/cmd/wcc/mem.ww — port of cmd/wcc/mem.c. // diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index f07757ea..4b4c704b 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -62,6 +62,7 @@ type nr = enum i64 { UNLINK = 87, GETCWD = 79, GETDENTS64 = 217, + NEWFSTATAT = 262, }; // open(2) flags. Linux values, matching . Hare names them @@ -367,6 +368,248 @@ export fn getenv(name: str) (str | void) = { return; }; +// ---- stat / lstat / fstat / exists ----------------------------------- +// +// 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. + +// at — flags for fstatat(2). Numeric values match . +// Bundled as an enum because top-level `def` of a negative literal +// (FDCWD = -100) doesn't get a DATA slot emitted by cstage cgen — +// linker surfaces as "undefined reference". Tracked as task #24; +// enum-member resolution sidesteps it cleanly. +type at = enum i64 { + FDCWD = -100, + SYMLINK_NOFOLLOW = 256, // 0x100 + EMPTY_PATH = 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, +}; + +// timespec — {sec, nsec} pair, matching Hare's time::instant +// (ref/hare/time/types.ha). Local to lib/os; graduates to +// lib/time.instant when lib/time and lib/fs ship. Same byte layout +// (i64+i64 = 16B) so a future migration is field-rename only. +export type timespec = struct { + sec: i64, + nsec: i64, +}; + +// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80 +// bytes. 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: timespec, // 32 (16) + mtime: timespec, // 48 (16) + ctime: timespec, // 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]]. +// +// Mode is typed as the public [[mode]] enum (rather than raw u32) +// so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage +// emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that +// wwstage skips (task #25); the in-tree shape sidesteps it. +// Identical byte layout (both 4B at offset 24). +type kstat = struct { + dev: u64, // 0 + ino: u64, // 8 + nlink: u64, // 16 + mode: mode, // 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. Internal helper used by all three public entry points. +// 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; + 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. +// `path` must be NUL-terminated (lib/os convention; see task #23 +// for a planned `path: str` migration). +// +// 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: *u8) (void | oserror) = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: 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: *u8) (void | oserror) = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: 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`. +// +// 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: *u8) bool = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: i64, (&k): i64, 0i64); + return r >= 0i64; +}; + // MODULE: wcc // selfhost/cmd/wcc/mem.ww — port of cmd/wcc/mem.c. // diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index a056ebf6..7a153dfe 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -62,6 +62,7 @@ type nr = enum i64 { UNLINK = 87, GETCWD = 79, GETDENTS64 = 217, + NEWFSTATAT = 262, }; // open(2) flags. Linux values, matching . Hare names them @@ -367,6 +368,248 @@ export fn getenv(name: str) (str | void) = { return; }; +// ---- stat / lstat / fstat / exists ----------------------------------- +// +// 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. + +// at — flags for fstatat(2). Numeric values match . +// Bundled as an enum because top-level `def` of a negative literal +// (FDCWD = -100) doesn't get a DATA slot emitted by cstage cgen — +// linker surfaces as "undefined reference". Tracked as task #24; +// enum-member resolution sidesteps it cleanly. +type at = enum i64 { + FDCWD = -100, + SYMLINK_NOFOLLOW = 256, // 0x100 + EMPTY_PATH = 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, +}; + +// timespec — {sec, nsec} pair, matching Hare's time::instant +// (ref/hare/time/types.ha). Local to lib/os; graduates to +// lib/time.instant when lib/time and lib/fs ship. Same byte layout +// (i64+i64 = 16B) so a future migration is field-rename only. +export type timespec = struct { + sec: i64, + nsec: i64, +}; + +// filestat — Hare's fs::filestat (ref/hare/fs/types.ha:141). 80 +// bytes. 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: timespec, // 32 (16) + mtime: timespec, // 48 (16) + ctime: timespec, // 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]]. +// +// Mode is typed as the public [[mode]] enum (rather than raw u32) +// so [[fillfilestat]]'s `out.mode = k.mode` needs no cast. Cstage +// emits a redundant `MOVL AX, AX` on u32 → enum-u32 casts that +// wwstage skips (task #25); the in-tree shape sidesteps it. +// Identical byte layout (both 4B at offset 24). +type kstat = struct { + dev: u64, // 0 + ino: u64, // 8 + nlink: u64, // 16 + mode: mode, // 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. Internal helper used by all three public entry points. +// 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; + 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. +// `path` must be NUL-terminated (lib/os convention; see task #23 +// for a planned `path: str` migration). +// +// 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: *u8) (void | oserror) = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: 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: *u8) (void | oserror) = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: 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`. +// +// 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: *u8) bool = { + let k: kstat; + let r: i64 = syscall4(nr.NEWFSTATAT, + at.FDCWD: i64, path: i64, (&k): i64, 0i64); + return r >= 0i64; +}; + // MODULE: strings // strings — operations over the immutable str type ({ *u8, len }). // Mirrors Hare's strings::; `len` and `is-empty` aren't functions diff --git a/test/wcc/976_stat_run.c b/test/wcc/976_stat_run.c new file mode 100644 index 00000000..6701de06 --- /dev/null +++ b/test/wcc/976_stat_run.c @@ -0,0 +1,107 @@ +/* + * 976_stat_run — execute the lib/os stat fixture under the C-side + * `ww run` driver and assert exit 0. + * + * Workflow (mirrors 975_dirs_run's mkdtemp + setenv + rm-rf shape): + * 1. mkdtemp /tmp/wwstat-XXXXXX — fresh per run, no parallel-test + * contention. + * 2. Populate the scratch tree: + * /regfile regular file, 11 bytes "hello world" + * /symlink → ./regfile (relative symlink) + * /subdir directory, mode 0700 + * 3. setenv WW_TEST_STAT_REGFILE / SYMLINK / SUBDIR / NOENT with + * the absolute paths; lib/os/stattest.ww reads them via + * os.getenv and exercises stat/lstat/fstat/exists. + * 4. exec `ww run lib/os/stattest.ww`. + * 5. system("rm -rf ") regardless of pass/fail. + * + * Same wrapper shape as 970-975; cleanup-on-failure is rm-rf'd so a + * regression never leaves residue under /tmp. + */ +#include +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + char tmpl[64]; + strcpy(tmpl, "/tmp/wwstat-XXXXXX"); + if (mkdtemp(tmpl) == NULL) { + perror("mkdtemp"); + return 1; + } + + char regfile[256], symlink_path[256], subdir[256], noent[256]; + snprintf(regfile, sizeof regfile, "%s/regfile", tmpl); + snprintf(symlink_path, sizeof symlink_path, "%s/symlink", tmpl); + snprintf(subdir, sizeof subdir, "%s/subdir", tmpl); + snprintf(noent, sizeof noent, "%s/does-not-exist", tmpl); + + /* Populate. */ + int fd = open(regfile, O_WRONLY | O_CREAT | O_EXCL, 0644); + if (fd < 0) { perror("open regfile"); goto cleanup_fail; } + if (write(fd, "hello world", 11) != 11) { + perror("write regfile"); close(fd); goto cleanup_fail; + } + close(fd); + if (symlink("./regfile", symlink_path) != 0) { + perror("symlink"); goto cleanup_fail; + } + if (mkdir(subdir, 0700) != 0) { + perror("mkdir subdir"); goto cleanup_fail; + } + + setenv("WW_TEST_STAT_REGFILE", regfile, 1); + setenv("WW_TEST_STAT_SYMLINK", symlink_path, 1); + setenv("WW_TEST_STAT_SUBDIR", subdir, 1); + setenv("WW_TEST_STAT_NOENT", noent, 1); + + const char *src = "lib/os/stattest.ww"; + char path[1024], cmd[2048]; + snprintf(path, sizeof path, "%s/%s", cwd, src); + snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path); + int rc = runwait(cmd); + + char rmcmd[256]; + snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpl); + (void)system(rmcmd); + + if (rc != 0) { + fprintf(stderr, "stat_run FAIL: %s exited %d\n", src, rc); + return 1; + } + printf("stat_run: %s ok\n", src); + return 0; + +cleanup_fail: { + char rmcmd[256]; + snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpl); + (void)system(rmcmd); + return 1; +} }