From 635429bef8b81aa09fe49dfd88e014640f743221 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 28 May 2026 21:58:06 +0900 Subject: [PATCH] lib/io: port mode/whence/error + reader/writer/closer fn-aliases (#94 fold-d) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New lib/io/types.ww mirrors ref/hare/io/types.ha — the surrounding port that lives alongside the existing lib/io/io.ww (pre-vtable stream + eof + underread). Hare splits the same way (stream.ha + types.ha share `module io`); ww does the equivalent via dir-enum. Each type cites Hare per CLAUDE.md rule 9: - mode (enum u8) — ref/hare/io/types.ha:29-34. RDWR=3 (not Hare's `READ | WRITE`) because ww enum-value positions don't fold expressions; bitfield value SSoT preserved, divergence inline. - whence (enum i32) — ref/hare/io/types.ha:37-41. Hare leaves the underlying implicit; ww requires one. i32 matches the `off` type fold-e wires in. - error — ref/hare/io/types.ha:11. Hare spreads `errors::error`; lib/errors not ported, so the union carries the two tags observable in this fold: underread (from io.ww) and the predeclared `nomem` (#29, type.c:72 / check.ww:78). NOT redefined here. - reader/writer/closer — ref/hare/io/types.ha:46/51/55. EOF=eof (not Hare's `done` singleton) per #93 and the io.ww:8 rationale. `*stream` forward-refs the existing pre-vtable struct in io.ww; same cross-file pattern Hare uses. Drew signoff (this fold only): seeker, copier, strerror, and the EOF=done singleton DEFERRED to fold-e — they need the `handle` sum and #93's done landing. Hare's `_unsafe` carve-out unaffected. eof / underread / stream re-used from io.ww (NOT redefined); io.ww keeps the pre-vtable struct unchanged, ditto its WHY-comments. Combined.ww regen (#110): selfhost/cmd/{w6c,wwdump}/main.combined.ww auto-pulled the new types.ww via dir-enum (+52 lines each, same package io). Makefile dep lines for wwdump_ww + w6c_ww add the new source so editing it triggers rebuild. Probe: test/wcc/768_io_types_run.c — 5 rows × 2 stages = 10 invocations. Pins enum value/underlying + the three fn-type aliases at the param slot. Both siblings filed inline in the probe header: - #189 wwstage `let r: io.reader = fn_name;` bails "let: not assignable". cstage accepts. Param + struct-field paths work in both stages, so io vtable port is unblocked. Probe uses the alias only at the param slot. - #190 wwstage match-arm on cross-module variant tag bails "case: not a variant of scrutinee (io.eof | io.error)". Likely same family as #178. cstage accepts. Probe uses `is` instead of `match` for the variant gate. make test: 201/201 (was 200; +1 from 768). 990-997 byte-id + combined_ww_fresh + sizelint all green. --- Makefile | 11 +- lib/io/types.ww | 51 ++++++ selfhost/cmd/w6c/main.combined.ww | 52 ++++++ selfhost/cmd/wwdump/main.combined.ww | 52 ++++++ test/wcc/768_io_types_run.c | 262 +++++++++++++++++++++++++++ 5 files changed, 426 insertions(+), 2 deletions(-) create mode 100644 lib/io/types.ww create mode 100644 test/wcc/768_io_types_run.c diff --git a/Makefile b/Makefile index 6a0bc086..37056351 100644 --- a/Makefile +++ b/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/io/io.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/io/io.ww lib/io/types.ww lib/memio/memio.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/io/io.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/io/io.ww lib/io/types.ww lib/memio/memio.ww lib/fmt/fmt.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) @mkdir -p $(BIN)/w6c_ww.d @@ -318,6 +318,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_star_fn_deref \ $(BIN)/test_star_fn_deref_call \ $(BIN)/test_match_variant_dispatch \ + $(BIN)/test_io_types_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 \ @@ -632,6 +633,12 @@ $(BIN)/test_match_variant_dispatch: test/wcc/767_match_variant_dispatch.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_io_types_run: test/wcc/768_io_types_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_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 \ diff --git a/lib/io/types.ww b/lib/io/types.ww new file mode 100644 index 00000000..807ad11c --- /dev/null +++ b/lib/io/types.ww @@ -0,0 +1,51 @@ +// types — error union, mode/whence enums, reader/writer/closer +// fn-type aliases. Project #94 fold-1 step (d) (drew Fold D). +// +// Hare splits the io module across stream.ha + types.ha and ww does +// the same: lib/io/io.ww owns the (pre-vtable) `stream`, `eof`, and +// `underread` shapes; this file owns the surrounding port. Both +// files share `package io;` so forward refs (e.g. reader → stream) +// resolve via the dir-enum concat order. +// +// Deferrals (drew-signed): `handle`, `seeker`, `copier`, `strerror` +// land with fold-e (need the `handle` sum). Hare's `EOF = done` +// stays as `io.eof` until the `done` singleton ships (#93). + +package io; + +// ref/hare/io/types.ha:29-34. RDWR is spelled `3` rather than Hare's +// `READ | WRITE` because the ww parser doesn't fold expressions in +// enum value positions; the value SSoT stays the same bitfield. +export type mode = enum u8 { + NONE = 0, + READ = 1, + WRITE = 2, + RDWR = 3, +}; + +// ref/hare/io/types.ha:37-41. Hare leaves the underlying implicit; +// ww requires one. i32 matches the off type that fold-e will plug +// into the seeker signature. +export type whence = enum i32 { + SET = 0, + CUR = 1, + END = 2, +}; + +// ref/hare/io/types.ha:11 spreads `errors::error` into the union; +// lib/errors isn't ported yet, so the union carries only the tags +// observable in this fold — `underread` (from io.ww) and the +// predeclared `nomem` (#29, type.c:72 / check.ww:78). +export type error = !(underread | nomem); + +// ref/hare/io/types.ha:46. `eof` (not Hare's `done`) per the io.ww:8 +// rationale; lifts to `done` with #93. `*stream` forward-refs the +// pre-vtable struct in io.ww — same cross-file pattern Hare uses. +export type reader = fn(s: *stream, buf: []u8) (size | eof | error); + +// ref/hare/io/types.ha:51. No `const` qualifier — ww has none, and +// lib/io/io.ww:30 + lib/sort/sort.ww:18 drop it the same way. +export type writer = fn(s: *stream, buf: []u8) (size | error); + +// ref/hare/io/types.ha:55. +export type closer = fn(s: *stream) (void | error); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index c68fb7d8..14d1bd1a 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -13879,6 +13879,58 @@ export fn close(s: *stream) (void | closed) = { return s.close(s); }; +// types — error union, mode/whence enums, reader/writer/closer +// fn-type aliases. Project #94 fold-1 step (d) (drew Fold D). +// +// Hare splits the io module across stream.ha + types.ha and ww does +// the same: lib/io/io.ww owns the (pre-vtable) `stream`, `eof`, and +// `underread` shapes; this file owns the surrounding port. Both +// files share `package io;` so forward refs (e.g. reader → stream) +// resolve via the dir-enum concat order. +// +// Deferrals (drew-signed): `handle`, `seeker`, `copier`, `strerror` +// land with fold-e (need the `handle` sum). Hare's `EOF = done` +// stays as `io.eof` until the `done` singleton ships (#93). + +package io; + +// ref/hare/io/types.ha:29-34. RDWR is spelled `3` rather than Hare's +// `READ | WRITE` because the ww parser doesn't fold expressions in +// enum value positions; the value SSoT stays the same bitfield. +export type mode = enum u8 { + NONE = 0, + READ = 1, + WRITE = 2, + RDWR = 3, +}; + +// ref/hare/io/types.ha:37-41. Hare leaves the underlying implicit; +// ww requires one. i32 matches the off type that fold-e will plug +// into the seeker signature. +export type whence = enum i32 { + SET = 0, + CUR = 1, + END = 2, +}; + +// ref/hare/io/types.ha:11 spreads `errors::error` into the union; +// lib/errors isn't ported yet, so the union carries only the tags +// observable in this fold — `underread` (from io.ww) and the +// predeclared `nomem` (#29, type.c:72 / check.ww:78). +export type error = !(underread | nomem); + +// ref/hare/io/types.ha:46. `eof` (not Hare's `done`) per the io.ww:8 +// rationale; lifts to `done` with #93. `*stream` forward-refs the +// pre-vtable struct in io.ww — same cross-file pattern Hare uses. +export type reader = fn(s: *stream, buf: []u8) (size | eof | error); + +// ref/hare/io/types.ha:51. No `const` qualifier — ww has none, and +// lib/io/io.ww:30 + lib/sort/sort.ww:18 drop it the same way. +export type writer = fn(s: *stream, buf: []u8) (size | error); + +// ref/hare/io/types.ha:55. +export type closer = fn(s: *stream) (void | error); + // memio — in-memory io stream. // // Hare's memio:: surface, drop underscores. Two flavours behind a diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index b7ed8548..8f6ea18c 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -13879,6 +13879,58 @@ export fn close(s: *stream) (void | closed) = { return s.close(s); }; +// types — error union, mode/whence enums, reader/writer/closer +// fn-type aliases. Project #94 fold-1 step (d) (drew Fold D). +// +// Hare splits the io module across stream.ha + types.ha and ww does +// the same: lib/io/io.ww owns the (pre-vtable) `stream`, `eof`, and +// `underread` shapes; this file owns the surrounding port. Both +// files share `package io;` so forward refs (e.g. reader → stream) +// resolve via the dir-enum concat order. +// +// Deferrals (drew-signed): `handle`, `seeker`, `copier`, `strerror` +// land with fold-e (need the `handle` sum). Hare's `EOF = done` +// stays as `io.eof` until the `done` singleton ships (#93). + +package io; + +// ref/hare/io/types.ha:29-34. RDWR is spelled `3` rather than Hare's +// `READ | WRITE` because the ww parser doesn't fold expressions in +// enum value positions; the value SSoT stays the same bitfield. +export type mode = enum u8 { + NONE = 0, + READ = 1, + WRITE = 2, + RDWR = 3, +}; + +// ref/hare/io/types.ha:37-41. Hare leaves the underlying implicit; +// ww requires one. i32 matches the off type that fold-e will plug +// into the seeker signature. +export type whence = enum i32 { + SET = 0, + CUR = 1, + END = 2, +}; + +// ref/hare/io/types.ha:11 spreads `errors::error` into the union; +// lib/errors isn't ported yet, so the union carries only the tags +// observable in this fold — `underread` (from io.ww) and the +// predeclared `nomem` (#29, type.c:72 / check.ww:78). +export type error = !(underread | nomem); + +// ref/hare/io/types.ha:46. `eof` (not Hare's `done`) per the io.ww:8 +// rationale; lifts to `done` with #93. `*stream` forward-refs the +// pre-vtable struct in io.ww — same cross-file pattern Hare uses. +export type reader = fn(s: *stream, buf: []u8) (size | eof | error); + +// ref/hare/io/types.ha:51. No `const` qualifier — ww has none, and +// lib/io/io.ww:30 + lib/sort/sort.ww:18 drop it the same way. +export type writer = fn(s: *stream, buf: []u8) (size | error); + +// ref/hare/io/types.ha:55. +export type closer = fn(s: *stream) (void | error); + // memio — in-memory io stream. // // Hare's memio:: surface, drop underscores. Two flavours behind a diff --git a/test/wcc/768_io_types_run.c b/test/wcc/768_io_types_run.c new file mode 100644 index 00000000..a9904145 --- /dev/null +++ b/test/wcc/768_io_types_run.c @@ -0,0 +1,262 @@ +/* + * 768_io_types_run — project #94 fold-1 step (d) (drew Fold D) sentinel. + * Pins the new lib/io/types.ww port (ref/hare/io/types.ha) — `mode` / + * `whence` enums, `error = !(underread | nomem)` union, and the + * `reader` / `writer` / `closer` fn-type aliases. The aliases forward- + * ref `*stream` from lib/io/io.ww (Hare splits the same way: + * stream.ha + types.ha share the `io` module). + * + * Each row is a self-contained `import io;` program: cstage `ww build` + * compiles it, we run the binary and assert the exit. Rows that hit a + * known wwstage checker gap (see SIBLINGS below) are cstage-only via + * stage_mask — the bulk run both stages so the dir-enum concat through + * types.ww + io.ww is exercised by ww_ww too. + * + * row | what it pins + * --------------------------+-------------------------------------- + * enum_mode_value | io.mode (enum u8) member access + + * | underlying numeric promotion. RDWR=3 + * | matches Hare's READ|WRITE bitfield + * | (1 SSoT; see types.ww header note on + * | the value spelling). + * enum_whence_value | io.whence (enum i32) member access. + * | END=2 matches ref/hare/io/types.ha:40. + * reader_alias_param | `io.reader` usable as a fn-VALUE param + * | type — the immediate use case the + * | upcoming vtable port needs. dummy_read + * | passed by-name, called through the + * | alias, `is size` gate confirms the + * | (size | io.eof | io.error) return + * | shape parses + dispatches. + * writer_alias_param | `io.writer` same shape; (size | + * | io.error) return. + * closer_alias_param | `io.closer` (void | io.error). Most + * | minimal of the three — pins the alias + * | resolves without depending on the + * | tagged-return discriminant path. + * + * SIBLINGS (out of scope for #94 fold-1(d), filed inline): + * + * - #189: wwstage checker bails "let: not assignable" on + * `let r: io.reader = some_fn_name;` + * i.e. binding a fn-name to a fn-VALUE-typed let. cstage accepts. + * Param + struct-field assignment work in both stages, so the io + * vtable port is not blocked. PROBE ROUTES AROUND by using the + * alias only at the param slot. + * + * - #190: wwstage match-arm on cross-module variant tag bails + * "case: not a variant of scrutinee (io.eof)" / "(io.error)" + * on `match (r(s,buf)) { case io.eof => ...; case let e: io.error + * => ...; }`. Likely the same family as #178 (typeeqast layer + * asymmetry). cstage accepts. PROBE ROUTES AROUND by using `is` + * instead of `match` for the variant gate. The Hare-faithful + * match form lands once that family is closed. + * + * GATE POLARITY: must stay GREEN. A red here means either types.ww + * stopped parsing/checking, an enum-value underlying narrowed, or the + * fn-type alias resolution regressed. + */ +#include +#include +#include +#include +#include +#include + +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; +}; + +static const struct row rows[] = { + { "enum_mode_value", + "package main;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let m: io.mode = io.mode.RDWR;\n" + " return m: i32;\n" + "};\n", + 3, + STAGE_CS | STAGE_WW }, + { "enum_whence_value", + "package main;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let w: io.whence = io.whence.END;\n" + " return w: i32;\n" + "};\n", + 2, + STAGE_CS | STAGE_WW }, + { "reader_alias_param", + "package main;\n" + "import io;\n" + "fn dummy_read(s: *io.stream, buf: []u8) (size | io.eof | io.error) = {\n" + " return 0: size;\n" + "};\n" + "fn use_reader(r: io.reader, s: *io.stream, buf: []u8) i32 = {\n" + " let v = r(s, buf);\n" + " if (v is size) { return 11; };\n" + " return -1;\n" + "};\n" + "export fn main() i32 = {\n" + " let st: io.stream;\n" + " let buf: [4]u8;\n" + " let bs: []u8 = buf[0:4];\n" + " return use_reader(dummy_read, &st, bs);\n" + "};\n", + 11, + STAGE_CS | STAGE_WW }, + { "writer_alias_param", + "package main;\n" + "import io;\n" + "fn dummy_write(s: *io.stream, buf: []u8) (size | io.error) = {\n" + " return buf.len: size;\n" + "};\n" + "fn use_writer(w: io.writer, s: *io.stream, buf: []u8) i32 = {\n" + " let v = w(s, buf);\n" + " if (v is size) { return 22; };\n" + " return -1;\n" + "};\n" + "export fn main() i32 = {\n" + " let st: io.stream;\n" + " let buf: [7]u8;\n" + " let bs: []u8 = buf[0:7];\n" + " return use_writer(dummy_write, &st, bs);\n" + "};\n", + 22, + STAGE_CS | STAGE_WW }, + { "closer_alias_param", + "package main;\n" + "import io;\n" + "fn dummy_close(s: *io.stream) (void | io.error) = {\n" + " return void;\n" + "};\n" + "fn use_closer(c: io.closer, s: *io.stream) i32 = {\n" + " let _v = c(s);\n" + " return 33;\n" + "};\n" + "export fn main() i32 = {\n" + " let st: io.stream;\n" + " return use_closer(dummy_close, &st);\n" + "};\n", + 33, + STAGE_CS | STAGE_WW }, +}; + +static int +build_with(const char *driver, const char *src_path, const char *tmpdir, + const char *cwd) +{ + char cmd[2048]; + snprintf(cmd, sizeof cmd, + "cd %s && %s build -I %s/lib %s 2>/dev/null", + tmpdir, driver, cwd, src_path); + return runwait(cmd); +} + +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 have_ww = (access(wdrv, X_OK) == 0); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + const struct row *r = &rows[i]; + + char src[64], tmpdir[64]; + snprintf(src, sizeof src, "/tmp/iotyp_%d_%d.ww", + getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/iotyp_%d_d_%d", + getpid(), i); + FILE *f = fopen(src, "wb"); + if (!f) { fail++; total++; continue; } + fputs(r->src, f); + fclose(f); + mkdir(tmpdir, 0755); + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + + if (r->stage_mask & STAGE_CS) { + total++; + if (build_with(cdrv, src, tmpdir, cwd) != 0) { + fprintf(stderr, + "io_types_run[cs][%s]: build failed\n", + r->label); + fail++; + } else { + int got = runwait(outbin); + if (got != r->want_exit) { + fprintf(stderr, + "io_types_run[cs][%s]: rc=%d want=%d\n", + r->label, got, r->want_exit); + fail++; + } + } + unlink(outbin); + } + + if (have_ww && (r->stage_mask & STAGE_WW)) { + total++; + if (build_with(wdrv, src, tmpdir, cwd) != 0) { + fprintf(stderr, + "io_types_run[ww][%s]: build failed\n", + r->label); + fail++; + } else { + int got = runwait(outbin); + if (got != r->want_exit) { + fprintf(stderr, + "io_types_run[ww][%s]: rc=%d want=%d\n", + r->label, got, r->want_exit); + fail++; + } + } + unlink(outbin); + } + + unlink(src); + rmdir(tmpdir); + } + + if (fail) { + fprintf(stderr, + "io_types_run: %d/%d rows failed\n", fail, total); + return 1; + } + printf("io_types_run: %d/%d ok\n", total, total); + return 0; +}