lib: extract rt module from os, sweep imports

Hare puts runtime allocation in rt::, not os:: (ref/hare/rt/malloc.ha:27,
README). ww's `@symbol("rt_alloc") fn alloc(n: u64) *void;` lived at
lib/os/os.ww as a historical bootstrap shortcut; this commit relocates
it to a new lib/rt/malloc.ww and sweeps every site that depended on
`import os` for the alloc decl over to `import rt`.

This is commit 1 of 3 in the lib/rt extraction (#35):
  1. (this) move decl, sweep imports — preserves shape
  2. rename rt_alloc → rt_malloc (#38)
  3. nullable return type + OOM-propagating builtin lowering (#39)

No rename here. Symbol stays rt_alloc, function stays `alloc`, return
stays *void. Behavior identical — same ffi resolution outcome, just
sourced from a different module file. The rt::ensure runtime helper at
selfhost/rt/ensure.ww is its own compilation unit with a local decl and
is untouched.

Side effect: every wcc cgen file used `rt` as a local *node variable
name for "return type." `import rt;` shadows the module, so each
selfhost/cmd/wcc/{check,cgenstmt,cgenexpr,cgenutil}.ww site renamed
to `rtyp`. Mechanical follow-through; only the wcc module-import was
forced to do this rename.

Verified 132/132 + 995_self_rebuild byte-identity (5 wwstage tools
round-trip byte-identical).
This commit is contained in:
2026-05-20 20:39:52 +09:00
parent a1ee817906
commit d68d3c7eb4
34 changed files with 530 additions and 488 deletions

View File

@@ -11,6 +11,7 @@
package w6l;
import os;
import rt;
import mem;
import sym;
@@ -134,7 +135,7 @@ fn slurpso(path: *u8) (*u8, u64) = {
case let v: i64 => n = v;
case let e: os.oserror => { os.close(fd); return nil, 0u64; };
};
let buf: *u8 = os.alloc(n: u64): *u8;
let buf: *u8 = rt.alloc(n: u64): *u8;
let rr: (i64 | os.oserror) = os.readall(fd, buf, n: u64);
os.close(fd);
let got: i64 = 0i64;

View File

@@ -25,6 +25,7 @@
package w6l;
import os;
import rt;
import mem;
import sym;
@@ -657,7 +658,7 @@ export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = {
};
// ---- assemble file buffer ----
let filebuf: *u8 = os.alloc(fileend): *u8;
let filebuf: *u8 = rt.alloc(fileend): *u8;
if (filebuf == nil) {
os.write(2, "w6l: out of memory\n".ptr, 18u64);
return 1;

View File

@@ -110,28 +110,6 @@ import time;
@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;
// 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;
@@ -749,6 +727,29 @@ export fn exists(path: str) bool = {
return r >= 0i64;
};
// rt — runtime primitives exposed to ww programs.
// Mirrors Hare's rt:: module placement (ref/hare/rt/).
package rt;
// alloc — mmap-backed page allocator. Untyped: `alloc(n)` returns a
// `*void`; callers cast to the target type. Diverges from Hare: Hare
// exposes `alloc` / `free` as typed language builtins 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 (task #39). ref/hare/rt/malloc.ha:27.
@symbol("rt_alloc") export fn alloc(n: u64) *void;
// selfhost/cmd/wcc/mem.ww — port of cmd/wcc/mem.c.
//
// Bump arena allocator. Backed by the runtime page allocator
@@ -762,6 +763,7 @@ export fn exists(path: str) bool = {
package wcc;
import os;
import rt;
def ALIGN: u64 = 16u64;
def INIT_CHUNK: u64 = 65536u64;
@@ -781,8 +783,8 @@ fn roundup(n: u64, a: u64) u64 = {
};
export fn newarena() *arena = {
let a: *arena = os.alloc(ARENA_SZ): *arena;
a.buf = os.alloc(INIT_CHUNK): *u8;
let a: *arena = rt.alloc(ARENA_SZ): *arena;
a.buf = rt.alloc(INIT_CHUNK): *u8;
a.off = 0u64;
a.cap = INIT_CHUNK;
a.next = nil;
@@ -799,14 +801,14 @@ fn grow(a: *arena, need: u64) bool = {
if (want > MAX_CHUNK) { want = MAX_CHUNK; };
if (want < need) { return false; }; // single allocation too big
let old: *arena = os.alloc(ARENA_SZ): *arena;
let old: *arena = rt.alloc(ARENA_SZ): *arena;
old.buf = a.buf;
old.off = a.off;
old.cap = a.cap;
old.next = a.next;
old.total = 0u64;
a.buf = os.alloc(want): *u8;
a.buf = rt.alloc(want): *u8;
a.off = 0u64;
a.cap = want;
a.next = old;
@@ -983,6 +985,7 @@ export fn lookup(l: *lnk, name: str) *lsym = {
package w6l;
import os;
import rt;
import mem;
import sym;
@@ -1058,7 +1061,7 @@ fn slurp(path: *u8) (*u8, u64) = {
case let v: i64 => n = v;
case let e: os.oserror => { os.close(fd); return nil, 0u64; };
};
let buf: *u8 = os.alloc(n: u64): *u8;
let buf: *u8 = rt.alloc(n: u64): *u8;
let rr: (i64 | os.oserror) = os.readall(fd, buf, n: u64);
os.close(fd);
let got: i64 = 0i64;
@@ -1080,7 +1083,7 @@ fn emittext(l: *lnk, src: *u8, n: u64) void = {
// Grow by mmap'ing a fresh region and copying. The old buffer
// is leaked into the page allocator; for a linker run this is
// trivial waste.
let nb: *u8 = os.alloc(nc): *u8;
let nb: *u8 = rt.alloc(nc): *u8;
let i: u64 = 0u64;
for (i < l.textlen) {
nb[i] = l.text[i];
@@ -1102,7 +1105,7 @@ fn emitdata(l: *lnk, src: *u8, n: u64) void = {
let nc: u64 = l.datacap;
if (nc == 0u64) { nc = 256u64; };
for (nc < l.datalen + n) { nc = nc * 2u64; };
let nb: *u8 = os.alloc(nc): *u8;
let nb: *u8 = rt.alloc(nc): *u8;
let i: u64 = 0u64;
for (i < l.datalen) {
nb[i] = l.data[i];
@@ -1594,6 +1597,7 @@ fn loadimage(l: *lnk, path: *u8, buf: *u8, len: u64) i32 = {
package w6l;
import os;
import rt;
import mem;
import sym;
@@ -1717,7 +1721,7 @@ fn slurpso(path: *u8) (*u8, u64) = {
case let v: i64 => n = v;
case let e: os.oserror => { os.close(fd); return nil, 0u64; };
};
let buf: *u8 = os.alloc(n: u64): *u8;
let buf: *u8 = rt.alloc(n: u64): *u8;
let rr: (i64 | os.oserror) = os.readall(fd, buf, n: u64);
os.close(fd);
let got: i64 = 0i64;
@@ -2145,6 +2149,7 @@ export fn relocate(l: *lnk, textva: u64, datava: u64) i32 = {
package w6l;
import os;
import rt;
import mem;
import sym;
@@ -2777,7 +2782,7 @@ export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = {
};
// ---- assemble file buffer ----
let filebuf: *u8 = os.alloc(fileend): *u8;
let filebuf: *u8 = rt.alloc(fileend): *u8;
if (filebuf == nil) {
os.write(2, "w6l: out of memory\n".ptr, 18u64);
return 1;
@@ -2889,6 +2894,7 @@ export fn emitdynelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = {
package w6l;
import os;
import rt;
import sym;
import dynout;
@@ -2967,7 +2973,7 @@ export fn emitelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = {
let datafilelen: u64 = l.datalen - bsslen;
// One contiguous header buffer covering [0..0x1000), then .text.
let hdr: *u8 = os.alloc(TEXT_OFF): *u8; // zero-initialised by mmap
let hdr: *u8 = rt.alloc(TEXT_OFF): *u8; // zero-initialised by mmap
// --- Ehdr (64 bytes) ---
hdr[0u64] = 127u8; // 0x7f
@@ -3067,6 +3073,7 @@ export fn emitelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = {
package main;
import os;
import rt;
import mem;
import sym;
import obj;
@@ -3121,7 +3128,7 @@ fn appenddec(dst: *u8, i: u64, n: u64) u64 = {
dst[i] = 48u8;
return i + 1u64;
};
let buf: *u8 = os.alloc(16u64): *u8;
let buf: *u8 = rt.alloc(16u64): *u8;
let k: u64 = 0u64;
let v: u64 = n;
for (v > 0u64) {
@@ -3160,7 +3167,7 @@ fn buildpathv(dst: *u8, dir: *u8, name: *u8, v: u64) u64 = {
fn islinkable(path: *u8) bool = {
let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32);
if (fd < 0) { return false; };
let mp: *u8 = os.alloc(8u64): *u8;
let mp: *u8 = rt.alloc(8u64): *u8;
let n: i64 = os.read(fd, mp, 8u64);
os.close(fd);
if (n < 4i64) { return false; };
@@ -3188,7 +3195,7 @@ fn islinkable(path: *u8) bool = {
// then lib<name>.a. Return arena-owned NUL-terminated path on success,
// nil on miss.
fn resolvelib(a: *arena, name: *u8, libdirs: **u8, nlibdirs: i32) *u8 = {
let bufp: *u8 = os.alloc(1024u64): *u8;
let bufp: *u8 = rt.alloc(1024u64): *u8;
let i: i32 = 0;
for (i < nlibdirs) {
let dir: *u8 = libdirs[i];
@@ -3229,7 +3236,7 @@ fn resolvelib(a: *arena, name: *u8, libdirs: **u8, nlibdirs: i32) *u8 = {
fn isso(path: *u8) i32 = {
let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32);
if (fd < 0) { return 0; };
let mp: *u8 = os.alloc(20u64): *u8;
let mp: *u8 = rt.alloc(20u64): *u8;
let n: i64 = os.read(fd, mp, 20u64);
os.close(fd);
if (n < 20i64) { return 0; };
@@ -3246,11 +3253,11 @@ fn isso(path: *u8) i32 = {
export fn main(argc: i32, argv: **u8) i32 = {
let outpath: *u8 = nil;
let maxinputs: i32 = 64;
let inputs: **u8 = os.alloc((maxinputs: u64) * 8u64): **u8;
let inputs: **u8 = rt.alloc((maxinputs: u64) * 8u64): **u8;
let ninputs: i32 = 0;
let libdirs: **u8 = os.alloc((maxinputs: u64) * 8u64): **u8;
let libdirs: **u8 = rt.alloc((maxinputs: u64) * 8u64): **u8;
let nlibdirs: i32 = 0;
let lflags: **u8 = os.alloc((maxinputs: u64) * 8u64): **u8;
let lflags: **u8 = rt.alloc((maxinputs: u64) * 8u64): **u8;
let nlflags: i32 = 0;
let i: i32 = 1;

View File

@@ -9,6 +9,7 @@
package main;
import os;
import rt;
import mem;
import sym;
import obj;
@@ -63,7 +64,7 @@ fn appenddec(dst: *u8, i: u64, n: u64) u64 = {
dst[i] = 48u8;
return i + 1u64;
};
let buf: *u8 = os.alloc(16u64): *u8;
let buf: *u8 = rt.alloc(16u64): *u8;
let k: u64 = 0u64;
let v: u64 = n;
for (v > 0u64) {
@@ -102,7 +103,7 @@ fn buildpathv(dst: *u8, dir: *u8, name: *u8, v: u64) u64 = {
fn islinkable(path: *u8) bool = {
let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32);
if (fd < 0) { return false; };
let mp: *u8 = os.alloc(8u64): *u8;
let mp: *u8 = rt.alloc(8u64): *u8;
let n: i64 = os.read(fd, mp, 8u64);
os.close(fd);
if (n < 4i64) { return false; };
@@ -130,7 +131,7 @@ fn islinkable(path: *u8) bool = {
// then lib<name>.a. Return arena-owned NUL-terminated path on success,
// nil on miss.
fn resolvelib(a: *arena, name: *u8, libdirs: **u8, nlibdirs: i32) *u8 = {
let bufp: *u8 = os.alloc(1024u64): *u8;
let bufp: *u8 = rt.alloc(1024u64): *u8;
let i: i32 = 0;
for (i < nlibdirs) {
let dir: *u8 = libdirs[i];
@@ -171,7 +172,7 @@ fn resolvelib(a: *arena, name: *u8, libdirs: **u8, nlibdirs: i32) *u8 = {
fn isso(path: *u8) i32 = {
let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32);
if (fd < 0) { return 0; };
let mp: *u8 = os.alloc(20u64): *u8;
let mp: *u8 = rt.alloc(20u64): *u8;
let n: i64 = os.read(fd, mp, 20u64);
os.close(fd);
if (n < 20i64) { return 0; };
@@ -188,11 +189,11 @@ fn isso(path: *u8) i32 = {
export fn main(argc: i32, argv: **u8) i32 = {
let outpath: *u8 = nil;
let maxinputs: i32 = 64;
let inputs: **u8 = os.alloc((maxinputs: u64) * 8u64): **u8;
let inputs: **u8 = rt.alloc((maxinputs: u64) * 8u64): **u8;
let ninputs: i32 = 0;
let libdirs: **u8 = os.alloc((maxinputs: u64) * 8u64): **u8;
let libdirs: **u8 = rt.alloc((maxinputs: u64) * 8u64): **u8;
let nlibdirs: i32 = 0;
let lflags: **u8 = os.alloc((maxinputs: u64) * 8u64): **u8;
let lflags: **u8 = rt.alloc((maxinputs: u64) * 8u64): **u8;
let nlflags: i32 = 0;
let i: i32 = 1;

View File

@@ -11,6 +11,7 @@
package w6l;
import os;
import rt;
import mem;
import sym;
@@ -86,7 +87,7 @@ fn slurp(path: *u8) (*u8, u64) = {
case let v: i64 => n = v;
case let e: os.oserror => { os.close(fd); return nil, 0u64; };
};
let buf: *u8 = os.alloc(n: u64): *u8;
let buf: *u8 = rt.alloc(n: u64): *u8;
let rr: (i64 | os.oserror) = os.readall(fd, buf, n: u64);
os.close(fd);
let got: i64 = 0i64;
@@ -108,7 +109,7 @@ fn emittext(l: *lnk, src: *u8, n: u64) void = {
// Grow by mmap'ing a fresh region and copying. The old buffer
// is leaked into the page allocator; for a linker run this is
// trivial waste.
let nb: *u8 = os.alloc(nc): *u8;
let nb: *u8 = rt.alloc(nc): *u8;
let i: u64 = 0u64;
for (i < l.textlen) {
nb[i] = l.text[i];
@@ -130,7 +131,7 @@ fn emitdata(l: *lnk, src: *u8, n: u64) void = {
let nc: u64 = l.datacap;
if (nc == 0u64) { nc = 256u64; };
for (nc < l.datalen + n) { nc = nc * 2u64; };
let nb: *u8 = os.alloc(nc): *u8;
let nb: *u8 = rt.alloc(nc): *u8;
let i: u64 = 0u64;
for (i < l.datalen) {
nb[i] = l.data[i];

View File

@@ -10,6 +10,7 @@
package w6l;
import os;
import rt;
import sym;
import dynout;
@@ -88,7 +89,7 @@ export fn emitelf(l: *lnk, fd: i32, base: u64, entry: u64) i32 = {
let datafilelen: u64 = l.datalen - bsslen;
// One contiguous header buffer covering [0..0x1000), then .text.
let hdr: *u8 = os.alloc(TEXT_OFF): *u8; // zero-initialised by mmap
let hdr: *u8 = rt.alloc(TEXT_OFF): *u8; // zero-initialised by mmap
// --- Ehdr (64 bytes) ---
hdr[0u64] = 127u8; // 0x7f