Hare's canonical runtime allocator is rt::malloc with linker symbol
rt.malloc (ref/hare/rt/malloc.ha:27,78). ww kept the dot→underscore
Plan 9 convention (CLAUDE.md rule 4) so the linker symbol becomes
rt_malloc; the lib/rt exported function name becomes malloc; ww
callers say rt.malloc(...).
The language builtin keyword stays `alloc(T)!` — unchanged from Hare
(ref/hare/hare/lex/token.ha:21 ltok::ALLOC, parse/expr.ha:398
builtin()). The rename only touches the lowered linker symbol and the
exported function name behind it; the user-facing syntax for
heap-allocation is identical to Hare.
Surface:
- rt/alloc.s: TEXT rt_alloc → TEXT rt_malloc, labels updated
- lib/rt/malloc.ww: @symbol("rt_malloc") fn malloc(...) (was rt_alloc/alloc)
- rt/ensure.ww: local FFI decl + call site updated to malloc; `!` dropped
on the direct FFI call (rt_malloc returns *void, not a tagged union)
- 18 .ww callers: rt.alloc(...) → rt.malloc(...)
- cstage cmd/wcc/check.c + wwstage selfhost/cmd/wcc/check.ww
alloc-builtin suppression gate routes through ffi_resolve("malloc")
for the lowering; the user-shadow check still keys on the BUILTIN
KEYWORD "alloc" since that is what `alloc(...)` parses as. Adding
"malloc" to the user-shadow check was unnecessary and was reverted
during pre-commit review.
- cstage cmd/w6c/cgen.c: 2× ffi_resolve("alloc") → ffi_resolve("malloc")
- wwstage cgenexpr/cgenstmt: 2× ffiresolve(c, "alloc") → ffiresolve(c, "malloc")
- Test fixtures (700_e2e, 758_cgalloc_str_field, 990_selfhost, 992_w6l_ww,
selfhost/test/tagged_ptr_ret.ww): updated inline ww sources to the new
decl + call form
This is commit 2 of 3 in the lib/rt extraction (#38). Commit 3 closes
the OOM contract — return type becomes nullable *void and the builtin
lowering null-checks + propagates nomem.
Verified 132/132 + 995_self_rebuild byte-identity (5 wwstage tools
round-trip identical) + make clean cold rebuild.
110 lines
2.7 KiB
Plaintext
110 lines
2.7 KiB
Plaintext
// selfhost/cmd/wcc/mem.ww — port of cmd/wcc/mem.c.
|
|
//
|
|
// Bump arena allocator. Backed by the runtime page allocator
|
|
// (rt_malloc / rt_free), no libc. Each chunk is mmap'd; when the
|
|
// current chunk runs out we link a fresh one. Freeing the arena
|
|
// unmaps the chain.
|
|
//
|
|
// Memory handed out is 16-byte aligned. The C version under
|
|
// cmd/wcc/ is retained until the three-stage bootstrap diffs clean.
|
|
|
|
package wcc;
|
|
|
|
import os;
|
|
import rt;
|
|
|
|
def ALIGN: u64 = 16u64;
|
|
def INIT_CHUNK: u64 = 65536u64;
|
|
def MAX_CHUNK: u64 = 4194304u64;
|
|
def ARENA_SZ: u64 = 48u64; // sizeof(arena), kept in sync below
|
|
|
|
type arena = struct {
|
|
buf: *u8,
|
|
off: u64,
|
|
cap: u64,
|
|
next: *arena,
|
|
total: u64,
|
|
};
|
|
|
|
fn roundup(n: u64, a: u64) u64 = {
|
|
return (n + a - 1u64) & ~(a - 1u64);
|
|
};
|
|
|
|
export fn newarena() *arena = {
|
|
let a: *arena = rt.malloc(ARENA_SZ): *arena;
|
|
a.buf = rt.malloc(INIT_CHUNK): *u8;
|
|
a.off = 0u64;
|
|
a.cap = INIT_CHUNK;
|
|
a.next = nil;
|
|
a.total = 0u64;
|
|
return a;
|
|
};
|
|
|
|
// Grow: link a fresh chunk in front of the head. We push the old
|
|
// chunk into `next` so the head always describes the current bump
|
|
// region. Chunk size doubles up to MAX_CHUNK.
|
|
fn grow(a: *arena, need: u64) bool = {
|
|
let want: u64 = a.cap * 2u64;
|
|
if (want < need) { want = need; };
|
|
if (want > MAX_CHUNK) { want = MAX_CHUNK; };
|
|
if (want < need) { return false; }; // single allocation too big
|
|
|
|
let old: *arena = rt.malloc(ARENA_SZ): *arena;
|
|
old.buf = a.buf;
|
|
old.off = a.off;
|
|
old.cap = a.cap;
|
|
old.next = a.next;
|
|
old.total = 0u64;
|
|
|
|
a.buf = rt.malloc(want): *u8;
|
|
a.off = 0u64;
|
|
a.cap = want;
|
|
a.next = old;
|
|
return true;
|
|
};
|
|
|
|
export fn amalloc(a: *arena, n: u64) *void = {
|
|
let need: u64 = roundup(n, ALIGN);
|
|
if (need > a.cap - a.off) {
|
|
if (!grow(a, need)) { return nil; };
|
|
};
|
|
let p: *u8 = a.buf + a.off;
|
|
a.off += need;
|
|
a.total += need;
|
|
// Zero the region. Plan 9 amalloc zeroes; we mirror that here so
|
|
// the checker can assume freshly allocated nodes start at 0.
|
|
let i: u64 = 0u64;
|
|
for (i < need) {
|
|
p[i] = 0u8;
|
|
i += 1u64;
|
|
};
|
|
return p: *void;
|
|
};
|
|
|
|
// astrndup — copy `n` bytes into the arena and produce a NUL-terminated
|
|
// view. Returns a `str` whose ptr is arena-owned and whose len is `n`
|
|
// (the trailing NUL is past `len`, so callers reading exactly n bytes
|
|
// see no padding). Used by the lexer to capture token text.
|
|
export fn astrndup(a: *arena, src: *u8, n: u64) str = {
|
|
let p: *u8 = amalloc(a, n + 1u64): *u8;
|
|
let i: u64 = 0u64;
|
|
for (i < n) {
|
|
p[i] = src[i];
|
|
i += 1u64;
|
|
};
|
|
p[n] = 0u8;
|
|
let r: str;
|
|
r.ptr = p;
|
|
r.len = n: i32;
|
|
return r;
|
|
};
|
|
|
|
export fn freearena(a: *arena) void = {
|
|
for (a != nil) {
|
|
let next: *arena = a.next;
|
|
os.free(a.buf: *void, a.cap);
|
|
os.free(a: *void, ARENA_SZ);
|
|
a = next;
|
|
};
|
|
};
|