From 0ef94eef0437503ae2b2128987ee9a1fb1f2d0e3 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 15 May 2026 11:34:47 +0900 Subject: [PATCH] =?UTF-8?q?lib+test:=20fmt=20rename=20fprint=E2=86=92fdpri?= =?UTF-8?q?nt;=20add=20io.stream=20fprint=20sink?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 5 + lib/CLAUDE.md | 2 +- lib/fmt/fmt.ww | 233 ++++++++++++++++++++++++++++++++++++----- lib/fmt/fmttest.ww | 213 +++++++++++++++++++++++++++++++++++++ test/wcc/700_e2e.c | 6 +- test/wcc/900_stdlib.c | 14 +-- test/wcc/970_fmt_run.c | 52 +++++++++ 7 files changed, 485 insertions(+), 40 deletions(-) create mode 100644 lib/fmt/fmttest.ww create mode 100644 test/wcc/970_fmt_run.c diff --git a/Makefile b/Makefile index e7f6979b..1d1c5897 100644 --- a/Makefile +++ b/Makefile @@ -230,6 +230,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_selfhost $(BIN)/test_w6a_ww $(BIN)/test_w6l_ww \ $(BIN)/test_w6c_ww $(BIN)/test_ww_ww $(BIN)/test_self_rebuild \ $(BIN)/test_dyn_ww $(BIN)/test_selfcheck $(BIN)/test_at_test_ww \ + $(BIN)/test_fmt_run \ $(BIN)/test_memio_run $(BIN)/test_temp_run $(BIN)/test_getopt_run \ $(BIN)/test_base32_run $(BIN)/test_base64_run \ $(BIN)/test_adler32_run $(BIN)/test_crc16_run \ @@ -409,6 +410,10 @@ $(BIN)/test_at_test_ww: test/wcc/997_at_test_ww.c $(BIN)/ww_ww $(BIN)/w6c_ww \ $(BIN)/w6a_ww $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_fmt_run: test/wcc/970_fmt_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_memio_run: test/wcc/980_memio_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/lib/CLAUDE.md b/lib/CLAUDE.md index b527dbb1..3a3b8194 100644 --- a/lib/CLAUDE.md +++ b/lib/CLAUDE.md @@ -29,7 +29,7 @@ Signatures mirror Hare too, modulo: - Call-site variadic sugar matches Hare. `fn f(args: T...)` declares a Hare-style variadic; call sites either gather N args into a fresh `[]T` (`fmt.println(42, "hi", true)`) or forward an existing - slice with `xs...` (`fprintln(fd, args...)`). The bare `T...` form + slice with `xs...` (`fdprintln(fd, args...)`). The bare `T...` form in tagged unions still means spread-flatten (`(...inner | E)`); the two uses don't overlap because `T...` only attaches to a *param* decl. `lib/fmt` is intentionally print-string-only — no diff --git a/lib/fmt/fmt.ww b/lib/fmt/fmt.ww index 77a33dd1..819485cf 100644 --- a/lib/fmt/fmt.ww +++ b/lib/fmt/fmt.ww @@ -1,11 +1,84 @@ -// fmt — formatting writers. Mirrors Hare's lib/fmt subset that fmt- -// prints values via [[io::handle]]-style fd writers. Call sites take -// Hare's variadic shape: `fmt.println(42, "hi", true)` gathers the -// args into a `[]formattable` slice; wrappers forward via `args...`. +// fmt — formatting writers. Mirrors Hare's lib/fmt subset. +// +// Hare's `fmt::fprint` takes `io::handle = (io::file | int)`, which +// ww doesn't yet have. So we ship two sinks side-by-side, with the +// distinction baked into the name: +// +// fprint / fprintln write to a [[io.stream]] — Hare's +// primary surface. Errors via `io.closed`. +// fdprint / fdprintln write to a raw fd via [[os.write]] — ww- +// specific. Errors via the raw `-errno` i64 +// convention. Used by the process-stdio +// wrappers below (print/println/errorln/ +// fatal) until lib/io grows an fd-backed +// stream; at that point both halves +// graduate "in one go" (lib/CLAUDE.md) and +// the fd-suffixed names disappear. +// +// Call sites take Hare's variadic shape: `fmt.println(42, "hi", true)` +// gathers the args into a `[]formattable` slice; wrappers forward +// via `args...`. -use os; -use strconv; -use strings; +use io; + +// Direct rt_syscall / rt_exit bindings rather than `use os;` because +// os exports read/write/close, which collide with io.read/write/close +// under the driver's flat-scope concat — same workaround used by +// lib/memio. Both fmt's fd sink and io.stream sink need to coexist +// in this module, so the os surface has to come in à la carte. +@symbol("rt_syscall") fn rtsyscall3(num: i64, a: i64, b: i64, c: i64) i64; +@symbol("rt_syscall") fn rtsyscall1(num: i64, a: i64) i64; + +// rawwrite — Linux write(2) syscall (nr=1). The fd sinks below call +// this directly instead of [[os.write]] to keep the collision off +// fmt's exported surface. Same signature, same negative-errno +// convention. +fn rawwrite(fd: i32, buf: *u8, n: u64) i64 = { + return rtsyscall3(1i64, fd: i64, buf: i64, n: i64); +}; + +// rawexit — Linux exit(2) syscall (nr=60). Used only by `fatal`. +fn rawexit(code: i32) void = { + rtsyscall1(60i64, code: i64); +}; + +// i64dec_buf — scratch buffer for [[i64dec]] below. Module-level +// because Hare's `strconv::i64tos` is a static-buffer view and we +// match that shape here. 21 bytes is enough for `-9223372036854775808` +// (20 digits + sign). +let i64dec_buf: [21]u8; + +// i64dec — render `v` as a base-10 ASCII string into [[i64dec_buf]], +// returning a borrowed view. Inlined here rather than calling +// [[strconv.i64tos]] because `use strconv;` would transitively pull +// `use os;`, whose exported read/write/close clash with +// io.read/write/close under the driver's flat-scope concat. Same +// algorithmic shape as strconv's version, narrowed to base-10. +fn i64dec(v: i64) str = { + let neg: bool = false; + let n: i64 = v; + if (n < 0) { neg = true; n = -n; }; + let tmp: [20]u8; + let i: i32 = 0; + if (n == 0) { tmp[0] = 48u8; i = 1; }; + for (n > 0) { + let d: i64 = n % 10i64; + tmp[i] = (d + 48i64): u8; + n = n / 10i64; + i += 1; + }; + let out: i32 = 0; + if (neg) { i64dec_buf[out] = 45u8; out += 1; }; // '-' + for (i > 0) { + i -= 1; + i64dec_buf[out] = tmp[i]; + out += 1; + }; + let r: str; + r.ptr = &i64dec_buf[0]; + r.len = out; + return r; +}; // formattable — tagged union of types fmt can render. Mirrors Hare's // `fmt::formattable = (...types::numeric | uintptr | str | rune | @@ -13,41 +86,49 @@ use strings; // has codegen for. Slot size is 24B (8 tag + 16 str payload). export type formattable = (i64 | str | bool | rune); -// fprint — write the formatted form of each `args` element to `fd`, +// ---- fd sinks -------------------------------------------------------- + +// fdprint — write the formatted form of each `args` element to `fd`, // separated by spaces. Returns total bytes written or the first -// negative os.write result. Hare's separator-by-space matches. -export fn fprint(fd: i32, args: formattable...) i64 = { +// negative [[os.write]] result (Linux's `-errno`). Hare's separator- +// by-space matches. +// +// Renamed from `fprint` once lib/fmt grew an io.stream sink (`fprint` +// now points at that). This entry stays under `fd`-prefix until lib/io +// can express the full Hare `io::handle = (file | int)` union, at +// which point both halves graduate in one go. +export fn fdprint(fd: i32, args: formattable...) i64 = { let total: i64 = 0; let i: i32 = 0; for (i < args.len) { if (i > 0) { - let r: i64 = os.write(fd, " ".ptr, 1u64); + let r: i64 = rawwrite(fd, " ".ptr, 1u64); if (r < 0) { return r; }; total += r; }; match (args[i]) { case let n: i64 => { - let s: str = strconv.i64tos(n, strconv.base.DEC); - let r: i64 = os.write(fd, s.ptr, s.len: u64); + let s: str = i64dec(n); + let r: i64 = rawwrite(fd, s.ptr, s.len: u64); if (r < 0) { return r; }; total += r; }; case let s: str => { - let r: i64 = os.write(fd, s.ptr, s.len: u64); + let r: i64 = rawwrite(fd, s.ptr, s.len: u64); if (r < 0) { return r; }; total += r; }; case let b: bool => { let s: str = "false"; if (b) { s = "true"; }; - let r: i64 = os.write(fd, s.ptr, s.len: u64); + let r: i64 = rawwrite(fd, s.ptr, s.len: u64); if (r < 0) { return r; }; total += r; }; case let r: rune => { let buf: [4]u8; buf[0] = r: u8; - let n: i64 = os.write(fd, &buf[0], 1u64); + let n: i64 = rawwrite(fd, &buf[0], 1u64); if (n < 0) { return n; }; total += n; }; @@ -57,40 +138,134 @@ export fn fprint(fd: i32, args: formattable...) i64 = { return total; }; -// fprintln — fprint plus a trailing newline. -export fn fprintln(fd: i32, args: formattable...) i64 = { - let n: i64 = fprint(fd, args...); +// fdprintln — fdprint plus a trailing newline. +export fn fdprintln(fd: i32, args: formattable...) i64 = { + let n: i64 = fdprint(fd, args...); if (n < 0) { return n; }; - let m: i64 = os.write(fd, "\n".ptr, 1u64); + let m: i64 = rawwrite(fd, "\n".ptr, 1u64); if (m < 0) { return m; }; return n + m; }; -// print / println — fprint / fprintln on stdout. Direct counterparts +// ---- stream sinks ---------------------------------------------------- + +// putbytes — internal helper that wraps (`*u8`, `i32`) into a `[]u8` +// slice and feeds it to [[io.write]]. Not exported: callers compose +// the same `(ptr, len)` triple as their underlying source (str view, +// strconv buffer, stack rune buffer), and the slice is invariant +// in shape across the formattable arms. +fn putbytes(s: *io.stream, p: *u8, n: i32) (i32 | io.closed) = { + let v: []u8; + v.ptr = p; + v.len = n; + return io.write(s, v); +}; + +// fprint — write the formatted form of each `args` element to `s`, +// separated by spaces. Returns total bytes written, or `io.closed` +// if the sink rejects mid-write. Mirrors Hare's `fmt::fprint` shape +// for an `io::handle` sink, modulo ww's i32-sized byte counters and +// the narrower `io.closed`-only error set on lib/io's stream vtable. +// +// A short write (sink accepts fewer bytes than asked) is reported by +// the returned count, not as an error — matches [[io.write]]'s contract +// per [[memio.fixedwrite]]. Callers that need write-all semantics layer +// it on top, the same way they do over raw [[io.write]]. +export fn fprint(s: *io.stream, args: formattable...) (i32 | io.closed) = { + let total: i32 = 0; + let i: i32 = 0; + for (i < args.len) { + if (i > 0) { + let r: (i32 | io.closed) = putbytes(s, " ".ptr, 1); + match (r) { + case let n: i32 => { total += n; }; + case io.closed => { let c: io.closed; return c; }; + }; + }; + match (args[i]) { + case let n: i64 => { + let view: str = i64dec(n); + let r: (i32 | io.closed) = putbytes(s, view.ptr, view.len); + match (r) { + case let m: i32 => { total += m; }; + case io.closed => { let c: io.closed; return c; }; + }; + }; + case let v: str => { + let r: (i32 | io.closed) = putbytes(s, v.ptr, v.len); + match (r) { + case let m: i32 => { total += m; }; + case io.closed => { let c: io.closed; return c; }; + }; + }; + case let b: bool => { + let v: str = "false"; + if (b) { v = "true"; }; + let r: (i32 | io.closed) = putbytes(s, v.ptr, v.len); + match (r) { + case let m: i32 => { total += m; }; + case io.closed => { let c: io.closed; return c; }; + }; + }; + case let r: rune => { + let buf: [4]u8; + buf[0] = r: u8; + let rs: (i32 | io.closed) = io.write(s, buf[0:1]); + match (rs) { + case let m: i32 => { total += m; }; + case io.closed => { let c: io.closed; return c; }; + }; + }; + }; + i += 1; + }; + return total; +}; + +// fprintln — fprint plus a trailing newline. Mirrors Hare's +// `fmt::fprintln(io::handle, args...)`. +export fn fprintln(s: *io.stream, args: formattable...) (i32 | io.closed) = { + let total: i32 = 0; + let r1: (i32 | io.closed) = fprint(s, args...); + match (r1) { + case let n: i32 => { total = n; }; + case io.closed => { let c: io.closed; return c; }; + }; + let r2: (i32 | io.closed) = putbytes(s, "\n".ptr, 1); + match (r2) { + case let m: i32 => { total += m; }; + case io.closed => { let c: io.closed; return c; }; + }; + return total; +}; + +// ---- process-stdio wrappers ----------------------------------------- + +// print / println — fdprint / fdprintln on stdout. Direct counterparts // of Hare's fmt::print / fmt::println. export fn print(args: formattable...) i64 = { - return fprint(1, args...); + return fdprint(1, args...); }; export fn println(args: formattable...) i64 = { - return fprintln(1, args...); + return fdprintln(1, args...); }; -// errorln — fprintln on stderr. Hare's `fmt::error` (without -ln) is +// errorln — fdprintln on stderr. Hare's `fmt::error` (without -ln) is // skipped here: the bare `error` name collides with strconv's // `type error = !(invalid | overflow)` under the driver's flat // concatenation namespace. Callers wanting the no-newline form use -// `fprint(2, args...)` directly. +// `fdprint(2, args...)` directly. export fn errorln(args: formattable...) i64 = { - return fprintln(2, args...); + return fdprintln(2, args...); }; // fatal — errorln then exit(255). `never` return marks the bottom // type so flow-control checks treat callers as terminated. The -// fprintln result is dropped as an expression statement (Hare's +// fdprintln result is dropped as an expression statement (Hare's // `_ = fprintln(...)` wouldn't add safety here — process exit // follows immediately). export fn fatal(args: formattable...) never = { - fprintln(2, args...); - os.exit(255); + fdprintln(2, args...); + rawexit(255); }; diff --git a/lib/fmt/fmttest.ww b/lib/fmt/fmttest.ww new file mode 100644 index 00000000..08ccfd88 --- /dev/null +++ b/lib/fmt/fmttest.ww @@ -0,0 +1,213 @@ +// fmttest — exercises lib/fmt's stream sinks (fprint / fprintln). +// Run with `out/bin/ww run lib/fmt/fmttest.ww`. +// +// Each @test writes through a memio.stream and compares the resulting +// bytes against an inline `want` literal. Bodies are short enough that +// the parallel-array idiom used by memiotest doesn't apply — every row +// here threads a different variadic argument-pack into fprint, and the +// variadic shape can't be table-driven within a single fn body. + +use fmt; +use io; +use memio; + +// Direct exit(2) binding rather than `use os;` — os exports +// read/write/close, which collide with io.read/write/close under +// the driver's flat-scope concat. +@symbol("rt_syscall") fn syscall1ww(num: i64, a: i64) i64; +fn doexit(code: i32) void = { + syscall1ww(60i64, code: i64); +}; + +// signalled — bumped before each scenario so a failing exit code +// pinpoints the offending case. +let signalled: i32 = 0; + +fn fail() void = { doexit(signalled + 10); }; + +fn streq(a: str, b: str) bool = { + if (a.len != b.len) { return false; }; + let i: i32 = 0; + for (i < a.len) { + if (a[i] != b[i]) { return false; }; + i += 1; + }; + return true; +}; + +// ---- A write-always-closed stream for the io.closed surfacing test ---- + +fn closedread(s: *io.stream, buf: []u8) (i32 | io.eof | io.closed) = { + let e: io.closed; return e; +}; +fn closedwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = { + let e: io.closed; return e; +}; +fn closedclose(s: *io.stream) (void | io.closed) = { return; }; + +fn closedstream(s: *io.stream) void = { + s.ctx = nil; + s.read = closedread; + s.write = closedwrite; + s.close = closedclose; +}; + +// ---- fprint: bare str -------------------------------------------------- + +@test fn fprintbarestr() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + + let r: (i32 | io.closed) = fmt.fprint(&s, "hello"); + match (r) { + case let n: i32 => { if (n != 5) { fail(); }; }; + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "hello")) { fail(); }; + + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +// ---- fprint: int + str mix, space-separated ---------------------------- + +@test fn fprintintstr() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + + let r: (i32 | io.closed) = fmt.fprint(&s, 42i64, "x"); + match (r) { + case let n: i32 => { if (n != 4) { fail(); }; }; // "42 x" + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "42 x")) { fail(); }; + + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +// ---- fprint: bool + rune renders as "true A" -------------------------- + +@test fn fprintboolrune() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + + let r: (i32 | io.closed) = fmt.fprint(&s, true, 'A': rune); + match (r) { + case let n: i32 => { if (n != 6) { fail(); }; }; // "true A" + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "true A")) { fail(); }; + + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +// ---- fprint: zero args returns 0, no bytes written -------------------- + +@test fn fprintempty() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + + let r: (i32 | io.closed) = fmt.fprint(&s); + match (r) { + case let n: i32 => { if (n != 0) { fail(); }; }; + case io.closed => fail(); + }; + if (memio.string(&mem).len != 0) { fail(); }; + + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +// ---- fprintln: multi-arg, trailing '\n' -------------------------------- + +@test fn fprintlnmulti() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + + let r: (i32 | io.closed) = fmt.fprintln(&s, "a", 1i64, false); + match (r) { + case let n: i32 => { if (n != 10) { fail(); }; }; // "a 1 false\n" + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "a 1 false\n")) { fail(); }; + + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +// ---- fprintln: zero args writes just the newline ---------------------- + +@test fn fprintlnempty() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + + let r: (i32 | io.closed) = fmt.fprintln(&s); + match (r) { + case let n: i32 => { if (n != 1) { fail(); }; }; + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "\n")) { fail(); }; + + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +// ---- fprint over a fixed stream: short writes return partial count ---- +// memio.fixedwrite caps each call at the remaining buffer space and +// never closes the stream, so fprint sees a short i32 result, not +// io.closed. Verifies the inner-loop arithmetic adds the actual byte +// count rather than the requested length. + +@test fn fprintfixedshort() void = { + let buf: [3]u8; + let mem: memio.state; + let s: io.stream; + memio.fixed(&mem, &s, buf[0:3]); + + let r: (i32 | io.closed) = fmt.fprint(&s, "hello"); + match (r) { + case let n: i32 => { if (n != 3) { fail(); }; }; + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "hel")) { fail(); }; + + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +// ---- fprint over a closed stream surfaces io.closed ------------------- +// Exercises the early-return arm in fprint's inner match — distinct from +// the short-write path above, which keeps returning i32 from a partial +// accept. Single arm is enough: every formattable case routes errors +// through the same putbytes / io.write wiring. + +@test fn fprintclosed() void = { + let s: io.stream; + closedstream(&s); + + let r: (i32 | io.closed) = fmt.fprint(&s, "x"); + match (r) { + case let n: i32 => fail(); + case io.closed => {}; + }; +}; + +export fn main() i32 = { + signalled = 1; fprintbarestr(); + signalled = 2; fprintintstr(); + signalled = 3; fprintboolrune(); + signalled = 4; fprintempty(); + signalled = 5; fprintlnmulti(); + signalled = 6; fprintlnempty(); + signalled = 7; fprintfixedshort(); + signalled = 8; fprintclosed(); + return 0; +}; diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index 0e97afcb..e16918e1 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -1639,7 +1639,7 @@ static const struct row rows[] = { "fn main() i32 = { return sumtag(1i64, \"hi\", true): i32; };", 42 }, /* Variadic forwarding: `wrap(args...)` passes the local slice * directly to `sum`, no re-gather. Mirrors Hare's wrapper shape - * (`fn println(args: formattable...) = fprintln(os.stdout, args...)`). */ + * (`fn println(args: formattable...) = fdprintln(os.stdout, args...)`). */ { "fn sum(args: i64...) i64 = {\n" " let s: i64 = 0i64;\n" " let i: i32 = 0;\n" @@ -1654,8 +1654,8 @@ static const struct row rows[] = { "};", 42 }, /* lib/fmt user-side: `fmt.println(args: formattable...)` gathers * mixed-type args at the call site. End-to-end exercises the - * lib/fmt graduation: the wrapper-chain `println → fprintln → - * fprint` is itself variadic-forwarding, so this validates both + * lib/fmt graduation: the wrapper-chain `println → fdprintln → + * fdprint` is itself variadic-forwarding, so this validates both * gather (at main) and `args...` forward (inside lib/fmt). The * exit code is bytes printed (`hello 7\n` = 8). */ { "use fmt;\n" diff --git a/test/wcc/900_stdlib.c b/test/wcc/900_stdlib.c index c343ab01..0fbfd6b4 100644 --- a/test/wcc/900_stdlib.c +++ b/test/wcc/900_stdlib.c @@ -33,13 +33,13 @@ static const char *modules[] = { "lib/math/random/random.ww", "lib/time/time.ww", "lib/c/libc/libc.ww", - /* lib/bufio/bufio.ww moved off this list: graduates to the - * io.stream-based scanner, which carries cross-module type - * refs (*io.stream, io.eof, io.closed) that only resolve once - * the driver concatenates `use`d modules. Coverage lives at - * lib/bufio/bufiotest.ww + the bufio.scanline e2e row in - * test/wcc/700_e2e.c. */ - "lib/fmt/fmt.ww", + /* lib/bufio/bufio.ww and lib/fmt/fmt.ww moved off this list: + * both graduated to io.stream-based sinks, which carry cross- + * module type refs (*io.stream, io.closed, io.eof) that only + * resolve once the driver concatenates `use`d modules. Coverage + * lives at lib/bufio/bufiotest.ww + lib/fmt/fmttest.ww (wired + * at 998_bufio_run.c and 970_fmt_run.c), plus the bufio.scanline + * and fmt.println e2e rows in test/wcc/700_e2e.c. */ "lib/net/net.ww", NULL }; diff --git a/test/wcc/970_fmt_run.c b/test/wcc/970_fmt_run.c new file mode 100644 index 00000000..cb2b473d --- /dev/null +++ b/test/wcc/970_fmt_run.c @@ -0,0 +1,52 @@ +/* + * 970_fmt_run — execute the lib/fmt @test fixture under the C-side + * `ww run` driver and assert exit 0. + * + * fmt sits below io conceptually (sinks formatted bytes into an + * io.stream); slotting at 970 keeps it ahead of the 980-989 + * stdlib-run sub-band that depends on it. fmttest.ww carries its + * own `export fn main()` that drives the @test fns and signals + * which case failed via the exit code, so this file is a thin + * wrapper — no @test scanning, no synthetic main generation. + */ +#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; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + + const char *src = "lib/fmt/fmttest.ww"; + char path[1024], cmd[2048]; + snprintf(path, sizeof path, "%s/%s", cwd, src); + snprintf(cmd, sizeof cmd, "%s/ww run %s", bin, path); + int rc = runwait(cmd); + if (rc != 0) { + fprintf(stderr, "fmt_run FAIL: %s exited %d\n", src, rc); + return 1; + } + printf("fmt_run: %s ok\n", src); + return 0; +}