Files
ww/lib/memio/memio.ww
Hojun-Cho a376ec89eb lib/rt: rename rt_alloc → rt_malloc; rt.alloc → rt.malloc
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.
2026-05-20 22:11:34 +09:00

210 lines
5.7 KiB
Plaintext

// memio — in-memory io stream.
//
// Hare's memio:: surface, drop underscores. Two flavours behind a
// single [[io.stream]]:
//
// fixed caller owns the buffer, writes stop when full.
// dynamic memio owns the buffer, writes grow it; close frees.
//
// Call shape divergence from Hare: the caller supplies both the
// memio `state` and the `io.stream` slot, by pointer. ww cgen does
// not yet implement &x.field or 32B-struct return-by-value, so the
// Hare `let s = memio::fixed(buf)` shape isn't reachable; collapse
// to a single returned struct when those land (lib/CLAUDE.md
// "graduate in one go").
//
// let mem: memio.state;
// let s: io.stream;
// memio.fixed(&mem, &s, buf);
// io.write(&s, bytes);
//
// Subset of Hare's surface: io.stream's variants are {eof, closed},
// so memio drops Hare's NONBLOCK flag (would need an `again` variant
// in lib/io) and string()'s utf8 validation (lib has no fromutf8
// today). Hare's seek / copy callbacks are likewise absent: lib/io's
// stream vtable has only read/write/close slots, so memio can't wire
// a seeker or copier even if we wanted to. All three come back when
// their dependencies do.
package memio;
import io;
import os;
import rt;
// state — memio's per-stream bookkeeping. The caller owns the slot
// and passes its address into a constructor. `ptr/len/cap` are the
// slice fields kept flat to dodge a chained-dot write through the
// state pointer (cgen doesn't store into `m.buf.ptr` reliably).
export type state = struct {
ptr: *u8,
len: i32,
cap: i32,
pos: i32,
};
// fixed — wire `s` over a caller-supplied buffer. Writes never grow;
// they return 0 once `pos` reaches the end of the buffer.
export fn fixed(m: *state, s: *io.stream, buf: []u8) void = {
m.ptr = buf.ptr;
m.len = buf.len;
m.cap = buf.len;
m.pos = 0;
s.ctx = m: *void;
s.read = readfn;
s.write = fixedwrite;
s.close = closenoop;
};
// dynamic — wire `s` with no initial buffer. Writes grow the backing
// allocation; [[io.close]] frees it.
export fn dynamic(m: *state, s: *io.stream) void = {
m.ptr = nil;
m.len = 0;
m.cap = 0;
m.pos = 0;
s.ctx = m: *void;
s.read = readfn;
s.write = dynamicwrite;
s.close = dynamicclose;
};
// dynamicfrom — like [[dynamic]] but seeded with an existing slice.
// Ownership of the slice transfers to the stream; [[io.close]] frees
// it. The slice must come from the runtime allocator: close calls
// [[os.free]] with `m.cap` bytes, which is taken from `buf.cap` (the
// slice's allocated capacity), not its logical length. Passing a
// half-filled append slice (len < cap) and using only `buf.len` here
// would under-free on close.
export fn dynamicfrom(m: *state, s: *io.stream, buf: []u8) void = {
m.ptr = buf.ptr;
m.len = buf.len;
m.cap = buf.cap;
m.pos = 0;
s.ctx = m: *void;
s.read = readfn;
s.write = dynamicwrite;
s.close = dynamicclose;
};
// buffer — borrowed view of bytes written so far (buf[..pos]).
// Seek to the end before calling if the full buffer is wanted.
export fn buffer(m: *state) []u8 = {
let r: []u8;
r.ptr = m.ptr;
r.len = m.pos;
return r;
};
// string — bytes written so far, as a str view. Hare returns
// (str | utf8::invalid); ww doesn't ship utf8 validation yet, so
// this returns the unchecked view.
export fn string(m: *state) str = {
let r: str;
r.ptr = m.ptr;
r.len = m.pos;
return r;
};
// reset — rewind the cursor and truncate the logical content to 0.
// Backing storage is preserved; subsequent writes (dynamic) re-fill
// from the start without reallocation.
export fn reset(m: *state) void = {
m.pos = 0;
m.len = 0;
};
// borrowedread — return an `amt`-byte view starting at `pos` without
// copying, advancing the cursor. eof if fewer bytes are available.
export fn borrowedread(m: *state, amt: i32) ([]u8 | io.eof) = {
if (m.len - m.pos < amt) {
let e: io.eof;
return e;
};
let r: []u8;
r.ptr = m.ptr + (m.pos: u64);
r.len = amt;
m.pos += amt;
return r;
};
// ---- vtable callbacks ------------------------------------------------
fn readfn(s: *io.stream, buf: []u8) (i32 | io.eof | io.closed) = {
let m: *state = s.ctx: *state;
if (m.pos >= m.len) {
let e: io.eof;
return e;
};
let avail: i32 = m.len - m.pos;
let n: i32 = buf.len;
if (avail < n) { n = avail; };
let i: i32 = 0;
for (i < n) {
buf[i] = m.ptr[m.pos + i];
i += 1;
};
m.pos += n;
return n;
};
fn fixedwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = {
let m: *state = s.ctx: *state;
if (m.pos >= m.len) { return 0; };
let space: i32 = m.len - m.pos;
let n: i32 = buf.len;
if (space < n) { n = space; };
let i: i32 = 0;
for (i < n) {
m.ptr[m.pos + i] = buf[i];
i += 1;
};
m.pos += n;
return n;
};
fn dynamicwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = {
let m: *state = s.ctx: *state;
let need: i32 = m.pos + buf.len;
if (need > m.cap) { grow(m, need); };
let i: i32 = 0;
for (i < buf.len) {
m.ptr[m.pos + i] = buf[i];
i += 1;
};
m.pos += buf.len;
if (m.pos > m.len) { m.len = m.pos; };
return buf.len;
};
fn dynamicclose(s: *io.stream) (void | io.closed) = {
let m: *state = s.ctx: *state;
if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); };
m.ptr = nil;
m.len = 0;
m.cap = 0;
m.pos = 0;
return;
};
fn closenoop(s: *io.stream) (void | io.closed) = {
return;
};
// Double-and-copy growth. Initial bump from 0 lands at 8 to amortise
// small write bursts without a tail of reallocs.
fn grow(m: *state, need: i32) void = {
let newcap: i32 = m.cap;
if (newcap < 8) { newcap = 8; };
for (newcap < need) { newcap *= 2; };
let nbuf: *u8 = rt.malloc(newcap: u64): *u8;
let i: i32 = 0;
for (i < m.len) {
nbuf[i] = m.ptr[i];
i += 1;
};
if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); };
m.ptr = nbuf;
m.cap = newcap;
};