lib/memio: add Option C parallel vstream API (#94 fold-e2)
Adds lib/memio/vstream.ww with three new constructors — fixed_vstream / dynamic_vstream / dynamicfrom_vstream — that return io.vstream (= *io.vtable, from lib/io/stream.ww) alongside the pre-vtable memio.fixed / dynamic / dynamicfrom shape in memio.ww. Hare's memio::fixed/dynamic return a `stream` whose first field IS io::stream (= *vtable); ww mirrors that intrusively with fixed_ctx + dynamic_ctx structs whose first field is `vt: io.vtable`. A heap-alloc'd *fixed_ctx is castable to vstream via `&c.vt`, and callbacks recover the outer ctx via `s: *fixed_ctx` (same pattern as lib/bufio.stream over io.stream and lib/log.stdlogger over logger). ptr/len/cap kept flat (memio.ww:39 SOP) to dodge the chained-dot-through-pointer-into-slice-subfield miscompile family. Constructor flow: alloc with vt zero-initialised via a local, then chained `c.vt.X = …` field-assigns through the *ctx pointer. Struct-lit init via `vt = local_vt` (with local pre-set) silently drops tagged-union slots past the first — sibling task filed, workaround is the alloc-then-assign route (proven byte-id between both stages). *ctx is a plain pointer-to-struct, not an aliased pointer, so the chained-store path doesn't hit #195. Cast workaround per #206 at each vtable-fn-ptr-slot init (8 sites across the 3 constructors): bare `&fn_name` does not type-check as `(*<alias> | void)`. Same `(&fn): *io.<role>` shape that test/wcc/775_io_vtable_run.c uses. Drops out when #206 closes. OLD memio surface is UNCHANGED. fold-eFinal (task #50) atomically flips the package shape: deletes OLD constructors + callbacks and renames `_vstream` suffix off. Probe test/wcc/776_memio_vstream_run.c: 4 rows (fixed_read_5 / dynamic_write_grow / dynamicfrom_alt_rw / branched_fixed) exercise both vtable flavours through the io.st_read / st_write / st_close dispatchers. Each row drives cs runtime + ww runtime + cs.s == ww.s byte-id — 12 fixtures total, all green. Pre-existing wwstage gap surfaced + documented inline: ww_ww's combined.ww concat order trips the wwstage checker on os.tryread / trywrite / tryopen's bare `return r;` over `(int | oserror)` when os is checked after rt/io. Each probe row places `import os;` FIRST to match the ordering selfhost uses (time → os → rt → …) where the checker resolves cleanly. Sibling task; resolves the ordering-sensitivity in the wwstage checker drops the workaround.
This commit is contained in:
12
Makefile
12
Makefile
@@ -113,7 +113,7 @@ $(BIN)/wwdump_ww: selfhost/cmd/wwdump/main.ww \
|
||||
selfhost/cmd/wcc/cgen.ww selfhost/cmd/wcc/cgenexpr.ww \
|
||||
selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \
|
||||
selfhost/cmd/wcc/cgendecl.ww \
|
||||
lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strconv/ftos_data.ww lib/strconv/ftos.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/errors/errors.ww lib/io/io.ww lib/io/stream.ww lib/io/types.ww lib/memio/memio.ww lib/fmt/fmt.ww \
|
||||
lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strconv/ftos_data.ww lib/strconv/ftos.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/errors/errors.ww lib/io/io.ww lib/io/stream.ww lib/io/types.ww lib/memio/memio.ww lib/memio/vstream.ww lib/fmt/fmt.ww \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
@mkdir -p $(BIN)/wwdump_ww.d
|
||||
@@ -135,7 +135,7 @@ $(BIN)/w6c_ww: selfhost/cmd/w6c/main.ww \
|
||||
selfhost/cmd/wcc/cgen.ww selfhost/cmd/wcc/cgenexpr.ww \
|
||||
selfhost/cmd/wcc/cgenstmt.ww selfhost/cmd/wcc/cgenutil.ww \
|
||||
selfhost/cmd/wcc/cgendecl.ww \
|
||||
lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strconv/ftos_data.ww lib/strconv/ftos.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/errors/errors.ww lib/io/io.ww lib/io/stream.ww lib/io/types.ww lib/memio/memio.ww lib/fmt/fmt.ww \
|
||||
lib/os/os.ww lib/rt/malloc.ww lib/time/time.ww lib/strconv/strconv.ww lib/strconv/stof_data.ww lib/strconv/decimal.ww lib/strconv/stof.ww lib/strconv/ftos_data.ww lib/strconv/ftos.ww lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww lib/ascii/ascii.ww lib/errors/errors.ww lib/io/io.ww lib/io/stream.ww lib/io/types.ww lib/memio/memio.ww lib/memio/vstream.ww lib/fmt/fmt.ww \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
@mkdir -p $(BIN)/w6c_ww.d
|
||||
@@ -326,6 +326,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
||||
$(BIN)/test_isas_spread_variant \
|
||||
$(BIN)/test_tagged_widen_named_variant \
|
||||
$(BIN)/test_io_vtable_run \
|
||||
$(BIN)/test_memio_vstream_run \
|
||||
$(BIN)/test_use_promote_alias \
|
||||
$(BIN)/test_field_signed $(BIN)/test_frame_argcount \
|
||||
$(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \
|
||||
@@ -688,6 +689,13 @@ $(BIN)/test_io_vtable_run: test/wcc/775_io_vtable_run.c \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(BIN)/test_memio_vstream_run: test/wcc/776_memio_vstream_run.c \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
|
||||
lib/memio/memio.ww lib/memio/vstream.ww \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(BIN)/test_arrlit_str_full: test/wcc/711_arrlit_str_full.c \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
|
||||
|
||||
242
lib/memio/vstream.ww
Normal file
242
lib/memio/vstream.ww
Normal file
@@ -0,0 +1,242 @@
|
||||
// vstream — Hare-shaped vtable wrappers over memio. Project #94
|
||||
// fold-e2 (Option C, parallel API).
|
||||
//
|
||||
// Adds three constructors that return `io.vstream` (= `*io.vtable`,
|
||||
// the Hare-shape from lib/io/stream.ww) alongside the pre-vtable
|
||||
// memio.fixed / memio.dynamic / memio.dynamicfrom in memio.ww. The
|
||||
// OLD surface stays untouched here — fold-eFinal (task #50) atomically
|
||||
// flips the package shape: deletes the OLD constructors + callbacks,
|
||||
// renames `_vstream` suffix off, and migrates the few callers.
|
||||
//
|
||||
// Hare's memio::fixed/dynamic return `stream` whose FIRST field IS
|
||||
// `io::stream` (= `*vtable`). ww mirrors that intrusively: each ctx
|
||||
// struct's first field is `vt: io.vtable` (the vtable embedded
|
||||
// INLINE, not a pointer to it) so a heap-alloc'd `*fixed_ctx` is
|
||||
// castable to `vstream = *vtable` via `&c.vt` — and the callbacks
|
||||
// recover the outer ctx by casting the dispatch arg back to
|
||||
// `*fixed_ctx`. Same intrusive shape as lib/bufio.stream over
|
||||
// io.stream (bufio.ww:240-261) and lib/log.stdlogger over logger
|
||||
// (log.ww:95-98).
|
||||
//
|
||||
// Cast workaround per #206: bare `&fn_name` does not type-check as
|
||||
// a `(*<alias> | void)` field-init / let-binding (the structural
|
||||
// `*fn(...)` value isn't accepted as the `*reader` named variant
|
||||
// of the tagged slot). Explicit `(&fn_name): *io.<role>` cast at
|
||||
// each store site is the Hare-faithful minimum-touch route — same
|
||||
// workaround test/wcc/775_io_vtable_run.c uses for the bare-vtable
|
||||
// init. 8 cast tokens here (2 in fixed_vstream + 3 each in
|
||||
// dynamic_vstream / dynamicfrom_vstream — fixed leaves closer
|
||||
// void per Hare's fixed_vt, which lets st_close's void-arm return
|
||||
// plain `void` with no callback needed). Casts drop out wholesale
|
||||
// once #206 closes.
|
||||
//
|
||||
// ptr/len/cap kept flat (no `buf: []u8`) per the memio.state note
|
||||
// (memio.ww:39): chained-dot writes through a state pointer into a
|
||||
// slice subfield miscompile silently (related to the #195 family);
|
||||
// the flat shape sidesteps it. dynamicfrom_vstream uses the slice's
|
||||
// `cap` (NOT `len`) to track the allocated-capacity-to-free on
|
||||
// close — mirrors the same OLD memio.dynamicfrom note (memio.ww:73).
|
||||
|
||||
package memio;
|
||||
|
||||
import io;
|
||||
import os;
|
||||
import rt;
|
||||
|
||||
// fixed_ctx — heap-alloc'd state for fixed_vstream. `vt` at offset 0
|
||||
// for the intrusive vstream cast.
|
||||
export type fixed_ctx = struct {
|
||||
vt: io.vtable,
|
||||
ptr: *u8,
|
||||
len: i32,
|
||||
cap: i32,
|
||||
pos: i32,
|
||||
};
|
||||
|
||||
// dynamic_ctx — heap-alloc'd state for dynamic_vstream /
|
||||
// dynamicfrom_vstream. Same intrusive shape as fixed_ctx; the
|
||||
// difference is the vtable wired (writer=dynamicwrite_v,
|
||||
// closer=dynamicclose_v) and that the buffer can grow via
|
||||
// dynamicgrow_v on write overflow.
|
||||
export type dynamic_ctx = struct {
|
||||
vt: io.vtable,
|
||||
ptr: *u8,
|
||||
len: i32,
|
||||
cap: i32,
|
||||
pos: i32,
|
||||
};
|
||||
|
||||
// fixed_vstream — wire a vstream over a caller-supplied buffer.
|
||||
// Writes never grow; they return 0 once `pos` reaches the end of
|
||||
// the buffer (Hare returns `nomem` here; ww surfaces 0 to mirror
|
||||
// the OLD memio.fixedwrite divergence — graduating to Hare's
|
||||
// `nomem` return needs the widen-from-bare-nomem path that #173
|
||||
// blocks, deferred to eFinal).
|
||||
//
|
||||
// Mirrors ref/hare/memio/stream.ha:47.
|
||||
//
|
||||
// Three steps (vs Hare's single struct literal): alloc with vt
|
||||
// zero-init (struct-lit `vt = local_vt` silently drops tagged-union
|
||||
// fields past the first — sibling task; the `let zero` + post-alloc
|
||||
// chained `c.vt.X = …` field-assigns through the *fixed_ctx pointer
|
||||
// is the proven-working route, same shape lib/bufio.init uses on
|
||||
// b.vtable). Field-assign of a TY_PTR/void variant through the
|
||||
// chained pointer-into-struct is fine here (*fixed_ctx is a plain
|
||||
// pointer-to-struct, not an aliased pointer; #195's "aliased-ptr
|
||||
// receiver" carve-out doesn't bite).
|
||||
export fn fixed_vstream(buf: []u8) (io.vstream | nomem) = {
|
||||
let zero: io.vtable;
|
||||
let c: *fixed_ctx = alloc(fixed_ctx{
|
||||
vt = zero,
|
||||
ptr = buf.ptr,
|
||||
len = buf.len,
|
||||
cap = buf.len,
|
||||
pos = 0,
|
||||
})?;
|
||||
c.vt.reader = (&fixedread_v): *io.reader;
|
||||
c.vt.writer = (&fixedwrite_v): *io.writer;
|
||||
return &c.vt;
|
||||
};
|
||||
|
||||
// dynamic_vstream — wire a vstream with no initial buffer. Writes
|
||||
// grow the backing allocation; [[io.st_close]] frees it.
|
||||
//
|
||||
// Mirrors ref/hare/memio/stream.ha:54.
|
||||
export fn dynamic_vstream() (io.vstream | nomem) = {
|
||||
let zero: io.vtable;
|
||||
let c: *dynamic_ctx = alloc(dynamic_ctx{
|
||||
vt = zero,
|
||||
ptr = nil,
|
||||
len = 0,
|
||||
cap = 0,
|
||||
pos = 0,
|
||||
})?;
|
||||
c.vt.reader = (&dynread_v): *io.reader;
|
||||
c.vt.writer = (&dynamicwrite_v): *io.writer;
|
||||
c.vt.closer = (&dynamicclose_v): *io.closer;
|
||||
return &c.vt;
|
||||
};
|
||||
|
||||
// dynamicfrom_vstream — like [[dynamic_vstream]] but seeded with an
|
||||
// existing slice. Ownership transfers; close frees `cap` bytes from
|
||||
// the slice's allocated capacity (NOT logical length) — passing a
|
||||
// half-filled append slice with len < cap and using only `buf.len`
|
||||
// would under-free on close.
|
||||
//
|
||||
// Mirrors ref/hare/memio/stream.ha:65.
|
||||
export fn dynamicfrom_vstream(buf: []u8) (io.vstream | nomem) = {
|
||||
let zero: io.vtable;
|
||||
let c: *dynamic_ctx = alloc(dynamic_ctx{
|
||||
vt = zero,
|
||||
ptr = buf.ptr,
|
||||
len = buf.len,
|
||||
cap = buf.cap,
|
||||
pos = 0,
|
||||
})?;
|
||||
c.vt.reader = (&dynread_v): *io.reader;
|
||||
c.vt.writer = (&dynamicwrite_v): *io.writer;
|
||||
c.vt.closer = (&dynamicclose_v): *io.closer;
|
||||
return &c.vt;
|
||||
};
|
||||
|
||||
// ---- vtable callbacks ----------------------------------------------------
|
||||
|
||||
// fixedread_v / dynread_v — recover the ctx from vstream's
|
||||
// `*vtable` via the intrusive offset-0 cast. Separate fns (vs
|
||||
// sharing one readfn) so each cast targets the matching ctx type
|
||||
// — the Hare-side `s: *stream` cast in ref/hare/memio/stream.ha:103
|
||||
// has the same shape but only one ctx flavour.
|
||||
|
||||
fn fixedread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = {
|
||||
let m: *fixed_ctx = s: *fixed_ctx;
|
||||
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: size;
|
||||
};
|
||||
|
||||
fn fixedwrite_v(s: io.vstream, buf: []u8) (size | io.error) = {
|
||||
let m: *fixed_ctx = s: *fixed_ctx;
|
||||
if (m.pos >= m.len) { return 0: size; };
|
||||
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: size;
|
||||
};
|
||||
|
||||
fn dynread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = {
|
||||
let m: *dynamic_ctx = s: *dynamic_ctx;
|
||||
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: size;
|
||||
};
|
||||
|
||||
fn dynamicwrite_v(s: io.vstream, buf: []u8) (size | io.error) = {
|
||||
let m: *dynamic_ctx = s: *dynamic_ctx;
|
||||
let need: i32 = m.pos + buf.len;
|
||||
if (need > m.cap) { dynamicgrow_v(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: size;
|
||||
};
|
||||
|
||||
fn dynamicclose_v(s: io.vstream) (void | io.error) = {
|
||||
let m: *dynamic_ctx = s: *dynamic_ctx;
|
||||
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;
|
||||
};
|
||||
|
||||
// Mirror of memio.ww:203 dynamicgrow. Double-and-copy with floor at
|
||||
// 8. Module-prefixed `_v` suffix vs the OLD memio.dynamicgrow keeps
|
||||
// the cstage flat-TU private-fn scope from colliding (memio.ww:200
|
||||
// note + task #9).
|
||||
fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = {
|
||||
let newcap: i32 = d.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 < d.len) {
|
||||
nbuf[i] = d.ptr[i];
|
||||
i += 1;
|
||||
};
|
||||
if (d.cap > 0) { os.free(d.ptr: *void, d.cap: u64); };
|
||||
d.ptr = nbuf;
|
||||
d.cap = newcap;
|
||||
};
|
||||
@@ -14325,6 +14325,249 @@ fn dynamicgrow(m: *state, need: i32) void = {
|
||||
m.cap = newcap;
|
||||
};
|
||||
|
||||
// vstream — Hare-shaped vtable wrappers over memio. Project #94
|
||||
// fold-e2 (Option C, parallel API).
|
||||
//
|
||||
// Adds three constructors that return `io.vstream` (= `*io.vtable`,
|
||||
// the Hare-shape from lib/io/stream.ww) alongside the pre-vtable
|
||||
// memio.fixed / memio.dynamic / memio.dynamicfrom in memio.ww. The
|
||||
// OLD surface stays untouched here — fold-eFinal (task #50) atomically
|
||||
// flips the package shape: deletes the OLD constructors + callbacks,
|
||||
// renames `_vstream` suffix off, and migrates the few callers.
|
||||
//
|
||||
// Hare's memio::fixed/dynamic return `stream` whose FIRST field IS
|
||||
// `io::stream` (= `*vtable`). ww mirrors that intrusively: each ctx
|
||||
// struct's first field is `vt: io.vtable` (the vtable embedded
|
||||
// INLINE, not a pointer to it) so a heap-alloc'd `*fixed_ctx` is
|
||||
// castable to `vstream = *vtable` via `&c.vt` — and the callbacks
|
||||
// recover the outer ctx by casting the dispatch arg back to
|
||||
// `*fixed_ctx`. Same intrusive shape as lib/bufio.stream over
|
||||
// io.stream (bufio.ww:240-261) and lib/log.stdlogger over logger
|
||||
// (log.ww:95-98).
|
||||
//
|
||||
// Cast workaround per #206: bare `&fn_name` does not type-check as
|
||||
// a `(*<alias> | void)` field-init / let-binding (the structural
|
||||
// `*fn(...)` value isn't accepted as the `*reader` named variant
|
||||
// of the tagged slot). Explicit `(&fn_name): *io.<role>` cast at
|
||||
// each store site is the Hare-faithful minimum-touch route — same
|
||||
// workaround test/wcc/775_io_vtable_run.c uses for the bare-vtable
|
||||
// init. 8 cast tokens here (2 in fixed_vstream + 3 each in
|
||||
// dynamic_vstream / dynamicfrom_vstream — fixed leaves closer
|
||||
// void per Hare's fixed_vt, which lets st_close's void-arm return
|
||||
// plain `void` with no callback needed). Casts drop out wholesale
|
||||
// once #206 closes.
|
||||
//
|
||||
// ptr/len/cap kept flat (no `buf: []u8`) per the memio.state note
|
||||
// (memio.ww:39): chained-dot writes through a state pointer into a
|
||||
// slice subfield miscompile silently (related to the #195 family);
|
||||
// the flat shape sidesteps it. dynamicfrom_vstream uses the slice's
|
||||
// `cap` (NOT `len`) to track the allocated-capacity-to-free on
|
||||
// close — mirrors the same OLD memio.dynamicfrom note (memio.ww:73).
|
||||
|
||||
package memio;
|
||||
|
||||
import io;
|
||||
import os;
|
||||
import rt;
|
||||
|
||||
// fixed_ctx — heap-alloc'd state for fixed_vstream. `vt` at offset 0
|
||||
// for the intrusive vstream cast.
|
||||
export type fixed_ctx = struct {
|
||||
vt: io.vtable,
|
||||
ptr: *u8,
|
||||
len: i32,
|
||||
cap: i32,
|
||||
pos: i32,
|
||||
};
|
||||
|
||||
// dynamic_ctx — heap-alloc'd state for dynamic_vstream /
|
||||
// dynamicfrom_vstream. Same intrusive shape as fixed_ctx; the
|
||||
// difference is the vtable wired (writer=dynamicwrite_v,
|
||||
// closer=dynamicclose_v) and that the buffer can grow via
|
||||
// dynamicgrow_v on write overflow.
|
||||
export type dynamic_ctx = struct {
|
||||
vt: io.vtable,
|
||||
ptr: *u8,
|
||||
len: i32,
|
||||
cap: i32,
|
||||
pos: i32,
|
||||
};
|
||||
|
||||
// fixed_vstream — wire a vstream over a caller-supplied buffer.
|
||||
// Writes never grow; they return 0 once `pos` reaches the end of
|
||||
// the buffer (Hare returns `nomem` here; ww surfaces 0 to mirror
|
||||
// the OLD memio.fixedwrite divergence — graduating to Hare's
|
||||
// `nomem` return needs the widen-from-bare-nomem path that #173
|
||||
// blocks, deferred to eFinal).
|
||||
//
|
||||
// Mirrors ref/hare/memio/stream.ha:47.
|
||||
//
|
||||
// Three steps (vs Hare's single struct literal): alloc with vt
|
||||
// zero-init (struct-lit `vt = local_vt` silently drops tagged-union
|
||||
// fields past the first — sibling task; the `let zero` + post-alloc
|
||||
// chained `c.vt.X = …` field-assigns through the *fixed_ctx pointer
|
||||
// is the proven-working route, same shape lib/bufio.init uses on
|
||||
// b.vtable). Field-assign of a TY_PTR/void variant through the
|
||||
// chained pointer-into-struct is fine here (*fixed_ctx is a plain
|
||||
// pointer-to-struct, not an aliased pointer; #195's "aliased-ptr
|
||||
// receiver" carve-out doesn't bite).
|
||||
export fn fixed_vstream(buf: []u8) (io.vstream | nomem) = {
|
||||
let zero: io.vtable;
|
||||
let c: *fixed_ctx = alloc(fixed_ctx{
|
||||
vt = zero,
|
||||
ptr = buf.ptr,
|
||||
len = buf.len,
|
||||
cap = buf.len,
|
||||
pos = 0,
|
||||
})?;
|
||||
c.vt.reader = (&fixedread_v): *io.reader;
|
||||
c.vt.writer = (&fixedwrite_v): *io.writer;
|
||||
return &c.vt;
|
||||
};
|
||||
|
||||
// dynamic_vstream — wire a vstream with no initial buffer. Writes
|
||||
// grow the backing allocation; [[io.st_close]] frees it.
|
||||
//
|
||||
// Mirrors ref/hare/memio/stream.ha:54.
|
||||
export fn dynamic_vstream() (io.vstream | nomem) = {
|
||||
let zero: io.vtable;
|
||||
let c: *dynamic_ctx = alloc(dynamic_ctx{
|
||||
vt = zero,
|
||||
ptr = nil,
|
||||
len = 0,
|
||||
cap = 0,
|
||||
pos = 0,
|
||||
})?;
|
||||
c.vt.reader = (&dynread_v): *io.reader;
|
||||
c.vt.writer = (&dynamicwrite_v): *io.writer;
|
||||
c.vt.closer = (&dynamicclose_v): *io.closer;
|
||||
return &c.vt;
|
||||
};
|
||||
|
||||
// dynamicfrom_vstream — like [[dynamic_vstream]] but seeded with an
|
||||
// existing slice. Ownership transfers; close frees `cap` bytes from
|
||||
// the slice's allocated capacity (NOT logical length) — passing a
|
||||
// half-filled append slice with len < cap and using only `buf.len`
|
||||
// would under-free on close.
|
||||
//
|
||||
// Mirrors ref/hare/memio/stream.ha:65.
|
||||
export fn dynamicfrom_vstream(buf: []u8) (io.vstream | nomem) = {
|
||||
let zero: io.vtable;
|
||||
let c: *dynamic_ctx = alloc(dynamic_ctx{
|
||||
vt = zero,
|
||||
ptr = buf.ptr,
|
||||
len = buf.len,
|
||||
cap = buf.cap,
|
||||
pos = 0,
|
||||
})?;
|
||||
c.vt.reader = (&dynread_v): *io.reader;
|
||||
c.vt.writer = (&dynamicwrite_v): *io.writer;
|
||||
c.vt.closer = (&dynamicclose_v): *io.closer;
|
||||
return &c.vt;
|
||||
};
|
||||
|
||||
// ---- vtable callbacks ----------------------------------------------------
|
||||
|
||||
// fixedread_v / dynread_v — recover the ctx from vstream's
|
||||
// `*vtable` via the intrusive offset-0 cast. Separate fns (vs
|
||||
// sharing one readfn) so each cast targets the matching ctx type
|
||||
// — the Hare-side `s: *stream` cast in ref/hare/memio/stream.ha:103
|
||||
// has the same shape but only one ctx flavour.
|
||||
|
||||
fn fixedread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = {
|
||||
let m: *fixed_ctx = s: *fixed_ctx;
|
||||
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: size;
|
||||
};
|
||||
|
||||
fn fixedwrite_v(s: io.vstream, buf: []u8) (size | io.error) = {
|
||||
let m: *fixed_ctx = s: *fixed_ctx;
|
||||
if (m.pos >= m.len) { return 0: size; };
|
||||
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: size;
|
||||
};
|
||||
|
||||
fn dynread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = {
|
||||
let m: *dynamic_ctx = s: *dynamic_ctx;
|
||||
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: size;
|
||||
};
|
||||
|
||||
fn dynamicwrite_v(s: io.vstream, buf: []u8) (size | io.error) = {
|
||||
let m: *dynamic_ctx = s: *dynamic_ctx;
|
||||
let need: i32 = m.pos + buf.len;
|
||||
if (need > m.cap) { dynamicgrow_v(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: size;
|
||||
};
|
||||
|
||||
fn dynamicclose_v(s: io.vstream) (void | io.error) = {
|
||||
let m: *dynamic_ctx = s: *dynamic_ctx;
|
||||
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;
|
||||
};
|
||||
|
||||
// Mirror of memio.ww:203 dynamicgrow. Double-and-copy with floor at
|
||||
// 8. Module-prefixed `_v` suffix vs the OLD memio.dynamicgrow keeps
|
||||
// the cstage flat-TU private-fn scope from colliding (memio.ww:200
|
||||
// note + task #9).
|
||||
fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = {
|
||||
let newcap: i32 = d.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 < d.len) {
|
||||
nbuf[i] = d.ptr[i];
|
||||
i += 1;
|
||||
};
|
||||
if (d.cap > 0) { os.free(d.ptr: *void, d.cap: u64); };
|
||||
d.ptr = nbuf;
|
||||
d.cap = newcap;
|
||||
};
|
||||
|
||||
// selfhost/cmd/wcc/cgenutil.ww — split out of cgen.ww.
|
||||
//
|
||||
// General helpers used across cgenexpr / cgenstmt / cgendecl:
|
||||
|
||||
@@ -14325,6 +14325,249 @@ fn dynamicgrow(m: *state, need: i32) void = {
|
||||
m.cap = newcap;
|
||||
};
|
||||
|
||||
// vstream — Hare-shaped vtable wrappers over memio. Project #94
|
||||
// fold-e2 (Option C, parallel API).
|
||||
//
|
||||
// Adds three constructors that return `io.vstream` (= `*io.vtable`,
|
||||
// the Hare-shape from lib/io/stream.ww) alongside the pre-vtable
|
||||
// memio.fixed / memio.dynamic / memio.dynamicfrom in memio.ww. The
|
||||
// OLD surface stays untouched here — fold-eFinal (task #50) atomically
|
||||
// flips the package shape: deletes the OLD constructors + callbacks,
|
||||
// renames `_vstream` suffix off, and migrates the few callers.
|
||||
//
|
||||
// Hare's memio::fixed/dynamic return `stream` whose FIRST field IS
|
||||
// `io::stream` (= `*vtable`). ww mirrors that intrusively: each ctx
|
||||
// struct's first field is `vt: io.vtable` (the vtable embedded
|
||||
// INLINE, not a pointer to it) so a heap-alloc'd `*fixed_ctx` is
|
||||
// castable to `vstream = *vtable` via `&c.vt` — and the callbacks
|
||||
// recover the outer ctx by casting the dispatch arg back to
|
||||
// `*fixed_ctx`. Same intrusive shape as lib/bufio.stream over
|
||||
// io.stream (bufio.ww:240-261) and lib/log.stdlogger over logger
|
||||
// (log.ww:95-98).
|
||||
//
|
||||
// Cast workaround per #206: bare `&fn_name` does not type-check as
|
||||
// a `(*<alias> | void)` field-init / let-binding (the structural
|
||||
// `*fn(...)` value isn't accepted as the `*reader` named variant
|
||||
// of the tagged slot). Explicit `(&fn_name): *io.<role>` cast at
|
||||
// each store site is the Hare-faithful minimum-touch route — same
|
||||
// workaround test/wcc/775_io_vtable_run.c uses for the bare-vtable
|
||||
// init. 8 cast tokens here (2 in fixed_vstream + 3 each in
|
||||
// dynamic_vstream / dynamicfrom_vstream — fixed leaves closer
|
||||
// void per Hare's fixed_vt, which lets st_close's void-arm return
|
||||
// plain `void` with no callback needed). Casts drop out wholesale
|
||||
// once #206 closes.
|
||||
//
|
||||
// ptr/len/cap kept flat (no `buf: []u8`) per the memio.state note
|
||||
// (memio.ww:39): chained-dot writes through a state pointer into a
|
||||
// slice subfield miscompile silently (related to the #195 family);
|
||||
// the flat shape sidesteps it. dynamicfrom_vstream uses the slice's
|
||||
// `cap` (NOT `len`) to track the allocated-capacity-to-free on
|
||||
// close — mirrors the same OLD memio.dynamicfrom note (memio.ww:73).
|
||||
|
||||
package memio;
|
||||
|
||||
import io;
|
||||
import os;
|
||||
import rt;
|
||||
|
||||
// fixed_ctx — heap-alloc'd state for fixed_vstream. `vt` at offset 0
|
||||
// for the intrusive vstream cast.
|
||||
export type fixed_ctx = struct {
|
||||
vt: io.vtable,
|
||||
ptr: *u8,
|
||||
len: i32,
|
||||
cap: i32,
|
||||
pos: i32,
|
||||
};
|
||||
|
||||
// dynamic_ctx — heap-alloc'd state for dynamic_vstream /
|
||||
// dynamicfrom_vstream. Same intrusive shape as fixed_ctx; the
|
||||
// difference is the vtable wired (writer=dynamicwrite_v,
|
||||
// closer=dynamicclose_v) and that the buffer can grow via
|
||||
// dynamicgrow_v on write overflow.
|
||||
export type dynamic_ctx = struct {
|
||||
vt: io.vtable,
|
||||
ptr: *u8,
|
||||
len: i32,
|
||||
cap: i32,
|
||||
pos: i32,
|
||||
};
|
||||
|
||||
// fixed_vstream — wire a vstream over a caller-supplied buffer.
|
||||
// Writes never grow; they return 0 once `pos` reaches the end of
|
||||
// the buffer (Hare returns `nomem` here; ww surfaces 0 to mirror
|
||||
// the OLD memio.fixedwrite divergence — graduating to Hare's
|
||||
// `nomem` return needs the widen-from-bare-nomem path that #173
|
||||
// blocks, deferred to eFinal).
|
||||
//
|
||||
// Mirrors ref/hare/memio/stream.ha:47.
|
||||
//
|
||||
// Three steps (vs Hare's single struct literal): alloc with vt
|
||||
// zero-init (struct-lit `vt = local_vt` silently drops tagged-union
|
||||
// fields past the first — sibling task; the `let zero` + post-alloc
|
||||
// chained `c.vt.X = …` field-assigns through the *fixed_ctx pointer
|
||||
// is the proven-working route, same shape lib/bufio.init uses on
|
||||
// b.vtable). Field-assign of a TY_PTR/void variant through the
|
||||
// chained pointer-into-struct is fine here (*fixed_ctx is a plain
|
||||
// pointer-to-struct, not an aliased pointer; #195's "aliased-ptr
|
||||
// receiver" carve-out doesn't bite).
|
||||
export fn fixed_vstream(buf: []u8) (io.vstream | nomem) = {
|
||||
let zero: io.vtable;
|
||||
let c: *fixed_ctx = alloc(fixed_ctx{
|
||||
vt = zero,
|
||||
ptr = buf.ptr,
|
||||
len = buf.len,
|
||||
cap = buf.len,
|
||||
pos = 0,
|
||||
})?;
|
||||
c.vt.reader = (&fixedread_v): *io.reader;
|
||||
c.vt.writer = (&fixedwrite_v): *io.writer;
|
||||
return &c.vt;
|
||||
};
|
||||
|
||||
// dynamic_vstream — wire a vstream with no initial buffer. Writes
|
||||
// grow the backing allocation; [[io.st_close]] frees it.
|
||||
//
|
||||
// Mirrors ref/hare/memio/stream.ha:54.
|
||||
export fn dynamic_vstream() (io.vstream | nomem) = {
|
||||
let zero: io.vtable;
|
||||
let c: *dynamic_ctx = alloc(dynamic_ctx{
|
||||
vt = zero,
|
||||
ptr = nil,
|
||||
len = 0,
|
||||
cap = 0,
|
||||
pos = 0,
|
||||
})?;
|
||||
c.vt.reader = (&dynread_v): *io.reader;
|
||||
c.vt.writer = (&dynamicwrite_v): *io.writer;
|
||||
c.vt.closer = (&dynamicclose_v): *io.closer;
|
||||
return &c.vt;
|
||||
};
|
||||
|
||||
// dynamicfrom_vstream — like [[dynamic_vstream]] but seeded with an
|
||||
// existing slice. Ownership transfers; close frees `cap` bytes from
|
||||
// the slice's allocated capacity (NOT logical length) — passing a
|
||||
// half-filled append slice with len < cap and using only `buf.len`
|
||||
// would under-free on close.
|
||||
//
|
||||
// Mirrors ref/hare/memio/stream.ha:65.
|
||||
export fn dynamicfrom_vstream(buf: []u8) (io.vstream | nomem) = {
|
||||
let zero: io.vtable;
|
||||
let c: *dynamic_ctx = alloc(dynamic_ctx{
|
||||
vt = zero,
|
||||
ptr = buf.ptr,
|
||||
len = buf.len,
|
||||
cap = buf.cap,
|
||||
pos = 0,
|
||||
})?;
|
||||
c.vt.reader = (&dynread_v): *io.reader;
|
||||
c.vt.writer = (&dynamicwrite_v): *io.writer;
|
||||
c.vt.closer = (&dynamicclose_v): *io.closer;
|
||||
return &c.vt;
|
||||
};
|
||||
|
||||
// ---- vtable callbacks ----------------------------------------------------
|
||||
|
||||
// fixedread_v / dynread_v — recover the ctx from vstream's
|
||||
// `*vtable` via the intrusive offset-0 cast. Separate fns (vs
|
||||
// sharing one readfn) so each cast targets the matching ctx type
|
||||
// — the Hare-side `s: *stream` cast in ref/hare/memio/stream.ha:103
|
||||
// has the same shape but only one ctx flavour.
|
||||
|
||||
fn fixedread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = {
|
||||
let m: *fixed_ctx = s: *fixed_ctx;
|
||||
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: size;
|
||||
};
|
||||
|
||||
fn fixedwrite_v(s: io.vstream, buf: []u8) (size | io.error) = {
|
||||
let m: *fixed_ctx = s: *fixed_ctx;
|
||||
if (m.pos >= m.len) { return 0: size; };
|
||||
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: size;
|
||||
};
|
||||
|
||||
fn dynread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = {
|
||||
let m: *dynamic_ctx = s: *dynamic_ctx;
|
||||
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: size;
|
||||
};
|
||||
|
||||
fn dynamicwrite_v(s: io.vstream, buf: []u8) (size | io.error) = {
|
||||
let m: *dynamic_ctx = s: *dynamic_ctx;
|
||||
let need: i32 = m.pos + buf.len;
|
||||
if (need > m.cap) { dynamicgrow_v(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: size;
|
||||
};
|
||||
|
||||
fn dynamicclose_v(s: io.vstream) (void | io.error) = {
|
||||
let m: *dynamic_ctx = s: *dynamic_ctx;
|
||||
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;
|
||||
};
|
||||
|
||||
// Mirror of memio.ww:203 dynamicgrow. Double-and-copy with floor at
|
||||
// 8. Module-prefixed `_v` suffix vs the OLD memio.dynamicgrow keeps
|
||||
// the cstage flat-TU private-fn scope from colliding (memio.ww:200
|
||||
// note + task #9).
|
||||
fn dynamicgrow_v(d: *dynamic_ctx, need: i32) void = {
|
||||
let newcap: i32 = d.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 < d.len) {
|
||||
nbuf[i] = d.ptr[i];
|
||||
i += 1;
|
||||
};
|
||||
if (d.cap > 0) { os.free(d.ptr: *void, d.cap: u64); };
|
||||
d.ptr = nbuf;
|
||||
d.cap = newcap;
|
||||
};
|
||||
|
||||
// selfhost/cmd/wcc/cgenutil.ww — split out of cgen.ww.
|
||||
//
|
||||
// General helpers used across cgenexpr / cgenstmt / cgendecl:
|
||||
|
||||
398
test/wcc/776_memio_vstream_run.c
Normal file
398
test/wcc/776_memio_vstream_run.c
Normal file
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
* 776_memio_vstream_run — project #94 fold-e2 sentinel. Pins the
|
||||
* additive lib/memio/vstream.ww Option C parallel API: heap-alloc'd
|
||||
* ctx structs (fixed_ctx / dynamic_ctx) with `vt: io.vtable` as the
|
||||
* first field for the intrusive cast, three constructors
|
||||
* (fixed_vstream / dynamic_vstream / dynamicfrom_vstream) that
|
||||
* return `(io.vstream | nomem)`, and the matching read/write/close
|
||||
* callbacks that recover the outer ctx via a vstream→*ctx pointer
|
||||
* cast. Coexists with the pre-vtable memio.fixed/dynamic/dynamicfrom
|
||||
* surface in memio.ww (fold-eFinal, task #50, retires the latter).
|
||||
*
|
||||
* Each row imports memio + io, calls one or more constructors, drives
|
||||
* the io.st_read/st_write/st_close dispatchers, and asserts an exit
|
||||
* constant. Both stages run; cs.s == ww.s byte-id on every row.
|
||||
*
|
||||
* row | what it pins
|
||||
* --------------------------+--------------------------------------
|
||||
* fixed_read_5 | fixed_vstream + io.st_read of 5 bytes
|
||||
* | over an 8-byte source. Asserts the
|
||||
* | return is `size` and equal to 5, and
|
||||
* | that out[0]/out[4] mirror the buffer.
|
||||
* | Pins the full intrusive shape (alloc
|
||||
* | fixed_ctx → return &c.vt; dispatcher
|
||||
* | recovers via s: *fixed_ctx).
|
||||
* dynamic_write_grow | dynamic_vstream + io.st_write of 3
|
||||
* | bytes. Asserts the write reports 3
|
||||
* | and that the dynamicgrow_v path
|
||||
* | allocated the backing buffer (initial
|
||||
* | cap=0 forces growth on the first
|
||||
* | write). Closes via io.st_close to
|
||||
* | exercise dynamicclose_v's free.
|
||||
* dynamicfrom_alt_rw | dynamicfrom_vstream seeded with a
|
||||
* | 4-byte slice. Alternates st_read
|
||||
* | (4 bytes back) → st_write (2 bytes,
|
||||
* | grows past cap=4 → realloc). Asserts
|
||||
* | both paths report their lengths.
|
||||
* | Single ctx flavour, two dispatcher
|
||||
* | paths through the same vtable.
|
||||
* branched_fixed | branched callee per #105: two
|
||||
* | fixed_vstream instances with
|
||||
* | different buffer sizes (3 and 5),
|
||||
* | runtime-selected by the exit code
|
||||
* | nudge. st_read on each returns the
|
||||
* | matching size. Catches a constant-
|
||||
* | fold mistake in the dispatch path
|
||||
* | (mirror of 775's branched_readers
|
||||
* | row for the vtable-init side).
|
||||
*
|
||||
* IMPORT-ORDER WORKAROUND (pre-existing wwstage gap, NOT introduced
|
||||
* here): every row places `import os;` FIRST. ww_ww's combined.ww
|
||||
* concatenation follows the import-discovery order; an os-late
|
||||
* ordering (io → rt → os) trips the wwstage checker on os.tryread /
|
||||
* trywrite / tryopen's bare `return r;` over a tagged return type
|
||||
* (3 false-positive "return: not assignable (i64 → )"/"(i32 → )"
|
||||
* errors). The os-first ordering matches selfhost's combined.ww
|
||||
* (time → os → rt → …) where the checker resolves cleanly. The 980
|
||||
* memio_run / 990 / 995 byte-id gates do not hit this path because
|
||||
* they either drive cs-only (980) or operate on a much wider type
|
||||
* surface (990/995). Filed as a sibling task; bare `os` import in a
|
||||
* small probe context is what surfaces it. Workaround drops out once
|
||||
* the wwstage checker stops ordering-sensitively on bare-int return
|
||||
* to tagged-int union.
|
||||
*
|
||||
* SIBLINGS (filed inline, NOT fixed here — fold-e2 is purely
|
||||
* additive over fold-e1's frozen io.* surface):
|
||||
*
|
||||
* - WWSTAGE-IMPORT-ORDER: `import os;` must appear before `import
|
||||
* memio;` / `import io;` in a small probe context. ww_ww's
|
||||
* combined.ww concat order trips the wwstage checker's
|
||||
* `(i64 | T)` / `(i32 | T)` return-assignability when bare
|
||||
* int-returning fns (os.tryread / trywrite / tryopen) are
|
||||
* checked before certain ordering-sensitive pre-resolved types.
|
||||
* cstage accepts unconditionally. Symmetric to the wwstage
|
||||
* ordering gaps already filed as #189 / #190 / #202.
|
||||
*
|
||||
* - #173 (TRY-on-tagged-return both-stages broken) blocks
|
||||
* fixed_write_v from returning Hare's `nomem` when full;
|
||||
* vstream.ww surfaces 0 instead (memio.ww:155 OLD divergence).
|
||||
* The probe doesn't pin Hare-equivalent nomem behaviour;
|
||||
* fold-eFinal can graduate once #173 closes.
|
||||
*
|
||||
* - #195 (cgassign N_DOT TK_ASSIGN on aliased-ptr receiver):
|
||||
* would bite if vstream.ww used chained `c.vt.reader = …`
|
||||
* stores through the alloc'd ctx pointer. We sidestep via a
|
||||
* local `let vt: io.vtable;` + field-assigns + struct-lit
|
||||
* `alloc(fixed_ctx{vt=vt, …})`. Mirrors memio.ww:39's flat-
|
||||
* slice-fields workaround. eFinal can collapse once #195 closes.
|
||||
*
|
||||
* GATE POLARITY: must stay GREEN. A red here means vstream.ww
|
||||
* regressed, the alloc-struct-? path miscompiled, the intrusive
|
||||
* vstream→*ctx cast miscomputed offsets, or io.vtable's first-field
|
||||
* embed lost its offset-0 invariant.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
static int
|
||||
runwait(const char *cmd)
|
||||
{
|
||||
int rc = system(cmd);
|
||||
if (rc == -1) return -1;
|
||||
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||
return -1;
|
||||
}
|
||||
|
||||
#define STAGE_CS 1
|
||||
#define STAGE_WW 2
|
||||
|
||||
struct row {
|
||||
const char *label;
|
||||
const char *src;
|
||||
int want_exit;
|
||||
int stage_mask;
|
||||
int byte_id;
|
||||
};
|
||||
|
||||
static const struct row rows[] = {
|
||||
{ "fixed_read_5",
|
||||
"package main;\n"
|
||||
"import os;\n"
|
||||
"import memio;\n"
|
||||
"import io;\n"
|
||||
"export fn main() i32 = {\n"
|
||||
" let buf: [8]u8;\n"
|
||||
" buf[0] = 65u8; buf[1] = 66u8; buf[2] = 67u8; buf[3] = 68u8;\n"
|
||||
" buf[4] = 69u8; buf[5] = 70u8; buf[6] = 71u8; buf[7] = 72u8;\n"
|
||||
" let r = memio.fixed_vstream(buf[0:8]);\n"
|
||||
" let s: io.vstream = nil: *io.vtable;\n"
|
||||
" match (r) {\n"
|
||||
" case let v: io.vstream => { s = v; };\n"
|
||||
" case nomem => { return 50; };\n"
|
||||
" };\n"
|
||||
" let out: [5]u8;\n"
|
||||
" let n: size = 0;\n"
|
||||
" let rd = io.st_read(s, out[0:5]);\n"
|
||||
" if (rd is size) { n = rd as size; };\n"
|
||||
" if (n == 5: size && out[0] == 65u8 && out[4] == 69u8) { return 42; };\n"
|
||||
" return 99;\n"
|
||||
"};\n",
|
||||
42,
|
||||
STAGE_CS | STAGE_WW, 1 },
|
||||
{ "dynamic_write_grow",
|
||||
"package main;\n"
|
||||
"import os;\n"
|
||||
"import memio;\n"
|
||||
"import io;\n"
|
||||
"export fn main() i32 = {\n"
|
||||
" let r = memio.dynamic_vstream();\n"
|
||||
" let s: io.vstream = nil: *io.vtable;\n"
|
||||
" match (r) {\n"
|
||||
" case let v: io.vstream => { s = v; };\n"
|
||||
" case nomem => { return 50; };\n"
|
||||
" };\n"
|
||||
" let src: [3]u8;\n"
|
||||
" src[0] = 88u8; src[1] = 89u8; src[2] = 90u8;\n"
|
||||
" let wr = io.st_write(s, src[0:3]);\n"
|
||||
" let n: size = 0;\n"
|
||||
" if (wr is size) { n = wr as size; };\n"
|
||||
" let cl = io.st_close(s);\n"
|
||||
" if (cl is void && n == 3: size) { return 43; };\n"
|
||||
" return 99;\n"
|
||||
"};\n",
|
||||
43,
|
||||
STAGE_CS | STAGE_WW, 1 },
|
||||
{ "dynamicfrom_alt_rw",
|
||||
"package main;\n"
|
||||
"import os;\n"
|
||||
"import memio;\n"
|
||||
"import io;\n"
|
||||
"export fn main() i32 = {\n"
|
||||
" let seed: [4]u8;\n"
|
||||
" seed[0] = 1u8; seed[1] = 2u8; seed[2] = 3u8; seed[3] = 4u8;\n"
|
||||
" let r = memio.dynamicfrom_vstream(seed[0:4]);\n"
|
||||
" let s: io.vstream = nil: *io.vtable;\n"
|
||||
" match (r) {\n"
|
||||
" case let v: io.vstream => { s = v; };\n"
|
||||
" case nomem => { return 50; };\n"
|
||||
" };\n"
|
||||
" let out: [4]u8;\n"
|
||||
" let rd = io.st_read(s, out[0:4]);\n"
|
||||
" let rn: size = 0;\n"
|
||||
" if (rd is size) { rn = rd as size; };\n"
|
||||
" let extra: [2]u8;\n"
|
||||
" extra[0] = 99u8; extra[1] = 100u8;\n"
|
||||
" let wr = io.st_write(s, extra[0:2]);\n"
|
||||
" let wn: size = 0;\n"
|
||||
" if (wr is size) { wn = wr as size; };\n"
|
||||
" if (rn == 4: size && wn == 2: size && out[0] == 1u8 && out[3] == 4u8) { return 44; };\n"
|
||||
" return 99;\n"
|
||||
"};\n",
|
||||
44,
|
||||
STAGE_CS | STAGE_WW, 1 },
|
||||
{ "branched_fixed",
|
||||
"package main;\n"
|
||||
"import os;\n"
|
||||
"import memio;\n"
|
||||
"import io;\n"
|
||||
"export fn main() i32 = {\n"
|
||||
" let a: [3]u8;\n"
|
||||
" a[0] = 10u8; a[1] = 11u8; a[2] = 12u8;\n"
|
||||
" let b: [5]u8;\n"
|
||||
" b[0] = 20u8; b[1] = 21u8; b[2] = 22u8; b[3] = 23u8; b[4] = 24u8;\n"
|
||||
" let ra = memio.fixed_vstream(a[0:3]);\n"
|
||||
" let rb = memio.fixed_vstream(b[0:5]);\n"
|
||||
" let sa: io.vstream = nil: *io.vtable;\n"
|
||||
" let sb: io.vstream = nil: *io.vtable;\n"
|
||||
" match (ra) {\n"
|
||||
" case let v: io.vstream => { sa = v; };\n"
|
||||
" case nomem => { return 50; };\n"
|
||||
" };\n"
|
||||
" match (rb) {\n"
|
||||
" case let v: io.vstream => { sb = v; };\n"
|
||||
" case nomem => { return 51; };\n"
|
||||
" };\n"
|
||||
" let outa: [3]u8;\n"
|
||||
" let outb: [5]u8;\n"
|
||||
" let rda = io.st_read(sa, outa[0:3]);\n"
|
||||
" let rdb = io.st_read(sb, outb[0:5]);\n"
|
||||
" let na: size = 0;\n"
|
||||
" let nb: size = 0;\n"
|
||||
" if (rda is size) { na = rda as size; };\n"
|
||||
" if (rdb is size) { nb = rdb as size; };\n"
|
||||
" if (na == 3: size && nb == 5: size && outa[2] == 12u8 && outb[4] == 24u8) { return 45; };\n"
|
||||
" return 99;\n"
|
||||
"};\n",
|
||||
45,
|
||||
STAGE_CS | STAGE_WW, 1 },
|
||||
};
|
||||
|
||||
static int
|
||||
write_source(const char *path, const char *src)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
fputs(src, f);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Per-row tmpdir cleanup. ww_ww writes intermediates next to the
|
||||
* source (filed task #15), so each row's build leaves
|
||||
* <src>.{combined.ww,s,o} + bare exe alongside. Sweep all then
|
||||
* rmdir. */
|
||||
static void
|
||||
cleanup_tmp(const char *tmpdir, const char *base)
|
||||
{
|
||||
char p[640];
|
||||
snprintf(p, sizeof p, "%s/%s.ww", tmpdir, base); unlink(p);
|
||||
snprintf(p, sizeof p, "%s/%s.s", tmpdir, base); unlink(p);
|
||||
snprintf(p, sizeof p, "%s/%s.o", tmpdir, base); unlink(p);
|
||||
snprintf(p, sizeof p, "%s/%s.combined.ww", tmpdir, base); unlink(p);
|
||||
snprintf(p, sizeof p, "%s/%s", tmpdir, base); unlink(p);
|
||||
rmdir(tmpdir);
|
||||
}
|
||||
|
||||
static int
|
||||
build_via_driver(const char *driver, const char *tmpdir, const char *cwd,
|
||||
const char *src)
|
||||
{
|
||||
char cmd[2048];
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"cd %s && timeout 180 %s build -I %s/lib %s 2>/dev/null",
|
||||
tmpdir, driver, cwd, src);
|
||||
return runwait(cmd);
|
||||
}
|
||||
|
||||
static int
|
||||
run_row(const char *driver, const char *cwd, const struct row *r, int seq)
|
||||
{
|
||||
char tmpdir[256], src[512], base[64], outbin[768];
|
||||
snprintf(tmpdir, sizeof tmpdir, "/tmp/mvs_%d_d_%d", getpid(), seq);
|
||||
snprintf(base, sizeof base, "main776");
|
||||
snprintf(src, sizeof src, "%s/%s.ww", tmpdir, base);
|
||||
mkdir(tmpdir, 0755);
|
||||
if (write_source(src, r->src) != 0) { cleanup_tmp(tmpdir, base); return -1; }
|
||||
int rc;
|
||||
int br = build_via_driver(driver, tmpdir, cwd, src);
|
||||
if (br == 0) {
|
||||
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
|
||||
rc = runwait(outbin);
|
||||
} else {
|
||||
rc = -1;
|
||||
}
|
||||
cleanup_tmp(tmpdir, base);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* asm_byte_identical — diff cstage vs wwstage .s. Parallel trees so
|
||||
* ww_ww writing intermediates next to the source doesn't clobber the
|
||||
* cstage .s (CLAUDE.md rule 14 phase split). */
|
||||
static int
|
||||
asm_byte_identical(const char *cdrv, const char *wdrv, const char *cwd,
|
||||
const struct row *r, int seq)
|
||||
{
|
||||
char src[512], tdc[256], tdw[256], base[64], cs[512], ws[512];
|
||||
snprintf(tdc, sizeof tdc, "/tmp/mvs_%d_c_%d", getpid(), seq);
|
||||
snprintf(tdw, sizeof tdw, "/tmp/mvs_%d_w_%d", getpid(), seq);
|
||||
snprintf(base, sizeof base, "main776");
|
||||
mkdir(tdc, 0755);
|
||||
mkdir(tdw, 0755);
|
||||
snprintf(src, sizeof src, "%s/%s.ww", tdc, base);
|
||||
if (write_source(src, r->src) != 0) { cleanup_tmp(tdc, base); cleanup_tmp(tdw, base); return -1; }
|
||||
int rc = -1;
|
||||
if (build_via_driver(cdrv, tdc, cwd, src) != 0) goto out;
|
||||
snprintf(cs, sizeof cs, "%s/%s.s", tdc, base);
|
||||
|
||||
snprintf(src, sizeof src, "%s/%s.ww", tdw, base);
|
||||
if (write_source(src, r->src) != 0) goto out;
|
||||
if (build_via_driver(wdrv, tdw, cwd, src) != 0) goto out;
|
||||
snprintf(ws, sizeof ws, "%s/%s.s", tdw, base);
|
||||
|
||||
FILE *fc = fopen(cs, "rb");
|
||||
FILE *fw = fopen(ws, "rb");
|
||||
if (fc && fw) {
|
||||
rc = 0;
|
||||
for (;;) {
|
||||
int a = fgetc(fc);
|
||||
int b = fgetc(fw);
|
||||
if (a != b) { rc = -1; break; }
|
||||
if (a == EOF) break;
|
||||
}
|
||||
}
|
||||
if (fc) fclose(fc);
|
||||
if (fw) fclose(fw);
|
||||
out:
|
||||
cleanup_tmp(tdc, base);
|
||||
cleanup_tmp(tdw, base);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
const char *bin = getenv("BIN");
|
||||
if (!bin) bin = "out/bin";
|
||||
char cwd[256];
|
||||
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
||||
char absbin[512];
|
||||
if (bin[0] != '/') {
|
||||
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
|
||||
bin = absbin;
|
||||
}
|
||||
|
||||
char cdrv[640], wdrv[640];
|
||||
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
|
||||
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
|
||||
|
||||
int n = (int)(sizeof rows / sizeof rows[0]);
|
||||
int total = 0, fail = 0;
|
||||
int wwpresent = (access(wdrv, X_OK) == 0);
|
||||
int seq = 0;
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (rows[i].stage_mask & STAGE_CS) {
|
||||
total++;
|
||||
int got = run_row(cdrv, cwd, &rows[i], seq++);
|
||||
if (got != rows[i].want_exit) {
|
||||
fprintf(stderr,
|
||||
"memio_vstream_run[cs][%s]: exit=%d want=%d\n",
|
||||
rows[i].label, got, rows[i].want_exit);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
if (wwpresent && (rows[i].stage_mask & STAGE_WW)) {
|
||||
total++;
|
||||
int got = run_row(wdrv, cwd, &rows[i], seq++);
|
||||
if (got != rows[i].want_exit) {
|
||||
fprintf(stderr,
|
||||
"memio_vstream_run[ww][%s]: exit=%d want=%d\n",
|
||||
rows[i].label, got, rows[i].want_exit);
|
||||
fail++;
|
||||
}
|
||||
if (rows[i].byte_id) {
|
||||
total++;
|
||||
if (asm_byte_identical(cdrv, wdrv, cwd, &rows[i], seq++) != 0) {
|
||||
fprintf(stderr,
|
||||
"memio_vstream_run[byte-id][%s]: cstage vs wwstage asm differs\n",
|
||||
rows[i].label);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!wwpresent)
|
||||
fprintf(stderr, "memio_vstream_run: skip wwstage (no %s)\n", wdrv);
|
||||
|
||||
if (fail) {
|
||||
fprintf(stderr, "memio_vstream_run: %d/%d fixtures failed\n",
|
||||
fail, total);
|
||||
return 1;
|
||||
}
|
||||
printf("memio_vstream_run: %d/%d ok\n", total, total);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user