Standard library
The standard library has two layers:
core– the implicit prelude: every module undercore/(io,array,string,math,conv,assert,error,is,default,collections) plus the runtime builtins. It is embedded in the compiler, and the public names of every core module are in scope in every program with no import –HashMapincluded.std– the shipped library tree: thestd/directory distributed beside the toolchain (fs,net,process,path,env,hash,regex,semver,url,http,data.json,data.toml, …), imported explicitly with thestd.prefix, e.g.import std.fs.{ read_file }. Native parts arrive as plugins, sostdlives on disk rather than in the compiler. The complete toolchain binds it automatically; see Installing Brass.
Most of the library is written in Brass itself, on top of a small set of
runtime primitives. Identifiers beginning with _ (e.g. _string_bytes,
_panic) are internal. The std.package_manager modules implement czpm;
their command-line interface is documented in the
package-manager guide.
Reserved builtin names that cannot be redefined: len, spawn, with,
sync, error, fields, typeof.
Builtins
Section titled “Builtins”| Function | Signature | Notes |
|---|---|---|
len(x) |
(array or string) -> int64 |
element count / byte length; also callable as x.len() |
error(x) |
Err wrapping an Error |
a prelude function; see Errors |
fields(x), typeof(x) |
compile-time | see Reflection |
spawn(f), with(c, f), sync() |
concurrency | see Concurrency |
Growable arrays (T[]) have these built-in methods (all rejected on
fixed-length T[n]):
| Method | Signature |
|---|---|
arr.push(v) |
(T) -> void |
arr.pop() |
() -> T? — null when empty |
arr.insert(i, v) |
(int64, T) -> void |
arr.remove(i) |
(int64) -> T |
arr.len() |
() -> int64 (both T[] and T[n]) |
Indexing is bounds-checked at runtime on both array kinds. Builtin slice methods enforce these arities exactly; extra or missing arguments are compile errors.
Errors (core.error)
Section titled “Errors (core.error)”The error value model behind error(..) and ! (normatively specified in
Error traces):
| Name | Shape / signature | Behavior |
|---|---|---|
Location |
{ file: string, line: int32, col: int32 } |
a source position; display() renders file:line:col |
Frame |
{ message, location: Location } |
one context annotation |
Error |
{ value, location: Location, frames: Frame[] } |
the payload error(..) raises; display() renders the nested trace |
error(value) |
-> infer! |
an Err wrapping value into an Error stamped with the call site |
r.context(msg) |
(string) -> Result |
appends a Frame to a failed result; leaves a success untouched |
error and context declare a trailing loc: Location parameter the
compiler fills with the call site (the
implicit location argument); a
caller may also pass it explicitly. The compiler’s reserved handling of
error(...) still validates that signature: one payload is required, only the
trailing Location may be omitted, and other arities or argument types fail.
Type tests and defaults
Section titled “Type tests and defaults”Each primitive type implements only its own is_<type> method:
is_string, is_bool, is_int8 … is_int64, is_uint8 … is_uint64,
is_float32, is_float64, and is_array on arrays. Calling one returns
true; the point is the uncalled
member-presence test,
which makes if v.is_string { ... } a compile-time type dispatch (records
and sums carry none of these members).
Every primitive type also has a static T.default() producing its zero
value: 0 for the numeric widths, false for bool, "" for string
(see the Default model).
core.io
Section titled “core.io”| Function | Signature | Behavior |
|---|---|---|
print(value) |
(any) -> void |
write the value’s text to stdout; combine values with interpolation |
println(value) |
(any) -> void |
print plus a newline |
input() |
() -> string! |
one line from stdin, without the trailing newline |
Files live in std.fs, not the prelude:
opening and moving bytes needs native code, so it ships as a plugin like
process and path.
core.array
Section titled “core.array”Methods on any array (fun infer[].m), so arr.map(f) works with no import:
| Method | Behavior |
|---|---|
map(f) |
new array of f(item) |
filter(pred) |
elements where pred(item) is true |
fold(init, f) |
left fold with accumulator |
each(f) |
run f for side effects |
slice(start, end) |
copy of the half-open range; indices are int64 |
reverse() |
reversed copy |
contains(x) |
membership by == |
sort() |
ascending copy (orders with </>) |
These return new arrays; only the builtin push/pop/insert/remove
mutate in place.
core.collections
Section titled “core.collections”Part of the implicit prelude: HashMap needs no import. Keys may be of any
type that renders to a stable string and compares with ==; values may be of
any type. Key and value types are inferred from set or from_pairs.
| Method | Signature | Behavior |
|---|---|---|
HashMap.new() |
() -> HashMap |
empty map |
HashMap.from_pairs(pairs) |
([[K, V]]) -> HashMap |
build from [key, value] pairs |
m.set(k, v) |
(K, V) -> void |
insert or replace |
m.get(k) |
(K) -> V? |
null when absent |
m.get_or(k, default) |
(K, V) -> V |
stored value or fallback |
m.contains_key(k) |
(K) -> bool |
whether a key exists |
m.delete(k) |
(K) -> bool |
remove and report whether it existed |
m.size() / m.is_empty() |
() -> int64 / () -> bool |
map size queries |
m.keys() / m.values() |
() -> K[] / () -> V[] |
unspecified slot order |
m.pairs() |
() -> [K, V][] |
same order as keys() |
m.clear() |
() -> void |
remove all pairs, retaining inferred types |
core.string
Section titled “core.string”String positions are UTF-8 byte offsets throughout: len, find, and
slicing agree on byte positions; the per-character helpers advance by each
character’s byte length.
| Method | Signature | Behavior |
|---|---|---|
s.split(sep) |
(string) -> string[] |
one field per separator boundary; empty sep yields [s] |
s.trim() |
() -> string |
strip leading/trailing ASCII whitespace |
s.starts_with(p) / s.ends_with(p) |
(string) -> bool |
|
s.find(sub) |
(string) -> int64? |
byte offset of first occurrence, else null |
s.replace(old, new) |
(string, string) -> string |
replace every occurrence; empty old is a no-op |
s.chars() |
() -> string[] |
one-character strings, multibyte-safe |
s.to_upper() / s.to_lower() |
() -> string |
ASCII case change |
parts.join(sep) |
string[].(string) -> string |
join a string array with sep |
s.len() |
() -> int64 |
byte length |
s.debug() |
() -> string |
quoted; escapes quotes, backslashes, and control bytes |
There is no public substring-slicing method and no direct s[i] indexing; use
chars, split, find, replace.
core.math
Section titled “core.math”abs(x), min(a, b), max(a, b) are polymorphic free functions (any type
supporting < and, for abs, -). The float routines take and return
float64: sqrt(x), floor(x), ceil(x), pow(base, exp). Signed integer
abs is wrapping: at that type’s minimum value, it returns the same minimum
instead of failing.
core.conv
Section titled “core.conv”Constants: INT32_MAX, INT32_MIN, INT64_MAX, INT64_MIN.
Byte/string conversion: to_bytes(s) -> uint8[] is the UTF-8 bytes of a
string, ready to write/send_to; to_text(bytes) -> string! decodes bytes
as UTF-8 text and fails on invalid input.
Free-function aliases of the conversion methods: int32_from(x) -> int32!,
int32_parse(s) -> int32!, float64_from(x) -> float64,
float64_parse(s) -> float64!, string_from(x) -> string. The method forms
(T.from, T.parse) are described in the
type system. Integer-to-float
conversion reads the full source width, so a uint64 above INT64_MAX remains
positive (with the expected floating-point rounding).
core.assert
Section titled “core.assert”assert(cond: bool, msg: string?) aborts the program when cond is false.
msg is a trailing nullable parameter, so assert(cond) works and prints a
generic message.
std.process
Section titled “std.process”import std.process.{ Command, Stdio }Spawn and control child processes. Its native implementation is included in
the complete toolchain and the installed std package resolves automatically.
It is unavailable in the browser playground. See
Installing Brass for the supported build layout.
Command is a builder: each method mutates the command and returns it, so
calls chain. spawn starts the process. A standard stream configured as
Stdio.Pipe is reachable through the Child as a File
(read/write/close); Inherit (the default) shares this process’s stream
and Null discards it.
| Method / function | Signature | Behavior |
|---|---|---|
Command.new(program) |
(string) -> Command |
program is looked up on PATH |
cmd.arg(value) |
(string) -> Command |
append one argument |
cmd.args(values) |
(string[]) -> Command |
append several arguments |
cmd.env(name, value) |
(string, string) -> Command |
set a variable in the child |
cmd.stdin/stdout/stderr(mode) |
(Stdio) -> Command |
connect a stream (Inherit/Pipe/Null) |
cmd.spawn() |
() -> Child! |
start the process |
child.stdin/stdout/stderr() |
() -> File! |
a piped stream (requires Stdio.Pipe) |
child.wait() |
() -> int32! |
block for exit; returns the exit code |
child.output() |
() -> Output! |
drain the piped streams, then wait |
exit(code) |
(int64) -> void |
end THIS process; never returns |
Stdio is | Inherit | Pipe | Null. Piped streams are Files, so the
prelude byte helpers to_bytes/to_text convert their contents. The
accessors may be called repeatedly: each hands back the same File.
exit(code) ends the running program itself rather than a child: it never
returns, so nothing after it runs. Standard output and error are flushed on the
way out, so a print still waiting for its newline is not swallowed, but
nothing else is cleaned up (a spawned child is not waited for, and an open
File’s own buffer is not written for it). On Unix only the low 8 bits reach
the caller: exit(256) reports 0 and exit(-1) reports 255, so keep to
0..=255.
The child inherits this process’s environment, and env adds to it (or
overrides one entry of it) rather than replacing it; setting the same name
twice keeps the last value. There is no way to unset an inherited variable or
to start from an empty environment.
const child = Command.new("sh") .args(["-c", "echo $GREETING"]) .env("GREETING", "hello") .stdout(Stdio.Pipe) .spawn()!wait blocks for exit and nothing else, so a child writing more to a pipe
than the OS buffers (about 64KiB on Linux) blocks on the full pipe while
wait blocks on the child. Read the piped streams before waiting, or use
output, which reads them while the child runs and cannot deadlock:
import std.process.{ Command, Stdio }
let child = Command.new("git") .args(["log", "--oneline"]) .stdout(Stdio.Pipe) .spawn()!
// `Output` is `{ code: int32, stdout: uint8[], stderr: uint8[] }`; a stream// that was not piped (or was taken through its accessor) comes back empty.let result = child.output()!print(to_text(result.stdout)!)println("exit: {result.code}")Waiting is idempotent: a second wait returns the same code, and a piped
stream stays readable afterwards, since the pipe still holds what the child
wrote before it exited.
Everything here runs on either back end: a piped stream is an fs File,
and the fs plugin executes natively under the interpreter too.
std.path
Section titled “std.path”import std.path.{ Path }Filesystem paths. Operating-system queries use the native plugin included in
the complete toolchain; lexical operations such as parse, join, and
normalize do not access the filesystem.
A Path is a sequence of components, absolute exactly when its first component
is the root /. Empty and repeated separators are dropped when a path is
parsed, so /usr//lib/ and /usr/lib are the same path. Every method that
answers with a path builds a new one, so a Path may be shared freely.
| Method / function | Signature | Behavior |
|---|---|---|
Path.parse(s) |
(string) -> Path |
absolute when s starts with / |
Path.current_dir() |
() -> Path! |
the working directory |
Path.home() / temp_dir() |
() -> Path! |
the home / temporary directory |
p.to_string() |
() -> string |
. for the empty path |
p.components() |
() -> string[] |
a copy, the root included as / |
p.depth() |
() -> int64 |
component count (len is a reserved builtin) |
p.is_absolute() / is_root() |
() -> bool |
shape of the path, not what is on disk |
p.parent() / basename() |
() -> Path |
the root is its own parent |
p.join(s) |
(string | string[] | Path) -> Path |
absolute s replaces p |
p.stem() / extension() |
() -> string / string? |
.gitignore is all stem |
p.with_extension(ext) |
(string) -> Path |
empty ext removes it |
p.normalize() |
() -> Path |
drops ., resolves .., no filesystem access |
p.to_absolute() |
() -> Path! |
against the working directory; links unresolved |
p.to_relative(base) |
(Path) -> Path! |
so that base.join(result) is p again |
p.starts_with(base) / equals(other) |
(Path) -> bool |
component-wise |
p.exists() / is_dir() / is_file() |
() -> bool |
false for a path that is not there |
p.is_sym_link() |
() -> bool |
about the link itself, not its target |
p.canonicalize() |
() -> Path! |
resolves links; the path must exist |
p.read_link() |
() -> Path! |
where a symbolic link points |
p.entries() |
() -> Path[]! |
a directory’s entries |
p.file_size() |
() -> int64! |
size in bytes |
join takes a string, an array of components, or another Path through one
parameter. It is not overloading: the argument’s members decide which arm of
the body survives compilation, as described under
member presence.
A file’s own location is not a method here. Every module is loaded with a
private _PATH constant holding its absolute source path, so the path of the
file you are writing is Path.parse(_PATH); an imported module reads its
own, not yours.
import std.path.{ Path }
const here = Path.parse(_PATH).parent()for entry in here.join("assets").entries()! { // `extension` is a `string?`: a name without one has no extension to test. if let ext = entry.extension() { if ext == "png" { println(entry.basename().to_string()) } }}std.fs
Section titled “std.fs”import std.fs.{ File, read_file, write_file, create_dir, remove_dir }File handles, byte I/O, and directories. Native file access is included in the complete toolchain and unavailable in the browser playground.
| Function / method | Signature | Behavior |
|---|---|---|
File.open(path, mode) |
(string or Path, string) -> File! |
"r" read, "w" truncate+create, "a" append |
read_file(path) |
(string or Path) -> string! |
whole file as text |
write_file(path, content) |
(string or Path, string) -> void! |
write text, truncating |
copy_file(source, target) |
(string or Path, string or Path) -> void! |
replaces an existing target |
move_file(source, target) |
(string or Path, string or Path) -> void! |
rename, or copy+delete across filesystems |
remove_file(path) |
(string or Path) -> void! |
a missing file is an error |
copy_dir(source, target) |
(string or Path, string or Path) -> void! |
the whole tree; target must not exist |
move_dir(source, target) |
(string or Path, string or Path) -> void! |
the whole tree; target must not exist |
copy(source, target) |
(string or Path, string or Path) -> void! |
file or directory, by what source is |
move(source, target) |
(string or Path, string or Path) -> void! |
file or directory, by what source is |
create_dir(path) |
(string or Path) -> void! |
recursive, like mkdir -p |
remove_dir(path) |
(string or Path) -> void! |
recursive, like rm -r |
f.read(n) |
(int64) -> uint8[]! |
up to n bytes; fewer at end-of-file |
f.write(bytes) |
(uint8[]) -> int64! |
write all of bytes |
f.seek(pos) |
(int64) -> void! |
absolute reposition |
f.size() |
() -> int64! |
by path (see below) |
f.close() |
() -> void! |
idempotent; standard streams are never closed |
File.from_fd(fd) |
(int64) -> File |
adopt an open descriptor (a pipe, a socket) |
File.stdin/stdout/stderr() |
() -> File |
the standard streams |
size() is answered by std.path (a stat by name needs no open
descriptor), so it works exactly for files opened by path; an adopted
descriptor or standard stream has no path to ask about and reports an error.
File.from_fd is how std.process and std.net hand their pipes and
sockets to the ordinary read/write/close methods.
Every path a function in this module takes may be a string or a Path:
File.open, read_file, write_file, create_dir, remove_dir. A path
built with std.path needs no to_string() on the way in. (The arm
that fits the argument is the only one compiled, so neither form costs the
other anything.)
copy_file and move_file both replace an existing target. A move within
one filesystem is a rename (atomic, the contents are never read); across
filesystems (and a temporary directory very often is another filesystem)
a rename cannot work, so it falls back to a copy followed by a delete, which is
not atomic. A directory is refused by both move_file and remove_file: a
tree is remove_dir’s business, and a directory move would succeed on one
filesystem and fail across two.
Removing a file that is not there is an error, not a quiet success: a typo
in a destructive call must not read as “done”. remove_file on a symbolic link
removes the link; what it points at is untouched.
The directory forms part ways with the file forms on one point: copy_dir and
move_dir refuse an existing target rather than replacing it. Replacing a
tree would delete files the copy never mentioned, which a copy should not do
behind your back; call remove_dir first if that is what you want. A target
inside the source is refused too (the walk would never finish), and a symbolic
link in the tree is recreated as a link rather than followed, as cp -R does.
copy and move take either kind, dispatching on what source turns out
to be, which is handy when the caller does not know or does not care. Each half
keeps its own rule about an existing target (a file is replaced, a tree is
refused), so reach for the specific call when it matters which one you are doing.
create_dir and remove_dir are both recursive: create_dir makes every missing parent
and treats an existing directory as success, while remove_dir takes the whole
tree, removing a symbolic link inside it as a link rather than following it. A
directory that is not there is an error for remove_dir too, matching
remove_file.
File I/O runs on both back ends: the plugin executes natively whether the
program is JIT-compiled or interpreted, so the old “the REPL refuses file
I/O” rule is gone (only spawn remains JIT-only). The playground has no
filesystem, so the examples here are not runnable in it.
std.env
Section titled “std.env”import std.env.{ args, var, vars, path_separator, current_dir }The process environment: command-line arguments, environment variables, and the working directory. These operating-system calls are unavailable in the browser playground.
| Function | Signature | Behavior |
|---|---|---|
args() |
() -> string[] |
the program file, then everything written after it |
var(name) |
(string) -> string! |
an unset variable is an error, not "" |
vars() |
() -> HashMap |
every variable, as a string -> string map |
path_separator() |
() -> string |
path-list separator (: on Unix, ; on Windows) |
current_dir() |
() -> Path! |
the working directory, as a std.path Path |
Everything after the program file on the command line belongs to the program, verbatim, flags included, with no separator needed:
brass main.cz --verbose input.txtgives args() == ["main.cz", "--verbose", "input.txt"]: the program file
as written, then the arguments (index 0 is the program, as in C’s argv).
The same holds for brass repl main.cz .... In an interactive REPL
session, or under an embedder that passes no arguments, args() is empty.
std.hash
Section titled “std.hash”import std.hash.{ sha256, hmac_sha256, hex, equal, Hasher }Message digests (MD5, SHA-1, SHA-2) and HMAC. A plugin under std/
wrapping the RustCrypto implementations, since these algorithms are built
from wrapping 32/64-bit arithmetic, which Brass does not have; a Brass
implementation would be a hand-masked emulation whose failure mode is a
silently wrong digest.
A digest is a uint8[]: the algorithm’s raw bytes. Hash text by its UTF-8
bytes with the prelude’s to_bytes, and render the result with hex:
println(hex(sha256(to_bytes("abc"))))// ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad| Function | Signature | Digest size |
|---|---|---|
md5(data) |
(uint8[]) -> uint8[] |
16 bytes |
sha1(data) |
(uint8[]) -> uint8[] |
20 bytes |
sha224 / sha256 |
(uint8[]) -> uint8[] |
28 / 32 |
sha384 / sha512 |
(uint8[]) -> uint8[] |
48 / 64 |
hmac_sha1/sha256/sha512(k,d) |
(uint8[], uint8[]) -> uint8[] |
20 / 32 / 64 |
hex(bytes) |
(uint8[]) -> string |
lowercase |
unhex(text) |
(string) -> uint8[]! |
inverse of hex |
equal(a, b) |
(uint8[], uint8[]) -> bool |
constant-time |
For input that is not in memory at once (a file read in chunks, a socket
stream), Hasher is the incremental form. finalize consumes the hasher:
a digest cannot be resumed once taken, so a second call is an error rather
than a meaningless second answer:
let h = Hasher.sha256()! // also .md5() .sha1() .sha224() .sha384() .sha512()h.update(chunk)!h.update(next)!println(hex(h.finalize()!))Security. md5 and sha1 are broken against collision attacks: use them
only to interoperate with something that already speaks them (a published MD5
checksum, a git object id), never to decide whether two inputs are “the same”.
Prefer sha256/sha512. Authenticate a message with hmac_sha256, not
sha256(key + data) (which is forgeable by length extension). Compare a
digest or MAC against an attacker-supplied one with equal, not ==: an
early-exit comparison leaks how many leading bytes of a forgery were right.
All of these are fast hashes: storing a password needs a purpose-built
slow KDF (argon2, scrypt, bcrypt), which this library deliberately does not
offer, so that a fast hash cannot be mistaken for one.
std.regex
Section titled “std.regex”import std.regex.{ Regex, escape }Regular expressions, on Rust’s regex engine: a finite automaton, so matching
is linear in the subject’s length however the pattern is written: a regex
over untrusted input cannot blow up the way a backtracking engine does. The
price is that backreferences (\1) and lookaround ((?=..), (?<=..)) do
not exist; a pattern using one fails to compile rather than quietly meaning
something else. Everything else is the usual syntax: classes (\d, \w,
\p{Greek}), repetition (*, +, ?, {m,n}, each with a lazy ? form),
alternation, anchors (^, $, \b), groups ((..), (?:..),
(?<name>..)), and inline flags ((?i), (?m), (?s), (?x)).
Writing a pattern
Section titled “Writing a pattern”A Brass string literal is not raw: it interprets \ and it interpolates
{expr}. A pattern therefore needs two escapes, and the second one bites
silently:
- a backslash doubles:
\\d,\\w,\\b; - an opening brace is escaped
\{:"\\d{4}"does not mean “four digits”: the{4}interpolates to the text4, so the pattern compiles as\d4(a digit, then the character4). Write"\\d\{4}". A closing brace needs nothing, and a quantifier with a comma ({2,3}) is a syntax error rather than a silent change.
In a replacement string, prefer $name and $1 over the braced ${name}
form for the same reason.
const date = Regex.new("(?<year>\\d\{4})-(\\d\{2})-(\\d\{2})")!if let m = date.find("due 2026-07-13, ok") { println(m.text) // 2026-07-13 println("{m.start}..{m.end}") // 4..14 if let y = m.named("year") { println(y.text) } // 2026}println(date.replace_all("2026-07-13", "$year/$2")) // 2026/07Regex.new(pattern) -> Regex! is where a bad pattern is reported; every method
below is infallible.
| Method | Signature | Behavior |
|---|---|---|
re.is_match(text) |
(string) -> bool |
cheapest — no groups recorded |
re.find(text) |
(string) -> Match? |
leftmost match, null when none |
re.find_from(text, from) |
(string, int64) -> Match? |
search starts at a byte offset |
re.find_all(text) |
(string) -> Match[] |
every non-overlapping match |
re.replace(text, rep) |
(string, string) -> string |
first match only |
re.replace_all(text, rep) |
(string, string) -> string |
$1 / $name / $$ expand in rep |
re.split(text) |
(string) -> string[] |
one more field than there were matches |
re.group_count() |
() -> int64 |
counts group 0, so no groups answers 1 |
escape(text) |
(string) -> string |
a pattern matching text literally |
A Match carries text, start, end (byte offsets into the subject) and
groups: Group?[], where groups[0] is the whole match. Reach a group by
number with m.group(i) or by name with m.named("year"); both answer null
when the group did not participate (the (a) of (a)|(b) against "b") or
the pattern has no such group. A Group is { text, start, end }.
A compiled Regex is never released (the language has no destructors), so
compile a pattern once and keep it: compiling inside a loop grows the
process. That is the right way to use any regex engine, since compilation
costs far more than matching.
std.semver
Section titled “std.semver”import std.semver.{ Version, sort }Semantic Versioning 2.0.0: parse a version, render it
back, and order two of them. Pure Brass on top of std.regex (it has no native
half of its own). Its grammar follows the official pattern from semver.org: no
leading zeros, an optional dot-separated pre-release, optional build metadata,
and nothing else in the string (v1.0.0 and 1.0 are rejected). Brass stores
the three core components as int64, so each is additionally capped at
INT64_MAX; this is a documented deviation from the unbounded SemVer grammar.
const v = Version.parse("1.4.2-rc.1+build.5")!println("{v.major}.{v.minor}.{v.patch}") // 1.4.2println(v.prerelease) // rc.1 (null when absent)println(v.compare(Version.parse("1.4.2")!)) // -1: a pre-release is LOWERVersion is { major, minor, patch: int64, prerelease, build: string? }.
The optional components are null when absent, which the grammar keeps
distinct from empty.
| Method / function | Signature | Behavior |
|---|---|---|
Version.parse(text) |
(string) -> Version! |
the whole string must be a version |
Version.new(major, minor, patch) |
(int64, int64, int64) -> Version! |
rejects negatives; no pre-release or build |
v.to_string() |
() -> string |
parsing the result yields an equal version |
v.compare(other) |
(Version) -> int64 |
-1 / 0 / 1 by precedence |
v.equals / less_than / greater_than |
(Version) -> bool |
in terms of compare |
v.is_prerelease() |
() -> bool |
|
v.prerelease_ids() |
() -> string[] |
"rc.1" → ["rc", "1"] |
sort(versions) |
(Version[]) -> Version[] |
a new array, ascending |
Precedence follows §11: major/minor/patch numerically, then a version with
a pre-release precedes the same version without one (1.0.0-rc.1 < 1.0.0).
Pre-release identifiers compare left to right: numeric ones numerically (so
beta.2 < beta.11, not lexically) and before alphanumeric ones, which compare
in ASCII order; if all shared identifiers are equal, the shorter list precedes.
Build metadata is ignored (§10), so 1.0.0+a and 1.0.0+b compare equal;
compare to_string() if textual identity is what you want.
std.net
Section titled “std.net”import std.net.{ Tcp, TcpListener, Udp, TlsStream }TCP and UDP sockets plus TLS client connections. The native networking plugin is included in the complete toolchain. Networking does not run in the browser playground.
Under the hood a plain socket is a File (an OS file descriptor) held
privately by each record: a connection cannot accept and a listener
cannot read.
Tcp: a bidirectional byte-stream connection:
| Method | Signature | Behavior |
|---|---|---|
Tcp.connect(host, port) |
(string, int64) -> Tcp! |
open a connection; host is an IP or a DNS name |
conn.read(max) |
(int64) -> uint8[]! |
up to max bytes; fewer on a short read |
conn.write(data) |
(uint8[]) -> int64! |
write all of data |
conn.local_addr() / conn.peer_addr() |
() -> string! |
the "ip:port" of each end |
conn.set_timeout(ms) |
(int64) -> void! |
read/write timeout; 0 clears it |
conn.close() |
() -> void! |
TcpListener: produces Tcp connections:
| Method | Signature | Behavior |
|---|---|---|
TcpListener.bind(host, port) |
(string, int64) -> TcpListener! |
bind and listen; port 0 picks an ephemeral port |
listener.accept() |
() -> Tcp! |
block until a connection arrives |
listener.local_addr() |
() -> string! |
reads back an OS-picked port |
listener.close() |
() -> void! |
Udp: a datagram socket:
| Method | Signature | Behavior |
|---|---|---|
Udp.bind(host, port) |
(string, int64) -> Udp! |
port 0 picks an ephemeral port |
sock.send_to(data, host, port) |
(uint8[], string, int64) -> int64! |
send one datagram |
sock.recv_from(max) |
(int64) -> Datagram! |
block for one datagram of up to max bytes |
sock.local_addr() |
() -> string! |
|
sock.set_timeout(ms) |
(int64) -> void! |
|
sock.close() |
() -> void! |
Datagram is { data: uint8[], addr: string }: one received datagram with
its sender’s address. The prelude helpers to_bytes(s) -> uint8[] and
to_text(bytes) -> string! convert between strings and socket bytes.
import std.net.{ Tcp, TcpListener }
let listener = TcpListener.bind("127.0.0.1", 0)!let port = int64.parse(listener.local_addr()!.split(":")[1])!
let client = Tcp.connect("127.0.0.1", port)!let server = listener.accept()!client.write(to_bytes("hello"))!println(to_text(server.read(64)!)!) // helloTwo practical notes for concurrent servers. A spawned closure should capture
the port (a copied scalar), not the listener: a shared listener is
auto-guarded by a cown lock that a blocking accept would then hold. And
TCP is a byte stream: one read may return less than what the peer wrote,
so frame messages or read in a loop.
TlsStream: TLS client connections, backed by rustls inside the
plugin. Certificate verification uses the bundled Mozilla root set with the
server name taken from host; there are no configuration knobs (no custom
CAs, no server side yet). TlsStream mirrors Tcp, so code written against
read/write structurally accepts either:
| Method | Signature | Behavior |
|---|---|---|
TlsStream.connect(host, port) |
(string, int64) -> TlsStream! |
TCP connect + full handshake; certificate errors fail here |
conn.read(max) |
(int64) -> uint8[]! |
up to max decrypted bytes; empty at end-of-stream |
conn.write(data) |
(uint8[]) -> int64! |
encrypt and send all of data |
conn.close() |
() -> void! |
sends the TLS close notification |
import std.net.{ TlsStream }
let conn = TlsStream.connect("example.com", 443)!conn.write(to_bytes("GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n"))!println(to_text(conn.read(16)!)!) // HTTP/1.1 200 OKconn.close()!Everything here runs on either back end: sockets are fs
Files and the plugins execute natively under the interpreter too.
std.url
Section titled “std.url”import std.url.URIRFC 3986 URI parsing and reference resolution, implemented in pure Brass.
Parsed components retain percent-encoding so delimiters remain unambiguous.
An absent component is null, which differs from an empty component: the
query in http://host/path is absent, while the query in
http://host/path? is empty.
type URI = { scheme: string? authority: Authority? path: string query: string? fragment: string?}| Method | Signature | Behavior |
|---|---|---|
URI.parse(text) |
(string) -> URI! |
parse an absolute URI; a scheme is required |
URI.parse_reference(text) |
(string) -> URI! |
also accepts relative references |
uri.to_string() |
() -> string |
reassemble without decoding components |
uri.authority_string() |
() -> string? |
serialized authority, or null |
uri.resolve(reference) |
(string) -> URI! |
resolve a relative or absolute reference against an absolute base |
uri.path_segments() |
() -> string[]! |
split and percent-decode path segments |
uri.query_pairs() |
() -> QueryPair[]! |
decode the query as form-style key/value pairs |
The public supporting modules are:
| Import | API |
|---|---|
std.url.authority.Authority |
{ userinfo: string?, host: string, port: uint16? }; parse, is_ip_literal, to_string |
std.url.query.QueryPair |
{ key: string, value: string }; parse_all(query), format_all(pairs) |
std.url.percent |
decode(text) -> string!, encode(text, extra) -> string, encode_component(text) -> string |
std.url.validate |
CharClass, validate(text, class) -> string!; low-level component validation |
std.url.charset |
RFC character predicates such as is_unreserved and is_path_char |
std.url.text |
character-array helpers substr and index_of used by URI parsers |
QueryPair parsing treats & as the pair separator, the first = as the
key/value separator, and + as a space. Formatting emits percent-encoded
pairs and uses %20 for spaces.
Reference resolution removes . and .. path segments without collapsing
empty segments: for example, resolving g//./h preserves the double slash as
g//h.
std.http
Section titled “std.http”import std.http.{ fetch, HttpClient, HttpRequest, HttpResponse, Header }An HTTP/1.x client and message parser over std.net. fetch
supports HTTP and HTTPS, follows relative or absolute redirects, and returns
the final response.
type Header = { name: string, value: string }type HttpRequest = { method: string path: string version: string headers: Header[] body: uint8[]}type HttpResponse = { version: string status: int32 reason: string headers: Header[] body: uint8[]}| Function / method | Signature | Behavior |
|---|---|---|
fetch(url) |
(string) -> HttpResponse! |
GET an HTTP(S) URL; follows at most MAX_REDIRECTS redirects |
request(req) |
(HttpRequest) -> HttpResponse! |
plain HTTP using the request’s Host header |
HttpClient.http(host, port) |
(string, int32) -> HttpClient |
create a plain client |
HttpClient.https(host, port) |
(string, int32) -> HttpClient |
create a TLS client |
client.fetch(path) |
(string) -> HttpResponse! |
GET a path on that client |
client.request(req) |
(HttpRequest) -> HttpResponse! |
send a request on that client |
HttpRequest.parse(raw) |
(string) -> HttpRequest! |
parse a complete request string |
request.serialize() |
() -> uint8[] |
serialize the request line, headers, and body |
HttpResponse.parse(raw) |
(string) -> HttpResponse! |
parse a complete response string |
response.serialize() |
() -> uint8[] |
serialize the status line, headers, and body |
response.body_text() |
() -> string! |
decode the body as UTF-8 |
The client accepts only http and https URLs. Host authorities support DNS
names, addresses, and bracketed IPv6 literals, with port syntax and range
validated. Request/status lines and headers are checked strictly, including
header-name tokens and forbidden control characters in values.
Response parsing and the client frame the body from the headers: a
Transfer-Encoding: chunked body is decoded (chunk extensions and trailer
fields are read and discarded) and takes precedence over Content-Length;
any other transfer coding is an error. Otherwise a validated, non-negative
decimal Content-Length applies (duplicate values must agree), and the
client reads until connection close when both framing headers are absent.
Informational, 204, and 304 responses are bodyless regardless of framing
headers. Serializing a message does not add Host, Content-Length, or
other headers; callers that construct requests or responses must supply the
required headers themselves, with a chunked body already in chunked form.
Networking is unavailable in the browser playground.
std.data.json
Section titled “std.data.json”import std.data.json.{ JsonValue }A JSON value tree, parser, accessors, serializer, and a reflective decoder.
The whole surface hangs off JsonValue, so the type is the only name to import.
A pure-Brass module under the installed std package.
type JsonValue = | Null | Bool { value: bool } | Number { value: float64 } | String { value: string } | Array { value: JsonValue[] } | Object { values: _JsonObject } // a string -> JsonValue HashMapAn Object keeps its members in a HashMap (a refinement pinning the key
to string and the value to JsonValue), so get is a hash lookup. One
consequence: stringify renders object members in the map’s slot order,
not the source document’s ordering (stable for a given input).
| Function / method | Signature | Behavior |
|---|---|---|
JsonValue.parse(text) |
(string) -> JsonValue! |
whole input must be one JSON value |
j.stringify() |
-> string |
serialize back to JSON text |
j.as_bool() / j.as_number() / j.as_string() |
-> bool! / float64! / string! |
payload, or a decode error naming the expected kind |
j.is_null() |
-> bool |
|
j.get(key) |
(string) -> JsonValue! |
object field, or an error naming the missing field |
j.at(index) |
(int64) -> JsonValue! |
array element, range-checked |
j.into() |
-> infer! |
decode into the type the call site expects — see Reflection |
String parsing recognizes the complete JSON escape set (\", \\, \/,
\b, \f, \n, \r, \t, and \uXXXX), combines valid UTF-16 surrogate
pairs, and rejects lone surrogates and raw control characters. stringify
escapes quotes, backslashes, and controls back to valid JSON. Numbers use the
strict JSON grammar: no leading +, leading zero, missing integer or fraction,
or malformed exponent, and the parsed float64 must be finite.
Decoding a whole document into a typed structure combines parse and into:
import std.data.json.{ JsonValue }
type Address = { city: string, zip: int64 }type User = { name: string, age: int64, address: Address }
const src = "\{\"name\": \"Aki\", \"age\": 30, \"address\": \{\"city\": \"Tokyo\", \"zip\": 100\}\}"const u: User = JsonValue.parse(src)!.into()!println("{u.name} {u.age} {u.address.city}") // Aki 30 Tokyostd.data.toml
Section titled “std.data.toml”import std.data.toml.TomlValueA pure-Brass TOML value tree, parser, serializer, and reflective decoder.
type TomlValue = | String { value: string } | Integer { value: int64 } | Float { value: float64 } | Bool { value: bool } | Datetime { value: string } | Array { value: TomlValue[] } | Table { values } // string -> TomlValue HashMap| Function / method | Signature | Behavior |
|---|---|---|
TomlValue.parse(text) |
(string) -> TomlValue! |
parse a complete TOML document; the root is a table |
value.stringify() |
() -> string |
serialize as TOML; nested containers use inline form |
value.as_string() |
() -> string! |
extract a string |
value.as_integer() |
() -> int64! |
extract an integer |
value.as_float() |
() -> float64! |
extract a float |
value.as_bool() |
() -> bool! |
extract a boolean |
value.as_datetime() |
() -> string! |
extract the original date/time text |
value.is_table() |
() -> bool |
whether the value is a table |
value.get(key) |
(string) -> TomlValue! |
table entry or an error |
value.at(index) |
(int64) -> TomlValue! |
range-checked array element |
value.as_array() |
() -> TomlValue[]! |
array elements or an error |
value.keys() |
() -> string[]! |
table keys in unspecified map order |
value.into() |
() -> infer! |
decode a scalar or record into the call site’s expected type |
The parser supports comments, bare and quoted dotted keys, basic and literal
strings, decimal and radix integers, floats, booleans, arrays, inline tables,
table headers, and arrays of tables. Duplicate keys, reopened tables, and any
extension of an inline table are rejected. Integer leading-zero and underscore
placement rules are enforced for every radix, and radix overflow fails instead
of wrapping. Date and time values are retained as source text after their
lexical fields and calendar ranges are validated. stringify() escapes control
characters and writes nan, inf, and -inf in lowercase. Tables use
HashMap, so key order is not preserved by keys() or stringify().
Reflective decoding supports scalar and record targets recursively. It does
not decode a TOML array directly into a typed array; use as_array() and
decode its elements individually.