Skip to content

std.string

std.string provides read-only helpers over NUL-terminated strings, integer parsing, and StringBuilder, a growable heap-backed string with concatenation. The module lives at lib/std/string.dusk and is written in dusk.

@import std.string

A dusk string is a pointer to a NUL-terminated buffer of char, a read-only view that costs one machine word. String literals do not heap allocate; the literal bytes live in static storage. A string value is immutable: to build or join strings at runtime you use StringBuilder, added in 0.2.0. See Types for the string type itself.

FunctionDescription
str_len(s: string) -> int64Length up to the NUL terminator.
str_eq(a: string, b: string) -> boolTrue when both strings hold the same bytes.

A string’s length is found by scanning to the NUL, which is what str_len does. str_eq compares byte by byte and requires the strings to end together.

inspect.dusk
@paradigm procedural
@import std.string
func main() -> int32 {
n := str_len("hello")
println(n) // 5
if str_eq("dusk", "dusk") {
println("same") // same
}
if !str_eq("dusk", "dawn") {
println("different") // different
}
return 0
}
FunctionDescription
parse_int(s: string) -> (int64, error)Parse a signed base 10 integer.
parse_int_radix(s: string, base: int64) -> (int64, error)Parse a signed integer in a base from 2 to 36.
parse_float(s: string) -> (float64, error)Parse a base 10 float. Builtin, no import needed.

Each parser returns the value paired with an error that you must handle: resolve it with exists, check, or ignore before it goes out of scope. See Error handling.

parse_int_radix accepts a base from 2 to 36; a base outside that range is an error. An optional leading - or + sign is accepted. Digits 0 to 9 map to 0 to 9 and letters to 10 to 35, upper or lower case. A base with a canonical prefix accepts it (0x or 0X for 16, 0o or 0O for 8, 0b or 0B for 2), but the base is never inferred from the prefix. A prefix that does not match the base, like 0x under base 10, is read as digits and fails on the bad digit. An empty input, a digit the base rejects, or a value that overflows int64 is an error.

parse_int is parse_int_radix with base 10, so a 0x, 0o, or 0b prefix fails on the prefix letter.

parse_float is a builtin rather than a library function, so it is available everywhere without an import, the same way print is. The std.string reference lists it here because it belongs with the other parsers; see Builtins for the full builtin list.

parse.dusk
@paradigm procedural
@import std.string
func main() -> int32 {
n, e := parse_int("42")
e.ignore()
println(n) // 42
h, he := parse_int_radix("0xFF", 16)
he.ignore()
println(h) // 255
bad, be := parse_int("0xFF")
if be.exists() {
println("not base 10") // not base 10
} else {
println(bad)
}
f, fe := parse_float("3.5")
fe.ignore()
println(f) // 3.5
return 0
}

StringBuilder is a growable, heap-backed string:

export struct StringBuilder {
data: *raw char,
len: int64,
cap: int64,
}

Build it on the heap with alloc(sb_new()) and pass it by pointer so growth persists across calls, the same shape std.vector uses. The buffer always holds a NUL after the last character, so sb_cstr hands back a valid string view with no extra work.

FunctionDescription
sb_new() -> StringBuilderA fresh empty builder.
sb_push_char(s: *StringBuilder, c: char) -> voidAppend one character.
sb_push(s: *StringBuilder, t: string) -> voidAppend every character of a string, up to its NUL.
sb_size(s: *StringBuilder) -> int64The number of characters built.
sb_cstr(s: *StringBuilder) -> stringView the built bytes as a string.
sb_free(s: *StringBuilder) -> voidFree the backing buffer.
concat(a: string, b: string) -> *StringBuilderJoin two strings into a fresh heap builder.
builder.dusk
@paradigm procedural
@import std.string
func main() -> int32 {
g: *StringBuilder = alloc(sb_new())
sb_push(g, "dusk")
sb_push_char(g, 32) // a space
sb_push(g, "and dawn")
println(sb_cstr(g)) // dusk and dawn
println(sb_size(g)) // 13
sb_free(g) // frees the buffer
free(g) // frees the builder struct
r: *StringBuilder = concat("hello, ", "world")
println(sb_cstr(r)) // hello, world
sb_free(r)
free(r)
return 0
}

The buffer starts at capacity 8 and doubles when a pushed character and its NUL terminator would not fit. Growth allocates a new buffer, copies the built characters, and frees the old one; the NUL terminator is rewritten after every push.

sb_cstr is a zero-cost reinterpret: because the buffer is always NUL-terminated, viewing it as a string does no copying. Underneath it uses the cstr builtin, which reinterprets a NUL-terminated *char buffer as a string at no runtime cost. The view borrows the buffer, so it is valid only until the builder next grows or is freed. Copy the bytes out or finish using the view before pushing again.

A builder owns its buffer, and a heap builder is two allocations: the buffer and the builder struct. Free both: sb_free releases the buffer and free releases the struct. See Memory for how alloc and free behave.

concat(a, b) appends both strings into a fresh heap builder and returns it. Ownership moves to the caller, who frees the buffer with sb_free and the builder struct with free, as in the example above. Read the result with sb_cstr, or keep pushing onto it. The return value is an ordinary *StringBuilder.

There is no + operator on strings; concat and sb_push are how strings are joined at runtime.