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 be strbuf.new .
let r result void strbuf.sb_error . be 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 builtin 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 u64), 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) → u64stops 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) → u64— (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 u64 . do
  var b strbuf.str_buf be strbuf.new .
  let r1 result void strbuf.sb_error . be 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 (index 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 . be 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 u64 be 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 u64 . effects none . do
  var b strbuf.str_buf be strbuf.new .
  let r1 result void strbuf.sb_error . be strbuf.append b small "ab" .
  guard is_ok r1 . else return 90 .
  let r2 result void strbuf.sb_error . be 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 . be strbuf.append_grow b small big "xyz" .
  guard is_ok r3 . else return 92 .
  guard eq (index big 0) 97 . else return 93 .
  guard eq (index 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 be 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 . be strbuf.append b buf "hello" .
let v slice u8 be 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 . be strbuf.append b buf1 "ab" .
let v slice u8 be 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#