From 87c088359dacb4b62287da8617451834be26e9a2 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 16 May 2026 01:54:20 +0900 Subject: [PATCH] lib/os+test: export alloc + free via rt_alloc/rt_free MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add os.alloc(n: u64) *void and os.free(p: *void, n: u64) void as `export fn` via @symbol("rt_alloc") / @symbol("rt_free"). Signatures mirror lib/memio's existing internal bindings byte-for-byte — only the name and `export` keyword change. lib/memio + lib/shlex + lib/ getopt drop their own copies in a follow-up commit. Doc comment spells out the actual failure ABI: rt_alloc wraps the raw mmap syscall (no libc), so OOM yields a negative-errno cast to `*void` (e.g. (void*)-12 for ENOMEM). Neither `== nil` nor the libc MAP_FAILED `(void*)-1` value catches it; deref faults. A typed fallible variant is future work (alongside #16 fmt.asprintf). Test (ostest test_alloc_free_roundtrip, signalled=5): alloc 4096B, write 0x5a at head + 0xa5 at tail, read-back asserts both, free. The head+tail write/read prevents DCE (failure path calls os.exit) and proves a real page is backing the returned pointer. --- lib/os/os.ww | 25 +++++++++++++++++++++++-- lib/os/ostest.ww | 28 ++++++++++++++++++++++++++-- selfhost/cmd/w6a/main.combined.ww | 25 +++++++++++++++++++++++-- selfhost/cmd/w6c/main.combined.ww | 25 +++++++++++++++++++++++-- selfhost/cmd/w6l/main.combined.ww | 25 +++++++++++++++++++++++-- selfhost/cmd/ww/main.combined.ww | 25 +++++++++++++++++++++++-- selfhost/cmd/wwdump/main.combined.ww | 25 +++++++++++++++++++++++-- selfhost/test/smoke.combined.ww | 25 +++++++++++++++++++++++-- 8 files changed, 187 insertions(+), 16 deletions(-) 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