lib+test: fmt rename fprint→fdprint; add io.stream fprint sink

This commit is contained in:
2026-05-15 11:34:47 +09:00
parent e6045f3ada
commit 0ef94eef04
7 changed files with 485 additions and 40 deletions

View File

@@ -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 $@ $<

View File

@@ -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

View File

@@ -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);
};

213
lib/fmt/fmttest.ww Normal file
View File

@@ -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;
};

View File

@@ -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"

View File

@@ -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
};

52
test/wcc/970_fmt_run.c Normal file
View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <unistd.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;
}
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;
}