os: graduate tryopen/trywrite/tryread to (T | oserror)

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.
This commit is contained in:
2026-05-12 02:42:49 +09:00
parent 594a2bad62
commit d9041ab45e
7 changed files with 76 additions and 72 deletions

View File

@@ -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) = {

View File

@@ -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) = {

View File

@@ -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);

View File

@@ -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);

View File

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

View File

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

View File

@@ -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. */