diff --git a/Makefile b/Makefile index daf96951..1e144c5a 100644 --- a/Makefile +++ b/Makefile @@ -353,6 +353,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_hex_run $(BIN)/test_utf8_run $(BIN)/test_bytes_run \ $(BIN)/test_decimal_run $(BIN)/test_stof_run $(BIN)/test_ftos_run \ $(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \ + $(BIN)/test_errno_run \ $(BIN)/test_base32_run $(BIN)/test_base64_run \ $(BIN)/test_adler32_run $(BIN)/test_crc16_run \ $(BIN)/test_crc32_run $(BIN)/test_crc64_run \ @@ -1306,6 +1307,10 @@ $(BIN)/test_getopt_run: test/wcc/982_getopt_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_errno_run: test/wcc/902_errno_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_base32_run: test/wcc/983_base32_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/lib/CLAUDE.md b/lib/CLAUDE.md index 0fcb5d8e..916576de 100644 --- a/lib/CLAUDE.md +++ b/lib/CLAUDE.md @@ -53,7 +53,12 @@ Modules with intentional divergence: - `lib/os` and `lib/net` stay below the Hare abstraction — they are syscall wrappers, not the high-level `io::handle` / `net::socket` API. Use them as the foundation that `lib/io` and the buffered - layers build on. + layers build on. `lib/os` also plays Hare's `sys` role (ww folds + `sys` into `os`), so it is the import floor: lib/os must never import + io OR errors — everything points down to os; os's only edge is the + import-free time leaf. This is what lets `errors` import `os` (for + `os.errno` / `os.strerror`) without a cycle, mirroring Hare's + `sys ← errors`, `sys ← io`. - `lib/io` keeps the ww-specific `stream` struct (vtable of fn pointers, no closures, no methods). The Hare `io::handle` family needs language features we don't have yet. diff --git a/lib/errors/errnotest.ww b/lib/errors/errnotest.ww new file mode 100644 index 00000000..521108e9 --- /dev/null +++ b/lib/errors/errnotest.ww @@ -0,0 +1,123 @@ +// errnotest — exercises [[errors.errno]] and the [[errors.opaque_]] +// tail. Run with `out/bin/ww run lib/errors/errnotest.ww`. +// +// Parallel `[N]T` arrays of (errno, expected-tag) rather than a +// `[N]struct{...}` table — the cstage cgen's chained `arr[i].field` +// store gap (task #6). [[errtag]] collapses each named-void variant to +// a small int so the row body is a single integer compare. + +package errors; + +import errors; +import os; + +@symbol("rt_syscall") fn syscall1ww(num: i64, a: i64) i64; +fn doexit(code: i32) void = { + syscall1ww(60i64, code: i64); +}; + +// signalled — bumped by main before each test so a failing exit code +// pinpoints the offending case. +let signalled: i32 = 0; +fn fail() void = { doexit(signalled + 10); }; + +fn streq(a: str, b: str) bool = { + if (a.len != b.len) { return false; }; + let i: i32 = 0; + for (i < a.len) { + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return true; +}; + +// errtag — the union discriminant as a stable int. opaque_ is 99 so a +// misrouted named condition can't masquerade as the unmapped tail. +fn errtag(e: errors.error) int = { + match (e) { + case errors.busy => return 1; + case errors.exists => return 2; + case errors.invalid => return 3; + case errors.noaccess => return 4; + case errors.noentry => return 5; + case errors.overflow => return 6; + case errors.unsupported => return 7; + case errors.timeout => return 8; + case errors.cancelled => return 9; + case errors.refused => return 10; + case errors.interrupted => return 11; + case errors.again => return 12; + case errors.netunreachable => return 13; + case let o: errors.opaque_ => return 99; + }; +}; + +// ---- errno → named condition -------------------------------------- +@test fn mapping() void = { + let ins: [12]os.errno; + ins[0] = os.ECONNREFUSED; + ins[1] = os.ECANCELED; + ins[2] = os.EOVERFLOW; + ins[3] = os.EACCES; + ins[4] = os.EINVAL; + ins[5] = os.EEXIST; + ins[6] = os.ENOENT; + ins[7] = os.ETIMEDOUT; + ins[8] = os.EBUSY; + ins[9] = os.EINTR; + ins[10] = os.EAGAIN; + ins[11] = os.ENETUNREACH; + + let exp: [12]int; + exp[0] = 10; // refused + exp[1] = 9; // cancelled + exp[2] = 6; // overflow + exp[3] = 4; // noaccess + exp[4] = 3; // invalid + exp[5] = 2; // exists + exp[6] = 5; // noentry + exp[7] = 8; // timeout + exp[8] = 1; // busy + exp[9] = 11; // interrupted + exp[10] = 12; // again + exp[11] = 13; // netunreachable + + let i: i32 = 0; + for (i < 12) { + // store-to-local, not inline errtag(errors.errno(ins[i])): the + // inline form hits #222 (large >4-eightbyte union sret-arg + // clobber, cs==ww). store-then-consume is the idiomatic shape + // regardless — it is how errno is actually used (cf + // lib/io/stream.ww:53, and Hare's errors/rt.ha callers). + let er: errors.error = errors.errno(ins[i]); + if (errtag(er) != exp[i]) { fail(); }; + i += 1; + }; +}; + +// ---- unmapped errno → opaque_ tail -------------------------------- +@test fn opaquetail() void = { + // EIO (5) is outside the mapped set, so it wraps opaque_. + let e: errors.error = errors.errno(5); + if (errtag(e) != 99) { fail(); }; + match (e) { + case let o: errors.opaque_ => + if (!streq((*o.strerror)(&o.data), "Unknown error")) { fail(); }; + case => + fail(); + }; +}; + +// ---- os.strerror mapped path -------------------------------------- +@test fn strerrortext() void = { + if (!streq(os.strerror(os.ENOENT), "No such file or directory")) { fail(); }; + if (!streq(os.strerror(os.EINVAL), "Invalid argument")) { fail(); }; + if (!streq(os.strerror(5), "Unknown error")) { fail(); }; +}; + +export fn main() i32 = { + signalled = 1; mapping(); + signalled = 2; opaquetail(); + signalled = 3; strerrortext(); + return 0; +}; diff --git a/lib/errors/errors.ww b/lib/errors/errors.ww index 231f6a75..5378c030 100644 --- a/lib/errors/errors.ww +++ b/lib/errors/errors.ww @@ -3,15 +3,23 @@ // Named-void tagged-union variants, so `(T | errors.invalid | ...)` // composes with every other module's error surface at the tag level. // Per-domain errors (io.eof, io.closed, io.underread, strconv.overflow, -// ...) live in their own modules; this module ships only the generic -// conditions Hare's errors:: exports. -// -// Subset shipped today; add more from Hare's errors/common.ha as callers -// need them. +// ...) live in their own modules; this module ships the generic +// conditions Hare's errors:: exports plus the [[errno]] bridge that +// wraps a raw [[os.errno]] into a portable [[error]]. -// A function was called with an invalid combination of arguments. package errors; +import os; + +// The named conditions, ordered per ref/hare/errors/common.ha. + +// The requested resource is not available. +export type busy = !void; + +// An attempt was made to create a resource which already exists. +export type exists = !void; + +// A function was called with an invalid combination of arguments. export type invalid = !void; // The user does not have permission to use this resource. @@ -20,8 +28,108 @@ export type noaccess = !void; // An entry was requested which does not exist. export type noentry = !void; -// An attempt was made to create a resource which already exists. -export type exists = !void; +// The requested operation caused a numeric overflow condition. +export type overflow = !void; // The requested operation is not supported. export type unsupported = !void; + +// The requested operation timed out. +export type timeout = !void; + +// The requested operation was cancelled. +export type cancelled = !void; + +// A connection attempt was refused. +export type refused = !void; + +// An operation was interrupted. +export type interrupted = !void; + +// The user should attempt an operation again. +export type again = !void; + +// Network unreachable +export type netunreachable = !void; + +// Up to 24 bytes of arbitrary, 8-byte-aligned storage for the opaque +// error type's domain-specific data. ref/hare/errors/opaque.ha:41. +export type opaque_data = [3]u64; + +// An "opaque" error wraps an implementation-specific underlying error +// behind a function that stringifies it plus a small storage area. +// ref/hare/errors/opaque.ha:32. The `strerror` field keeps Hare's `*fn` +// pointer (filled via `(&fn): *fn(...)`, mirroring io's `*reader` slot, +// lib/io/types.ww:53); only Hare's `const` is dropped (ww has none, cf +// lib/io/types.ww:57). +export type opaque_ = !struct { + strerror: *fn(op: *opaque_data) str, + data: opaque_data, +}; + +// A tagged union of all error types. ref/hare/errors/common.ha:43. +// Enumerated explicitly rather than spread via Hare's `...error` — +// ww's spread-flatten layout is the deferred #199b / #204 fix. +export type error = !( + busy | + exists | + invalid | + noaccess | + noentry | + overflow | + unsupported | + timeout | + cancelled | + refused | + interrupted | + again | + netunreachable | + opaque_ +); + +// Wraps an [[os.errno]] to produce an [[error]], which may be +// [[opaque_]]. ref/hare/errors/rt.ha:9. The mapped errnos become named +// conditions; the unmapped tail is carried in an [[opaque_]] whose +// stringifier defers to [[os.strerror]]. +export fn errno(e: os.errno) error = { + // A `!void` condition is returned by instance, not by name: a bare + // `return refused;` would emit an undefined symbol reference (the + // type, not a value). cf lib/io/stream.ww:53-56. The instance + // auto-widens to the [[error]] union on return. + switch (e) { + case os.ECONNREFUSED: { let r: refused; return r; }; + case os.ECANCELED: { let r: cancelled; return r; }; + case os.EOVERFLOW: { let r: overflow; return r; }; + case os.EACCES: { let r: noaccess; return r; }; + case os.EINVAL: { let r: invalid; return r; }; + case os.EEXIST: { let r: exists; return r; }; + case os.ENOENT: { let r: noentry; return r; }; + case os.ETIMEDOUT: { let r: timeout; return r; }; + case os.EBUSY: { let r: busy; return r; }; + case os.EINTR: { let r: interrupted; return r; }; + case os.EAGAIN: { let r: again; return r; }; + case os.ENETUNREACH: { let r: netunreachable; return r; }; + }; + + // An unmapped errno falls through the switch into the opaque_ wrap + // below. ww switches aren't required exhaustive, so Hare's terminal + // `case => void;` (rt.ha:24, present only to satisfy exhaustiveness) + // is dropped rather than written as an explicit no-op default — + // matching the sibling [[os.strerror]] fall-through. + // + // ww has no `static assert`; Hare guards size(errno) <= + // size(opaque_data) there. The invariant holds structurally: + // opaque_data is [3]u64 (24B), errno is one machine int. + let err: opaque_; + err.strerror = (&rt_strerror): *fn(op: *opaque_data) str; + let ptr = (&err.data): *os.errno; + *ptr = e; + return err; +}; + +// rt_strerror — the [[opaque_]] stringifier for an errno wrapped by +// [[errno]]. ref/hare/errors/rt.ha:31. +fn rt_strerror(op: *opaque_data) str = { + let e = (op): *os.errno; + return os.strerror(*e); +}; diff --git a/lib/os/os.ww b/lib/os/os.ww index 8e42e1a3..2800e037 100644 --- a/lib/os/os.ww +++ b/lib/os/os.ww @@ -167,6 +167,57 @@ export fn lseek(fd: i32, off: i64, w: whence) i64 = { // errors::errno carried inside io::error. export type oserror = !i64; +// errno — the raw Linux errno as a positive code (ref/hare/sys/+linux/ +// errno.ha:5, `errno = !int`). ww folds Hare's `sys` role into os +// (lib/CLAUDE.md), so the sys::errno machinery lands here. Spelled i32 +// rather than int: Linux errnos are kernel ints (32-bit), keeping os's +// kernel-facing surface uniformly i32. Distinct from [[oserror]] (!i64, +// the syscall's *negative* raw return) — the two model different +// things, so they are not unified; the negative→positive normalization +// lives at the oserror→errors.error boundary in those callers. +export type errno = !i32; + +// Mapped errno values, ref/hare/sys/+linux/errno.ha:559-682. Positive, +// matching Hare's defs (the kernel returns -N; the wrap-to-positive is +// the caller's concern). Subset: exactly the errnos [[errors.errno]] +// maps to a named condition; grow as callers surface more. +export def ENOENT: errno = 2; +export def EINTR: errno = 4; +export def EAGAIN: errno = 11; +export def EACCES: errno = 13; +export def EBUSY: errno = 16; +export def EEXIST: errno = 17; +export def EINVAL: errno = 22; +export def EOVERFLOW: errno = 75; +export def ENETUNREACH: errno = 101; +export def ETIMEDOUT: errno = 110; +export def ECONNREFUSED: errno = 111; +export def ECANCELED: errno = 125; + +// strerror — human-readable text for an [[errno]] (Hare's +// sys::strerror, ref/hare/sys/+linux/errno.ha:18). FAITHFUL MINIMAL +// SUBSET: the mapped errnos above plus a generic fallback; grow the +// switch as callers surface more (lib/CLAUDE.md documented-subset, not +// a workaround). Messages verbatim from the reference. Hare's +// unknown_errno formats the numeric value; that is deferred. +export fn strerror(err: errno) str = { + switch (err) { + case ENOENT: return "No such file or directory"; + case EINTR: return "Interrupted system call"; + case EAGAIN: return "Resource temporarily unavailable"; + case EACCES: return "Permission denied"; + case EBUSY: return "Device or resource busy"; + case EEXIST: return "File exists"; + case EINVAL: return "Invalid argument"; + case EOVERFLOW: return "Value too large for defined data type"; + case ENETUNREACH: return "Network is unreachable"; + case ETIMEDOUT: return "Connection timed out"; + case ECONNREFUSED: return "Connection refused"; + case ECANCELED: return "Operation canceled"; + }; + return "Unknown error"; +}; + // filesize — byte length of an open fd via lseek-to-end-and-back. export fn filesize(fd: i32) (i64 | oserror) = { let end: i64 = lseek(fd, 0i64, whence.END); diff --git a/selfhost/cmd/w6a/main.combined.ww b/selfhost/cmd/w6a/main.combined.ww index fff171ac..d54ae563 100644 --- a/selfhost/cmd/w6a/main.combined.ww +++ b/selfhost/cmd/w6a/main.combined.ww @@ -265,6 +265,57 @@ export fn lseek(fd: i32, off: i64, w: whence) i64 = { // errors::errno carried inside io::error. export type oserror = !i64; +// errno — the raw Linux errno as a positive code (ref/hare/sys/+linux/ +// errno.ha:5, `errno = !int`). ww folds Hare's `sys` role into os +// (lib/CLAUDE.md), so the sys::errno machinery lands here. Spelled i32 +// rather than int: Linux errnos are kernel ints (32-bit), keeping os's +// kernel-facing surface uniformly i32. Distinct from [[oserror]] (!i64, +// the syscall's *negative* raw return) — the two model different +// things, so they are not unified; the negative→positive normalization +// lives at the oserror→errors.error boundary in those callers. +export type errno = !i32; + +// Mapped errno values, ref/hare/sys/+linux/errno.ha:559-682. Positive, +// matching Hare's defs (the kernel returns -N; the wrap-to-positive is +// the caller's concern). Subset: exactly the errnos [[errors.errno]] +// maps to a named condition; grow as callers surface more. +export def ENOENT: errno = 2; +export def EINTR: errno = 4; +export def EAGAIN: errno = 11; +export def EACCES: errno = 13; +export def EBUSY: errno = 16; +export def EEXIST: errno = 17; +export def EINVAL: errno = 22; +export def EOVERFLOW: errno = 75; +export def ENETUNREACH: errno = 101; +export def ETIMEDOUT: errno = 110; +export def ECONNREFUSED: errno = 111; +export def ECANCELED: errno = 125; + +// strerror — human-readable text for an [[errno]] (Hare's +// sys::strerror, ref/hare/sys/+linux/errno.ha:18). FAITHFUL MINIMAL +// SUBSET: the mapped errnos above plus a generic fallback; grow the +// switch as callers surface more (lib/CLAUDE.md documented-subset, not +// a workaround). Messages verbatim from the reference. Hare's +// unknown_errno formats the numeric value; that is deferred. +export fn strerror(err: errno) str = { + switch (err) { + case ENOENT: return "No such file or directory"; + case EINTR: return "Interrupted system call"; + case EAGAIN: return "Resource temporarily unavailable"; + case EACCES: return "Permission denied"; + case EBUSY: return "Device or resource busy"; + case EEXIST: return "File exists"; + case EINVAL: return "Invalid argument"; + case EOVERFLOW: return "Value too large for defined data type"; + case ENETUNREACH: return "Network is unreachable"; + case ETIMEDOUT: return "Connection timed out"; + case ECONNREFUSED: return "Connection refused"; + case ECANCELED: return "Operation canceled"; + }; + return "Unknown error"; +}; + // filesize — byte length of an open fd via lseek-to-end-and-back. export fn filesize(fd: i32) (i64 | oserror) = { let end: i64 = lseek(fd, 0i64, whence.END); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 79a7f0d4..e3eaee42 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -265,6 +265,57 @@ export fn lseek(fd: i32, off: i64, w: whence) i64 = { // errors::errno carried inside io::error. export type oserror = !i64; +// errno — the raw Linux errno as a positive code (ref/hare/sys/+linux/ +// errno.ha:5, `errno = !int`). ww folds Hare's `sys` role into os +// (lib/CLAUDE.md), so the sys::errno machinery lands here. Spelled i32 +// rather than int: Linux errnos are kernel ints (32-bit), keeping os's +// kernel-facing surface uniformly i32. Distinct from [[oserror]] (!i64, +// the syscall's *negative* raw return) — the two model different +// things, so they are not unified; the negative→positive normalization +// lives at the oserror→errors.error boundary in those callers. +export type errno = !i32; + +// Mapped errno values, ref/hare/sys/+linux/errno.ha:559-682. Positive, +// matching Hare's defs (the kernel returns -N; the wrap-to-positive is +// the caller's concern). Subset: exactly the errnos [[errors.errno]] +// maps to a named condition; grow as callers surface more. +export def ENOENT: errno = 2; +export def EINTR: errno = 4; +export def EAGAIN: errno = 11; +export def EACCES: errno = 13; +export def EBUSY: errno = 16; +export def EEXIST: errno = 17; +export def EINVAL: errno = 22; +export def EOVERFLOW: errno = 75; +export def ENETUNREACH: errno = 101; +export def ETIMEDOUT: errno = 110; +export def ECONNREFUSED: errno = 111; +export def ECANCELED: errno = 125; + +// strerror — human-readable text for an [[errno]] (Hare's +// sys::strerror, ref/hare/sys/+linux/errno.ha:18). FAITHFUL MINIMAL +// SUBSET: the mapped errnos above plus a generic fallback; grow the +// switch as callers surface more (lib/CLAUDE.md documented-subset, not +// a workaround). Messages verbatim from the reference. Hare's +// unknown_errno formats the numeric value; that is deferred. +export fn strerror(err: errno) str = { + switch (err) { + case ENOENT: return "No such file or directory"; + case EINTR: return "Interrupted system call"; + case EAGAIN: return "Resource temporarily unavailable"; + case EACCES: return "Permission denied"; + case EBUSY: return "Device or resource busy"; + case EEXIST: return "File exists"; + case EINVAL: return "Invalid argument"; + case EOVERFLOW: return "Value too large for defined data type"; + case ENETUNREACH: return "Network is unreachable"; + case ETIMEDOUT: return "Connection timed out"; + case ECONNREFUSED: return "Connection refused"; + case ECANCELED: return "Operation canceled"; + }; + return "Unknown error"; +}; + // filesize — byte length of an open fd via lseek-to-end-and-back. export fn filesize(fd: i32) (i64 | oserror) = { let end: i64 = lseek(fd, 0i64, whence.END); @@ -14167,15 +14218,23 @@ export fn close(s: stream) (void | error) = { // Named-void tagged-union variants, so `(T | errors.invalid | ...)` // composes with every other module's error surface at the tag level. // Per-domain errors (io.eof, io.closed, io.underread, strconv.overflow, -// ...) live in their own modules; this module ships only the generic -// conditions Hare's errors:: exports. -// -// Subset shipped today; add more from Hare's errors/common.ha as callers -// need them. +// ...) live in their own modules; this module ships the generic +// conditions Hare's errors:: exports plus the [[errno]] bridge that +// wraps a raw [[os.errno]] into a portable [[error]]. -// A function was called with an invalid combination of arguments. package errors; +import os; + +// The named conditions, ordered per ref/hare/errors/common.ha. + +// The requested resource is not available. +export type busy = !void; + +// An attempt was made to create a resource which already exists. +export type exists = !void; + +// A function was called with an invalid combination of arguments. export type invalid = !void; // The user does not have permission to use this resource. @@ -14184,12 +14243,112 @@ export type noaccess = !void; // An entry was requested which does not exist. export type noentry = !void; -// An attempt was made to create a resource which already exists. -export type exists = !void; +// The requested operation caused a numeric overflow condition. +export type overflow = !void; // The requested operation is not supported. export type unsupported = !void; +// The requested operation timed out. +export type timeout = !void; + +// The requested operation was cancelled. +export type cancelled = !void; + +// A connection attempt was refused. +export type refused = !void; + +// An operation was interrupted. +export type interrupted = !void; + +// The user should attempt an operation again. +export type again = !void; + +// Network unreachable +export type netunreachable = !void; + +// Up to 24 bytes of arbitrary, 8-byte-aligned storage for the opaque +// error type's domain-specific data. ref/hare/errors/opaque.ha:41. +export type opaque_data = [3]u64; + +// An "opaque" error wraps an implementation-specific underlying error +// behind a function that stringifies it plus a small storage area. +// ref/hare/errors/opaque.ha:32. The `strerror` field keeps Hare's `*fn` +// pointer (filled via `(&fn): *fn(...)`, mirroring io's `*reader` slot, +// lib/io/types.ww:53); only Hare's `const` is dropped (ww has none, cf +// lib/io/types.ww:57). +export type opaque_ = !struct { + strerror: *fn(op: *opaque_data) str, + data: opaque_data, +}; + +// A tagged union of all error types. ref/hare/errors/common.ha:43. +// Enumerated explicitly rather than spread via Hare's `...error` — +// ww's spread-flatten layout is the deferred #199b / #204 fix. +export type error = !( + busy | + exists | + invalid | + noaccess | + noentry | + overflow | + unsupported | + timeout | + cancelled | + refused | + interrupted | + again | + netunreachable | + opaque_ +); + +// Wraps an [[os.errno]] to produce an [[error]], which may be +// [[opaque_]]. ref/hare/errors/rt.ha:9. The mapped errnos become named +// conditions; the unmapped tail is carried in an [[opaque_]] whose +// stringifier defers to [[os.strerror]]. +export fn errno(e: os.errno) error = { + // A `!void` condition is returned by instance, not by name: a bare + // `return refused;` would emit an undefined symbol reference (the + // type, not a value). cf lib/io/stream.ww:53-56. The instance + // auto-widens to the [[error]] union on return. + switch (e) { + case os.ECONNREFUSED: { let r: refused; return r; }; + case os.ECANCELED: { let r: cancelled; return r; }; + case os.EOVERFLOW: { let r: overflow; return r; }; + case os.EACCES: { let r: noaccess; return r; }; + case os.EINVAL: { let r: invalid; return r; }; + case os.EEXIST: { let r: exists; return r; }; + case os.ENOENT: { let r: noentry; return r; }; + case os.ETIMEDOUT: { let r: timeout; return r; }; + case os.EBUSY: { let r: busy; return r; }; + case os.EINTR: { let r: interrupted; return r; }; + case os.EAGAIN: { let r: again; return r; }; + case os.ENETUNREACH: { let r: netunreachable; return r; }; + }; + + // An unmapped errno falls through the switch into the opaque_ wrap + // below. ww switches aren't required exhaustive, so Hare's terminal + // `case => void;` (rt.ha:24, present only to satisfy exhaustiveness) + // is dropped rather than written as an explicit no-op default — + // matching the sibling [[os.strerror]] fall-through. + // + // ww has no `static assert`; Hare guards size(errno) <= + // size(opaque_data) there. The invariant holds structurally: + // opaque_data is [3]u64 (24B), errno is one machine int. + let err: opaque_; + err.strerror = (&rt_strerror): *fn(op: *opaque_data) str; + let ptr = (&err.data): *os.errno; + *ptr = e; + return err; +}; + +// rt_strerror — the [[opaque_]] stringifier for an errno wrapped by +// [[errno]]. ref/hare/errors/rt.ha:31. +fn rt_strerror(op: *opaque_data) str = { + let e = (op): *os.errno; + return os.strerror(*e); +}; + // types — error union, mode/whence enums, reader/writer/closer // fn-type aliases. Project #94 fold-eFinal; the fn-aliases target // `stream` (= `*vtable`, the single io surface). diff --git a/selfhost/cmd/w6l/main.combined.ww b/selfhost/cmd/w6l/main.combined.ww index d5617568..e4e7da16 100644 --- a/selfhost/cmd/w6l/main.combined.ww +++ b/selfhost/cmd/w6l/main.combined.ww @@ -265,6 +265,57 @@ export fn lseek(fd: i32, off: i64, w: whence) i64 = { // errors::errno carried inside io::error. export type oserror = !i64; +// errno — the raw Linux errno as a positive code (ref/hare/sys/+linux/ +// errno.ha:5, `errno = !int`). ww folds Hare's `sys` role into os +// (lib/CLAUDE.md), so the sys::errno machinery lands here. Spelled i32 +// rather than int: Linux errnos are kernel ints (32-bit), keeping os's +// kernel-facing surface uniformly i32. Distinct from [[oserror]] (!i64, +// the syscall's *negative* raw return) — the two model different +// things, so they are not unified; the negative→positive normalization +// lives at the oserror→errors.error boundary in those callers. +export type errno = !i32; + +// Mapped errno values, ref/hare/sys/+linux/errno.ha:559-682. Positive, +// matching Hare's defs (the kernel returns -N; the wrap-to-positive is +// the caller's concern). Subset: exactly the errnos [[errors.errno]] +// maps to a named condition; grow as callers surface more. +export def ENOENT: errno = 2; +export def EINTR: errno = 4; +export def EAGAIN: errno = 11; +export def EACCES: errno = 13; +export def EBUSY: errno = 16; +export def EEXIST: errno = 17; +export def EINVAL: errno = 22; +export def EOVERFLOW: errno = 75; +export def ENETUNREACH: errno = 101; +export def ETIMEDOUT: errno = 110; +export def ECONNREFUSED: errno = 111; +export def ECANCELED: errno = 125; + +// strerror — human-readable text for an [[errno]] (Hare's +// sys::strerror, ref/hare/sys/+linux/errno.ha:18). FAITHFUL MINIMAL +// SUBSET: the mapped errnos above plus a generic fallback; grow the +// switch as callers surface more (lib/CLAUDE.md documented-subset, not +// a workaround). Messages verbatim from the reference. Hare's +// unknown_errno formats the numeric value; that is deferred. +export fn strerror(err: errno) str = { + switch (err) { + case ENOENT: return "No such file or directory"; + case EINTR: return "Interrupted system call"; + case EAGAIN: return "Resource temporarily unavailable"; + case EACCES: return "Permission denied"; + case EBUSY: return "Device or resource busy"; + case EEXIST: return "File exists"; + case EINVAL: return "Invalid argument"; + case EOVERFLOW: return "Value too large for defined data type"; + case ENETUNREACH: return "Network is unreachable"; + case ETIMEDOUT: return "Connection timed out"; + case ECONNREFUSED: return "Connection refused"; + case ECANCELED: return "Operation canceled"; + }; + return "Unknown error"; +}; + // filesize — byte length of an open fd via lseek-to-end-and-back. export fn filesize(fd: i32) (i64 | oserror) = { let end: i64 = lseek(fd, 0i64, whence.END); diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww index 0f37ea44..abf46901 100644 --- a/selfhost/cmd/ww/main.combined.ww +++ b/selfhost/cmd/ww/main.combined.ww @@ -265,6 +265,57 @@ export fn lseek(fd: i32, off: i64, w: whence) i64 = { // errors::errno carried inside io::error. export type oserror = !i64; +// errno — the raw Linux errno as a positive code (ref/hare/sys/+linux/ +// errno.ha:5, `errno = !int`). ww folds Hare's `sys` role into os +// (lib/CLAUDE.md), so the sys::errno machinery lands here. Spelled i32 +// rather than int: Linux errnos are kernel ints (32-bit), keeping os's +// kernel-facing surface uniformly i32. Distinct from [[oserror]] (!i64, +// the syscall's *negative* raw return) — the two model different +// things, so they are not unified; the negative→positive normalization +// lives at the oserror→errors.error boundary in those callers. +export type errno = !i32; + +// Mapped errno values, ref/hare/sys/+linux/errno.ha:559-682. Positive, +// matching Hare's defs (the kernel returns -N; the wrap-to-positive is +// the caller's concern). Subset: exactly the errnos [[errors.errno]] +// maps to a named condition; grow as callers surface more. +export def ENOENT: errno = 2; +export def EINTR: errno = 4; +export def EAGAIN: errno = 11; +export def EACCES: errno = 13; +export def EBUSY: errno = 16; +export def EEXIST: errno = 17; +export def EINVAL: errno = 22; +export def EOVERFLOW: errno = 75; +export def ENETUNREACH: errno = 101; +export def ETIMEDOUT: errno = 110; +export def ECONNREFUSED: errno = 111; +export def ECANCELED: errno = 125; + +// strerror — human-readable text for an [[errno]] (Hare's +// sys::strerror, ref/hare/sys/+linux/errno.ha:18). FAITHFUL MINIMAL +// SUBSET: the mapped errnos above plus a generic fallback; grow the +// switch as callers surface more (lib/CLAUDE.md documented-subset, not +// a workaround). Messages verbatim from the reference. Hare's +// unknown_errno formats the numeric value; that is deferred. +export fn strerror(err: errno) str = { + switch (err) { + case ENOENT: return "No such file or directory"; + case EINTR: return "Interrupted system call"; + case EAGAIN: return "Resource temporarily unavailable"; + case EACCES: return "Permission denied"; + case EBUSY: return "Device or resource busy"; + case EEXIST: return "File exists"; + case EINVAL: return "Invalid argument"; + case EOVERFLOW: return "Value too large for defined data type"; + case ENETUNREACH: return "Network is unreachable"; + case ETIMEDOUT: return "Connection timed out"; + case ECONNREFUSED: return "Connection refused"; + case ECANCELED: return "Operation canceled"; + }; + return "Unknown error"; +}; + // filesize — byte length of an open fd via lseek-to-end-and-back. export fn filesize(fd: i32) (i64 | oserror) = { let end: i64 = lseek(fd, 0i64, whence.END); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index b9cb9098..243bdf3a 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -265,6 +265,57 @@ export fn lseek(fd: i32, off: i64, w: whence) i64 = { // errors::errno carried inside io::error. export type oserror = !i64; +// errno — the raw Linux errno as a positive code (ref/hare/sys/+linux/ +// errno.ha:5, `errno = !int`). ww folds Hare's `sys` role into os +// (lib/CLAUDE.md), so the sys::errno machinery lands here. Spelled i32 +// rather than int: Linux errnos are kernel ints (32-bit), keeping os's +// kernel-facing surface uniformly i32. Distinct from [[oserror]] (!i64, +// the syscall's *negative* raw return) — the two model different +// things, so they are not unified; the negative→positive normalization +// lives at the oserror→errors.error boundary in those callers. +export type errno = !i32; + +// Mapped errno values, ref/hare/sys/+linux/errno.ha:559-682. Positive, +// matching Hare's defs (the kernel returns -N; the wrap-to-positive is +// the caller's concern). Subset: exactly the errnos [[errors.errno]] +// maps to a named condition; grow as callers surface more. +export def ENOENT: errno = 2; +export def EINTR: errno = 4; +export def EAGAIN: errno = 11; +export def EACCES: errno = 13; +export def EBUSY: errno = 16; +export def EEXIST: errno = 17; +export def EINVAL: errno = 22; +export def EOVERFLOW: errno = 75; +export def ENETUNREACH: errno = 101; +export def ETIMEDOUT: errno = 110; +export def ECONNREFUSED: errno = 111; +export def ECANCELED: errno = 125; + +// strerror — human-readable text for an [[errno]] (Hare's +// sys::strerror, ref/hare/sys/+linux/errno.ha:18). FAITHFUL MINIMAL +// SUBSET: the mapped errnos above plus a generic fallback; grow the +// switch as callers surface more (lib/CLAUDE.md documented-subset, not +// a workaround). Messages verbatim from the reference. Hare's +// unknown_errno formats the numeric value; that is deferred. +export fn strerror(err: errno) str = { + switch (err) { + case ENOENT: return "No such file or directory"; + case EINTR: return "Interrupted system call"; + case EAGAIN: return "Resource temporarily unavailable"; + case EACCES: return "Permission denied"; + case EBUSY: return "Device or resource busy"; + case EEXIST: return "File exists"; + case EINVAL: return "Invalid argument"; + case EOVERFLOW: return "Value too large for defined data type"; + case ENETUNREACH: return "Network is unreachable"; + case ETIMEDOUT: return "Connection timed out"; + case ECONNREFUSED: return "Connection refused"; + case ECANCELED: return "Operation canceled"; + }; + return "Unknown error"; +}; + // filesize — byte length of an open fd via lseek-to-end-and-back. export fn filesize(fd: i32) (i64 | oserror) = { let end: i64 = lseek(fd, 0i64, whence.END); @@ -14167,15 +14218,23 @@ export fn close(s: stream) (void | error) = { // Named-void tagged-union variants, so `(T | errors.invalid | ...)` // composes with every other module's error surface at the tag level. // Per-domain errors (io.eof, io.closed, io.underread, strconv.overflow, -// ...) live in their own modules; this module ships only the generic -// conditions Hare's errors:: exports. -// -// Subset shipped today; add more from Hare's errors/common.ha as callers -// need them. +// ...) live in their own modules; this module ships the generic +// conditions Hare's errors:: exports plus the [[errno]] bridge that +// wraps a raw [[os.errno]] into a portable [[error]]. -// A function was called with an invalid combination of arguments. package errors; +import os; + +// The named conditions, ordered per ref/hare/errors/common.ha. + +// The requested resource is not available. +export type busy = !void; + +// An attempt was made to create a resource which already exists. +export type exists = !void; + +// A function was called with an invalid combination of arguments. export type invalid = !void; // The user does not have permission to use this resource. @@ -14184,12 +14243,112 @@ export type noaccess = !void; // An entry was requested which does not exist. export type noentry = !void; -// An attempt was made to create a resource which already exists. -export type exists = !void; +// The requested operation caused a numeric overflow condition. +export type overflow = !void; // The requested operation is not supported. export type unsupported = !void; +// The requested operation timed out. +export type timeout = !void; + +// The requested operation was cancelled. +export type cancelled = !void; + +// A connection attempt was refused. +export type refused = !void; + +// An operation was interrupted. +export type interrupted = !void; + +// The user should attempt an operation again. +export type again = !void; + +// Network unreachable +export type netunreachable = !void; + +// Up to 24 bytes of arbitrary, 8-byte-aligned storage for the opaque +// error type's domain-specific data. ref/hare/errors/opaque.ha:41. +export type opaque_data = [3]u64; + +// An "opaque" error wraps an implementation-specific underlying error +// behind a function that stringifies it plus a small storage area. +// ref/hare/errors/opaque.ha:32. The `strerror` field keeps Hare's `*fn` +// pointer (filled via `(&fn): *fn(...)`, mirroring io's `*reader` slot, +// lib/io/types.ww:53); only Hare's `const` is dropped (ww has none, cf +// lib/io/types.ww:57). +export type opaque_ = !struct { + strerror: *fn(op: *opaque_data) str, + data: opaque_data, +}; + +// A tagged union of all error types. ref/hare/errors/common.ha:43. +// Enumerated explicitly rather than spread via Hare's `...error` — +// ww's spread-flatten layout is the deferred #199b / #204 fix. +export type error = !( + busy | + exists | + invalid | + noaccess | + noentry | + overflow | + unsupported | + timeout | + cancelled | + refused | + interrupted | + again | + netunreachable | + opaque_ +); + +// Wraps an [[os.errno]] to produce an [[error]], which may be +// [[opaque_]]. ref/hare/errors/rt.ha:9. The mapped errnos become named +// conditions; the unmapped tail is carried in an [[opaque_]] whose +// stringifier defers to [[os.strerror]]. +export fn errno(e: os.errno) error = { + // A `!void` condition is returned by instance, not by name: a bare + // `return refused;` would emit an undefined symbol reference (the + // type, not a value). cf lib/io/stream.ww:53-56. The instance + // auto-widens to the [[error]] union on return. + switch (e) { + case os.ECONNREFUSED: { let r: refused; return r; }; + case os.ECANCELED: { let r: cancelled; return r; }; + case os.EOVERFLOW: { let r: overflow; return r; }; + case os.EACCES: { let r: noaccess; return r; }; + case os.EINVAL: { let r: invalid; return r; }; + case os.EEXIST: { let r: exists; return r; }; + case os.ENOENT: { let r: noentry; return r; }; + case os.ETIMEDOUT: { let r: timeout; return r; }; + case os.EBUSY: { let r: busy; return r; }; + case os.EINTR: { let r: interrupted; return r; }; + case os.EAGAIN: { let r: again; return r; }; + case os.ENETUNREACH: { let r: netunreachable; return r; }; + }; + + // An unmapped errno falls through the switch into the opaque_ wrap + // below. ww switches aren't required exhaustive, so Hare's terminal + // `case => void;` (rt.ha:24, present only to satisfy exhaustiveness) + // is dropped rather than written as an explicit no-op default — + // matching the sibling [[os.strerror]] fall-through. + // + // ww has no `static assert`; Hare guards size(errno) <= + // size(opaque_data) there. The invariant holds structurally: + // opaque_data is [3]u64 (24B), errno is one machine int. + let err: opaque_; + err.strerror = (&rt_strerror): *fn(op: *opaque_data) str; + let ptr = (&err.data): *os.errno; + *ptr = e; + return err; +}; + +// rt_strerror — the [[opaque_]] stringifier for an errno wrapped by +// [[errno]]. ref/hare/errors/rt.ha:31. +fn rt_strerror(op: *opaque_data) str = { + let e = (op): *os.errno; + return os.strerror(*e); +}; + // types — error union, mode/whence enums, reader/writer/closer // fn-type aliases. Project #94 fold-eFinal; the fn-aliases target // `stream` (= `*vtable`, the single io surface). diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 45bde6fe..dcfd439f 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -265,6 +265,57 @@ export fn lseek(fd: i32, off: i64, w: whence) i64 = { // errors::errno carried inside io::error. export type oserror = !i64; +// errno — the raw Linux errno as a positive code (ref/hare/sys/+linux/ +// errno.ha:5, `errno = !int`). ww folds Hare's `sys` role into os +// (lib/CLAUDE.md), so the sys::errno machinery lands here. Spelled i32 +// rather than int: Linux errnos are kernel ints (32-bit), keeping os's +// kernel-facing surface uniformly i32. Distinct from [[oserror]] (!i64, +// the syscall's *negative* raw return) — the two model different +// things, so they are not unified; the negative→positive normalization +// lives at the oserror→errors.error boundary in those callers. +export type errno = !i32; + +// Mapped errno values, ref/hare/sys/+linux/errno.ha:559-682. Positive, +// matching Hare's defs (the kernel returns -N; the wrap-to-positive is +// the caller's concern). Subset: exactly the errnos [[errors.errno]] +// maps to a named condition; grow as callers surface more. +export def ENOENT: errno = 2; +export def EINTR: errno = 4; +export def EAGAIN: errno = 11; +export def EACCES: errno = 13; +export def EBUSY: errno = 16; +export def EEXIST: errno = 17; +export def EINVAL: errno = 22; +export def EOVERFLOW: errno = 75; +export def ENETUNREACH: errno = 101; +export def ETIMEDOUT: errno = 110; +export def ECONNREFUSED: errno = 111; +export def ECANCELED: errno = 125; + +// strerror — human-readable text for an [[errno]] (Hare's +// sys::strerror, ref/hare/sys/+linux/errno.ha:18). FAITHFUL MINIMAL +// SUBSET: the mapped errnos above plus a generic fallback; grow the +// switch as callers surface more (lib/CLAUDE.md documented-subset, not +// a workaround). Messages verbatim from the reference. Hare's +// unknown_errno formats the numeric value; that is deferred. +export fn strerror(err: errno) str = { + switch (err) { + case ENOENT: return "No such file or directory"; + case EINTR: return "Interrupted system call"; + case EAGAIN: return "Resource temporarily unavailable"; + case EACCES: return "Permission denied"; + case EBUSY: return "Device or resource busy"; + case EEXIST: return "File exists"; + case EINVAL: return "Invalid argument"; + case EOVERFLOW: return "Value too large for defined data type"; + case ENETUNREACH: return "Network is unreachable"; + case ETIMEDOUT: return "Connection timed out"; + case ECONNREFUSED: return "Connection refused"; + case ECANCELED: return "Operation canceled"; + }; + return "Unknown error"; +}; + // filesize — byte length of an open fd via lseek-to-end-and-back. export fn filesize(fd: i32) (i64 | oserror) = { let end: i64 = lseek(fd, 0i64, whence.END); diff --git a/test/wcc/900_stdlib.c b/test/wcc/900_stdlib.c index b87fadf7..b81ab05f 100644 --- a/test/wcc/900_stdlib.c +++ b/test/wcc/900_stdlib.c @@ -13,7 +13,6 @@ static const char *modules[] = { "lib/types/types.ww", "lib/ascii/ascii.ww", "lib/io/io.ww", - "lib/errors/errors.ww", "lib/strconv/strconv.ww", "lib/sort/sort.ww", "lib/path/path.ww", @@ -30,19 +29,21 @@ static const char *modules[] = { "lib/math/random/random.ww", "lib/time/time.ww", "lib/c/libc/libc.ww", - /* lib/bufio/bufio.ww, lib/bytes/bytes.ww, lib/fmt/fmt.ww, - * lib/os/os.ww, and lib/strings/strings.ww moved off this list: - * each has cross-module type refs that only resolve once the - * driver concatenates `use`d modules. bufio / fmt graduated to - * io.stream-based sinks (io.stream = *io.vtable, io.eof, io.error); lib/os - * carries time.instant in filestat post-Commit B; - * lib/strings.iterator + lib/strings.next reference utf8.decoder - * / utf8.done; lib/bytes.tokenize references os.assert + - * types.I64_MAX/MIN per ref/hare/bytes/tokenize.ha:23-24,42-43. - * Coverage lives at lib/bufio/bufiotest.ww + lib/bytes/bytestest.ww - * + lib/fmt/fmttest.ww + lib/os/stattest.ww + - * lib/strings/stringstest.ww (wired at 998_bufio_run.c, - * 967_bytes_run.c, 970_fmt_run.c, 976_stat_run.c, + /* lib/bufio/bufio.ww, lib/bytes/bytes.ww, lib/errors/errors.ww, + * lib/fmt/fmt.ww, lib/os/os.ww, and lib/strings/strings.ww moved + * off this list: each has cross-module type refs that only resolve + * once the driver concatenates `use`d modules. bufio / fmt + * graduated to io.stream-based sinks (io.stream = *io.vtable, + * io.eof, io.error); lib/os carries time.instant in filestat + * post-Commit B; lib/errors.errno references os.errno / os.E* / + * os.strerror (ww folds Hare's sys role into os); lib/strings.iterator + * + lib/strings.next reference utf8.decoder / utf8.done; + * lib/bytes.tokenize references os.assert + types.I64_MAX/MIN per + * ref/hare/bytes/tokenize.ha:23-24,42-43. Coverage lives at + * lib/bufio/bufiotest.ww + lib/bytes/bytestest.ww + + * lib/errors/errnotest.ww + lib/fmt/fmttest.ww + lib/os/stattest.ww + * + lib/strings/stringstest.ww (wired at 998_bufio_run.c, + * 967_bytes_run.c, 902_errno_run.c, 970_fmt_run.c, 976_stat_run.c, * 966_strings_run.c), plus the bufio.scanline / fmt.println e2e * rows in test/wcc/700_e2e.c. */ "lib/net/net.ww", diff --git a/test/wcc/902_errno_run.c b/test/wcc/902_errno_run.c new file mode 100644 index 00000000..f72a7940 --- /dev/null +++ b/test/wcc/902_errno_run.c @@ -0,0 +1,51 @@ +/* + * 902_errno_run — execute the lib/errors errno @test fixture under the + * C-side `ww run` driver and assert exit 0. + * + * Sibling to 982_getopt_run / 980_memio_run. errnotest.ww carries its + * own `export fn main()` that drives the @test fns and signals which + * case failed via the exit code, so this file is a thin wrapper — no + * @test scanning, no synthetic main generation. + */ +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + const char *src = "lib/errors/errnotest.ww"; + char path[1024], cmd[2048]; + snprintf(path, sizeof path, "%s/%s", cwd, src); + snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path); + (void)cwd; + int rc = runwait(cmd); + if (rc != 0) { + fprintf(stderr, "errno_run FAIL: %s exited %d\n", src, rc); + return 1; + } + printf("errno_run: %s ok\n", src); + return 0; +}