Lowent Manual←↑→

strbuf — owned string buffer and null-terminated cstr

Source
lib/strbuf.low
Layer
L0 — pure computation (the caller’s buffer)
Capabilities
none · only as_cstr is unsafe

Builds strings by appending little by little. Use it to assemble a path from a directory, “/” and a name, or to build a message from several pieces. If strings is views (reading), strbuf is writing — it appends, puts in as much as fits, and grows.

use strbuf .

var b strbuf.str_buf strbuf.new. .
let r result void strbuf.sb_error strbuf.append b. buf. "hello" . .
guard is_ok r. . else return 1 . .

In this module a “buffer” is two pieces — the state b remembering how much was written, and the place buf where the bytes actually go. They are separate, so ops always receive both. Where the buffer comes from (cap allocator, a region, a static buffer) is the caller’s business, and the library never allocates behind your back. The only core op added is cstr_of, which gives the raw pointer of a slice.

Design and boundaries#

What to do when room runs out is chosen by the caller, by name. It is the same discipline as wrap_add, sat_add and chk_add. No single op decides for itself depending on the situation.

opKindBehaviour
appendAll or nothingIf it does not fit, it refuses without writing a single byte
append_truncAs much as fitsGives the count written — being truncated comes out as a value
append_growGrowThe caller provides the larger place

Table 50.1 — Three kinds of appending

The null seal

Every append reserves 1 byte — the content capacity is len(buf) − 1. After writing it seals with buf[len] = 0, so as_cstr is O(1) and allocation-free. The buffer must be at least 1 byte; in practice, remember a buffer of n + 1 bytes for n bytes of content.

There are three types. str_buf (state len usize), sb_error (one failure name, no_room — failure is a value, not a stop), and cstr (a newtype of a raw pointer to a null-terminated char*, only for the boundary with other languages).

Ops at a glance#

opSignatureWhen it cannot
newfn () → str_buf—
as_strfn (b str_buf, buf slice u8) → slice u8— (a view, not a copy)
roomfn (b str_buf, buf slice u8) → usizestops on underflow for an empty (length 0) buffer
as_cstrunsafe proc (b str_buf, buf mut slice u8) → cstrstops out of bounds if len + 1 > len(buf)
appendproc (b mut str_buf, buf mut slice u8, s slice u8) → result void sb_errorerror no_room . (buffer unchanged)
append_truncproc (b mut str_buf, buf mut slice u8, s slice u8) → usize— (known from count written < len s. .)
append_growproc (b mut str_buf, old mut slice u8, new_buf mut slice u8, s slice u8) → result void sb_errorerror no_room . (original unchanged)

Table 50.2 — Ops of strbuf

Ops in detail#

Using it#

The three kinds behave differently. The caller provides the buffer.

module demo .

use strbuf .

proc build input buf mut slice u8 . output usize . effects none . do
  var b strbuf.str_buf strbuf.new. .
  let r1 result void strbuf.sb_error strbuf.append b. buf. "ab" . .
  guard is_ok r1. . else return 90 . .
  guard eq field b. len . 2 . else return 91 . .
  rem the seal --- a null at position len (buf[2])
  guard eq idx buf. 2 . 0 . else return 92 . .
  rem buffer 5 = content 4 + null 1 --- 2 + 3 + 1 > 5, so refused and the buffer stays clean
  let r2 result void strbuf.sb_error strbuf.append b. subslice buf. 0 5 . "xyz" . .
  guard is_error r2. . else return 93 . .
  guard eq field b. len . 2 . else return 94 . .
  rem in the same situation trunc writes as much as fits (2) and returns 2
  let n usize strbuf.append_trunc b. subslice buf. 0 5 . "xyz" . .
  guard eq n. 2 . else return 95 . .
  return field b. len . .
end .

Growth is expressed by the caller giving a new place.

proc grow input small mut slice u8 . input big mut slice u8 . output usize . effects none . do
  var b strbuf.str_buf strbuf.new. .
  let r1 result void strbuf.sb_error strbuf.append b. small. "ab" . .
  guard is_ok r1. . else return 90 . .
  let r2 result void strbuf.sb_error strbuf.append b. small. "xyz" . .
  guard is_error r2. . else return 91 . .
  rem I provide the larger place --- the library does not allocate
  let r3 result void strbuf.sb_error strbuf.append_grow b. small. big. "xyz" . .
  guard is_ok r3. . else return 92 . .
  guard eq idx big. 0 . 97 . else return 93 . .
  guard eq idx big. 2 . 120 . else return 94 . .
  return field b. len . .
end .

build gives 4 (“ab” 2 + 2 written by trunc) and grow gives 5 (“abxyz”). The real use is path assembly — append with append b pathbuf dpath, append b pathbuf "/", append b pathbuf name, then take the view with as_str b pathbuf and pass it to files.

Counter-examples#

Counter-example. Calling as_cstr from pure code

fn f input b strbuf.str_buf . input buf mut slice u8 . output u64 . do
  let p strbuf.cstr strbuf.as_cstr b. buf. . .   rem ✗ calling an unsafe proc from a fn
  return 0 .
end .

Rejected at translation with E-EFFECT-CALC. Making a cstr is a guarantee of null termination, and that guarantee stands only inside an unsafe boundary. Make the caller an unsafe proc.

Counter-example. A buffer size forgetting the null

If the buffer is exactly 2 bytes, not even "ab" fits (2 + 0 + null 1 > 2). The very first append is error no_room . and field b. len . stays at 0. Content of n bytes needs a buffer of n + 1 bytes.

Counter-example. Reading the buffer without checking the result

let r result void strbuf.sb_error strbuf.append b. buf. "hello" . .
let v slice u8 strbuf.as_str b. buf. . .   rem ✗ r was not checked

No stop and no error. The buffer is not broken either — refusal does not touch it. You just get a short string missing what you expected. guard is_ok r else … comes first.

Counter-example. Mixing state and buffer

let r result void strbuf.sb_error strbuf.append b. buf1. "ab" . .
let v slice u8 strbuf.as_str b. buf2. . .   rem ✗ length from buf1, bytes from buf2

With no error, a view of the wrong bytes comes out. Pair one str_buf with one buffer only (or its successor moved by append_grow).

Cautions#