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.stringA 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.
Read-only helpers
Section titled “Read-only helpers”| Function | Description |
|---|---|
str_len(s: string) -> int64 | Length up to the NUL terminator. |
str_eq(a: string, b: string) -> bool | True 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.
@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}Parsing numbers
Section titled “Parsing numbers”| Function | Description |
|---|---|
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.
@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
Section titled “StringBuilder”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.
| Function | Description |
|---|---|
sb_new() -> StringBuilder | A fresh empty builder. |
sb_push_char(s: *StringBuilder, c: char) -> void | Append one character. |
sb_push(s: *StringBuilder, t: string) -> void | Append every character of a string, up to its NUL. |
sb_size(s: *StringBuilder) -> int64 | The number of characters built. |
sb_cstr(s: *StringBuilder) -> string | View the built bytes as a string. |
sb_free(s: *StringBuilder) -> void | Free the backing buffer. |
concat(a: string, b: string) -> *StringBuilder | Join two strings into a fresh heap builder. |
@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}Growth and views
Section titled “Growth and views”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.
Ownership
Section titled “Ownership”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.
Concatenation
Section titled “Concatenation”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.