diff --git a/Makefile b/Makefile index 3c7f1b52..ef0d5821 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/strconv/strconv.ww lib/strings/strings.ww lib/ascii/ascii.ww lib/fmt/fmt.ww \ + lib/os/os.ww lib/strconv/strconv.ww lib/strings/strings.ww lib/ascii/ascii.ww lib/io/io.ww lib/memio/memio.ww lib/fmt/fmt.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) cd $(BIN) && ./ww build \ @@ -134,7 +134,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/strconv/strconv.ww lib/strings/strings.ww lib/ascii/ascii.ww lib/fmt/fmt.ww \ + lib/os/os.ww lib/strconv/strconv.ww lib/strings/strings.ww lib/ascii/ascii.ww lib/io/io.ww lib/memio/memio.ww lib/fmt/fmt.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) cd $(BIN) && ./ww build \ diff --git a/lib/CLAUDE.md b/lib/CLAUDE.md index 3a3b8194..0fcb5d8e 100644 --- a/lib/CLAUDE.md +++ b/lib/CLAUDE.md @@ -32,8 +32,10 @@ Signatures mirror Hare too, modulo: 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 - `printf`-family. + *param* decl. `lib/fmt` ships both the print family (`print` / + `println` / `fprint` / `fprintln`) and the {n}-placeholder family + (`printf` / `fprintf` / `bsprintf` / `fatalf`); the `%`-parametric + modifier form is parsed but its `*mods` arg slot aborts on dispatch. - `(T | U)` sum-typed parameters dispatch via `match` inside the callee. `strings.byteindex(haystack: str, needle: (str | rune))`, diff --git a/lib/fmt/fmt.ww b/lib/fmt/fmt.ww index 6e0a6178..a3f73b31 100644 --- a/lib/fmt/fmt.ww +++ b/lib/fmt/fmt.ww @@ -15,12 +15,19 @@ // graduate "in one go" (lib/CLAUDE.md) and // the fd-suffixed names disappear. // +// The printf-family ({n}-placeholder parser) is provided alongside — +// see the "{n}-placeholder parser" section below. Same sink split: +// `fprintf` / `fprintfln` on streams; `fdprintf` / `fdprintfln` on +// raw fds; the process-stdio wrappers route through fd 1 / fd 2. +// // Call sites take Hare's variadic shape: `fmt.println(42, "hi", true)` // gathers the args into a `[]formattable` slice; wrappers forward // via `args...`. use io; +use memio; use os; +use strconv; // i64dec_buf — scratch buffer for [[i64dec]] below. Module-level // because Hare's `strconv::i64tos` is a static-buffer view and we @@ -29,11 +36,9 @@ use os; 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. +// returning a borrowed view. Preserved as a base-10 specialisation +// of strconv.i64tos for the pre-printf-family print path; the +// printf-family path below dispatches through strconv directly. fn i64dec(v: i64) str = { let neg: bool = false; let n: i64 = v; @@ -249,3 +254,502 @@ export fn fatal(args: formattable...) never = { fdprintln(2, args...); os.exit(255); }; + +// ---- {n}-placeholder parser ----------------------------------------- +// +// Mirrors ref/hare/fmt/{iter,print,wrappers}.ha. Format sequences: +// +// {} implicit-positional next arg +// {N} explicit-positional arg N +// {:mods} inline modifiers on next arg +// {N:mods} inline modifiers on arg N +// {{ }} literal '{' / '}' +// +// Modifier set (subset of iter.ha:129 scan_modifiers): +// +// - align LEFT +// = align CENTER +// _ pad rune (default space when : is seen) +// ' ' sign SPACE +// + sign PLUS +// x X o b base +// width (first char 1..9) +// . precision +// +// Not v1: '%' parametric form ({0%1}); float modifiers (e/f/g/F…); +// void / null arms (not in formattable). `asprintf` is deferred until +// lib/os exports alloc (task #14). Invalid format aborts via os.exit +// — Hare uses `abort()`; same effect. + +// neg, alignment — mirror iter.ha:19 / iter.ha:26. +export type neg = enum i32 { NONE = 0, SPACE = 1, PLUS = 2 }; +export type alignment = enum i32 { RIGHT = 0, CENTER = 1, LEFT = 2 }; + +// mods — per-placeholder modifier set. Mirrors iter.ha:33 minus +// `ffmt` / `fflags` — formattable has no float arm, so the float- +// formatter knobs would be dead. Graduate when fmt.formattable does. +export type mods = struct { + alignment: alignment, + pad: rune, + neg: neg, + width: i32, + prec: i32, + base: strconv.base, +}; + +// field — variadic arg slot. `(...formattable | *mods)` per iter.ha:11. +// The *mods arm is the parametric-modifier form ({0%1}); accepted by +// the type checker today, but the '%' parser branch is deferred — +// passing a *mods that the parser reaches will abort the program. +export type field = (...formattable | *mods); + +// fmtabort — invalid format string. Matches Hare's abort() (iter.ha:71) +// in effect; exit code 255 matches [[fatal]]. +fn fmtabort() never = { os.exit(255); }; + +// modsinit — reset `m` to the all-zero default. pad stays 0 here; +// scan_modifiers (Hare iter.ha:130) defaults it to ' ' only when ':' +// is seen — bare `{}` never reaches the padding loop (width=0). +fn modsinit(m: *mods) void = { + m.alignment = alignment.RIGHT; + m.pad = 0: rune; + m.neg = neg.NONE; + m.width = 0; + m.prec = 0; + m.base = strconv.base.DEFAULT; +}; + +// scandigits — consume a digit run at *pos in `s`, advancing *pos +// past it; return the value. Aborts on no digits or overflow past +// i32. Mirrors iter.ha:173 scan_sz. +fn scandigits(s: str, pos: *i32) i32 = { + let v: i32 = 0; + let any: bool = false; + for (*pos < s.len) { + let c: u8 = s[*pos]; + if (c < 48u8 || c > 57u8) { + if (!any) { fmtabort(); }; + return v; + }; + any = true; + if (v > 214748364) { fmtabort(); }; + v = v * 10 + (c - 48u8): i32; + *pos += 1; + }; + if (!any) { fmtabort(); }; + return v; +}; + +// scanmods — parse the `:`-modifier run starting at *pos. *pos is on +// the first byte after `:`. Returns with *pos on the closing `}`. +// Mirrors iter.ha:129 scan_modifiers. +fn scanmods(s: str, pos: *i32, m: *mods) void = { + m.pad = 32: rune; // ' ' — Hare iter.ha:130 + for (*pos < s.len) { + let c: u8 = s[*pos]; + if (c == 125u8) { return; }; // '}' + *pos += 1; + if (c == 45u8) { m.alignment = alignment.LEFT; } // '-' + else if (c == 61u8) { m.alignment = alignment.CENTER; } // '=' + else if (c == 95u8) { // '_' + if (*pos >= s.len) { fmtabort(); }; + m.pad = s[*pos]: rune; + *pos += 1; + } + else if (c == 32u8) { m.neg = neg.SPACE; } // ' ' + else if (c == 43u8) { m.neg = neg.PLUS; } // '+' + else if (c == 120u8) { m.base = strconv.base.HEX_LOWER; } // 'x' + else if (c == 88u8) { m.base = strconv.base.HEX_UPPER; } // 'X' + else if (c == 111u8) { m.base = strconv.base.OCT; } // 'o' + else if (c == 98u8) { m.base = strconv.base.BIN; } // 'b' + else if (c == 46u8) { // '.' + m.prec = scandigits(s, pos); + } + else if (c >= 49u8 && c <= 57u8) { // '1'..'9' + *pos -= 1; + m.width = scandigits(s, pos); + } + else { fmtabort(); }; + }; + fmtabort(); // ran off end without '}' +}; + +// digitsu64 — count base-b digits of `v`. Mirrors the explicit-loop +// shape strconv.u64tos uses internally; avoids materialising the +// digit string twice in the formatone width path. +fn digitsu64(v: u64, b: i64) i32 = { + if (v == 0u64) { return 1; }; + let n: i32 = 0; + let nb: u64 = b: u64; + let x: u64 = v; + for (x > 0u64) { + n += 1; + x = x / nb; + }; + return n; +}; + +// basenum — copy of strconv's internal basenum, lifted here because +// strconv keeps it private. Same shape as strconv.ww:40. +fn basenum(b: strconv.base) i64 = { + if (b == strconv.base.BIN) { return 2; }; + if (b == strconv.base.OCT) { return 8; }; + if (b == strconv.base.HEX) { return 16; }; + if (b == strconv.base.HEX_UPPER) { return 16; }; + if (b == strconv.base.HEX_LOWER) { return 16; }; + return 10; +}; + +// signof — sign byte for `v` under `m.neg`, or 0u8 if none. +fn signof(neg_flag: bool, m: *mods) u8 = { + if (neg_flag) { return 45u8; }; // '-' + if (m.neg == neg.PLUS) { return 43u8; }; // '+' + if (m.neg == neg.SPACE) { return 32u8; }; // ' ' + return 0u8; +}; + +// rawleni64 — bytes the raw render of `v` under `m` would emit. Used +// for width-alignment without rendering twice. +fn rawleni64(v: i64, m: *mods) i32 = { + let neg_flag: bool = v < 0; + let u: u64 = v: u64; + if (neg_flag) { u = (-v): u64; }; + let signlen: i32 = 0; + if (signof(neg_flag, m) != 0u8) { signlen = 1; }; + let dlen: i32 = digitsu64(u, basenum(m.base)); + let inner: i32 = dlen; + if (m.prec > signlen + dlen) { inner = m.prec - signlen; }; + return signlen + inner; +}; + +// rawlenstr — bytes the raw render of `s` would emit (after `prec` +// truncation, per Hare print.ha:86). +fn rawlenstr(s: str, m: *mods) i32 = { + if (m.prec > 0 && m.prec < s.len) { return m.prec; }; + return s.len; +}; + +// formatraw — write the bare value (no width padding) to `s`. +// Mirrors print.ha:76 format_raw, narrowed to the formattable arms +// fmt.formattable carries today. +fn formatraw(out: *io.stream, arg: formattable, m: *mods) (i32 | io.closed) = { + match (arg) { + case let v: i64 => { + let neg_flag: bool = v < 0; + let u: u64 = v: u64; + if (neg_flag) { u = (-v): u64; }; + let sb: u8 = signof(neg_flag, m); + let total: i32 = 0; + if (sb != 0u8) { + let buf: [1]u8; + buf[0] = sb; + let r: (i32 | io.closed) = putbytes(out, &buf[0], 1); + match (r) { + case let n: i32 => { total += n; }; + case io.closed => { let c: io.closed; return c; }; + }; + }; + let dlen: i32 = digitsu64(u, basenum(m.base)); + let signlen: i32 = 0; + if (sb != 0u8) { signlen = 1; }; + let pad0: i32 = 0; + if (m.prec > signlen + dlen) { pad0 = m.prec - signlen - dlen; }; + let pi: i32 = 0; + for (pi < pad0) { + let buf: [1]u8; + buf[0] = 48u8; // '0' + let r: (i32 | io.closed) = putbytes(out, &buf[0], 1); + match (r) { + case let n: i32 => { total += n; }; + case io.closed => { let c: io.closed; return c; }; + }; + pi += 1; + }; + let view: str = strconv.u64tos(u, m.base); + let r: (i32 | io.closed) = putbytes(out, view.ptr, view.len); + match (r) { + case let n: i32 => { total += n; }; + case io.closed => { let c: io.closed; return c; }; + }; + return total; + }; + case let v: str => { + let n: i32 = rawlenstr(v, m); + return putbytes(out, v.ptr, n); + }; + case let b: bool => { + let v: str = "false"; + if (b) { v = "true"; }; + return putbytes(out, v.ptr, v.len); + }; + case let r: rune => { + let buf: [4]u8; + buf[0] = r: u8; + return putbytes(out, &buf[0], 1); + }; + }; + let z: io.closed; return z; // unreachable — match is exhaustive +}; + +// rawlen — bytes formatraw would emit for `arg` under `m`. Used by +// formatone's width-alignment path (Hare's print.ha:53 calls +// format_raw with io::empty for the same purpose). +fn rawlen(arg: formattable, m: *mods) i32 = { + match (arg) { + case let v: i64 => return rawleni64(v, m); + case let v: str => return rawlenstr(v, m); + case let b: bool => { if (b) { return 4; }; return 5; }; + case let r: rune => return 1; + }; + return 0; // unreachable — match is exhaustive +}; + +// formatone — render `arg` to `s` with `m`'s width / alignment / pad +// applied. Mirrors print.ha:45 format. +fn formatone(out: *io.stream, arg: formattable, m: *mods) (i32 | io.closed) = { + let start: i32 = 0; + if (m.width > 0 && m.alignment != alignment.LEFT) { + let raw: i32 = rawlen(arg, m); + let pad: i32 = 0; + if (raw < m.width) { pad = m.width - raw; }; + if (m.alignment == alignment.CENTER) { start = (pad + 1) / 2; } + else { start = pad; }; + }; + let total: i32 = 0; + let i: i32 = 0; + let padb: [1]u8; + padb[0] = m.pad: u8; + for (i < start) { + let r: (i32 | io.closed) = putbytes(out, &padb[0], 1); + match (r) { + case let n: i32 => { total += n; }; + case io.closed => { let c: io.closed; return c; }; + }; + i += 1; + }; + let r1: (i32 | io.closed) = formatraw(out, arg, m); + match (r1) { + case let n: i32 => { total += n; }; + case io.closed => { let c: io.closed; return c; }; + }; + // Tail-pad: drive with a counter (mirrors the start-pad shape + // above). ww's memio.fixed reports a full buffer as a 0-byte + // partial write, not io.closed (unlike Hare's errors::overflow + // + `?` shape at print.ha:69); looping on `total < m.width` + // would spin on bsprintf when the sink runs out of room. + let need: i32 = 0; + if (m.width > total) { need = m.width - total; }; + let j: i32 = 0; + for (j < need) { + let r: (i32 | io.closed) = putbytes(out, &padb[0], 1); + match (r) { + case let n: i32 => { total += n; }; + case io.closed => { let c: io.closed; return c; }; + }; + j += 1; + }; + return total; +}; + +// formatfield — dispatch on the `field` slot directly. A +// `field→formattable` widen helper called from fprintf's for-loop +// would trip task #18 (silent miscompile of 24B return-by-value in +// for-loop context); inline-per-arm sidesteps it and is independently +// the cleaner shape. +fn formatfield(out: *io.stream, f: field, m: *mods) (i32 | io.closed) = { + match (f) { + case let v: i64 => { + let a: formattable = v; + return formatone(out, a, m); + }; + case let v: str => { + let a: formattable = v; + return formatone(out, a, m); + }; + case let b: bool => { + let a: formattable = b; + return formatone(out, a, m); + }; + case let r: rune => { + let a: formattable = r; + return formatone(out, a, m); + }; + case let p: *mods => { fmtabort(); let c: io.closed; return c; }; + }; +}; + +// fprintf — Hare's primary printf-family surface. Mirrors print.ha:26. +// Returns total bytes written or io.closed on the first sink failure. +export fn fprintf(s: *io.stream, fmt: str, args: field...) (i32 | io.closed) = { + let total: i32 = 0; + let i: i32 = 0; + let nextimpl: i32 = 0; + let checkunused: bool = true; + for (i < fmt.len) { + let c: u8 = fmt[i]; + if (c == 123u8) { // '{' + i += 1; + if (i >= fmt.len) { fmtabort(); }; + if (fmt[i] == 123u8) { // '{{' literal + let r: (i32 | io.closed) = putbytes(s, fmt.ptr + i: u64, 1); + match (r) { + case let n: i32 => { total += n; }; + case io.closed => { let cl: io.closed; return cl; }; + }; + i += 1; + } else { + let idx: i32 = 0; + let d: u8 = fmt[i]; + if (d >= 48u8 && d <= 57u8) { + checkunused = false; + idx = scandigits(fmt, &i); + } else { + idx = nextimpl; + nextimpl += 1; + }; + let m: mods; + modsinit(&m); + if (i < fmt.len && fmt[i] == 58u8) { // ':' + i += 1; + scanmods(fmt, &i, &m); + }; + if (i >= fmt.len || fmt[i] != 125u8) { fmtabort(); }; + i += 1; + if (idx >= args.len) { fmtabort(); }; + let r: (i32 | io.closed) = formatfield(s, args[idx], &m); + match (r) { + case let n: i32 => { total += n; }; + case io.closed => { let cl: io.closed; return cl; }; + }; + }; + } else if (c == 125u8) { // '}' + i += 1; + if (i >= fmt.len || fmt[i] != 125u8) { fmtabort(); }; + let r: (i32 | io.closed) = putbytes(s, fmt.ptr + i: u64, 1); + match (r) { + case let n: i32 => { total += n; }; + case io.closed => { let cl: io.closed; return cl; }; + }; + i += 1; + } else { + let r: (i32 | io.closed) = putbytes(s, fmt.ptr + i: u64, 1); + match (r) { + case let n: i32 => { total += n; }; + case io.closed => { let cl: io.closed; return cl; }; + }; + i += 1; + }; + }; + if (checkunused && nextimpl != args.len) { fmtabort(); }; + return total; +}; + +// fprintfln — fprintf plus a trailing newline. Mirrors wrappers.ha:69. +export fn fprintfln(s: *io.stream, fmt: str, args: field...) (i32 | io.closed) = { + let total: i32 = 0; + let r1: (i32 | io.closed) = fprintf(s, fmt, 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; +}; + +// ---- fd-sink printf twins ------------------------------------------- +// +// Hare's wrappers.ha:10 routes printf through os::stdout (an +// io::handle). ww has no fd-backed stream yet, so the fd path here +// wraps the fd in a stack-resident io.stream and dispatches through +// fprintf. -errno from [[os.write]] collapses to io.closed inside the +// wrapper; the public i64 return signals success-bytes vs -1 failure. +// Symmetric with the fdprint / fdprintln pair above. + +fn fdsinkread(s: *io.stream, buf: []u8) (i32 | io.eof | io.closed) = { + let e: io.eof; return e; +}; +fn fdsinkwrite(s: *io.stream, buf: []u8) (i32 | io.closed) = { + let fdp: *i32 = s.ctx: *i32; + let r: i64 = os.write(*fdp, buf.ptr, buf.len: u64); + if (r < 0) { let c: io.closed; return c; }; + return r: i32; +}; +fn fdsinkclose(s: *io.stream) (void | io.closed) = { return; }; + +fn fdwrap(fd: *i32, s: *io.stream) void = { + s.ctx = fd: *void; + s.read = fdsinkread; + s.write = fdsinkwrite; + s.close = fdsinkclose; +}; + +// fdprintf / fdprintfln — fprintf / fprintfln over a raw fd. Failure +// (any underlying os.write returning -errno) collapses to -1; success +// returns total bytes written. See header comment. +export fn fdprintf(fd: i32, fmt: str, args: field...) i64 = { + let f: i32 = fd; + let s: io.stream; + fdwrap(&f, &s); + let r: (i32 | io.closed) = fprintf(&s, fmt, args...); + match (r) { + case let n: i32 => return n: i64; + case io.closed => return -1i64; + }; +}; + +export fn fdprintfln(fd: i32, fmt: str, args: field...) i64 = { + let f: i32 = fd; + let s: io.stream; + fdwrap(&f, &s); + let r: (i32 | io.closed) = fprintfln(&s, fmt, args...); + match (r) { + case let n: i32 => return n: i64; + case io.closed => return -1i64; + }; +}; + +// ---- process-stdio printf wrappers ---------------------------------- +// +// printf / printfln — wrappers.ha:10 / :15 on os.stdout (fd 1). +// errorfln — wrappers.ha:24 on stderr. Hare ships `errorf` too, but +// the bare `error` name collides with strconv.error under the +// driver's flat-scope concat (same divergence the existing [[errorln]] +// notes at line 234); ship the -ln form only and document. +// fatalf — wrappers.ha:54 errorfln-then-exit(255). + +export fn printf(fmt: str, args: field...) i64 = { + return fdprintf(1, fmt, args...); +}; + +export fn printfln(fmt: str, args: field...) i64 = { + return fdprintfln(1, fmt, args...); +}; + +export fn errorfln(fmt: str, args: field...) i64 = { + return fdprintfln(2, fmt, args...); +}; + +export fn fatalf(fmt: str, args: field...) never = { + fdprintfln(2, fmt, args...); + os.exit(255); +}; + +// bsprintf — render into a caller-supplied buffer through memio.fixed; +// return the str view of the bytes actually written. Mirrors +// wrappers.ha:42. io.closed propagates as-is (Hare's `nomem` arm). On +// short writes the returned str is just the prefix that fit — Hare's +// memio.fixed contract is the same. +export fn bsprintf(buf: []u8, fmt: str, args: field...) (str | io.closed) = { + let m: memio.state; + let s: io.stream; + memio.fixed(&m, &s, buf); + let r: (i32 | io.closed) = fprintf(&s, fmt, args...); + match (r) { + case let n: i32 => { return memio.string(&m); }; + case io.closed => { let c: io.closed; return c; }; + }; +}; diff --git a/lib/fmt/fmttest.ww b/lib/fmt/fmttest.ww index 0eea56b1..e13f4472 100644 --- a/lib/fmt/fmttest.ww +++ b/lib/fmt/fmttest.ww @@ -193,6 +193,258 @@ fn closedstream(s: *io.stream) void = { }; }; +// ---- fprintf scenarios ------------------------------------------------- +// Each scenario writes through memio.dynamic and compares bytes against +// an inline `want`. Variadic call-site shape forces one body per shape; +// see the file header. + +@test fn fprintf_implicit() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + let r: (i32 | io.closed) = fmt.fprintf(&s, "hello {} {}", "world", 42i64); + match (r) { + case let n: i32 => { if (n != 14) { fail(); }; }; + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "hello world 42")) { fail(); }; + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +@test fn fprintf_indexed() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + let r: (i32 | io.closed) = fmt.fprintf(&s, "{1} {0} {1}", "hello", "world"); + match (r) { + case let n: i32 => { if (n != 17) { fail(); }; }; + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "world hello world")) { fail(); }; + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +@test fn fprintf_literal_braces() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + let r: (i32 | io.closed) = fmt.fprintf(&s, "{{ {} }}", 1i64); + match (r) { + case let n: i32 => { if (n != 5) { fail(); }; }; + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "{ 1 }")) { fail(); }; + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +@test fn fprintf_width_right() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + let r: (i32 | io.closed) = fmt.fprintf(&s, "{:5}", 42i64); + match (r) { + case let n: i32 => { if (n != 5) { fail(); }; }; + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), " 42")) { fail(); }; + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +@test fn fprintf_width_left() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + let r: (i32 | io.closed) = fmt.fprintf(&s, "{:-5}", 42i64); + match (r) { + case let n: i32 => { if (n != 5) { fail(); }; }; + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "42 ")) { fail(); }; + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +@test fn fprintf_width_center() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + let r: (i32 | io.closed) = fmt.fprintf(&s, "{:=5}", "hi"); + match (r) { + case let n: i32 => { if (n != 5) { fail(); }; }; + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), " hi ")) { fail(); }; + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +@test fn fprintf_pad_underscore() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + let r: (i32 | io.closed) = fmt.fprintf(&s, "{:_05}", "hi"); + match (r) { + case let n: i32 => { if (n != 5) { fail(); }; }; + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "000hi")) { fail(); }; + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +@test fn fprintf_base_hex() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + let r: (i32 | io.closed) = fmt.fprintf(&s, "{:x} {:X}", 48879i64, 61453i64); + match (r) { + case let n: i32 => { if (n != 9) { fail(); }; }; + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "beef F00D")) { fail(); }; + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +@test fn fprintf_base_oct_bin() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + let r: (i32 | io.closed) = fmt.fprintf(&s, "{:o} {:b}", 493i64, 27i64); + match (r) { + case let n: i32 => { if (n != 9) { fail(); }; }; // "755 11011" + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "755 11011")) { fail(); }; + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +@test fn fprintf_prec_int() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + let r: (i32 | io.closed) = fmt.fprintf(&s, "{:.5}", 42i64); + match (r) { + case let n: i32 => { if (n != 5) { fail(); }; }; + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "00042")) { fail(); }; + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +@test fn fprintf_prec_str_trunc() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + let r: (i32 | io.closed) = fmt.fprintf(&s, "{:.3}", "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(); }; +}; + +@test fn fprintf_sign_neg() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + let r: (i32 | io.closed) = fmt.fprintf(&s, "{} {:+} {: }", -7i64, 7i64, 7i64); + match (r) { + case let n: i32 => { if (n != 8) { fail(); }; }; // "-7 +7 7" + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "-7 +7 7")) { fail(); }; + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +@test fn fprintfln_basic() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + let r: (i32 | io.closed) = fmt.fprintfln(&s, "x={}", 9i64); + match (r) { + case let n: i32 => { if (n != 4) { fail(); }; }; // "x=9\n" + case io.closed => fail(); + }; + if (!streq(memio.string(&mem), "x=9\n")) { fail(); }; + let c: (void | io.closed) = io.close(&s); + match (c) { case void => {}; case io.closed => fail(); }; +}; + +@test fn fprintf_closed() void = { + let s: io.stream; + closedstream(&s); + let r: (i32 | io.closed) = fmt.fprintf(&s, "hello {}", 1i64); + match (r) { + case let n: i32 => fail(); + case io.closed => {}; + }; +}; + +@test fn bsprintf_basic() void = { + let buf: [16]u8; + let r: (str | io.closed) = fmt.bsprintf(buf[0:16], "{} {}", "hi", 42i64); + match (r) { + case let s: str => { if (!streq(s, "hi 42")) { fail(); }; }; + case io.closed => fail(); + }; +}; + +// Pins the formatfield bool / rune arms inside fprintf's for-loop — +// the codegen-smell repro shape (task #18). Pre-task-#18, the str arm +// is also exercised by fprintf_implicit; this adds the two remaining +// tagged arms so a future task-#18 fix that re-introduces the 24B +// widen-by-value cannot regress silently. +@test fn fprintf_bool_rune() void = { + let mem: memio.state; + let s: io.stream; + memio.dynamic(&mem, &s); + let r: (i32 | io.closed) = fmt.fprintf(&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(); }; +}; + +// Verifies bsprintf's documented truncation contract — short writes +// return the prefix that fit (memio.fixedwrite returns 0 once full, +// not io.closed). Pairs with fprintfixedshort above which covers the +// underlying fprint path. +@test fn bsprintf_trunc() void = { + let buf: [3]u8; + let r: (str | io.closed) = fmt.bsprintf(buf[0:3], "{} {}", "hi", 42i64); + match (r) { + case let s: str => { if (!streq(s, "hi ")) { fail(); }; }; + case io.closed => fail(); + }; +}; + +// Pins formatone's tail-pad counter shape. Without the counter, the +// `total < m.width` loop spins on memio.fixed's 0-byte-on-full +// partial-write contract (ww divergence from Hare's errors::overflow +// + `?`). Buffer 3B, target " 42" (5B); first 3 spaces fit, then +// formatraw + tail-pad both write 0 — must terminate. +@test fn bsprintf_width_trunc() void = { + let buf: [3]u8; + let r: (str | io.closed) = fmt.bsprintf(buf[0:3], "{:5}", 42i64); + match (r) { + case let s: str => { if (!streq(s, " ")) { fail(); }; }; + case io.closed => fail(); + }; +}; + export fn main() i32 = { signalled = 1; fprintbarestr(); signalled = 2; fprintintstr(); @@ -202,5 +454,23 @@ export fn main() i32 = { signalled = 6; fprintlnempty(); signalled = 7; fprintfixedshort(); signalled = 8; fprintclosed(); + signalled = 9; fprintf_implicit(); + signalled = 10; fprintf_indexed(); + signalled = 11; fprintf_literal_braces(); + signalled = 12; fprintf_width_right(); + signalled = 13; fprintf_width_left(); + signalled = 14; fprintf_width_center(); + signalled = 15; fprintf_pad_underscore(); + signalled = 16; fprintf_base_hex(); + signalled = 17; fprintf_base_oct_bin(); + signalled = 18; fprintf_prec_int(); + signalled = 19; fprintf_prec_str_trunc(); + signalled = 20; fprintf_sign_neg(); + signalled = 21; fprintfln_basic(); + signalled = 22; fprintf_closed(); + signalled = 23; bsprintf_basic(); + signalled = 24; fprintf_bool_rune(); + signalled = 25; bsprintf_trunc(); + signalled = 26; bsprintf_width_trunc(); return 0; };