From d9041ab45e75e00e718ee84aa622015083bcba19 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 12 May 2026 02:42:49 +0900 Subject: [PATCH] os: graduate tryopen/trywrite/tryread to (T | oserror) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Final piece of the os module graduation: the three try* wrappers move off the (T | str) placeholder shape. `oserror` becomes a real Hare-style error type (`!i64` instead of plain `i64`), so it's picked up by ?-propagation as the error half without callers having to name it. tryread: (i64 | oserror) was (i64 | str) trywrite: (i64 | oserror) was (i64 | str) tryopen: (i32 | oserror) was (i32 | str) Callsites updated: wwdump uses tryopen; the e2e trywrite probe matches on os.oserror and validates -EBADF for a bad fd (-9 instead of the old "write failed" string length). selfhost/test/smoke.ww switched to raw os.open(2) instead of os.tryopen for probe 7 — same reason as the os.readall switch in the prior commit: probe 6 in 990_selfhost compiles smoke.ww standalone, and cross-module type refs like `os.oserror` don't resolve in that mode. --- lib/os/os.ww | 25 ++++++++++--------- selfhost/cmd/w6c/main.combined.ww | 25 ++++++++++--------- selfhost/cmd/wwdump/main.combined.ww | 29 +++++++++++----------- selfhost/cmd/wwdump/main.ww | 4 +-- selfhost/test/smoke.combined.ww | 37 ++++++++++++++-------------- selfhost/test/smoke.ww | 12 ++++----- test/wcc/700_e2e.c | 16 ++++++------ 7 files changed, 76 insertions(+), 72 deletions(-) diff --git a/lib/os/os.ww b/lib/os/os.ww index 70cacfe5..0b68988a 100644 --- a/lib/os/os.ww +++ b/lib/os/os.ww @@ -73,18 +73,18 @@ 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) = { +// Fallible wrappers. The error variant is `oserror` (an i64 carrying +// -errno). The sum type makes success/failure explicit and lets +// callers `?` the result up the stack. +export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { let r: i64 = read(fd, buf, n); - if (r < 0) { return "read failed"; }; + if (r < 0) { return r: oserror; }; return r; }; -export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | str) = { +export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { let r: i64 = write(fd, buf, n); - if (r < 0) { return "write failed"; }; + if (r < 0) { return r: oserror; }; return r; }; @@ -96,9 +96,9 @@ 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) = { +export fn tryopen(path: *u8, flags: i32, mode: i32) (i32 | oserror) = { let fd: i32 = open(path, flags, mode); - if (fd < 0) { return "open failed"; }; + if (fd < 0) { return fd: i64: oserror; }; return fd; }; @@ -111,9 +111,10 @@ export fn lseek(fd: i32, off: i64, whence: i32) i64 = { // oserror — the underlying errno from a failed syscall, as a // negative i64 (Linux's int convention; e.g. -2 = ENOENT). The -// NAMED-i64 alias makes it a distinct variant tag from a "good" -// i64 byte count. Hare's analogue is errors::errno. -export type oserror = i64; +// `!`-flagged alias makes ?-propagation pick this variant as the +// error half of any (T | oserror) shape. Hare's analogue is +// errors::errno carried inside io::error. +export type oserror = !i64; // filesize — byte length of an open fd via lseek-to-end-and-back. export fn filesize(fd: i32) (i64 | oserror) = { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 37ff58da..a12ba3d4 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -74,18 +74,18 @@ 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) = { +// Fallible wrappers. The error variant is `oserror` (an i64 carrying +// -errno). The sum type makes success/failure explicit and lets +// callers `?` the result up the stack. +export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { let r: i64 = read(fd, buf, n); - if (r < 0) { return "read failed"; }; + if (r < 0) { return r: oserror; }; return r; }; -export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | str) = { +export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { let r: i64 = write(fd, buf, n); - if (r < 0) { return "write failed"; }; + if (r < 0) { return r: oserror; }; return r; }; @@ -97,9 +97,9 @@ 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) = { +export fn tryopen(path: *u8, flags: i32, mode: i32) (i32 | oserror) = { let fd: i32 = open(path, flags, mode); - if (fd < 0) { return "open failed"; }; + if (fd < 0) { return fd: i64: oserror; }; return fd; }; @@ -112,9 +112,10 @@ export fn lseek(fd: i32, off: i64, whence: i32) i64 = { // oserror — the underlying errno from a failed syscall, as a // negative i64 (Linux's int convention; e.g. -2 = ENOENT). The -// NAMED-i64 alias makes it a distinct variant tag from a "good" -// i64 byte count. Hare's analogue is errors::errno. -export type oserror = i64; +// `!`-flagged alias makes ?-propagation pick this variant as the +// error half of any (T | oserror) shape. Hare's analogue is +// errors::errno carried inside io::error. +export type oserror = !i64; // filesize — byte length of an open fd via lseek-to-end-and-back. export fn filesize(fd: i32) (i64 | oserror) = { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 935f4220..a60ce817 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -74,18 +74,18 @@ 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) = { +// Fallible wrappers. The error variant is `oserror` (an i64 carrying +// -errno). The sum type makes success/failure explicit and lets +// callers `?` the result up the stack. +export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { let r: i64 = read(fd, buf, n); - if (r < 0) { return "read failed"; }; + if (r < 0) { return r: oserror; }; return r; }; -export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | str) = { +export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { let r: i64 = write(fd, buf, n); - if (r < 0) { return "write failed"; }; + if (r < 0) { return r: oserror; }; return r; }; @@ -97,9 +97,9 @@ 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) = { +export fn tryopen(path: *u8, flags: i32, mode: i32) (i32 | oserror) = { let fd: i32 = open(path, flags, mode); - if (fd < 0) { return "open failed"; }; + if (fd < 0) { return fd: i64: oserror; }; return fd; }; @@ -112,9 +112,10 @@ export fn lseek(fd: i32, off: i64, whence: i32) i64 = { // oserror — the underlying errno from a failed syscall, as a // negative i64 (Linux's int convention; e.g. -2 = ENOENT). The -// NAMED-i64 alias makes it a distinct variant tag from a "good" -// i64 byte count. Hare's analogue is errors::errno. -export type oserror = i64; +// `!`-flagged alias makes ?-propagation pick this variant as the +// error half of any (T | oserror) shape. Hare's analogue is +// errors::errno carried inside io::error. +export type oserror = !i64; // filesize — byte length of an open fd via lseek-to-end-and-back. export fn filesize(fd: i32) (i64 | oserror) = { @@ -8091,11 +8092,11 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; }; - let fdorerr: (i32 | str) = os.tryopen(path, os.O_RDONLY, 0i32); + let fdorerr: (i32 | os.oserror) = os.tryopen(path, os.O_RDONLY, 0i32); let fd: i32 = -1; match (fdorerr) { case let v: i32 => fd = v; - case let e: str => { + case let e: os.oserror => { os.write(2, "wwdump: cannot open ".ptr, 20u64); os.write(2, path, argstrlen(path): u64); os.write(2, "\n".ptr, 1u64); diff --git a/selfhost/cmd/wwdump/main.ww b/selfhost/cmd/wwdump/main.ww index 78f7f709..22fa2685 100644 --- a/selfhost/cmd/wwdump/main.ww +++ b/selfhost/cmd/wwdump/main.ww @@ -74,11 +74,11 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; }; - let fdorerr: (i32 | str) = os.tryopen(path, os.O_RDONLY, 0i32); + let fdorerr: (i32 | os.oserror) = os.tryopen(path, os.O_RDONLY, 0i32); let fd: i32 = -1; match (fdorerr) { case let v: i32 => fd = v; - case let e: str => { + case let e: os.oserror => { os.write(2, "wwdump: cannot open ".ptr, 20u64); os.write(2, path, argstrlen(path): u64); os.write(2, "\n".ptr, 1u64); diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 6fdda02d..96819efb 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -74,18 +74,18 @@ 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) = { +// Fallible wrappers. The error variant is `oserror` (an i64 carrying +// -errno). The sum type makes success/failure explicit and lets +// callers `?` the result up the stack. +export fn tryread(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { let r: i64 = read(fd, buf, n); - if (r < 0) { return "read failed"; }; + if (r < 0) { return r: oserror; }; return r; }; -export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | str) = { +export fn trywrite(fd: i32, buf: *u8, n: u64) (i64 | oserror) = { let r: i64 = write(fd, buf, n); - if (r < 0) { return "write failed"; }; + if (r < 0) { return r: oserror; }; return r; }; @@ -97,9 +97,9 @@ 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) = { +export fn tryopen(path: *u8, flags: i32, mode: i32) (i32 | oserror) = { let fd: i32 = open(path, flags, mode); - if (fd < 0) { return "open failed"; }; + if (fd < 0) { return fd: i64: oserror; }; return fd; }; @@ -112,9 +112,10 @@ export fn lseek(fd: i32, off: i64, whence: i32) i64 = { // oserror — the underlying errno from a failed syscall, as a // negative i64 (Linux's int convention; e.g. -2 = ENOENT). The -// NAMED-i64 alias makes it a distinct variant tag from a "good" -// i64 byte count. Hare's analogue is errors::errno. -export type oserror = i64; +// `!`-flagged alias makes ?-propagation pick this variant as the +// error half of any (T | oserror) shape. Hare's analogue is +// errors::errno carried inside io::error. +export type oserror = !i64; // filesize — byte length of an open fd via lseek-to-end-and-back. export fn filesize(fd: i32) (i64 | oserror) = { @@ -572,12 +573,12 @@ export fn main() i32 = { // 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; - }; + // Use raw os.open here (returns i32 with -errno) for the same + // reason as os.read below: probe 6 in 990_selfhost compiles + // smoke.ww standalone (no `use` expansion), so cross-module type + // references like `os.oserror` don't resolve at that step. + let fd: i32 = os.open(path.ptr, os.O_RDONLY, 0i32); + if (fd < 0) { return 21; }; let rbuf: [128]u8; // Use raw os.read here (single syscall, plain i64) instead of // os.readall: the 990 cgen-match probe compiles smoke.ww diff --git a/selfhost/test/smoke.ww b/selfhost/test/smoke.ww index bcf0c9ff..68b1561b 100644 --- a/selfhost/test/smoke.ww +++ b/selfhost/test/smoke.ww @@ -152,12 +152,12 @@ export fn main() i32 = { // 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; - }; + // Use raw os.open here (returns i32 with -errno) for the same + // reason as os.read below: probe 6 in 990_selfhost compiles + // smoke.ww standalone (no `use` expansion), so cross-module type + // references like `os.oserror` don't resolve at that step. + let fd: i32 = os.open(path.ptr, os.O_RDONLY, 0i32); + if (fd < 0) { return 21; }; let rbuf: [128]u8; // Use raw os.read here (single syscall, plain i64) instead of // os.readall: the 990 cgen-match probe compiles smoke.ww diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index 91148270..14ca3265 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -747,26 +747,26 @@ static const struct row rows[] = { " return acc;\n" "};", 19 }, /* End-to-end stdlib usage: pull in lib/os and exercise the - * fallible API tryread/trywrite returning (i64 | str) over a - * real syscall. Validates that imported tagged-union returns - * survive the linker as well as the call ABI. */ + * fallible API tryread/trywrite returning (i64 | oserror) over + * a real syscall. oserror carries -errno; on a bad fd we expect + * -EBADF (-9). */ { "use os;\n" "fn main() i32 = {\n" " let buf: [3]u8;\n" " buf[0] = 88: u8;\n" - " let ok: (i64 | str) = os.trywrite(1, buf.ptr, 1u64);\n" - " let bad: (i64 | str) = os.trywrite(999: i32, buf.ptr, 1u64);\n" + " let ok: (i64 | os.oserror) = os.trywrite(1, buf.ptr, 1u64);\n" + " let bad: (i64 | os.oserror) = os.trywrite(999: i32, buf.ptr, 1u64);\n" " let acc: i32 = 0;\n" " match (ok) {\n" " case let n: i64 => acc += n: i32;\n" - " case let e: str => acc += -100;\n" + " case let e: os.oserror => acc += -100;\n" " };\n" " match (bad) {\n" " case let n: i64 => acc += -100;\n" - " case let e: str => acc += e.len: i32;\n" + " case let e: os.oserror => acc += (- (e: i64)): i32;\n" " };\n" " return acc;\n" - "};", 13 }, /* 1 byte written to fd 1, plus len(\"write failed\")=12 */ + "};", 10 }, /* 1 byte written + 9 (EBADF) */ /* strconv.stoi64: fallible signed decimal, graduated to * (i64 | invalid | overflow). invalid carries the offending * index; overflow is the void variant. */