fmt — formatting assembled into the caller’s buffer
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#
- There is one convention — every writer is
(buf, pos) → new pos. Success givessome newpos, shortage givesnone. It refuses with a value rather than stopping. Give the newposto the next call as it is and the text joins up. - All or nothing. If room runs out, not a byte is written — if “12” remains instead of “1234”, nobody notices. That is why
put_u64andput_hexcount the digits first (dec_width,hex_width) and write from the front. - A line break is one LF (
put_nl). It is the same convention astake_lineinio, and CRLF is neither made nor removed. - What it does not do — floating-point formatting, padding and alignment, locale (thousands separators). If needed, measure the width with
dec_widthand fill spaces yourself withput_byte. - No state. No struct, no enum. The buffer and cursor both belong to the caller, so any number of uses anywhere at once do not interfere.
Ops at a glance#
| op | Signature | When it cannot |
|---|---|---|
put_byte | proc (buf mut slice u8, pos u64, b u8) → option u64 | none if no room, buffer unchanged |
put_str | proc (buf, pos, s slice u8) → option u64 | none unless all fits, buffer unchanged |
dec_width | fn (n u64) → u64 | never fails |
put_u64 | proc (buf, pos, n u64) → option u64 | none if no room, buffer unchanged |
put_i64 | proc (buf, pos, n i64) → option u64 | none if no room |
hex_width | fn (n u64) → u64 | never fails |
put_hex | proc (buf, pos, n u64) → option u64 | none if no room, buffer unchanged |
put_bool | proc (buf, pos, b bool) → option u64 | none if no room |
put_nl | proc (buf, pos) → option u64 | none 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”).
put_byte— writes the single bytebtobuf[pos]('-'is 45, LF is 10). Success givessome (pos+1).put_str— writes the whole byte strings.sis only read, so a string literal can be given as is.noneifpos + len sexceedslen buf.dec_width·hex_width— the number of digits when writingnin decimal or hex. At least 1 (0is one digit). They precompute for all-or-nothing, but you may use them directly to measure in advance “how many bytes are enough”.put_u64·put_hex— unsigned decimal and hex (lowercase, no0x). If you need0x, putput_str buf pos "0x"in front.put_i64— signed decimal. If negative, writes-first and handles the magnitude inu64. The i64 minimum (−9223372036854775808) cannot be negated to a positive, so0 − nis computed inu64. This boundary value prints correctly too.put_bool— writes"true"or"false". Not"1"/"0", so 4 or 5 bytes are needed.put_nl— one LF byte. Same asput_byte buf pos 10.
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 .
endThe 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 .
endCounter-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" .
endThe 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 p1It 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
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
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#
- Running short is a normal path. It is an expected answer, not an error, so receive it with
guard. If you need an upper bound, measure in advance —u64decimal is at most 20 digits, hex at most 16, andi64adds one for the sign. posis both an index and a length. After assembly,posequals the bytes written so far, sosubslice buf 0 posis the result.- To reuse the buffer, just reset
posto 0. Old content need not be cleared — emit only up to the currentposand the old bytes are not seen. - Write CRLF yourself. If a protocol needs it, write
put_str buf pos "\r\n". - Every op is
effects none. They can be called inside afnor an actor handler. Only output needscap ioandeffects io. - Prefixes, signs and padding are the caller’s job. Attach
0x,+, thousands commas or filler spaces yourself withput_strandput_byte.