lib/memio+shlex+getopt: use os.alloc / os.free; drop @symbol bindings

Migrate the three modules that still carried private
@symbol("rt_alloc") / @symbol("rt_free") bindings onto the public
lib/os.alloc / lib/os.free surface that landed in 87c0883.

memio: 1 alloc (grow) + 2 free (grow's old-buffer drop, dynamicclose).
shlex: 1 alloc (dupstr) + 2 free (freepartial: element strs + slice
header). getopt: 1 alloc (tryparse) + 2 free (tryparse + finish).
ABI identity holds — same rt syms, same shapes, just routed through
the public surface.

rt_ensure stays inline in shlex + getopt; the slice-growth helper
isn't part of os and has no stdlib facade. Comments explain why.

Header rationale comments updated: dropped the now-stale
"lib/io ↔ lib/os C-symbol collision" framing on shlex's inlined
dupstr (that was a pre-#9 concern); reworded shlex's OOM trailer to
match lib/os.ww's documented contract (poisonous pointer, not nil,
fault on deref); fixed memio's dynamicfrom doc to reference
[[os.free]] instead of the retired rt_free name.

980_memio_run / 973_shlex_run / 982_getopt_run all green; bootstrap
byte-identical.
This commit is contained in:
2026-05-16 02:03:18 +09:00
parent 87c088359d
commit db2b05bbe5
3 changed files with 27 additions and 44 deletions

View File

@@ -91,18 +91,15 @@
// }; // };
// defer getopt.finish(&cmd); // defer getopt.finish(&cmd);
use os;
use strings; use strings;
// Direct rt_free / rt_ensure bindings rather than `use os;` — os
// exports read/write/close, which collide with io.read/write/close
// in callers that mix both (task #7). Same pattern as lib/memio.
//
// rt_ensure is the runtime slice-growth helper invoked by the // rt_ensure is the runtime slice-growth helper invoked by the
// `append(s, v)` builtin. We bind it directly because the builtin's // `append(s, v)` builtin. We bind it directly because the builtin's
// expansion stores only 8 bytes of the new element (cgen emits a // expansion stores only 8 bytes of the new element (cgen emits a
// single MOVQ), losing the `value: str` half of an [[option]]. // single MOVQ), losing the `value: str` half of an [[option]].
// [[appendoption]] grows manually and stores both fields via *option. // [[appendoption]] grows manually and stores both fields via *option.
@symbol("rt_free") fn rtfree(p: *void, n: u64) void; // No public stdlib facade exposes rt_ensure, hence the direct @symbol.
@symbol("rt_ensure") fn rtensure(s: *void, membsz: u64) void; @symbol("rt_ensure") fn rtensure(s: *void, membsz: u64) void;
// helpkind — which slot of [[help]] is meaningful. Hare's getopt // helpkind — which slot of [[help]] is meaningful. Hare's getopt
@@ -296,7 +293,7 @@ export fn tryparse(out: *command, argv: []str, help: []help) (void | error) = {
match (look) { match (look) {
case void => { case void => {
if (opts.cap > 0) { if (opts.cap > 0) {
rtfree(opts.ptr: *void, os.free(opts.ptr: *void,
(opts.cap: u64) * 24u64); (opts.cap: u64) * 24u64);
}; };
let e: error; let e: error;
@@ -318,7 +315,7 @@ export fn tryparse(out: *command, argv: []str, help: []help) (void | error) = {
} else { } else {
if (i + 1 >= argv.len) { if (i + 1 >= argv.len) {
if (opts.cap > 0) { if (opts.cap > 0) {
rtfree(opts.ptr: *void, os.free(opts.ptr: *void,
(opts.cap: u64) * 24u64); (opts.cap: u64) * 24u64);
}; };
let e: error; let e: error;
@@ -355,7 +352,7 @@ export fn tryparse(out: *command, argv: []str, help: []help) (void | error) = {
export fn finish(cmd: *command) void = { export fn finish(cmd: *command) void = {
if (cmd.optscap > 0) { if (cmd.optscap > 0) {
// option layout: rune (4) + pad (4) + str (16) = 24B. // option layout: rune (4) + pad (4) + str (16) = 24B.
rtfree(cmd.optsptr: *void, (cmd.optscap: u64) * 24u64); os.free(cmd.optsptr: *void, (cmd.optscap: u64) * 24u64);
}; };
cmd.optsptr = nil: *option; cmd.optsptr = nil: *option;
cmd.optslen = 0; cmd.optslen = 0;

View File

@@ -27,13 +27,7 @@
// their dependencies do. // their dependencies do.
use io; use io;
use os;
// Direct rt_alloc/rt_free bindings — the runtime heap primitives
// memio.dynamic / dynamicfrom need to own the backing buffer. lib/os
// holds the same FFI shape but doesn't export it; same pattern as
// rt/ensure.ww.
@symbol("rt_alloc") fn rtalloc(n: u64) *void;
@symbol("rt_free") fn rtfree(p: *void, n: u64) void;
// state — memio's per-stream bookkeeping. The caller owns the slot // state — memio's per-stream bookkeeping. The caller owns the slot
// and passes its address into a constructor. `ptr/len/cap` are the // and passes its address into a constructor. `ptr/len/cap` are the
@@ -75,7 +69,7 @@ export fn dynamic(m: *state, s: *io.stream) void = {
// dynamicfrom — like [[dynamic]] but seeded with an existing slice. // dynamicfrom — like [[dynamic]] but seeded with an existing slice.
// Ownership of the slice transfers to the stream; [[io.close]] frees // Ownership of the slice transfers to the stream; [[io.close]] frees
// it. The slice must come from the runtime allocator: close calls // it. The slice must come from the runtime allocator: close calls
// rt_free with `m.cap` bytes, which is taken from `buf.cap` (the // [[os.free]] with `m.cap` bytes, which is taken from `buf.cap` (the
// slice's allocated capacity), not its logical length. Passing a // slice's allocated capacity), not its logical length. Passing a
// half-filled append slice (len < cap) and using only `buf.len` here // half-filled append slice (len < cap) and using only `buf.len` here
// would under-free on close. // would under-free on close.
@@ -182,7 +176,7 @@ fn dynamicwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = {
fn dynamicclose(s: *io.stream) (void | io.closed) = { fn dynamicclose(s: *io.stream) (void | io.closed) = {
let m: *state = s.ctx: *state; let m: *state = s.ctx: *state;
if (m.cap > 0) { rtfree(m.ptr: *void, m.cap: u64); }; if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); };
m.ptr = nil; m.ptr = nil;
m.len = 0; m.len = 0;
m.cap = 0; m.cap = 0;
@@ -200,13 +194,13 @@ fn grow(m: *state, need: i32) void = {
let newcap: i32 = m.cap; let newcap: i32 = m.cap;
if (newcap < 8) { newcap = 8; }; if (newcap < 8) { newcap = 8; };
for (newcap < need) { newcap *= 2; }; for (newcap < need) { newcap *= 2; };
let nbuf: *u8 = rtalloc(newcap: u64): *u8; let nbuf: *u8 = os.alloc(newcap: u64): *u8;
let i: i32 = 0; let i: i32 = 0;
for (i < m.len) { for (i < m.len) {
nbuf[i] = m.ptr[i]; nbuf[i] = m.ptr[i];
i += 1; i += 1;
}; };
if (m.cap > 0) { rtfree(m.ptr: *void, m.cap: u64); }; if (m.cap > 0) { os.free(m.ptr: *void, m.cap: u64); };
m.ptr = nbuf; m.ptr = nbuf;
m.cap = newcap; m.cap = newcap;
}; };

View File

@@ -28,9 +28,10 @@
// //
// Divergences from Hare: // Divergences from Hare:
// //
// - Drop nomem: ww os.alloc aborts on OOM (same precedent as // - Drop nomem: ww os.alloc has no recoverable failure path (OOM
// strings.dup, getopt.appendoption). Hare's // yields a poisonous pointer that faults on deref; see lib/os.ww
// (...|syntaxerr|nomem) collapses to (...|syntaxerr). // comment). Same precedent as strings.dup, getopt.appendoption.
// Hare's (...|syntaxerr|nomem) collapses to (...|syntaxerr).
// //
// - Byte-wise iteration via i32 cursor instead of Hare's // - Byte-wise iteration via i32 cursor instead of Hare's
// strings::iterator (no UTF-8 rune iteration in the language // strings::iterator (no UTF-8 rune iteration in the language
@@ -66,13 +67,10 @@
// greppable across modules. When the append-builtin is fixed, // greppable across modules. When the append-builtin is fixed,
// all three (getopt, shlex, anywhere else) collapse in one go. // all three (getopt, shlex, anywhere else) collapse in one go.
// //
// - Internal [[dupstr]] inlined rather than `use strings;`: lib/io // - Internal [[dupstr]] inlined rather than `use strings;`: keeps
// and lib/os both export read/write/close as C symbols with // the dep surface small for a 6-line helper. shlex doesn't reach
// different signatures, and `use strings;` would transitively // for any other strings:: routine, and callers free split()'s
// pull `use os;` (strings:6) which collides with `use io;` here. // result via [[strings.freeall]] directly.
// Same defensive shape lib/fmt and lib/memio use until task #17
// (cgen module-mangles fn labels) lands; at that point dupstr
// graduates to strings.dup and the workaround retires.
// //
// Caller layout — split: // Caller layout — split:
// //
@@ -96,18 +94,13 @@
use io; use io;
use memio; use memio;
use os;
// Direct rt_alloc / rt_free / rt_ensure bindings rather than `use os;`
// — os exports read/write/close, which collide with io.read/write/close
// under the driver's flat-scope concat. Same workaround as lib/memio,
// lib/fmt, lib/log; retires when task #17 (cgen mod-mangling) lands.
//
// rt_ensure is the runtime slice-growth helper invoked by the // rt_ensure is the runtime slice-growth helper invoked by the
// `append(s, v)` builtin. We bind it directly because the builtin's // `append(s, v)` builtin. We bind it directly because the builtin's
// expansion stores only 8 bytes of the new element (cgen emits a // expansion stores only 8 bytes of the new element (cgen emits a
// single MOVQ), losing the `len` half of a `str` (16B). // single MOVQ), losing the `len` half of a `str` (16B). No public
@symbol("rt_alloc") fn rtalloc(n: u64) *void; // stdlib facade exposes it, hence the direct @symbol.
@symbol("rt_free") fn rtfree(p: *void, n: u64) void;
@symbol("rt_ensure") fn rtensure(s: *void, membsz: u64) void; @symbol("rt_ensure") fn rtensure(s: *void, membsz: u64) void;
// syntaxerr — the input wasn't a valid shell-tokenizable string // syntaxerr — the input wasn't a valid shell-tokenizable string
@@ -122,16 +115,15 @@ export fn strerror(err: syntaxerr) str = {
return "Invalid shell syntax"; return "Invalid shell syntax";
}; };
// dupstr — local strings.dup. Inlined to keep `use strings;` out of // dupstr — local strings.dup. Inlined to avoid a `use strings;` dep
// this module (would re-trip the lib/io ↔ lib/os C-symbol collision; // on this small site; same algorithm and same {nil, 0} handling for
// see file header). Same algorithm and same {nil, 0} handling for // empty input.
// empty input. Retires when task #17 lands.
fn dupstr(s: str) str = { fn dupstr(s: str) str = {
let r: str; let r: str;
r.ptr = nil; r.ptr = nil;
r.len = 0; r.len = 0;
if (s.len == 0) { return r; }; if (s.len == 0) { return r; };
let buf: *u8 = rtalloc(s.len: u64): *u8; let buf: *u8 = os.alloc(s.len: u64): *u8;
let i: i32 = 0; let i: i32 = 0;
for (i < s.len) { buf[i] = s[i]; i += 1; }; for (i < s.len) { buf[i] = s[i]; i += 1; };
r.ptr = buf; r.ptr = buf;
@@ -160,12 +152,12 @@ fn freepartial(slice: []str) void = {
let i: i32 = 0; let i: i32 = 0;
for (i < slice.len) { for (i < slice.len) {
if (slice[i].len > 0) { if (slice[i].len > 0) {
rtfree(slice[i].ptr: *void, slice[i].len: u64); os.free(slice[i].ptr: *void, slice[i].len: u64);
}; };
i += 1; i += 1;
}; };
if (slice.cap > 0) { if (slice.cap > 0) {
rtfree(slice.ptr: *void, (slice.cap: u64) * 16u64); os.free(slice.ptr: *void, (slice.cap: u64) * 16u64);
}; };
}; };