// os — process and filesystem facade. The body of each call lands // either in libwwrt.a (rt_syscall trampoline) or libc bindings, // depending on how the program was linked. @symbol("rt_syscall") fn syscall0(num: i64) i64; @symbol("rt_syscall") fn syscall1(num: i64, a: i64) i64; @symbol("rt_syscall") fn syscall2(num: i64, a: i64, b: i64) i64; @symbol("rt_syscall") fn syscall3(num: i64, a: i64, b: i64, c: i64) i64; @symbol("rt_syscall") fn syscall4(num: i64, a: i64, b: i64, c: i64, d: i64) i64; @symbol("rt_alloc") fn alloc(n: u64) *void; @symbol("rt_free") fn free(p: *void, n: u64) void; @symbol("rt_abort") fn abort(msg: str) void; // Hare-style runtime check. Caller passes a message that's printed // to stderr before exit(1). export fn assert(cond: bool, msg: str) void = { if (!cond) { abort(msg); }; }; def SYS_READ: i64 = 0; def SYS_WRITE: i64 = 1; def SYS_OPEN: i64 = 2; def SYS_CLOSE: i64 = 3; def SYS_LSEEK: i64 = 8; def SYS_ACCESS: i64 = 21; def SYS_DUP2: i64 = 33; def SYS_GETPID: i64 = 39; def SYS_FORK: i64 = 57; def SYS_EXECVE: i64 = 59; def SYS_EXIT: i64 = 60; def SYS_WAIT4: i64 = 61; def SYS_UNLINK: i64 = 87; def SYS_GETCWD: i64 = 79; def SYS_GETDENTS64: i64 = 217; // open(2) flags. Linux values, matching . def O_RDONLY: i32 = 0; def O_WRONLY: i32 = 1; def O_RDWR: i32 = 2; def O_CREAT: i32 = 64; // 0x40 def O_TRUNC: i32 = 512; // 0x200 // lseek(2) whence. def SEEK_SET: i32 = 0; def SEEK_CUR: i32 = 1; def SEEK_END: i32 = 2; export fn exit(code: i32) void = { syscall1(SYS_EXIT, code: i64); }; // Raw, non-fallible primitives. These return Linux's int conventions // (negative = -errno, non-negative = bytes/fd/etc). Callers wanting a // Hare-style fallible API use the wrappers below. export fn write(fd: i32, buf: *u8, n: u64) i64 = { return syscall3(SYS_WRITE, fd: i64, buf: i64, n: i64); }; export fn read(fd: i32, buf: *u8, n: u64) i64 = { return syscall3(SYS_READ, fd: i64, buf: i64, n: i64); }; export fn close(fd: i32) i32 = { return syscall1(SYS_CLOSE, fd: i64): i32; }; // dup2(2): make `newfd` refer to the same description as `oldfd`, // closing `newfd` first if open. Returns `newfd` on success or a // negative errno. Used by w6c_ww to redirect stdout into an output // file without changing the cgen emit path. export fn dup2(oldfd: i32, newfd: i32) i32 = { return syscall2(SYS_DUP2, oldfd: i64, newfd: i64): i32; }; // Fallible wrappers. The error variant is a plain str (Plan 9 errstr // model, see lib/errors); the sum type makes success/failure explicit // without overloading length-zero. export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | str) = { let r: i64 = read(fd, buf, n); if (r < 0) { return "read failed"; }; return r; }; export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | str) = { let r: i64 = write(fd, buf, n); if (r < 0) { return "write failed"; }; return r; }; // open — Linux open(2). Path must be NUL-terminated; callers using ww // `str` must ensure the bytes are followed by a 0 byte (literals are, // arena-copied paths usually are by construction). Returns -errno on // failure, fd otherwise. Higher-level callers prefer `tryopen`. export fn open(path: *u8, flags: i32, mode: i32) i32 = { return syscall3(SYS_OPEN, path: i64, flags: i64, mode: i64): i32; }; export fn tryopen(path: *u8, flags: i32, mode: i32) (i32 | str) = { let fd: i32 = open(path, flags, mode); if (fd < 0) { return "open failed"; }; return fd; }; // lseek — set/inspect the fd's position. Returns the new offset or // a negative errno. We use this for fstat-free file-size discovery // (open ⇒ lseek to end ⇒ lseek back). export fn lseek(fd: i32, off: i64, whence: i32) i64 = { return syscall3(SYS_LSEEK, fd: i64, off, whence: i64); }; // filesize — convenience: returns the byte length of an open fd by // seeking to the end and back. -1 on error. export fn filesize(fd: i32) i64 = { let end: i64 = lseek(fd, 0i64, SEEK_END); if (end < 0) { return -1i64; }; let r: i64 = lseek(fd, 0i64, SEEK_SET); if (r < 0) { return -1i64; }; return end; }; // readfull — keep reading until `n` bytes have arrived or the fd // closes early. Returns bytes read (0..=n) or -1 on read error. export fn readfull(fd: i32, buf: *u8, n: u64) i64 = { let got: u64 = 0u64; for (got < n) { let r: i64 = read(fd, buf + got, n - got); if (r < 0) { return -1i64; }; if (r == 0) { return got: i64; }; // short read: caller decides got += r: u64; }; return got: i64; }; // writefull — keep writing until `n` bytes have been accepted or the // fd refuses progress. Returns bytes written or -1. export fn writefull(fd: i32, buf: *u8, n: u64) i64 = { let sent: u64 = 0u64; for (sent < n) { let r: i64 = write(fd, buf + sent, n - sent); if (r < 0) { return -1i64; }; if (r == 0) { return sent: i64; }; sent += r: u64; }; return sent: i64; }; // ---- process and filesystem helpers used by the `ww` driver ---------- // access(2): returns 0 if the file is reachable, negative errno // otherwise. mode is the bitset described in (F_OK=0). export fn access(path: *u8, mode: i32) i32 = { return syscall2(SYS_ACCESS, path: i64, mode: i64): i32; }; // unlink(2). export fn unlink(path: *u8) i32 = { return syscall1(SYS_UNLINK, path: i64): i32; }; // getpid(2). Used by the driver to mint unique scratch paths. export fn getpid() i32 = { return syscall0(SYS_GETPID): i32; }; // fork(2): 0 in the child, child pid in the parent, negative errno // on failure. export fn fork() i32 = { return syscall0(SYS_FORK): i32; }; // execve(2): on success, does not return. export fn execve(path: *u8, argv: **u8, envp: **u8) i32 = { return syscall3(SYS_EXECVE, path: i64, argv: i64, envp: i64): i32; }; // wait4(2): wait for `pid` (or any child if -1), store status in // `*status_out`, return the pid that ended (or negative errno). export fn wait4(pid: i32, status_out: *i32, options: i32, rusage: *void) i32 = { return syscall4(SYS_WAIT4, pid: i64, status_out: i64, options: i64, rusage: i64): i32; }; // getcwd(2) — Linux flavour. Writes the NUL-terminated cwd into `buf` // and returns the number of bytes written (including the NUL), or a // negative errno. The driver uses it to expand `.` to the cwd's // basename for `ww build` / `ww test`. export fn getcwd(buf: *u8, n: u64) i64 = { return syscall2(SYS_GETCWD, buf: i64, n: i64); }; // getdents64(2) — Linux directory enumeration. The fd must be opened // with O_RDONLY on a directory. `buf` receives a packed sequence of // linux_dirent64 records: // // struct linux_dirent64 { // u64 d_ino; // 0..7 // i64 d_off; // 8..15 // u16 d_reclen; // 16..17 — total bytes for this record // u8 d_type; // 18 — DT_REG/DT_DIR/... // u8 d_name[]; // 19.. — NUL-terminated name + padding // }; // // Returns bytes written into `buf` (advance by d_reclen to walk), // 0 at end-of-directory, or a negative errno. export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = { return syscall3(SYS_GETDENTS64, fd: i64, buf: i64, n: i64); }; // strconv — number↔string conversions. Decimal i64 to/from a fixed // buffer. Two error idioms ship side by side: // - Plan 9 style (atoi64): tuple `(value, ok)`. Pre-dates the // tagged-union work; kept for callers that already use it. // - Hare style (parse64/parseu64): `(value | str)`. The error // variant carries a short, allocation-free message describing // why the parse failed. Prefer this for new code. // u64toa — write `v` in decimal into `buf` and return the byte count. // Unsigned-only so callers don't have to think about wraparound when // printing a u64 that happens to have the high bit set. export fn u64toa(buf: []u8, v: u64) i32 = { let tmp: [32]u8; let i: i32 = 0; let n: u64 = v; for (n > 0u64) { tmp[i] = ((n % 10u64) + 48u64): u8; n = n / 10u64; i += 1; }; if (i == 0) { tmp[0] = 48u8; i = 1; }; let out: i32 = 0; for (i > 0) { i -= 1; buf[out] = tmp[i]; out += 1; }; return out; }; export fn i64toa(buf: []u8, v: i64) i32 = { let neg: bool = false; let n: i64 = v; if (n < 0) { neg = true; n = -n; }; let tmp: [32]u8; let i: i32 = 0; for (n > 0) { tmp[i] = ((n % 10) + 48): u8; n = n / 10; i += 1; }; if (i == 0) { tmp[0] = 48u8; i = 1; }; let out: i32 = 0; if (neg) { buf[out] = 45u8; // '-' out += 1; }; for (i > 0) { i -= 1; buf[out] = tmp[i]; out += 1; }; return out; }; export fn atoi64(s: str) (i64, bool) = { let v: i64 = 0; let i: i32 = 0; let neg: bool = false; if (s.len > 0) { if (s[0] == 45u8) { neg = true; i = 1; }; }; if (i >= s.len) { return 0, false; }; for (i < s.len) { let c: u8 = s[i]; if (c < 48u8) { return 0, false; }; if (c > 57u8) { return 0, false; }; v = v * 10 + ((c: i64) - 48); i += 1; }; if (neg) { v = -v; }; return v, true; }; // parse64 — Hare-style fallible signed decimal parser. The value // variant is i64; the error variant is a short str describing the // reason. No locale, no whitespace, no underscores: a leading '-' is // the only non-digit accepted, and only at position 0. export fn parse64(s: str) (i64 | str) = { if (s.len == 0) { return "parse: empty"; }; let i: i32 = 0; let neg: bool = false; if (s[0] == 45u8) { neg = true; i = 1; }; if (i >= s.len) { return "parse: lone sign"; }; let v: i64 = 0; for (i < s.len) { let c: u8 = s[i]; if (c < 48u8) { return "parse: invalid digit"; }; if (c > 57u8) { return "parse: invalid digit"; }; v = v * 10 + ((c: i64) - 48); i += 1; }; if (neg) { v = -v; }; return v; }; // parseu64 — fallible unsigned decimal parser. No leading sign. export fn parseu64(s: str) (u64 | str) = { if (s.len == 0) { return "parse: empty"; }; let v: u64 = 0u64; let i: i32 = 0; for (i < s.len) { let c: u8 = s[i]; if (c < 48u8) { return "parse: invalid digit"; }; if (c > 57u8) { return "parse: invalid digit"; }; v = v * 10u64 + ((c: u64) - 48u64); i += 1; }; return v; }; // ascii — byte-class predicates and case folding for the ASCII range. // Matches Hare's ascii::isdigit family. Bytes outside 0..127 always // answer `false`. The lexer hot path uses these inline; they are // expected to inline to a couple of compares. export fn isdigit(c: u8) bool = { if (c < 48u8) { return false; }; if (c > 57u8) { return false; }; return true; }; export fn isupper(c: u8) bool = { if (c < 65u8) { return false; }; if (c > 90u8) { return false; }; return true; }; export fn islower(c: u8) bool = { if (c < 97u8) { return false; }; if (c > 122u8) { return false; }; return true; }; export fn isalpha(c: u8) bool = { if (isupper(c)) { return true; }; return islower(c); }; export fn isalnum(c: u8) bool = { if (isalpha(c)) { return true; }; return isdigit(c); }; // isspace — the C/Hare set: space, tab, NL, VT, FF, CR. export fn isspace(c: u8) bool = { if (c == 32u8) { return true; }; // ' ' if (c == 9u8) { return true; }; // '\t' if (c == 10u8) { return true; }; // '\n' if (c == 11u8) { return true; }; // '\v' if (c == 12u8) { return true; }; // '\f' if (c == 13u8) { return true; }; // '\r' return false; }; export fn ishex(c: u8) bool = { if (isdigit(c)) { return true; }; if (c >= 65u8) { if (c <= 70u8) { return true; }; // 'A'..'F' }; if (c >= 97u8) { if (c <= 102u8) { return true; }; // 'a'..'f' }; return false; }; // digitval — value of `c` as a hex/decimal digit, or -1 if not one. // Useful when scanning numeric literals. export fn digitval(c: u8) i32 = { if (isdigit(c)) { return (c - 48u8): i32; }; if (c >= 65u8) { if (c <= 70u8) { return ((c - 65u8) + 10u8): i32; }; }; if (c >= 97u8) { if (c <= 102u8) { return ((c - 97u8) + 10u8): i32; }; }; return -1; }; // isidstart / isidpart — identifier classes used by the lexer. // Alpha or '_' starts; alnum or '_' continues. export fn isidstart(c: u8) bool = { if (isalpha(c)) { return true; }; if (c == 95u8) { return true; }; // '_' return false; }; export fn isidpart(c: u8) bool = { if (isalnum(c)) { return true; }; if (c == 95u8) { return true; }; return false; }; // tolower / toupper — fold ASCII case. Non-letters pass through. export fn tolower(c: u8) u8 = { if (isupper(c)) { return c + 32u8; }; return c; }; export fn toupper(c: u8) u8 = { if (islower(c)) { return c - 32u8; }; return c; }; // selfhost/test/smoke.ww — end-to-end smoke for the selfhost path. // // Exercises the patterns the real ww-side compiler port will use: // - bump arena allocator (mem.ww shape) // - error idiom (T | str) // - struct of fn pointers + ctx pointer (the io.stream-style // polymorphism we use instead of interfaces) // - byte-level scanning that mirrors the hot path inside lex.ww // - strconv round-trip via the real stdlib // // `main` returns 42 when every check passes, 1..N on failure // indicating which probe broke. The 990_selfhost test asserts 42. // // Note: only stack-local mutable state. Top-level `let` mutation // requires a writable .data segment in w6l, which is a separate // task; until then we exercise polymorphism via ctx pointers, which // is what the real port wants anyway. use os; use strconv; use ascii; // --- bump arena --------------------------------------------------------- type arena = struct { buf: *u8, off: u64, cap: u64, }; // In-place init. Returning a 24-byte struct by value isn't yet // supported in w6c (SysV requires a hidden return-slot pointer for // structs >16 bytes), so we initialize through a pointer like the // real compiler does today. fn arena_init(a: *arena, buf: *u8, cap: u64) void = { a.buf = buf; a.off = 0u64; a.cap = cap; }; fn arena_alloc(a: *arena, n: u64) *u8 = { if (n > a.cap - a.off) { return nil; }; let p: *u8 = a.buf + a.off; a.off += n; return p; }; // --- (i32 | str) error idiom ------------------------------------------- fn checked_div(num: i32, den: i32) (i32 | str) = { if (den == 0) { return "div by zero"; }; return num / den; }; // --- struct-of-fn-pointer polymorphism --------------------------------- // // A trivial "writer" abstraction: a function pointer plus a context. // This mirrors how io.stream / Plan 9 Bio work. The ctx pointer lets // the implementation own its own state without a global. type counter = struct { n: i32, }; type writer = struct { ctx: *void, emit: fn(ctx: *void, b: u8) void, }; fn count_emit(ctx: *void, b: u8) void = { let c: *counter = ctx: *counter; c.n += 1; }; // --- byte scanner like lex.ww's hot path ------------------------------- fn count_digits(s: str) i32 = { let i: i32 = 0; let n: i32 = 0; for (i < s.len) { let c: u8 = s[i]; if (c >= 48u8) { if (c <= 57u8) { n += 1; }; }; i += 1; }; return n; }; // --- entry -------------------------------------------------------------- export fn main() i32 = { // Probe 1 — arena hands out distinct pointers, refuses oversize. let buf: [256]u8; let a: arena; arena_init(&a, buf.ptr, 256u64); let p1: *u8 = arena_alloc(&a, 32u64); let p2: *u8 = arena_alloc(&a, 32u64); if (p1 == nil) { return 1; }; if (p2 == nil) { return 2; }; if (p1 == p2) { return 3; }; let p3: *u8 = arena_alloc(&a, 1024u64); if (p3 != nil) { return 4; }; // Probe 2 — error union both ways. let r_ok: (i32 | str) = checked_div(84, 2); let r_bad: (i32 | str) = checked_div(1, 0); let acc: i32 = 0; match (r_ok) { case let v: i32 => acc = v; case let e: str => return 5; }; if (acc != 42) { return 6; }; match (r_bad) { case let v: i32 => return 7; case let e: str => acc = e.len: i32; }; if (acc != 11) { return 8; }; // len("div by zero") == 11 // Probe 3 — struct-of-fn-pointer dispatch via ctx pointer. let c: counter = counter { n = 0 }; let w: writer = writer { ctx = (&c): *void, emit = count_emit }; w.emit(w.ctx, 65u8); w.emit(w.ctx, 66u8); w.emit(w.ctx, 67u8); if (c.n != 3) { return 9; }; // Probe 4 — byte scan over a literal. let dn: i32 = count_digits("ww123abc"); if (dn != 3) { return 10; }; // Probe 5 — strconv round-trip via the real stdlib. let outbuf: [32]u8; let nb: i32 = strconv.i64toa(outbuf[0:32], 4242i64); if (nb != 4) { return 11; }; if (outbuf[0] != 52u8) { return 12; }; // '4' if (outbuf[3] != 50u8) { return 13; }; // '2' // Probe 6 — ascii classifications. if (!ascii.isdigit(53u8)) { return 14; }; // '5' if (ascii.isdigit(65u8)) { return 15; }; // 'A' is not a digit if (!ascii.isalpha(122u8)) { return 16; }; // 'z' if (!ascii.isidstart(95u8)) { return 17; }; // '_' if (!ascii.isidpart(48u8)) { return 18; }; // '0' is part if (ascii.digitval(70u8) != 15) { return 19; }; // 'F' = 15 if (ascii.tolower(65u8) != 97u8) { return 20; }; // 'A' -> 'a' // Probe 7 — file open/read via the new os APIs. /proc/self/cmdline // always exists on Linux, no write side, and is non-empty. let path: str = "/proc/self/cmdline"; let fd_or_err: (i32 | str) = os.tryopen(path.ptr, os.O_RDONLY, 0i32); let fd: i32 = 0; match (fd_or_err) { case let v: i32 => fd = v; case let e: str => return 21; }; let rbuf: [128]u8; let n: i64 = os.readfull(fd, rbuf.ptr, 128u64); os.close(fd); if (n <= 0i64) { return 22; }; return 42; };