diff --git a/lib/os/os.ww b/lib/os/os.ww index 49bc9253..4e61a4ee 100644 --- a/lib/os/os.ww +++ b/lib/os/os.ww @@ -8,8 +8,29 @@ @symbol("rt_syscall") fn syscall3(num: nr, a: i64, b: i64, c: i64) i64; @symbol("rt_syscall") fn syscall4(num: nr, 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; +// alloc / free — runtime mmap-backed page allocator. Untyped: +// `alloc(n)` returns a `*void` and `free(p, n)` requires the byte +// count back because rt_free is munmap-based and doesn't track +// mapping sizes (the kernel needs the length to release the +// reservation). +// +// Diverges from Hare. Hare exposes `alloc` / `free` as typed +// language builtins (`alloc(value, cap)?` / `free(ptr)`) that the +// compiler lowers to rt::malloc/rt::free; ww has no such builtins, +// so the rt-symbol surface is exposed directly. Stdlib callers +// that need a typed allocation pattern wrap this with a cast plus +// a stored capacity (see [[strings.dup]], [[memio.dynamic]]). +// +// OOM: rt_alloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with +// no error path. The raw Linux mmap syscall returns a negative +// errno cast to `*void` on failure (e.g. `(void*)-12` for ENOMEM); +// the `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention +// that rt_alloc doesn't apply. Neither `== nil` nor `== (void*)-1` +// catches it; any deref of such a return faults. Today the stdlib +// does not check; OOM faults on first dereference. A typed +// fallible variant is a future task. +@symbol("rt_alloc") export fn alloc(n: u64) *void; +@symbol("rt_free") export 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 diff --git a/lib/os/ostest.ww b/lib/os/ostest.ww index 23c2ef2c..6fd5622d 100644 --- a/lib/os/ostest.ww +++ b/lib/os/ostest.ww @@ -1,6 +1,9 @@ // ostest — exercises lib/os surface that doesn't have a dedicated -// test elsewhere. v1 covers [[os.getenv]] only, against an env state -// pre-arranged by the C driver (test/wcc/974_getenv_run.c). +// test elsewhere. Covers [[os.getenv]] (against env state pre- +// arranged by test/wcc/974_getenv_run.c) and a direct +// [[os.alloc]] / [[os.free]] roundtrip. memio's tests indirectly +// cover alloc/free; the direct row here pins the FFI shape under +// lib/os itself so future bindings refactors can't quietly drift. // // Convention follows the stdlib `_run` test fixtures: hand-rolled // @test fns dispatched from `main()` in numeric order, with a @@ -89,10 +92,31 @@ fn streq(a: str, b: str) bool = { }; }; +// ---- alloc/free: mmap-backed runtime allocator ---------------------- +// +// Direct round-trip. memio's dynamic-buffer tests already exercise +// os.alloc / os.free transitively; the row here pins the FFI shape +// at the lib/os layer (write+read-back proves the returned page is +// dereferenceable, not just non-nil). + +@test fn test_alloc_free_roundtrip() void = { + let p: *u8 = os.alloc(4096u64): *u8; + if (p == nil: *u8) { fail(); }; + // Write a sentinel at the head and tail of the page, read it + // back. A miscompiled binding (wrong arg order, wrong ABI, etc.) + // would either fault or return zero here. + p[0] = 90u8; // 0x5a + p[4095] = 165u8; // 0xa5 + if (p[0] != 90u8) { fail(); }; + if (p[4095] != 165u8) { fail(); }; + os.free(p: *void, 4096u64); +}; + export fn main() i32 = { signalled = 1; test_getenv_set(); signalled = 2; test_getenv_empty(); signalled = 3; test_getenv_unset(); signalled = 4; test_getenv_prefix_no_match(); + signalled = 5; test_alloc_free_roundtrip(); return 0; }; diff --git a/selfhost/cmd/w6a/main.combined.ww b/selfhost/cmd/w6a/main.combined.ww index 9cf59285..c0d266a0 100644 --- a/selfhost/cmd/w6a/main.combined.ww +++ b/selfhost/cmd/w6a/main.combined.ww @@ -9,8 +9,29 @@ @symbol("rt_syscall") fn syscall3(num: nr, a: i64, b: i64, c: i64) i64; @symbol("rt_syscall") fn syscall4(num: nr, 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; +// alloc / free — runtime mmap-backed page allocator. Untyped: +// `alloc(n)` returns a `*void` and `free(p, n)` requires the byte +// count back because rt_free is munmap-based and doesn't track +// mapping sizes (the kernel needs the length to release the +// reservation). +// +// Diverges from Hare. Hare exposes `alloc` / `free` as typed +// language builtins (`alloc(value, cap)?` / `free(ptr)`) that the +// compiler lowers to rt::malloc/rt::free; ww has no such builtins, +// so the rt-symbol surface is exposed directly. Stdlib callers +// that need a typed allocation pattern wrap this with a cast plus +// a stored capacity (see [[strings.dup]], [[memio.dynamic]]). +// +// OOM: rt_alloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with +// no error path. The raw Linux mmap syscall returns a negative +// errno cast to `*void` on failure (e.g. `(void*)-12` for ENOMEM); +// the `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention +// that rt_alloc doesn't apply. Neither `== nil` nor `== (void*)-1` +// catches it; any deref of such a return faults. Today the stdlib +// does not check; OOM faults on first dereference. A typed +// fallible variant is a future task. +@symbol("rt_alloc") export fn alloc(n: u64) *void; +@symbol("rt_free") export 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 diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 7ef2a17b..a5a4ccc4 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -9,8 +9,29 @@ @symbol("rt_syscall") fn syscall3(num: nr, a: i64, b: i64, c: i64) i64; @symbol("rt_syscall") fn syscall4(num: nr, 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; +// alloc / free — runtime mmap-backed page allocator. Untyped: +// `alloc(n)` returns a `*void` and `free(p, n)` requires the byte +// count back because rt_free is munmap-based and doesn't track +// mapping sizes (the kernel needs the length to release the +// reservation). +// +// Diverges from Hare. Hare exposes `alloc` / `free` as typed +// language builtins (`alloc(value, cap)?` / `free(ptr)`) that the +// compiler lowers to rt::malloc/rt::free; ww has no such builtins, +// so the rt-symbol surface is exposed directly. Stdlib callers +// that need a typed allocation pattern wrap this with a cast plus +// a stored capacity (see [[strings.dup]], [[memio.dynamic]]). +// +// OOM: rt_alloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with +// no error path. The raw Linux mmap syscall returns a negative +// errno cast to `*void` on failure (e.g. `(void*)-12` for ENOMEM); +// the `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention +// that rt_alloc doesn't apply. Neither `== nil` nor `== (void*)-1` +// catches it; any deref of such a return faults. Today the stdlib +// does not check; OOM faults on first dereference. A typed +// fallible variant is a future task. +@symbol("rt_alloc") export fn alloc(n: u64) *void; +@symbol("rt_free") export 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 diff --git a/selfhost/cmd/w6l/main.combined.ww b/selfhost/cmd/w6l/main.combined.ww index fd3f9baf..2487c911 100644 --- a/selfhost/cmd/w6l/main.combined.ww +++ b/selfhost/cmd/w6l/main.combined.ww @@ -9,8 +9,29 @@ @symbol("rt_syscall") fn syscall3(num: nr, a: i64, b: i64, c: i64) i64; @symbol("rt_syscall") fn syscall4(num: nr, 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; +// alloc / free — runtime mmap-backed page allocator. Untyped: +// `alloc(n)` returns a `*void` and `free(p, n)` requires the byte +// count back because rt_free is munmap-based and doesn't track +// mapping sizes (the kernel needs the length to release the +// reservation). +// +// Diverges from Hare. Hare exposes `alloc` / `free` as typed +// language builtins (`alloc(value, cap)?` / `free(ptr)`) that the +// compiler lowers to rt::malloc/rt::free; ww has no such builtins, +// so the rt-symbol surface is exposed directly. Stdlib callers +// that need a typed allocation pattern wrap this with a cast plus +// a stored capacity (see [[strings.dup]], [[memio.dynamic]]). +// +// OOM: rt_alloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with +// no error path. The raw Linux mmap syscall returns a negative +// errno cast to `*void` on failure (e.g. `(void*)-12` for ENOMEM); +// the `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention +// that rt_alloc doesn't apply. Neither `== nil` nor `== (void*)-1` +// catches it; any deref of such a return faults. Today the stdlib +// does not check; OOM faults on first dereference. A typed +// fallible variant is a future task. +@symbol("rt_alloc") export fn alloc(n: u64) *void; +@symbol("rt_free") export 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 diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww index c38dfb3c..8290dd53 100644 --- a/selfhost/cmd/ww/main.combined.ww +++ b/selfhost/cmd/ww/main.combined.ww @@ -9,8 +9,29 @@ @symbol("rt_syscall") fn syscall3(num: nr, a: i64, b: i64, c: i64) i64; @symbol("rt_syscall") fn syscall4(num: nr, 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; +// alloc / free — runtime mmap-backed page allocator. Untyped: +// `alloc(n)` returns a `*void` and `free(p, n)` requires the byte +// count back because rt_free is munmap-based and doesn't track +// mapping sizes (the kernel needs the length to release the +// reservation). +// +// Diverges from Hare. Hare exposes `alloc` / `free` as typed +// language builtins (`alloc(value, cap)?` / `free(ptr)`) that the +// compiler lowers to rt::malloc/rt::free; ww has no such builtins, +// so the rt-symbol surface is exposed directly. Stdlib callers +// that need a typed allocation pattern wrap this with a cast plus +// a stored capacity (see [[strings.dup]], [[memio.dynamic]]). +// +// OOM: rt_alloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with +// no error path. The raw Linux mmap syscall returns a negative +// errno cast to `*void` on failure (e.g. `(void*)-12` for ENOMEM); +// the `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention +// that rt_alloc doesn't apply. Neither `== nil` nor `== (void*)-1` +// catches it; any deref of such a return faults. Today the stdlib +// does not check; OOM faults on first dereference. A typed +// fallible variant is a future task. +@symbol("rt_alloc") export fn alloc(n: u64) *void; +@symbol("rt_free") export 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 diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 784b19a0..f07757ea 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -9,8 +9,29 @@ @symbol("rt_syscall") fn syscall3(num: nr, a: i64, b: i64, c: i64) i64; @symbol("rt_syscall") fn syscall4(num: nr, 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; +// alloc / free — runtime mmap-backed page allocator. Untyped: +// `alloc(n)` returns a `*void` and `free(p, n)` requires the byte +// count back because rt_free is munmap-based and doesn't track +// mapping sizes (the kernel needs the length to release the +// reservation). +// +// Diverges from Hare. Hare exposes `alloc` / `free` as typed +// language builtins (`alloc(value, cap)?` / `free(ptr)`) that the +// compiler lowers to rt::malloc/rt::free; ww has no such builtins, +// so the rt-symbol surface is exposed directly. Stdlib callers +// that need a typed allocation pattern wrap this with a cast plus +// a stored capacity (see [[strings.dup]], [[memio.dynamic]]). +// +// OOM: rt_alloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with +// no error path. The raw Linux mmap syscall returns a negative +// errno cast to `*void` on failure (e.g. `(void*)-12` for ENOMEM); +// the `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention +// that rt_alloc doesn't apply. Neither `== nil` nor `== (void*)-1` +// catches it; any deref of such a return faults. Today the stdlib +// does not check; OOM faults on first dereference. A typed +// fallible variant is a future task. +@symbol("rt_alloc") export fn alloc(n: u64) *void; +@symbol("rt_free") export 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 diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index e4582fec..a056ebf6 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -9,8 +9,29 @@ @symbol("rt_syscall") fn syscall3(num: nr, a: i64, b: i64, c: i64) i64; @symbol("rt_syscall") fn syscall4(num: nr, 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; +// alloc / free — runtime mmap-backed page allocator. Untyped: +// `alloc(n)` returns a `*void` and `free(p, n)` requires the byte +// count back because rt_free is munmap-based and doesn't track +// mapping sizes (the kernel needs the length to release the +// reservation). +// +// Diverges from Hare. Hare exposes `alloc` / `free` as typed +// language builtins (`alloc(value, cap)?` / `free(ptr)`) that the +// compiler lowers to rt::malloc/rt::free; ww has no such builtins, +// so the rt-symbol surface is exposed directly. Stdlib callers +// that need a typed allocation pattern wrap this with a cast plus +// a stored capacity (see [[strings.dup]], [[memio.dynamic]]). +// +// OOM: rt_alloc is a bare mmap(MAP_ANON|MAP_PRIVATE) wrapper with +// no error path. The raw Linux mmap syscall returns a negative +// errno cast to `*void` on failure (e.g. `(void*)-12` for ENOMEM); +// the `MAP_FAILED` (`(void*)-1`) value is a libc-wrapper convention +// that rt_alloc doesn't apply. Neither `== nil` nor `== (void*)-1` +// catches it; any deref of such a return faults. Today the stdlib +// does not check; OOM faults on first dereference. A typed +// fallible variant is a future task. +@symbol("rt_alloc") export fn alloc(n: u64) *void; +@symbol("rt_free") export 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