From 532b0a88aeebe85496981b3f277a9b22297f177a Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 15:49:25 +0900 Subject: [PATCH] lib/memio: wire seeker (io.seek dispatch landed; #5-era deferral stale) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/io/types.ww | 14 +- lib/memio/memio.ww | 49 ++++++- lib/memio/memiotest.ww | 191 +++++++++++++++++++++++++++ selfhost/cmd/w6c/main.combined.ww | 63 +++++++-- selfhost/cmd/wwdump/main.combined.ww | 63 +++++++-- test/wcc/791_io_handle_run.c | 16 ++- 6 files changed, 365 insertions(+), 31 deletions(-) diff --git a/lib/io/types.ww b/lib/io/types.ww index c985bef1..1c365dc2 100644 --- a/lib/io/types.ww +++ b/lib/io/types.ww @@ -10,9 +10,10 @@ // 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; @@ -58,8 +59,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 diff --git a/lib/memio/memio.ww b/lib/memio/memio.ww index eb265137..46157356 100644 --- a/lib/memio/memio.ww +++ b/lib/memio/memio.ww @@ -30,9 +30,10 @@ // 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 @@ -49,6 +50,7 @@ package memio; +import errors; import io; import os; import rt; @@ -78,6 +80,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; @@ -94,6 +97,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; @@ -111,6 +115,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; @@ -142,6 +147,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; }; diff --git a/lib/memio/memiotest.ww b/lib/memio/memiotest.ww index 2cae7f28..5b8073f5 100644 --- a/lib/memio/memiotest.ww +++ b/lib/memio/memiotest.ww @@ -10,6 +10,7 @@ package memio; import bytes; +import errors; import io; import memio; import os; @@ -344,6 +345,193 @@ fn putstr(s: str, into: []u8, off: i32) i32 = { match (c) { case void => {}; case let e: io.error => fail(); }; }; +// ---- seekcases: SET/CUR/END × in-bounds/OOB over one fixed stream -------- + +// Sequential rows over one 8-byte fixed stream; CUR rows depend on the +// cursor left by earlier rows. want = resulting pos for valid rows, +// -1 = errors.invalid expected. After EVERY row an io.tell readback +// checks the cursor against a shadow `pos` — invalid rows must leave +// it untouched (ref/hare/memio/stream.ha:122-140 returns before the +// pos store). +@test fn seekcases() void = { + let arr: [8]u8; + arr[0]=10u8; arr[1]=11u8; arr[2]=12u8; arr[3]=13u8; + arr[4]=14u8; arr[5]=15u8; arr[6]=16u8; arr[7]=17u8; + + // (whence, off, want) + let wh: [12]i32; // 0 SET / 1 CUR / 2 END + let off: [12]i64; + let want: [12]i64; + wh[0] =0; off[0] =3; want[0] =3; // SET forward + wh[1] =1; off[1] =2; want[1] =5; // CUR forward + wh[2] =1; off[2] =-5; want[2] =0; // CUR back to start + wh[3] =2; off[3] =0; want[3] =8; // END exact end + wh[4] =2; off[4] =-8; want[4] =0; // END back to start + wh[5] =0; off[5] =9; want[5] =-1; // SET past end + wh[6] =1; off[6] =-1; want[6] =-1; // CUR before start + wh[7] =2; off[7] =1; want[7] =-1; // END past end + wh[8] =2; off[8] =-9; want[8] =-1; // END before start + wh[9] =0; off[9] =8; want[9] =8; // SET end exact + wh[10]=1; off[10]=0; want[10]=8; // CUR no-op at end + wh[11]=0; off[11]=0; want[11]=0; // SET rewind + + let st: memio.stream = memio.fixed(arr[0:8]); + let s: io.stream = &st.vt; + + let pos: i64 = 0; + let i: i32 = 0; + for (i < 12) { + let w: io.whence = wh[i]: io.whence; + let r: (io.off | io.error) = io.seek(s, off[i], w); + match (r) { + case let o: io.off => { + if (want[i] < 0) { fail(); }; + if ((o: i64) != want[i]) { fail(); }; + pos = o: i64; + }; + case let e: io.error => { + if (want[i] >= 0) { fail(); }; + match (e) { + case errors.invalid => {}; + case => fail(); + }; + }; + }; + let t: (io.off | io.error) = io.tell(s); + match (t) { + case let o: io.off => { if ((o: i64) != pos) { fail(); }; }; + case let e: io.error => fail(); + }; + i += 1; + }; + + // Data readback through the repositioned cursor: SET 5 then read + // drains bytes 5..7, then eof. + let r: (io.off | io.error) = io.seek(s, 5, io.whence.SET); + match (r) { + case let o: io.off => { if ((o: i64) != 5) { fail(); }; }; + case let e: io.error => fail(); + }; + let out: [8]u8; + let rd: (size | io.eof | io.error) = io.read(s, out[0:8]); + match (rd) { + case let nz: size => { + if (nz: i32 != 3) { fail(); }; + if (out[0] != 15u8) { fail(); }; + if (out[2] != 17u8) { fail(); }; + }; + case io.eof => fail(); + case let e: io.error => fail(); + }; + let rd2: (size | io.eof | io.error) = io.read(s, out[0:8]); + match (rd2) { + case io.eof => {}; + case let nz: size => fail(); + case let e: io.error => fail(); + }; +}; + +// ---- emptyseek: every whence over a 0-length buffer ---------------------- + +@test fn emptyseek() void = { + let arr: [1]u8; + arr[0] = 0u8; + let st: memio.stream = memio.fixed(arr[0:0]); + let s: io.stream = &st.vt; + + let wh: [5]i32; + let off: [5]i64; + let want: [5]i64; + wh[0]=0; off[0]=0; want[0]=0; // SET 0 — only valid target + wh[1]=2; off[1]=0; want[1]=0; // END 0 == start + wh[2]=0; off[2]=1; want[2]=-1; + wh[3]=1; off[3]=-1; want[3]=-1; + wh[4]=2; off[4]=-1; want[4]=-1; + + let i: i32 = 0; + for (i < 5) { + let w: io.whence = wh[i]: io.whence; + let r: (io.off | io.error) = io.seek(s, off[i], w); + match (r) { + case let o: io.off => { + if (want[i] < 0) { fail(); }; + if ((o: i64) != want[i]) { fail(); }; + }; + case let e: io.error => { + if (want[i] >= 0) { fail(); }; + match (e) { + case errors.invalid => {}; + case => fail(); + }; + }; + }; + i += 1; + }; +}; + +// ---- dynamicseek: rewind-and-reread over written data -------------------- + +// The dynamic vtable wires the same seekfn; bounds run against the +// LOGICAL length (m.len), not capacity (Hare: len(s.buf), +// ref/hare/memio/stream.ha:131,136). +@test fn dynamicseek() void = { + let st: memio.stream = memio.dynamic(); + let s: io.stream = &st.vt; + + let src: [5]u8; + src[0]=30u8; src[1]=31u8; src[2]=32u8; src[3]=33u8; src[4]=34u8; + let wr: (size | io.error) = io.write(s, src[0:5]); + match (wr) { + case let n: size => { if (n: i32 != 5) { fail(); }; }; + case let e: io.error => fail(); + }; + + // END 0 → 5 (logical length); cap is 8 post-grow, so END 1 must + // still be invalid even though capacity would fit it. + let r: (io.off | io.error) = io.seek(s, 0, io.whence.END); + match (r) { + case let o: io.off => { if ((o: i64) != 5) { fail(); }; }; + case let e: io.error => fail(); + }; + let r1: (io.off | io.error) = io.seek(s, 1, io.whence.END); + match (r1) { + case let o: io.off => fail(); + case let e: io.error => { + match (e) { + case errors.invalid => {}; + case => fail(); + }; + }; + }; + + // CUR -2 → 3; read drains 33,34 then eof. + let r2: (io.off | io.error) = io.seek(s, -2, io.whence.CUR); + match (r2) { + case let o: io.off => { if ((o: i64) != 3) { fail(); }; }; + case let e: io.error => fail(); + }; + let out: [4]u8; + let rd: (size | io.eof | io.error) = io.read(s, out[0:4]); + match (rd) { + case let nz: size => { + if (nz: i32 != 2) { fail(); }; + if (out[0] != 33u8) { fail(); }; + if (out[1] != 34u8) { fail(); }; + }; + case io.eof => fail(); + case let e: io.error => fail(); + }; + let rd2: (size | io.eof | io.error) = io.read(s, out[0:4]); + match (rd2) { + case io.eof => {}; + case let nz: size => fail(); + case let e: io.error => fail(); + }; + + let c: (void | io.error) = io.close(s); + match (c) { case void => {}; case let e: io.error => fail(); }; +}; + export fn main() i32 = { signalled = 1; fixedread(); signalled = 2; fixedwritecases(); @@ -352,5 +540,8 @@ export fn main() i32 = { signalled = 5; borrowedreadcases(); signalled = 6; stringview(); signalled = 7; dynamicfromseed(); + signalled = 8; seekcases(); + signalled = 9; emptyseek(); + signalled = 10; dynamicseek(); return 0; }; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 4afc5164..1993d6b3 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index d510a536..c77476d7 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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; }; diff --git a/test/wcc/791_io_handle_run.c b/test/wcc/791_io_handle_run.c index b6584f7c..7f1906dc 100644 --- a/test/wcc/791_io_handle_run.c +++ b/test/wcc/791_io_handle_run.c @@ -24,9 +24,13 @@ * | the backing buffer, then read them back * | through io.read on a fresh fixed stream. * | exit 43. - * stream_seek_unsupported | memio wires no seeker slot; io.seek - * | takes st_seek's void-arm and returns an - * | io.error (not an off). exit 44. + * stream_seek_unsupported | hand-built vtable with the seeker slot + * | left void; io.seek takes st_seek's + * | void-arm and returns an io.error (not + * | an off). exit 44. (Was memio.fixed: + * | memio wires a seeker now, so its seek + * | succeeds — pinned by memiotest; this + * | row keeps the void-arm dispatch pin.) * stream_seeker_tell | hand-built vtable WITH a seeker fn: * | io.seek returns the seeker's off, and * | io.tell == seek(s, 0, CUR). exit 45. @@ -142,11 +146,9 @@ static const struct row rows[] = { { "stream_seek_unsupported", "package main;\n" "import io;\n" - "import memio;\n" "export fn main() i32 = {\n" - " let buf: [4]u8;\n" - " let st: memio.stream = memio.fixed(buf[0:4]);\n" - " let s: io.stream = &st.vt;\n" + " let vt: io.vtable;\n" + " let s: io.stream = &vt;\n" " match (io.seek(s, 0: io.off, io.whence.SET)) {\n" " case let o: io.off => return 61;\n" " case let e: io.error => return 44;\n"