lib/os+test: add stat / lstat / fstat / exists

Hare-shaped filestat introspection. New types: filestat (80B,
mirrors fs::filestat ref/hare/fs/types.ha:141), mode (31-member
enum mirroring fs::mode ref/hare/fs/types.ha:63), stat_mask (7 bits
mirroring fs::stat_mask ref/hare/fs/types.ha:129), timespec (i64+i64,
layout-compatible with future lib/time::instant).

APIs: stat / lstat / fstat (*filestat, *u8|i32) (void|oserror) over
SYS_newfstatat (nr=262). The out-param shape sidesteps the cgreturn
24B ABI cap; commented inline. exists(*u8) bool goes through the
syscall directly rather than wrapping stat()? — dodges task #22's
80B-scrutinee match-slot disagreement until that lands.

Three latent cgen workarounds in tree, all pointer'd to filed tasks:
  #22: os.exists sidesteps the (void|oserror) match shape
  #24: `at` enum bundles AT_FDCWD/SYMLINK_NOFOLLOW/EMPTY_PATH instead
       of three top-level `def`s (negative-literal def DATA omit)
  #25: kstat.mode typed as `mode` (enum) rather than u32 to skip the
       redundant u32→enum cast emit

Tests: 976_stat_run, 9 rows — stat/lstat/fstat × regfile/dir/symlink
plus exists × {regfile,dir,noent}. Row 1 also pins perm-bit and
atime/mtime/ctime!=0 to catch silent kstat→filestat offset miscompiles
(kstat fields at 72/88/104).

Graduation to lib/fs when it ships is noted inline; signatures stay
rename-compatible.
This commit is contained in:
2026-05-16 02:42:41 +09:00
parent db2b05bbe5
commit 2f9d6dc43a
10 changed files with 1998 additions and 0 deletions

View File

@@ -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 $@ $<

View File

@@ -61,6 +61,7 @@ type nr = enum i64 {
UNLINK = 87,
GETCWD = 79,
GETDENTS64 = 217,
NEWFSTATAT = 262,
};
// open(2) flags. Linux values, matching <fcntl.h>. 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 <fcntl.h>.
// 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;
};

185
lib/os/stattest.ww Normal file
View File

@@ -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 = "<tmp>/regfile" regular file, 11 bytes
// WW_TEST_STAT_SYMLINK = "<tmp>/symlink" symlink → ./regfile
// WW_TEST_STAT_SUBDIR = "<tmp>/subdir" directory, 0700
// WW_TEST_STAT_NOENT = "<tmp>/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;
};

View File

@@ -62,6 +62,7 @@ type nr = enum i64 {
UNLINK = 87,
GETCWD = 79,
GETDENTS64 = 217,
NEWFSTATAT = 262,
};
// open(2) flags. Linux values, matching <fcntl.h>. 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 <fcntl.h>.
// 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.
//

View File

@@ -62,6 +62,7 @@ type nr = enum i64 {
UNLINK = 87,
GETCWD = 79,
GETDENTS64 = 217,
NEWFSTATAT = 262,
};
// open(2) flags. Linux values, matching <fcntl.h>. 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 <fcntl.h>.
// 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.
//

View File

@@ -62,6 +62,7 @@ type nr = enum i64 {
UNLINK = 87,
GETCWD = 79,
GETDENTS64 = 217,
NEWFSTATAT = 262,
};
// open(2) flags. Linux values, matching <fcntl.h>. 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 <fcntl.h>.
// 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.
//

View File

@@ -62,6 +62,7 @@ type nr = enum i64 {
UNLINK = 87,
GETCWD = 79,
GETDENTS64 = 217,
NEWFSTATAT = 262,
};
// open(2) flags. Linux values, matching <fcntl.h>. 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 <fcntl.h>.
// 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.
//

View File

@@ -62,6 +62,7 @@ type nr = enum i64 {
UNLINK = 87,
GETCWD = 79,
GETDENTS64 = 217,
NEWFSTATAT = 262,
};
// open(2) flags. Linux values, matching <fcntl.h>. 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 <fcntl.h>.
// 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.
//

View File

@@ -62,6 +62,7 @@ type nr = enum i64 {
UNLINK = 87,
GETCWD = 79,
GETDENTS64 = 217,
NEWFSTATAT = 262,
};
// open(2) flags. Linux values, matching <fcntl.h>. 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 <fcntl.h>.
// 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

107
test/wcc/976_stat_run.c Normal file
View File

@@ -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:
* <tmp>/regfile regular file, 11 bytes "hello world"
* <tmp>/symlink → ./regfile (relative symlink)
* <tmp>/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 <tmp>") 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>
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;
} }