diff --git a/Makefile b/Makefile index e01ca66d..cba435ce 100644 --- a/Makefile +++ b/Makefile @@ -329,6 +329,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_memio_vstream_run \ $(BIN)/test_fmt_vstream_run \ $(BIN)/test_bufio_vstream_run \ + $(BIN)/test_log_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 \ @@ -712,6 +713,14 @@ $(BIN)/test_bufio_vstream_run: test/wcc/778_bufio_vstream_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_log_vstream_run: test/wcc/779_log_vstream_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + lib/log/log.ww lib/log/vstream.ww \ + lib/fmt/fmt.ww lib/fmt/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 \ diff --git a/lib/fmt/vstream.ww b/lib/fmt/vstream.ww index 4838e4c7..39e3ffd6 100644 --- a/lib/fmt/vstream.ww +++ b/lib/fmt/vstream.ww @@ -170,8 +170,10 @@ fn vformatfield(vs: io.vstream, f: field) (size | io.error) = { // vfprint — vstream-side fprint. Mirror of fmt.fprint (fmt.ww:177). // Per-arg loop with a space separator; returns total bytes written -// or the first io.error. -fn vfprint(vs: io.vstream, args: formattable...) (size | io.error) = { +// or the first io.error. Exported so lib/log's V API (fold-e5) can +// dispatch through it; eFinal (#50) collapses fprint over the unified +// surface. +export fn vfprint(vs: io.vstream, args: formattable...) (size | io.error) = { let total: size = 0; let i: i32 = 0; for (i < args.len) { @@ -196,8 +198,9 @@ fn vfprint(vs: io.vstream, args: formattable...) (size | io.error) = { // {n}-placeholder parser routed through vformatfield + vputbytes // instead of fmt.formatfield + fmt.putbytes; the placeholder language // (incl. `{N:mods}` modifier subset) is identical — scandigits / -// scanmods / modsinit reused directly from fmt.ww. -fn vfprintf(vs: io.vstream, fmt: str, args: field...) (size | io.error) = { +// scanmods / modsinit reused directly from fmt.ww. Exported (same +// rationale as vfprint above) so lib/log's V API dispatches through it. +export fn vfprintf(vs: io.vstream, fmt: str, args: field...) (size | io.error) = { let total: size = 0; let i: i32 = 0; let nextimpl: i32 = 0; diff --git a/lib/log/vstream.ww b/lib/log/vstream.ww new file mode 100644 index 00000000..bb9df654 --- /dev/null +++ b/lib/log/vstream.ww @@ -0,0 +1,297 @@ +// vstream — Hare-shaped vtable wrappers over the logger surface. +// Project #94 fold-e5 (Option C, parallel API for lib/log). Last +// per-caller migration before fold-eFinal (task #50). +// +// Adds the ten _v variants of OLD log.ww's surface — new_v / +// lprintln_v / println_v / lprintfln_v / printfln_v / lfatal_v / +// fatal_v / lfatalf_v / fatalf_v / setlogger_v — alongside a vlogger +// vtable + vstdlogger over an io.vstream sink. The OLD surface stays +// untouched here — fold-eFinal (task #50) atomically flips the +// package shape: deletes the OLD logger / stdlogger / globals + the +// pre-vtable stderrsink + ensureinit, renames the `_v` suffix off +// (drops to bare `new` / `println` / etc. per Hare's +// ref/hare/log/{logger,funcs,global,silent}.ha), and migrates the +// few callers (lib/log/logtest.ww plus any downstream). +// +// drew acks (3/3): +// - module-static stderrsink_ctx (zero-init per #129 A.2/A.3 SSoT) +// - vt FIRST field intrusive cast (matches fmt/memio/bufio fold-e +// ctxs) +// - all 10 OLD fns get _v variants (Option-C consistency) +// +// ken mandates honored: +// - bootstrap byte-id mechanical pre==post: log is NOT embedded +// in any selfhost/cmd/*/main.combined.ww (grep-confirmed +// pre-impl), so adding vstream.ww + minimal export bumps in +// lib/fmt does not perturb the embedded surfaces. 990-997 +// gates stay green by virtue of log being test-only. +// - module-static ctx via #129 A.2/A.3 (cs == ww symmetric) +// - ctx shape simple scalars/ptrs (vt is the established intrusive +// first-field idiom; the only other field is `fd: i32`. No f32 +// per #165b; no nested aggregate beyond vt per #18). +// +// Hare reference: ref/hare/log/{logger.ha,funcs.ha,global.ha,silent.ha} +// + ref/hare/log/log.ha (re-export hub). +// +// Cast workaround per #206: bare `&fn_name` does not type-check as +// a `(* | void)` field-init / let-binding. Explicit +// `(&fn_name): *io.` cast at each store site is the Hare- +// faithful minimum-touch route — same workaround memio/vstream.ww +// + fmt/vstream.ww + bufio/vstream.ww use. 2 cast tokens here at +// the stderrsink_ctx_g vt wire-up (reader + writer; no closer — +// the process owns fd 2, same divergence as OLD stderrclose at +// log.ww:162). Both casts drop out wholesale once #206 closes. +// +// Chained-N_DOT-on-module-static field-assign per OLD log.ww:134-140 +// (`_default.logger.println = stdprintln;`) — proven-working spine. +// stderrsink_ctx_g.vt.X writes pattern through same shape; the +// tagged-union assign with widening cast composes via #199 α +// (concrete *fn → NAMED variant of (*reader | void) parent). +// +// Newline emission: vfprint / vfprintf are bare (no trailing \n); +// Hare's ref/hare/fmt/wrappers.ha:60-68 appends the newline at the +// fprintln site (after fprint). ww mirrors: stdprintln_v / stdprintfln_v +// call fmt.vfprint / fmt.vfprintf then io.st_write a one-byte slice +// `[10u8]` for the LF. Same shape OLD log.stdprintln uses via +// fmt.fprintln (log.ww:169). +// +// Sibling tasks parked here (filed, NOT fixed): +// +// - eFinal (#50): atomic flip + delete OLD log surface + drop the +// `_v` suffix wholesale (graduates to Hare's bare names). +// +// - #206 (bare &fn → (*alias|void)): 2 cast sites at ensureinit_v; +// drop out wholesale on close. +// +// - #173 (TRY-on-tagged-return both-stages broken): stderrwrite_v +// constructs nomem and widens to io.error explicitly rather than +// using `os.trywrite(...)?`; same shape memio.vstream.ww + +// fmt.vstream.ww + bufio.vstream.ww adopt. +// +// - #207 (struct-lit multi-tagged-field copy drops past first): +// not bitten here — module-static stderrsink_ctx_g is zero-init +// at link time per #129; vt fields wired via post-decl chained +// assigns rather than struct-lit, sidestepping #207 wholesale. +// The pattern is the module-static analogue of memio.vstream.ww's +// alloc-zero-chain. +// +// - #209 (wwstage formattable match-arm bail when fmt imported): +// log imports fmt transitively (via the formattable / field +// types in vlogger's vtable + fmt.vfprint / vfprintf calls in +// the std callbacks). All probe rows in test/wcc/779 run +// STAGE_CS-only; eFinal closes the wwstage half when #209 lands. + +package log; + +import fmt; +import io; +import os; + +// stderrsink_ctx — module-static state for the default vlogger's +// stderr sink. vt FIRST field for the intrusive vstream→*stderrsink_ctx +// cast in stderrwrite_v. Mirrors fmt.fd_ctx (fmt/vstream.ww:66) + +// memio.fixed_ctx (memio/vstream.ww:48); only scalars/ptrs beyond +// vt per ken's mandate. +type stderrsink_ctx = struct { + vt: io.vtable, + fd: i32, +}; + +// Zero-init at link time per #129 A.2/A.3 SSoT; ensureinit_v wires +// vt.reader / vt.writer + fd lazily on first dispatch. Same shape +// OLD log.ww uses for the pre-vtable stderrsink (log.ww:104). +let stderrsink_ctx_g: stderrsink_ctx; + +// vlogger — V-side counterpart to OLD logger (log.ww:85). Same shape +// modulo the `_v` field-suffix (collapses on #50). Dispatchers route +// through io.vstream rather than *io.stream. +// +// Mirrors ref/hare/log/logger.ha:9. +export type vlogger = struct { + println_v: fn(l: *vlogger, args: fmt.formattable...) void, + printfln_v: fn(l: *vlogger, format: str, fields: fmt.field...) void, +}; + +// vstdlogger — concrete vlogger forwarding to an io.vstream sink. +// First-field embed: `&sl.logger` yields a `*vlogger` (intrusive +// outer→inner cast); the vtable callback recovers the outer via +// `*vstdlogger` cast. +// +// Mirrors ref/hare/log/logger.ha:18 (stdlogger). +export type vstdlogger = struct { + logger: vlogger, + sink: io.vstream, +}; + +// _silent_v / _default_v — backing storage for the silent_v / +// default_v *vlogger globals. Zero-init at link time; ensureinit_v +// fills the println_v / printfln_v slots. +let _silent_v: vlogger; +let _default_v: vstdlogger; + +// silent_v / default_v / global_v — Hare-style *vlogger globals. +// Zero-init at link time; lazily wired by ensureinit_v on the first +// call to any exported fn. Same divergence as OLD log.ww:116-118 +// (header rationale for the lazy-init route applies wholesale: ww +// cstage emit_lets only constexprs primitive / str-literal / +// array-literal lets — struct- and pointer-literal init aren't +// reachable). #50 / eFinal graduates if static-init lands. +export let silent_v: *vlogger; +export let default_v: *vlogger; +export let global_v: *vlogger; + +// initdone_v — guards ensureinit_v so the one-shot wiring runs once. +// Mirrors OLD initdone (log.ww:122) + lib/temp's rnginit pattern. +let initdone_v: i32 = 0; + +fn ensureinit_v() void = { + if (initdone_v != 0) { return; }; + stderrsink_ctx_g.fd = 2; + stderrsink_ctx_g.vt.reader = (&stderrread_v): *io.reader; + stderrsink_ctx_g.vt.writer = (&stderrwrite_v): *io.writer; + + _silent_v.println_v = silentprintln_v; + _silent_v.printfln_v = silentprintfln_v; + + _default_v.logger.println_v = stdprintln_v; + _default_v.logger.printfln_v = stdprintfln_v; + _default_v.sink = &stderrsink_ctx_g.vt; + + silent_v = &_silent_v; + default_v = &_default_v.logger; + global_v = default_v; + initdone_v = 1; +}; + +// stderrread_v / stderrwrite_v — vtable callbacks for the private +// stderr sink. Read is eof-only — lib/log writes; never reads. +// Mirror of OLD stderrread / stderrwrite (log.ww:148-160) modulo the +// (size | io.eof | io.error) / (size | io.error) return surface; +// -errno collapses to a nomem-widened io.error per the +// fmt.fdsinkwrite_v precedent (fmt/vstream.ww:88-92). +fn stderrread_v(s: io.vstream, buf: []u8) (size | io.eof | io.error) = { + let e: io.eof; + return e; +}; + +fn stderrwrite_v(s: io.vstream, buf: []u8) (size | io.error) = { + let c: *stderrsink_ctx = s: *stderrsink_ctx; + let r: i64 = os.write(c.fd, buf.ptr, buf.len: u64); + if (r < 0) { + let nm: nomem; + let e: io.error = nm; + return e; + }; + return r: size; +}; + +// stdprintln_v / stdprintfln_v — vtable callbacks for vstdlogger. +// Forwards to fmt.vfprint / vfprintf on the sink + appends the +// trailing newline via io.st_write. Hare's fmt.fprintln emits the +// newline inside one wrapper (ref/hare/fmt/wrappers.ha:60-68); we +// inline that split at the log site because fmt.vfprint is the bare +// (no-newline) primitive. The (size | io.error) results are dropped +// — lib/log's surface is `void` (matches Hare + OLD stdprintln at +// log.ww:167). +fn stdprintln_v(l: *vlogger, args: fmt.formattable...) void = { + let sl: *vstdlogger = l: *vstdlogger; + fmt.vfprint(sl.sink, args...); + let nl: [1]u8; + nl[0] = 10u8; + let v: []u8 = nl[0:1]; + io.st_write(sl.sink, v); +}; + +fn stdprintfln_v(l: *vlogger, format: str, fields: fmt.field...) void = { + let sl: *vstdlogger = l: *vstdlogger; + fmt.vfprintf(sl.sink, format, fields...); + let nl: [1]u8; + nl[0] = 10u8; + let v: []u8 = nl[0:1]; + io.st_write(sl.sink, v); +}; + +// silentprintln_v / silentprintfln_v — vtable callbacks for the +// silent vlogger. Discard every record without dispatching through +// fmt; keeps silent truly silent if fmt ever gets stateful. Mirror +// of OLD silentprintln / silentprintfln (log.ww:183-187). +fn silentprintln_v(l: *vlogger, args: fmt.formattable...) void = { }; +fn silentprintfln_v(l: *vlogger, format: str, fields: fmt.field...) void = { }; + +// ---- public API (10 _v variants) ----------------------------------------- + +// new_v — wire `sl` as a vstdlogger over `sink`. Hare returns by +// value (ref/hare/log/logger.ha:20); ww cgen can't return wide +// structs, so we take an out-parameter pointer (same shape as OLD +// new at log.ww:193 + memio.fixed + bufio.init). +export fn new_v(sl: *vstdlogger, sink: io.vstream) void = { + ensureinit_v(); + sl.logger.println_v = stdprintln_v; + sl.logger.printfln_v = stdprintfln_v; + sl.sink = sink; +}; + +// lprintln_v — dispatch `args` through `log`. Mirrors OLD lprintln +// (log.ww:202) + ref/hare/log/funcs.ha:8. +export fn lprintln_v(log: *vlogger, args: fmt.formattable...) void = { + ensureinit_v(); + log.println_v(log, args...); +}; + +// println_v — dispatch through the [[global_v]] vlogger. Mirrors +// OLD println (log.ww:208) + ref/hare/log/funcs.ha:18. +export fn println_v(args: fmt.formattable...) void = { + ensureinit_v(); + lprintln_v(global_v, args...); +}; + +// lfatal_v — lprintln_v to `log` then exit(255). `never` return +// marks the bottom type so flow-control checks treat callers as +// terminated. Mirrors OLD lfatal (log.ww:216) + +// ref/hare/log/funcs.ha:28. +export fn lfatal_v(log: *vlogger, args: fmt.formattable...) never = { + lprintln_v(log, args...); + os.exit(255); +}; + +// fatal_v — lprintln_v to [[global_v]] then exit(255). Mirrors OLD +// fatal (log.ww:223) + ref/hare/log/funcs.ha:45. +export fn fatal_v(args: fmt.formattable...) never = { + println_v(args...); + os.exit(255); +}; + +// setlogger_v — install `log` as the [[global_v]] vlogger. Mirrors +// OLD setlogger (log.ww:230) + ref/hare/log/global.ha:20. +export fn setlogger_v(log: *vlogger) void = { + ensureinit_v(); + global_v = log; +}; + +// lprintfln_v — dispatch a format-string record through `log`. +// Mirrors OLD lprintfln (log.ww:237) + ref/hare/log/funcs.ha:13. +export fn lprintfln_v(log: *vlogger, format: str, fields: fmt.field...) void = { + ensureinit_v(); + log.printfln_v(log, format, fields...); +}; + +// printfln_v — dispatch through the [[global_v]] vlogger. Mirrors +// OLD printfln (log.ww:244) + ref/hare/log/funcs.ha:23. +export fn printfln_v(format: str, fields: fmt.field...) void = { + ensureinit_v(); + lprintfln_v(global_v, format, fields...); +}; + +// lfatalf_v — lprintfln_v to `log` then exit(255). Mirrors OLD +// lfatalf (log.ww:251) + ref/hare/log/funcs.ha:35. +export fn lfatalf_v(log: *vlogger, format: str, fields: fmt.field...) never = { + lprintfln_v(log, format, fields...); + os.exit(255); +}; + +// fatalf_v — lprintfln_v to [[global_v]] then exit(255). Mirrors +// OLD fatalf (log.ww:258) + ref/hare/log/funcs.ha:52. +export fn fatalf_v(format: str, fields: fmt.field...) never = { + printfln_v(format, fields...); + os.exit(255); +}; diff --git a/test/wcc/779_log_vstream_run.c b/test/wcc/779_log_vstream_run.c new file mode 100644 index 00000000..bd37d0c1 --- /dev/null +++ b/test/wcc/779_log_vstream_run.c @@ -0,0 +1,385 @@ +/* + * 779_log_vstream_run — project #94 fold-e5 sentinel. Pins the + * additive lib/log/vstream.ww Option C parallel API: module-static + * stderrsink_ctx_g with `vt: io.vtable` as the first field for the + * intrusive vstream cast, the 10 _v variants of OLD log.ww's surface + * (new_v / lprintln_v / println_v / lprintfln_v / printfln_v / lfatal_v + * / fatal_v / lfatalf_v / fatalf_v / setlogger_v), a vlogger vtable + + * vstdlogger over io.vstream sink, and lazy ensureinit_v wiring the + * stderr default + silent_v / default_v / global_v *vlogger globals. + * Coexists with the pre-vtable log.logger / log.stdlogger / log.new / + * log.println / etc. surface in log.ww (fold-eFinal, task #50, retires + * the latter and drops the `_v` suffix wholesale to match Hare's bare + * names). + * + * Each row imports os + log + memio + io. log itself imports fmt, + * so the formattable / field types flow through transitively. + * + * Cstage-only per row (no STAGE_WW, no byte_id) — pre-existing + * wwstage bug #209 (sibling of #190): the wwstage checker bails on + * match-arm-over-formattable when fmt is imported transitively, which + * lib/log does for the vlogger vtable signatures + the fmt.vfprint / + * vfprintf dispatch in stdprintln_v / stdprintfln_v. The bug bites the + * OLD log surface identically — `log.println("hi")` from a probe + * trips the same trace. Existing 970 logtest runs the @test fixture + * under cstage `ww run` only, so the OLD surface is fine via cstage. + * 995 self-rebuild dodges it because no selfhost cmd imports log + * transitively. Fix-of-the-bug = one-class checker repair, out-of- + * scope for the additive fold-e5; closes the wwstage half here when + * it lands. Byte-id graduates with #209 close. + * + * row | what it pins + * --------------------------+-------------------------------------- + * println_v_default_stderr | log.println_v through the [[default_v]] + * | global, routed to fd 2 via the + * | module-static stderrsink_ctx_g. Asserts + * | ensureinit_v wires + dispatches without + * | crashing; stderr capture is left to the + * | shell runner. Pins the full chain: + * | println_v → lprintln_v(global_v) → + * | (*vlogger).println_v → stdprintln_v → + * | fmt.vfprint + io.st_write("\n"). + * printfln_v_default_stderr | Same chain as row 1 but through the + * | format-string slot (printfln_v → + * | lprintfln_v(global_v) → stdprintfln_v + * | → fmt.vfprintf + io.st_write("\n")). + * | Pins the {n}-placeholder dispatch + * | through vformatfield reaches the + * | default sink. + * lprintln_v_custom_sink | memio.fixed_vstream-backed vstdlogger; + * | log.new_v wires the vtable + sink, + * | log.lprintln_v dispatches "hi" + 7i64 + * | through stdprintln_v. Asserts the + * | exact bytes ("hi 7\n") land in the + * | caller's buffer (fixed_vstream borrows + * | the slice, so the bytes are visible + * | back through buf[]). + * branched_lprintln_v | branched callee per #105: two distinct + * | vstdlogger sinks runtime-selected via + * | *vlogger pointer. lprintln_v dispatches + * | through the picked sink only; the + * | other stays empty. Catches a constant- + * | fold mistake in the fn-ptr dispatch + * | (mirror of 778's branched_bufio_vstream). + * + * SIBLINGS (filed inline, NOT fixed here — fold-e5 is purely + * additive over fold-e1/e2/e3/e4's frozen io.* + memio.* + fmt.* + + * bufio.* surface plus the minimal `export` bump on fmt.vfprint / + * fmt.vfprintf): + * + * - eFinal (#50): atomic flip + delete OLD log surface + drop the + * `_v` suffix wholesale. + * + * - #206 (bare &fn → (*alias|void)): 2 cast sites at ensureinit_v + * in lib/log/vstream.ww; drop out wholesale on close. + * + * - #173 (TRY-on-tagged-return both-stages broken): stderrwrite_v + * constructs nomem and widens to io.error explicitly rather than + * using `os.trywrite(...)?`; same shape memio.vstream.ww + + * fmt.vstream.ww + bufio.vstream.ww adopt. + * + * - #209 (wwstage formattable match-arm bail): every row is + * STAGE_CS-only; byte-id deferred until #209 lands. + * + * BOOTSTRAP-EMBED CHECK: log is NOT embedded in any selfhost + * combined.ww (grep `package log\|import log` returned empty pre- + * impl). Adding lib/log/vstream.ww + the two `export` bumps on + * lib/fmt/vstream.ww does NOT require a Makefile regen (#110); only + * lib/log/ + lib/fmt/ test paths see the new module surface. 990-997 + * byte-id gates stay green by virtue of log being test-only and the + * fmt vstream.ww changes being non-embedded. + * + * GATE POLARITY: must stay GREEN. A red here means vstream.ww + * regressed, the module-static stderrsink_ctx_g zero-init or chained + * vt-field assigns regressed, the intrusive vstream→*stderrsink_ctx + * cast miscomputed offsets, ensureinit_v's one-shot guard broke, or + * the fn-ptr dispatch through vlogger.println_v / printfln_v stopped + * resolving. + */ +#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; + int byte_id; +}; + +static const struct row rows[] = { + { "println_v_default_stderr", + "package main;\n" + "import os;\n" + "import log;\n" + "export fn main() i32 = {\n" + " log.println_v(\"hello\");\n" + " return 42;\n" + "};\n", + 42, + STAGE_CS, 0 }, + { "printfln_v_default_stderr", + "package main;\n" + "import os;\n" + "import log;\n" + "export fn main() i32 = {\n" + " log.printfln_v(\"v={}\", 7i64);\n" + " return 43;\n" + "};\n", + 43, + STAGE_CS, 0 }, + { "lprintln_v_custom_sink", + "package main;\n" + "import os;\n" + "import log;\n" + "import memio;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let buf: [16]u8;\n" + " let r = memio.fixed_vstream(buf[0:16]);\n" + " let vs: io.vstream = nil: *io.vtable;\n" + " match (r) {\n" + " case let v: io.vstream => { vs = v; };\n" + " case nomem => { return 50; };\n" + " };\n" + " let sl: log.vstdlogger;\n" + " log.new_v(&sl, vs);\n" + " log.lprintln_v(&sl.logger, \"hi\", 7i64);\n" + " if (buf[0] != 104u8) { return 91; };\n" + " if (buf[1] != 105u8) { return 92; };\n" + " if (buf[2] != 32u8) { return 93; };\n" + " if (buf[3] != 55u8) { return 94; };\n" + " if (buf[4] != 10u8) { return 95; };\n" + " return 44;\n" + "};\n", + 44, + STAGE_CS, 0 }, + { "branched_lprintln_v", + "package main;\n" + "import os;\n" + "import log;\n" + "import memio;\n" + "import io;\n" + "export fn main() i32 = {\n" + " let bufA: [16]u8;\n" + " let bufB: [16]u8;\n" + " let rA = memio.fixed_vstream(bufA[0:16]);\n" + " let vsA: io.vstream = nil: *io.vtable;\n" + " match (rA) {\n" + " case let v: io.vstream => { vsA = v; };\n" + " case nomem => { return 50; };\n" + " };\n" + " let rB = memio.fixed_vstream(bufB[0:16]);\n" + " let vsB: io.vstream = nil: *io.vtable;\n" + " match (rB) {\n" + " case let v: io.vstream => { vsB = v; };\n" + " case nomem => { return 51; };\n" + " };\n" + " let slA: log.vstdlogger;\n" + " let slB: log.vstdlogger;\n" + " log.new_v(&slA, vsA);\n" + " log.new_v(&slB, vsB);\n" + " let sel: i32 = 1;\n" + " let chosen: *log.vlogger = &slA.logger;\n" + " if (sel == 0) { chosen = &slB.logger; };\n" + " log.lprintln_v(chosen, \"pick\");\n" + " if (bufA[0] != 112u8) { return 91; };\n" + " if (bufA[1] != 105u8) { return 92; };\n" + " if (bufA[2] != 99u8) { return 93; };\n" + " if (bufA[3] != 107u8) { return 94; };\n" + " if (bufA[4] != 10u8) { return 95; };\n" + " if (bufB[0] != 0u8) { return 96; };\n" + " return 45;\n" + "};\n", + 45, + STAGE_CS, 0 }, +}; + +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 + * .{combined.ww,s,o} + bare exe alongside. Sweep all then + * rmdir. Mirror of 778's cleanup_tmp. */ +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/lvs_%d_d_%d", getpid(), seq); + snprintf(base, sizeof base, "main779"); + 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); + /* Suppress stderr for rows 1/2 — the default-sink probes + * write through fd 2; don't pollute the test runner's + * stderr. */ + char runcmd[1024]; + snprintf(runcmd, sizeof runcmd, "%s 2>/dev/null", outbin); + rc = runwait(runcmd); + } 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). Mirror of 778's. Unused + * for fold-e5 — every row is STAGE_CS-only per #209 — but kept + * scaffolded for when #209 closes and byte-id graduates. */ +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/lvs_%d_c_%d", getpid(), seq); + snprintf(tdw, sizeof tdw, "/tmp/lvs_%d_w_%d", getpid(), seq); + snprintf(base, sizeof base, "main779"); + 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, + "log_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, + "log_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, + "log_vstream_run[byte-id][%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + } + } + } + + if (!wwpresent) + fprintf(stderr, "log_vstream_run: skip wwstage (no %s)\n", wdrv); + + if (fail) { + fprintf(stderr, "log_vstream_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("log_vstream_run: %d/%d ok\n", total, total); + return 0; +}