Skip to content

Output Formatting And Testing

Chris Michael edited this page Sep 2, 2026 · 2 revisions

Pudu

Output, Formatting, And Testing

Out.rendered(&Out.stdout(), &["compiled", "3 modules"])
Fmt.int(&Fmt.grouped(&Fmt.spec(), ","), 12000)
Test.run(&Test.suite("parser", &checks))

Status: Std.Out, Std.Fmt, Std.Test, and Std.Test.Property ship with the compiler and execute through the interpreter.

These modules keep decisions in values. Rendering is separate from writing, shaping is separate from rendering, and running a suite is separate from presenting its report. A program can therefore test each stage by comparing ordinary values.

Printing

print takes text and ends the line. Std.Out.Printer names the decisions beyond that: stream, separator, ending, and per-line prefix.

Std.Out holds those decisions as a Printer.

import Std.Out as Out

let out = Out.stdout()
let csv = Out.separatedBy(&out, ",")
let prompt = Out.endingWith(&out, "")

Out.rendered(&out, &["compiled", "3 modules"])  // "compiled 3 modules\n"
Out.rendered(&csv, &["a", "b", "c"])            // "a,b,c\n"
Out.rendered(&prompt, &["Name: "])               // "Name: "

Pudu has no keyword or variadic arguments. A positional print(x, sep, end) API would repeat every choice at every call. A Printer can instead be named, transformed, and passed to the function that reports through it.

rendered answers exactly the text write sends to its selected stream. write returns Result[(), Str]; callers handle or propagate that result like any other recoverable effect.

fn report(printer: &Out.Printer) -> Result[(), Str] {
  Out.write(printer, &["compiled", "3 modules"]) ?
  Ok(())
}

Every writing function is stated in terms of it, and every write answers a Result.

A prefix applies to every line of a piece, not only the first, so indenting a block indents all of it and indentedBy nests rather than replaces.

Shaping values

Std.Fmt.Spec holds width, fill, alignment, sign, and digit grouping. The module parses no format strings. A malformed choice is rejected as ordinary Pudu syntax or typing rather than discovered while rendering.

import Std.Fmt as Fmt

let column = Fmt.rightAligned(&Fmt.width(&Fmt.spec(), 8))
Fmt.text(&column, "42")                              // "      42"
Fmt.hex(&column, 255)                                // "      ff"
Fmt.zeroPadded(&Fmt.width(&Fmt.spec(), 4), 42)       // "0042"

A width is a least width: losing characters is worse than losing a column, and truncated is how the other behaviour is asked for by name.

fixed takes a Decimal and never a Float. A figure shown to two places is money or a measurement, and admitting the type that cannot hold those exactly would reintroduce at the last step the error Std.Decimal exists to prevent.

Testing

Std.Test makes a check a value. run is a pure function from a Suite to a Report.

import Std.Test as Test

let suite = Test.suite("orders", &[
  Test.equals("total", &42, &42),
  Test.that("is paid", true),
  Test.todo("refunds", "not implemented"),
])

let report = Test.run(&suite)

Nothing throws, so one run reports every failure rather than the first. A failure says what it expected, what it found, and the path that reaches it — a suite answering "thirty-nine of forty" has told the reader the least useful true thing about their program.

A pending check is counted apart from both passes and failures. Counting it as a pass lets a suite quietly shrink by deleting what has not been done; this module's own fixture caught exactly that defect in it.

A table of cases becomes one named check per row, named by a function rather than by a template with holes in it:

Test.each(&cases, fn(c: Case) -> Str => c.name, fn(c: Case) -> Bool => c.holds)

A template carrying %s and %d is a small language inside a string that nothing checks; a function is checked against the row's own type.

Skipping leaves a check pending rather than absent, so a suite cannot quietly shrink on the machine where a condition is false. Focusing is a filter over a suite rather than a mark on a check, which a value does not need.

Properties

Std.Test.Property generates values and reduces a counterexample before reporting it.

import Std.Test.Property as Property

Property.forAllInts(
  "generated values stay below the bound",
  7u64,
  200,
  50,
  fn(n: Int) -> Bool => n < 50
)

Shrinking halves toward zero rather than stepping, so a failure at a million reduces in about twenty tries. The seed is given, not clocked: a property check is a pure function of that seed, reports it beside the counterexample, and can therefore be repeated exactly rather than run again and hoped about.

What is deliberately absent

  • No format strings.
  • No captured standard output in tests. Everything these modules do is reachable through a pure rendering function.
  • No concurrent or sequential test modes: Pudu has no threads.
  • No setup/teardown fixtures. They exist to sequence effects, and a check here is a value.

Related

Clone this wiki locally