lib/memio: wire seeker (io.seek dispatch landed; #5-era deferral stale)

io.error grows errors.invalid (ref/hare/io/types.ha:11 spreads
...errors::error, which includes it; Hare's memio seek returns it on
out-of-bounds, stream.ha:134-136) — appended last so existing member
tags stay put; no exhaustive io.error matches exist in lib.

seekfn mirrors ref/hare/memio/stream.ha:122-140 over the flat header,
shared by fixed/dynamic/dynamicfrom (Hare wires the same seek into
both vtables). The io.off-vs-i64 arithmetic runs on an i64 copy:
cstage binop typing is nominal on aliases, wwstage accepts (filed,
ww-core #54).

791's stream_seek_unsupported row re-pins st_seek's void-arm on a
hand-built seekerless vtable: its old premise (memio wires no seeker)
is retired by this commit; memio seek success is pinned by memiotest.

w6c/wwdump main.combined.ww regen'd: they embed lib/io + lib/memio
(the freshness gates are blind to this — embedded-source discipline);
w6a/w6l/ww don't embed io, verified untouched.
This commit is contained in:
2026-06-04 15:49:25 +09:00
parent bc048ebe65
commit 532b0a88ae
6 changed files with 365 additions and 31 deletions

View File

@@ -15483,9 +15483,10 @@ fn rt_strerror(op: *opaque_data) str = {
// resolve via the dir-enum concat order (io.ww < stream.ww < types.ww
// — `stream` lands before the reader/writer/closer aliases below).
//
// Deferrals (drew-signed): `handle`, `seeker`, `copier`, `strerror`
// land with io fold-2 (#5; need the `handle` sum). Hare's `EOF = done`
// stays as `io.eof` until the `done` singleton ships (#93).
// Deferrals (drew-signed): `copier`, `strerror` (the #5 list's
// `handle` + `seeker` landed with the #5 arc and the memio-seeker
// port). Hare's `EOF = done` stays as `io.eof` until the `done`
// singleton ships (#93).
package io;
@@ -15531,8 +15532,11 @@ export type handle = (file | stream);
// layout mismatch; #199b layout-extension is the deferred fix). The
// read/write dispatchers in stream.ww return `errors.unsupported`
// when the matching vtable slot is unset, so the variant lands here
// directly.
export type error = !(errors.unsupported | underread | nomem);
// directly. `errors.invalid` rides the memio-seeker port (Hare's
// memio seek returns errors::invalid on out-of-bounds,
// ref/hare/memio/stream.ha:134-136); appended last so existing
// member tags stay put.
export type error = !(errors.unsupported | underread | nomem | errors.invalid);
// ref/hare/io/types.ha:46. `eof` (not Hare's `done`) per the io.ww
// rationale; lifts to `done` with #93. `stream` forward-refs the
@@ -15584,9 +15588,10 @@ export type seeker = fn(s: stream, off: off, w: whence) (off | error);
// Subset of Hare's surface: io's variants are {eof, error}, so memio
// drops Hare's NONBLOCK flag (would need an `again` variant in lib/io).
// string()'s utf8-validating constructor is omitted per CLAUDE.md
// rule 9 carve-out — see [[string]]. Hare's seek / copy callbacks are
// likewise absent: io's stream vtable has only read/write/close slots,
// so memio can't wire a seeker or copier yet (io fold-2, #5).
// rule 9 carve-out — see [[string]]. Hare's copy callback is absent:
// io's vtable has no copier slot yet (deferred with `handle`-typed
// io.copy, lib/io/stream.ww header). The seeker is wired — see
// [[seekfn]].
//
// Cast workaround per #206-payoff (ken: KEEP the explicit casts; they
// are cgen-neutral and sidestep the #214 over-acceptance surface). The
@@ -15603,6 +15608,7 @@ export type seeker = fn(s: stream, off: off, w: whence) (off | error);
package memio;
import errors;
import io;
import os;
import rt;
@@ -15632,6 +15638,7 @@ export fn fixed(buf: []u8) stream = {
let r: stream;
r.vt.reader = (&readfn): *io.reader;
r.vt.writer = (&fixedwrite): *io.writer;
r.vt.seeker = (&seekfn): *io.seeker;
r.ptr = buf.ptr;
r.len = buf.len;
r.cap = buf.len;
@@ -15648,6 +15655,7 @@ export fn dynamic() stream = {
r.vt.reader = (&readfn): *io.reader;
r.vt.writer = (&dynamicwrite): *io.writer;
r.vt.closer = (&dynamicclose): *io.closer;
r.vt.seeker = (&seekfn): *io.seeker;
r.ptr = nil;
r.len = 0;
r.cap = 0;
@@ -15665,6 +15673,7 @@ export fn dynamicfrom(buf: []u8) stream = {
r.vt.reader = (&readfn): *io.reader;
r.vt.writer = (&dynamicwrite): *io.writer;
r.vt.closer = (&dynamicclose): *io.closer;
r.vt.seeker = (&seekfn): *io.seeker;
r.ptr = buf.ptr;
r.len = buf.len;
r.cap = buf.cap;
@@ -15696,6 +15705,44 @@ fn readfn(s: io.stream, buf: []u8) (size | io.eof | io.error) = {
return n: size;
};
// seekfn — SET/CUR/END cursor reposition over the common header;
// fixed and dynamic share it (Hare wires the same `seek` into both
// vtables, ref/hare/memio/stream.ha:26,34).
//
// Mirrors ref/hare/memio/stream.ha:122-140. len(s.buf) → m.len;
// `pos` is i32 while io.off is i64, so the arithmetic runs in i64 and
// narrows only after the bounds check. Hare's two-sided check works
// in unsigned `size` with a negation dance; the signed i64 spelling
// here is the same predicate without it.
fn seekfn(s: io.stream, off: io.off, w: io.whence) (io.off | io.error) = {
let m: *stream = s: *stream;
// cstage binop typing is nominal (no alias peel: `off` vs i64
// rejected, wwstage accepts — ww-core #54), so the arithmetic
// runs on an i64 copy.
let n: i64 = off: i64;
let start: i64 = 0;
switch (w) {
case io.whence.SET: start = 0;
case io.whence.CUR: start = m.pos: i64;
case io.whence.END: start = m.len: i64;
};
if (n < 0) {
if (start < -n) {
let v: errors.invalid;
let e: io.error = v;
return e;
};
} else {
if ((m.len: i64) - start < n) {
let v: errors.invalid;
let e: io.error = v;
return e;
};
};
m.pos = (start + n): i32;
return m.pos: io.off;
};
fn fixedwrite(s: io.stream, buf: []u8) (size | io.error) = {
let m: *stream = s: *stream;
if (m.pos >= m.len) { return 0: size; };

View File

@@ -15483,9 +15483,10 @@ fn rt_strerror(op: *opaque_data) str = {
// resolve via the dir-enum concat order (io.ww < stream.ww < types.ww
// — `stream` lands before the reader/writer/closer aliases below).
//
// Deferrals (drew-signed): `handle`, `seeker`, `copier`, `strerror`
// land with io fold-2 (#5; need the `handle` sum). Hare's `EOF = done`
// stays as `io.eof` until the `done` singleton ships (#93).
// Deferrals (drew-signed): `copier`, `strerror` (the #5 list's
// `handle` + `seeker` landed with the #5 arc and the memio-seeker
// port). Hare's `EOF = done` stays as `io.eof` until the `done`
// singleton ships (#93).
package io;
@@ -15531,8 +15532,11 @@ export type handle = (file | stream);
// layout mismatch; #199b layout-extension is the deferred fix). The
// read/write dispatchers in stream.ww return `errors.unsupported`
// when the matching vtable slot is unset, so the variant lands here
// directly.
export type error = !(errors.unsupported | underread | nomem);
// directly. `errors.invalid` rides the memio-seeker port (Hare's
// memio seek returns errors::invalid on out-of-bounds,
// ref/hare/memio/stream.ha:134-136); appended last so existing
// member tags stay put.
export type error = !(errors.unsupported | underread | nomem | errors.invalid);
// ref/hare/io/types.ha:46. `eof` (not Hare's `done`) per the io.ww
// rationale; lifts to `done` with #93. `stream` forward-refs the
@@ -15584,9 +15588,10 @@ export type seeker = fn(s: stream, off: off, w: whence) (off | error);
// Subset of Hare's surface: io's variants are {eof, error}, so memio
// drops Hare's NONBLOCK flag (would need an `again` variant in lib/io).
// string()'s utf8-validating constructor is omitted per CLAUDE.md
// rule 9 carve-out — see [[string]]. Hare's seek / copy callbacks are
// likewise absent: io's stream vtable has only read/write/close slots,
// so memio can't wire a seeker or copier yet (io fold-2, #5).
// rule 9 carve-out — see [[string]]. Hare's copy callback is absent:
// io's vtable has no copier slot yet (deferred with `handle`-typed
// io.copy, lib/io/stream.ww header). The seeker is wired — see
// [[seekfn]].
//
// Cast workaround per #206-payoff (ken: KEEP the explicit casts; they
// are cgen-neutral and sidestep the #214 over-acceptance surface). The
@@ -15603,6 +15608,7 @@ export type seeker = fn(s: stream, off: off, w: whence) (off | error);
package memio;
import errors;
import io;
import os;
import rt;
@@ -15632,6 +15638,7 @@ export fn fixed(buf: []u8) stream = {
let r: stream;
r.vt.reader = (&readfn): *io.reader;
r.vt.writer = (&fixedwrite): *io.writer;
r.vt.seeker = (&seekfn): *io.seeker;
r.ptr = buf.ptr;
r.len = buf.len;
r.cap = buf.len;
@@ -15648,6 +15655,7 @@ export fn dynamic() stream = {
r.vt.reader = (&readfn): *io.reader;
r.vt.writer = (&dynamicwrite): *io.writer;
r.vt.closer = (&dynamicclose): *io.closer;
r.vt.seeker = (&seekfn): *io.seeker;
r.ptr = nil;
r.len = 0;
r.cap = 0;
@@ -15665,6 +15673,7 @@ export fn dynamicfrom(buf: []u8) stream = {
r.vt.reader = (&readfn): *io.reader;
r.vt.writer = (&dynamicwrite): *io.writer;
r.vt.closer = (&dynamicclose): *io.closer;
r.vt.seeker = (&seekfn): *io.seeker;
r.ptr = buf.ptr;
r.len = buf.len;
r.cap = buf.cap;
@@ -15696,6 +15705,44 @@ fn readfn(s: io.stream, buf: []u8) (size | io.eof | io.error) = {
return n: size;
};
// seekfn — SET/CUR/END cursor reposition over the common header;
// fixed and dynamic share it (Hare wires the same `seek` into both
// vtables, ref/hare/memio/stream.ha:26,34).
//
// Mirrors ref/hare/memio/stream.ha:122-140. len(s.buf) → m.len;
// `pos` is i32 while io.off is i64, so the arithmetic runs in i64 and
// narrows only after the bounds check. Hare's two-sided check works
// in unsigned `size` with a negation dance; the signed i64 spelling
// here is the same predicate without it.
fn seekfn(s: io.stream, off: io.off, w: io.whence) (io.off | io.error) = {
let m: *stream = s: *stream;
// cstage binop typing is nominal (no alias peel: `off` vs i64
// rejected, wwstage accepts — ww-core #54), so the arithmetic
// runs on an i64 copy.
let n: i64 = off: i64;
let start: i64 = 0;
switch (w) {
case io.whence.SET: start = 0;
case io.whence.CUR: start = m.pos: i64;
case io.whence.END: start = m.len: i64;
};
if (n < 0) {
if (start < -n) {
let v: errors.invalid;
let e: io.error = v;
return e;
};
} else {
if ((m.len: i64) - start < n) {
let v: errors.invalid;
let e: io.error = v;
return e;
};
};
m.pos = (start + n): i32;
return m.pos: io.off;
};
fn fixedwrite(s: io.stream, buf: []u8) (size | io.error) = {
let m: *stream = s: *stream;
if (m.pos >= m.len) { return 0: size; };