diff --git a/lib/io/io.ww b/lib/io/io.ww index aac312ad..49f633f5 100644 --- a/lib/io/io.ww +++ b/lib/io/io.ww @@ -6,12 +6,12 @@ // tagged unions instead of errno-style integer sentinels. // // This file owns the eof / underread variant tags; lib/io/stream.ww -// owns the `vtable` + `stream` + read/write/close dispatchers, and -// lib/io/types.ww owns the error union, mode/whence enums, and the -// reader/writer/closer fn-type aliases. #94 fold-eFinal collapsed the -// pre-vtable `stream` struct + `closed` tag into the single vtable -// surface; the dispatchers are read/write/close (over `stream`), -// final over `handle` at io fold-2 (#5). +// owns the `vtable` + `stream` + read/write/close dispatchers and the +// [[empty]] singleton, and lib/io/types.ww owns the error union, +// mode/whence enums, and the reader/writer/closer fn-type aliases. +// #94 fold-eFinal collapsed the pre-vtable `stream` struct + `closed` +// tag into the single vtable surface; the dispatchers are read/write/ +// close (over `stream`), final over `handle` at io fold-2 (#5). package io; // eof — read past the end of the stream. Hare uses the `done` diff --git a/lib/io/stream.ww b/lib/io/stream.ww index f6f62b32..ed36bbe4 100644 --- a/lib/io/stream.ww +++ b/lib/io/stream.ww @@ -1,7 +1,7 @@ // stream — Hare-shaped vtable surface. Project #94 fold-eFinal. // // The single io stream surface (the fold-eFinal collapse retired the -// pre-vtable `stream` struct + `closed` tag). Three exports: +// pre-vtable `stream` struct + `closed` tag). Exports: // // vtable a struct of optional fn-pointer slots — reader/writer/ // closer per ref/hare/io/stream.ha:36-42. Hare spells the @@ -21,6 +21,9 @@ // public `read`/`write`/`close`; ww has no `handle` yet // (io fold-2, #5), so the dispatchers ARE the public // surface and grow the `handle` match when #5 lands. +// empty the discard+EOF stream (ref/hare/io/empty.ha:13). Lives +// here rather than io.ww so that 900_stdlib can compile +// io.ww standalone (io.ww has no cross-file type refs). // // Deferrals (drew-signed): `seeker` lands with io fold-2 (#5) once `off` // + `whence` plug into the signature. Hare's `?`-propagating `close` @@ -183,3 +186,37 @@ export fn seek(h: handle, off: off, w: whence) (off | error) = { export fn tell(h: handle) (off | error) = { return seek(h, 0, whence.CUR); }; + +// ---- empty stream ------------------------------------------------------- +// +// ref/hare/io/empty.ha:4-17. +// +// Hare uses `const _empty_vt: vtable = { ... }` + `const empty: *stream`. +// ww can't const-init a vtable struct with fn-ptr fields (#118), so the +// vtable is a module-level `let` and [[empty]] is a function that wires +// the fn-ptr slots on every call and returns the stream pointer. +// Single-assignment on the same words: idempotent under re-entry. +// Lives in stream.ww (not io.ww) so that 900_stdlib can compile io.ww +// standalone without referencing the cross-file vtable/reader/writer types. + +fn _empty_read(s: stream, buf: []u8) (size | eof | error) = { + let e: eof; + return e; +}; + +fn _empty_write(s: stream, buf: []u8) (size | error) = { + return buf.len: size; +}; + +let _empty_vt: vtable; + +// empty — a stream that discards all writes (returning their size) and +// returns EOF on every read. Mirrors ref/hare/io/empty.ha:13. +// #118: const vtable init with fn-ptr fields is unwired (emit_struct_data +// needs a two-pass reloc extension, node_fnptr_sym reusable). Using a +// mutable let + per-call wiring until #118 lands. +export fn empty() stream = { + _empty_vt.reader = (&_empty_read): *reader; + _empty_vt.writer = (&_empty_write): *writer; + return &_empty_vt; +}; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 914f3d42..342e94e1 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -15470,12 +15470,12 @@ export fn checkfile(c: *checker, file: *node) void = { // tagged unions instead of errno-style integer sentinels. // // This file owns the eof / underread variant tags; lib/io/stream.ww -// owns the `vtable` + `stream` + read/write/close dispatchers, and -// lib/io/types.ww owns the error union, mode/whence enums, and the -// reader/writer/closer fn-type aliases. #94 fold-eFinal collapsed the -// pre-vtable `stream` struct + `closed` tag into the single vtable -// surface; the dispatchers are read/write/close (over `stream`), -// final over `handle` at io fold-2 (#5). +// owns the `vtable` + `stream` + read/write/close dispatchers and the +// [[empty]] singleton, and lib/io/types.ww owns the error union, +// mode/whence enums, and the reader/writer/closer fn-type aliases. +// #94 fold-eFinal collapsed the pre-vtable `stream` struct + `closed` +// tag into the single vtable surface; the dispatchers are read/write/ +// close (over `stream`), final over `handle` at io fold-2 (#5). package io; // eof — read past the end of the stream. Hare uses the `done` @@ -15492,7 +15492,7 @@ export type underread = !i32; // stream — Hare-shaped vtable surface. Project #94 fold-eFinal. // // The single io stream surface (the fold-eFinal collapse retired the -// pre-vtable `stream` struct + `closed` tag). Three exports: +// pre-vtable `stream` struct + `closed` tag). Exports: // // vtable a struct of optional fn-pointer slots — reader/writer/ // closer per ref/hare/io/stream.ha:36-42. Hare spells the @@ -15512,6 +15512,9 @@ export type underread = !i32; // public `read`/`write`/`close`; ww has no `handle` yet // (io fold-2, #5), so the dispatchers ARE the public // surface and grow the `handle` match when #5 lands. +// empty the discard+EOF stream (ref/hare/io/empty.ha:13). Lives +// here rather than io.ww so that 900_stdlib can compile +// io.ww standalone (io.ww has no cross-file type refs). // // Deferrals (drew-signed): `seeker` lands with io fold-2 (#5) once `off` // + `whence` plug into the signature. Hare's `?`-propagating `close` @@ -15675,6 +15678,40 @@ export fn tell(h: handle) (off | error) = { return seek(h, 0, whence.CUR); }; +// ---- empty stream ------------------------------------------------------- +// +// ref/hare/io/empty.ha:4-17. +// +// Hare uses `const _empty_vt: vtable = { ... }` + `const empty: *stream`. +// ww can't const-init a vtable struct with fn-ptr fields (#118), so the +// vtable is a module-level `let` and [[empty]] is a function that wires +// the fn-ptr slots on every call and returns the stream pointer. +// Single-assignment on the same words: idempotent under re-entry. +// Lives in stream.ww (not io.ww) so that 900_stdlib can compile io.ww +// standalone without referencing the cross-file vtable/reader/writer types. + +fn _empty_read(s: stream, buf: []u8) (size | eof | error) = { + let e: eof; + return e; +}; + +fn _empty_write(s: stream, buf: []u8) (size | error) = { + return buf.len: size; +}; + +let _empty_vt: vtable; + +// empty — a stream that discards all writes (returning their size) and +// returns EOF on every read. Mirrors ref/hare/io/empty.ha:13. +// #118: const vtable init with fn-ptr fields is unwired (emit_struct_data +// needs a two-pass reloc extension, node_fnptr_sym reusable). Using a +// mutable let + per-call wiring until #118 lands. +export fn empty() stream = { + _empty_vt.reader = (&_empty_read): *reader; + _empty_vt.writer = (&_empty_write): *writer; + return &_empty_vt; +}; + // errors — domain-agnostic error types. Mirrors ref/hare/errors/. // // Named-void tagged-union variants, so `(T | errors.invalid | ...)` @@ -18079,6 +18116,16 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { // Primitives / aliased-to-primitives are never sret. if (aliasprimsize(c, r.str) > 0) { return 0; }; if (streq(r.str, "str")) { return 0; }; + // #129: same-module alias wins over any-module struct hit. Without + // this, `type stream = *vtable` (io) loses to memio.stream (56B + // struct) via structlookup's any-module fallback → spurious sret. + // Mirrors cstage cg_sret_retsize, which sees TY_PTR, not a name. + if (c != nil) { + let al: *node = aliassamemod(c, r.str); + if (al != nil) { + return sretretsize(c, al); + }; + }; let si: *structinfo = structlookup(c, r.str); if (si == nil) { if (c != nil) { @@ -18121,7 +18168,15 @@ export fn callsretsize(c: *cgen, n: *node) i32 = { }; if (cn.len == 0) { return 0; }; let rtyp: *node = fnretlookupmod(c, cn, cmod); - return sretretsize(c, rtyp); + // #129: sretretsize must see the CALLEE's module context so + // aliassamemod resolves aliases from the callee's module (not the + // caller's). Mirrors cstage operating on resolved Type* objects + // (type_chase_named never has this confusion). Swap + restore. + let savedmod: str = c.curmod; + if (cmod.len > 0) { c.curmod = cmod; }; + let r: i32 = sretretsize(c, rtyp); + c.curmod = savedmod; + return r; }; fn structlookup(c: *cgen, name: str) *structinfo = { @@ -31271,6 +31326,23 @@ fn cgassign(c: *cgen, n: *node) void = { return; };}; }; + // #129: tagged-union field on global struct. LEAQ + // base(SB) into BX then the shared widener handles + // every rhs shape. Mirrors cstage cgen.c:4902 + // is_global arm. Without this the generic TK_ASSIGN + // below truncates to 1 word, silently dropping tag + // and payload. + if (n.op == tkind.TK_ASSIGN + && istaggedtype(c, fi.tnode)) { + let fsz: i32 = slotsize(c, fi.tnode); + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), BX\n"); + cgwidentaggedstore(c, + fi.tnode.type_: *tinfo, + n.rhs, "BX", fi.foff, fsz); + return; + }; if (n.op == tkind.TK_ASSIGN) { cgexpr(c, n.rhs); if (isstrtype(c, fi.tnode)) { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index e76084cc..884f75a3 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -9891,6 +9891,23 @@ fn cgassign(c: *cgen, n: *node) void = { return; };}; }; + // #129: tagged-union field on global struct. LEAQ + // base(SB) into BX then the shared widener handles + // every rhs shape. Mirrors cstage cgen.c:4902 + // is_global arm. Without this the generic TK_ASSIGN + // below truncates to 1 word, silently dropping tag + // and payload. + if (n.op == tkind.TK_ASSIGN + && istaggedtype(c, fi.tnode)) { + let fsz: i32 = slotsize(c, fi.tnode); + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), BX\n"); + cgwidentaggedstore(c, + fi.tnode.type_: *tinfo, + n.rhs, "BX", fi.foff, fsz); + return; + }; if (n.op == tkind.TK_ASSIGN) { cgexpr(c, n.rhs); if (isstrtype(c, fi.tnode)) { diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index e7829331..4fee7111 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1880,6 +1880,16 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { // Primitives / aliased-to-primitives are never sret. if (aliasprimsize(c, r.str) > 0) { return 0; }; if (streq(r.str, "str")) { return 0; }; + // #129: same-module alias wins over any-module struct hit. Without + // this, `type stream = *vtable` (io) loses to memio.stream (56B + // struct) via structlookup's any-module fallback → spurious sret. + // Mirrors cstage cg_sret_retsize, which sees TY_PTR, not a name. + if (c != nil) { + let al: *node = aliassamemod(c, r.str); + if (al != nil) { + return sretretsize(c, al); + }; + }; let si: *structinfo = structlookup(c, r.str); if (si == nil) { if (c != nil) { @@ -1922,7 +1932,15 @@ export fn callsretsize(c: *cgen, n: *node) i32 = { }; if (cn.len == 0) { return 0; }; let rtyp: *node = fnretlookupmod(c, cn, cmod); - return sretretsize(c, rtyp); + // #129: sretretsize must see the CALLEE's module context so + // aliassamemod resolves aliases from the callee's module (not the + // caller's). Mirrors cstage operating on resolved Type* objects + // (type_chase_named never has this confusion). Swap + restore. + let savedmod: str = c.curmod; + if (cmod.len > 0) { c.curmod = cmod; }; + let r: i32 = sretretsize(c, rtyp); + c.curmod = savedmod; + return r; }; fn structlookup(c: *cgen, name: str) *structinfo = { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 4457e611..0f3fd57c 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -15470,12 +15470,12 @@ export fn checkfile(c: *checker, file: *node) void = { // tagged unions instead of errno-style integer sentinels. // // This file owns the eof / underread variant tags; lib/io/stream.ww -// owns the `vtable` + `stream` + read/write/close dispatchers, and -// lib/io/types.ww owns the error union, mode/whence enums, and the -// reader/writer/closer fn-type aliases. #94 fold-eFinal collapsed the -// pre-vtable `stream` struct + `closed` tag into the single vtable -// surface; the dispatchers are read/write/close (over `stream`), -// final over `handle` at io fold-2 (#5). +// owns the `vtable` + `stream` + read/write/close dispatchers and the +// [[empty]] singleton, and lib/io/types.ww owns the error union, +// mode/whence enums, and the reader/writer/closer fn-type aliases. +// #94 fold-eFinal collapsed the pre-vtable `stream` struct + `closed` +// tag into the single vtable surface; the dispatchers are read/write/ +// close (over `stream`), final over `handle` at io fold-2 (#5). package io; // eof — read past the end of the stream. Hare uses the `done` @@ -15492,7 +15492,7 @@ export type underread = !i32; // stream — Hare-shaped vtable surface. Project #94 fold-eFinal. // // The single io stream surface (the fold-eFinal collapse retired the -// pre-vtable `stream` struct + `closed` tag). Three exports: +// pre-vtable `stream` struct + `closed` tag). Exports: // // vtable a struct of optional fn-pointer slots — reader/writer/ // closer per ref/hare/io/stream.ha:36-42. Hare spells the @@ -15512,6 +15512,9 @@ export type underread = !i32; // public `read`/`write`/`close`; ww has no `handle` yet // (io fold-2, #5), so the dispatchers ARE the public // surface and grow the `handle` match when #5 lands. +// empty the discard+EOF stream (ref/hare/io/empty.ha:13). Lives +// here rather than io.ww so that 900_stdlib can compile +// io.ww standalone (io.ww has no cross-file type refs). // // Deferrals (drew-signed): `seeker` lands with io fold-2 (#5) once `off` // + `whence` plug into the signature. Hare's `?`-propagating `close` @@ -15675,6 +15678,40 @@ export fn tell(h: handle) (off | error) = { return seek(h, 0, whence.CUR); }; +// ---- empty stream ------------------------------------------------------- +// +// ref/hare/io/empty.ha:4-17. +// +// Hare uses `const _empty_vt: vtable = { ... }` + `const empty: *stream`. +// ww can't const-init a vtable struct with fn-ptr fields (#118), so the +// vtable is a module-level `let` and [[empty]] is a function that wires +// the fn-ptr slots on every call and returns the stream pointer. +// Single-assignment on the same words: idempotent under re-entry. +// Lives in stream.ww (not io.ww) so that 900_stdlib can compile io.ww +// standalone without referencing the cross-file vtable/reader/writer types. + +fn _empty_read(s: stream, buf: []u8) (size | eof | error) = { + let e: eof; + return e; +}; + +fn _empty_write(s: stream, buf: []u8) (size | error) = { + return buf.len: size; +}; + +let _empty_vt: vtable; + +// empty — a stream that discards all writes (returning their size) and +// returns EOF on every read. Mirrors ref/hare/io/empty.ha:13. +// #118: const vtable init with fn-ptr fields is unwired (emit_struct_data +// needs a two-pass reloc extension, node_fnptr_sym reusable). Using a +// mutable let + per-call wiring until #118 lands. +export fn empty() stream = { + _empty_vt.reader = (&_empty_read): *reader; + _empty_vt.writer = (&_empty_write): *writer; + return &_empty_vt; +}; + // errors — domain-agnostic error types. Mirrors ref/hare/errors/. // // Named-void tagged-union variants, so `(T | errors.invalid | ...)` @@ -18079,6 +18116,16 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { // Primitives / aliased-to-primitives are never sret. if (aliasprimsize(c, r.str) > 0) { return 0; }; if (streq(r.str, "str")) { return 0; }; + // #129: same-module alias wins over any-module struct hit. Without + // this, `type stream = *vtable` (io) loses to memio.stream (56B + // struct) via structlookup's any-module fallback → spurious sret. + // Mirrors cstage cg_sret_retsize, which sees TY_PTR, not a name. + if (c != nil) { + let al: *node = aliassamemod(c, r.str); + if (al != nil) { + return sretretsize(c, al); + }; + }; let si: *structinfo = structlookup(c, r.str); if (si == nil) { if (c != nil) { @@ -18121,7 +18168,15 @@ export fn callsretsize(c: *cgen, n: *node) i32 = { }; if (cn.len == 0) { return 0; }; let rtyp: *node = fnretlookupmod(c, cn, cmod); - return sretretsize(c, rtyp); + // #129: sretretsize must see the CALLEE's module context so + // aliassamemod resolves aliases from the callee's module (not the + // caller's). Mirrors cstage operating on resolved Type* objects + // (type_chase_named never has this confusion). Swap + restore. + let savedmod: str = c.curmod; + if (cmod.len > 0) { c.curmod = cmod; }; + let r: i32 = sretretsize(c, rtyp); + c.curmod = savedmod; + return r; }; fn structlookup(c: *cgen, name: str) *structinfo = { @@ -31271,6 +31326,23 @@ fn cgassign(c: *cgen, n: *node) void = { return; };}; }; + // #129: tagged-union field on global struct. LEAQ + // base(SB) into BX then the shared widener handles + // every rhs shape. Mirrors cstage cgen.c:4902 + // is_global arm. Without this the generic TK_ASSIGN + // below truncates to 1 word, silently dropping tag + // and payload. + if (n.op == tkind.TK_ASSIGN + && istaggedtype(c, fi.tnode)) { + let fsz: i32 = slotsize(c, fi.tnode); + emitline("\tLEAQ\t"); + emitsymname(c, bn); + emitline("(SB), BX\n"); + cgwidentaggedstore(c, + fi.tnode.type_: *tinfo, + n.rhs, "BX", fi.foff, fsz); + return; + }; if (n.op == tkind.TK_ASSIGN) { cgexpr(c, n.rhs); if (isstrtype(c, fi.tnode)) { diff --git a/test/wcc/989_lib_byteid.c b/test/wcc/989_lib_byteid.c index f8506502..cc46a5e2 100644 --- a/test/wcc/989_lib_byteid.c +++ b/test/wcc/989_lib_byteid.c @@ -95,6 +95,15 @@ static const struct ent ents[] = { { .fixture = "lib/strings/stringstest.ww", .mode = M_ID }, { .fixture = "lib/temp/temptest.ww", .mode = M_ID }, { .fixture = "lib/time/timetest.ww", .mode = M_ID }, + /* graduated from #59.2 DIVERGE by the #129 sretretsize fixes + * (same-module alias priority + callsretsize callee-context swap; + * bufio imports io so cross-module io.stream returns were spuriously + * sret-classified, producing divergent caller prologues) */ + { .fixture = "lib/bufio/bufiotest.ww", .mode = M_ID }, + /* graduated from #59.6 DIVERGE by the #129 same fix (fmt imports + * io transitively through bufio; sretretsize curmod swap closed + * the getopt.printusage spurious LEAQ DI caller divergence) */ + { .fixture = "lib/fmt/fmttest.ww", .mode = M_ID }, /* fixtureless modules, import-probe shape */ { .probe = "package main;\nimport sort;\nfn main() i32 = { return 0; };\n", .mode = M_ID, .sentinel = "package sort;", .moddir = "lib/sort" }, @@ -119,15 +128,13 @@ static const struct ent ents[] = { /* -------- documented-allowed cs≠ww (task #59) ------------ */ { .fixture = "lib/ascii/asciitest.ww", .mode = M_DIVERGE, .cite = "#59.1" }, - { .fixture = "lib/bufio/bufiotest.ww", - .mode = M_DIVERGE, .cite = "#59.2" }, + /* #59.2 bufio graduated to M_ID above (#129 fix) */ { .fixture = "lib/encoding/base64/base64_test.ww", .mode = M_DIVERGE, .cite = "#59.3" }, /* #59.4 hextest graduated to M_ID above (#22a reviewer fixes) */ { .fixture = "lib/errors/errnotest.ww", .mode = M_DIVERGE, .cite = "#59.5" }, - { .fixture = "lib/fmt/fmttest.ww", - .mode = M_DIVERGE, .cite = "#59.6" }, + /* #59.6 fmt graduated to M_ID above (#129 fix) */ /* #59.7 siphash graduated to M_ID above (#61 fix) */ { .fixture = "lib/log/logtest.ww", .mode = M_DIVERGE, .cite = "#59.8" },