Bytes, Text & Lines
These modules are the first practical dynamic-storage layer in cnegative.
They matter because fixed arrays and plain string literals stop being enough once you start building:
- terminal apps
- editors
- parsers
- file transforms
- anything that grows data while it runs
The short split
std.bytesis for growable raw bytesstd.linesis for growable owned lines of textstd.textis for building text and turning it into a final ownedstr
If you are new, read std.text first, then std.lines, then come back to std.bytes when you start caring about raw byte data.
std.bytes
import std.bytes as bytes;std.bytes.Buffer is a heap-owned growable byte container.
Current API:
bytes.Buffer { data:ptr u8; length:int; capacity:int }bytes.new() -> result ptr bytes.Bufferbytes.with_capacity(int) -> result ptr bytes.Bufferbytes.release(ptr bytes.Buffer) -> result boolbytes.clear(ptr bytes.Buffer) -> result boolbytes.length(ptr bytes.Buffer) -> intbytes.capacity(ptr bytes.Buffer) -> intbytes.push(ptr bytes.Buffer, u8) -> result boolbytes.append(ptr bytes.Buffer, slice u8) -> result boolbytes.get(ptr bytes.Buffer, int) -> result u8bytes.set(ptr bytes.Buffer, int, u8) -> result boolbytes.view(ptr bytes.Buffer) -> slice u8
What to notice
Bufferitself is a heap-owned runtime object- release it with
bytes.release(...) bytes.view(...)does not copy the databytes.view(...)returns a non-owningslice u8
Small example
import std.bytes as bytes;
fn:result int run() {
try buffer = bytes.new();
let seed:byte[3] = [1, 2, 3];
if bytes.append(buffer, seed).ok == false {
bytes.release(buffer);
return err;
}
let view:slice byte = bytes.view(buffer);
let size:int = view.length;
bytes.release(buffer);
return ok size;
}std.text
import std.text as text;std.text.Builder is a heap-owned growable text builder.
Current API:
text.Builder { data:ptr u8; length:int; capacity:int }text.new() -> result ptr text.Buildertext.with_capacity(int) -> result ptr text.Buildertext.release(ptr text.Builder) -> result booltext.clear(ptr text.Builder) -> result booltext.length(ptr text.Builder) -> inttext.capacity(ptr text.Builder) -> inttext.append(ptr text.Builder, str) -> result booltext.append_int(ptr text.Builder, int) -> result booltext.push_byte(ptr text.Builder, u8) -> result booltext.build(ptr text.Builder) -> result strtext.view(ptr text.Builder) -> slice u8
What to notice
- the builder object is released with
text.release(...) - the final string from
text.build(...)is a normal owned runtime string - so the final string is freed with raw
free
That split is important:
text.release(builder)releases the builder objectfree built_string;releases the built result string
Small example
import std.io as io;
import std.text as text;
fn:result int run() {
try builder = text.new();
if text.append(builder, "hello").ok == false {
text.release(builder);
return err;
}
if text.append_int(builder, 42).ok == false {
text.release(builder);
return err;
}
if text.push_byte(builder, 33).ok == false {
text.release(builder);
return err;
}
let made:result str = text.build(builder);
if made.ok == false {
text.release(builder);
return err;
}
io.write_line(made.value);
free made.value;
text.release(builder);
return ok 0;
}std.lines
import std.lines as lines;std.lines.Buffer is a heap-owned growable list of lines.
Current API:
lines.Buffer { data:ptr str; length:int; capacity:int }lines.new() -> result ptr lines.Bufferlines.with_capacity(int) -> result ptr lines.Bufferlines.release(ptr lines.Buffer) -> result boollines.clear(ptr lines.Buffer) -> result boollines.length(ptr lines.Buffer) -> intlines.capacity(ptr lines.Buffer) -> intlines.get(ptr lines.Buffer, int) -> result strlines.set(ptr lines.Buffer, int, str) -> result boollines.push(ptr lines.Buffer, str) -> result boollines.insert(ptr lines.Buffer, int, str) -> result boollines.remove(ptr lines.Buffer, int) -> result bool
What to notice
- the buffer owns copies of inserted lines
lines.release(...)frees that owned storagelines.get(...)returns a borrowedstr- do not
freethe result oflines.get(...) - that borrowed string stops being valid after
set,remove,clear, orrelease
Small example
import std.io as io;
import std.lines as lines;
fn:result int run() {
try buffer = lines.new();
if lines.push(buffer, "alpha").ok == false {
lines.release(buffer);
return err;
}
if lines.insert(buffer, 1, "beta").ok == false {
lines.release(buffer);
return err;
}
let picked:result str = lines.get(buffer, 0);
if picked.ok == false {
lines.release(buffer);
return err;
}
io.write_line(picked.value);
lines.release(buffer);
return ok 0;
}Beginner recommendation
Learn these first:
text.new()text.append(...)text.build(...)lines.new()lines.push(...)lines.get(...)bytes.new()bytes.append(...)bytes.view(...)
That is enough to understand the model without turning this into a full collections course.
The mental model
Think of the stack like this:
slice Tis the core language view typestd.bytes.Bufferis growable owned byte storagestd.lines.Bufferis growable owned line storagestd.text.Builderis text building on top of growable bytes
That is why these modules are important: they are the first place where slices and growable runtime storage become practical for real apps.