Lowent Manual←↑→

fmt — formatting assembled into the caller’s buffer

Source
lib/fmt.low
Layer
L0 — pure computation (the caller’s buffer)
Capabilities
none

Prints numbers and text into a buffer the caller provides to make strings. Use it to assemble a line to output or one line of a log. This module makes no paper; it only writes on the paper handed to it (mut slice u8).

use fmt .

let p option u64 . be fmt.put_u64 buf 0 1234 .
guard is_some p . else return 1 .

0 is pos (the next place to write), and the option u64 returned is the new pos on success, none if room ran out.

Formatting is not output. Every op only assembles bytes into the caller’s buffer and, without exception, is effects none. Actual output is done separately by a caller holding cap io, with write_out or outbuf. So assembly code can be called from anywhere without capabilities, tests compare buffers rather than screens, and the caller decides what to emit and when. Turning numbers into bytes needs neither kernel nor allocation, so it is a library — no builtin was added, and the whole file is inside the VM/native cross-check.

Design and boundaries#

Ops at a glance#

opSignatureWhen it cannot
put_byteproc (buf mut slice u8, pos u64, b u8) → option u64none if no room, buffer unchanged
put_strproc (buf, pos, s slice u8) → option u64none unless all fits, buffer unchanged
dec_widthfn (n u64) → u64never fails
put_u64proc (buf, pos, n u64) → option u64none if no room, buffer unchanged
put_i64proc (buf, pos, n i64) → option u64none if no room
hex_widthfn (n u64) → u64never fails
put_hexproc (buf, pos, n u64) → option u64none if no room, buffer unchanged
put_boolproc (buf, pos, b bool) → option u64none if no room
put_nlproc (buf, pos) → option u64none if no room

Table 50.1 — Ops of fmt — all effects none

put_ in a name means “write into the buffer” and _width means “only count how many bytes are needed”. Those that change the buffer are proc, those that only count are fn, but none of the nine touches the outside.

Ops in detail#

Every writer’s first two parameters are the same. buf is where to write (received because the module does not allocate, mut because it changes it), and pos is from which place in it (the module does not remember the cursor, so passing the new pos from the previous call is the only way to say “continue”).

Using it#

Assemble purely, output once at the end with cap io. The key is passing the returned new pos on to the next call.

module report .

use fmt .

proc main
  input out cap io .
  input al  cap allocator .
  output u8 .
  effects alloc io .
do
  let g option mut slice u8 . . be alloc_bytes al capacity 64 .
  guard is_some g . else return 70 .
  let buf mut slice u8 . be some_value g .

  rem assembly --- not a byte has gone out yet
  let p1 option u64 . be fmt.put_str buf 0 "answer=" .
  guard is_some p1 . else return 71 .
  let p2 option u64 . be fmt.put_u64 buf (some_value p1) 42 .
  guard is_some p2 . else return 72 .
  let p3 option u64 . be fmt.put_str buf (some_value p2) " hex=" .
  guard is_some p3 . else return 73 .
  let p4 option u64 . be fmt.put_hex buf (some_value p3) 255 .
  guard is_some p4 . else return 74 .
  let p5 option u64 . be fmt.put_nl buf (some_value p4) .
  guard is_some p5 . else return 75 .

  rem output --- only the assembled front part (0 … p5) goes out
  let w u64 be write_out out 1 (subslice buf 0 (some_value p5)) .
  return 0 .
end

The output is answer=42 hex=ff and one line break. Giving each piece a different exit code tells at once where room ran out. Measuring the buffer size in advance works with the same tools.

fn need_for input n u64 . output u64 . do
  let w u64 be fmt.dec_width n .
  return add (add 7 w) 1 .
end

Counter-examples#

Counter-example. Outputting inside an effects none op

proc bad input out cap io . output u64 . effects none . do
  return write_out out 1 "x" .
end

The declaration becomes a lie and is rejected with E-EFFECT (E-EFFECT-CALC for a fn). Even changed to effects io, it is E-EFFECT-NO-CAP without a cap io input.

Counter-example. Discarding the returned pos and reusing the old one

let p1 option u64 . be fmt.put_str buf 0 "answer=" .
let p2 option u64 . be fmt.put_u64 buf 0 42 .        rem ✗ pos should be some_value p1

It translates, and the result overlaps like 42swer= instead of answer=42. If the front of the output is mangled, first check whether pos was passed on.

Counter-example. Carrying on ignoring none

If room runs out, that piece was never written. Carrying on without guard is_some … else return … sends the output with the middle piece missing. If output goes wrong only for long values, the buffer is short. All or nothing guarantees one op, not a whole sequence of calls. Conversely, taking out with some_value without guard stops with E-VM-NONE the moment room runs out — testing only with short values never reveals it.

Counter-example. Giving the whole buffer to write_out

Giving buf instead of subslice buf 0 (some_value p5) sends the unwritten back part too. Mysterious bytes stick to the end of the line. The last pos is the length written.

Cautions#