Standard library
Reed ships a standard library of ten modules, imported with the std: prefix:
use std.math.*;
export func check as "check";
func check() -> i32 {
let n: i32 = 12;
return n.gcd(18) + n.isqrt() - 9; // 6 + 3 - 9 = 0
}
It is written in Reed, lives in
crates/reedc-core/src/std/,
and is documented normatively by
spec section 5.2.
Browse the full standard library reference →
Every function with its signature, description, and parameters, one page per module. That
page is generated by reedc doc from the library's own source and served exactly as the
tool writes it — same HTML, same stylesheet, no theming from this site. This page explains
why each module is shaped the way it is; that one is the lookup table.
The modules
| Module | Provides |
|---|---|
std.array | fixed-length arrays, and the shared element types |
std.math | integer math: abs, gcd, isqrt, pow, clamp, saturating add |
std.bits | bit fields, reversal, byte swap, non-wrapping shifts |
std.ascii | character classification and case conversion |
std.str | strings: search, split, join, trim, parse, format, UTF-8 |
std.list | growable IntList, LongList, RefList |
std.vec | a growable vector generator, for any element type |
std.sort | heapsort, stable merge sort, binary search |
std.map | IntMap and StrMap hash maps |
std.set | IntSet hash set and BitSet bit vector |
std.rand | seeded pseudorandom generation |
Import every pub name with .*, or name what you want:
use std.str.*; // everything
use std.list.{IntList, int_list}; // just these
A method is imported under its qualified name, since that is its name (section 7.1):
use std.math.{i32.gcd}; // not `.gcd`
Only what you use is emitted
A standard library function appears in your module only if something can reach it.
Importing std.sort with .* and calling one function emits that function and whatever it
calls, and nothing else. Reachability is transitive, so an unused function's own callees
are dropped too.
$ reedc build uses_one_function.reed -o out.wat
$ grep -c '(func \$' out.wat
3 # check, sort, sift_down -- not the other 14 functions in std.sort
It never warns at you
Library code is not yours to fix, so its diagnostics are suppressed: no unused-decl for
the forty functions you did not call, and no escape-hatch-alternative for the wasm.*
calls in its internals. A clean program that imports all ten modules compiles with zero
diagnostics.
std.array
The foundation. Declares the array types the rest of the library shares —
I32Values, I64Values, F64Values, AnyValues — plus operations on them.
They live in one module for a concrete reason. Reed compiles a whole unit into one module
with no name mangling (section 5.1), so
if std.list and std.sort each declared their own I32Values, importing both would be a
hard error. Sharing the declaration also makes them the same type, which is what lets
IntList.to_array() feed sort() with no conversion.
use std.array.*;
export func check as "check";
func check() -> i32 {
let a: &I32Values = array_range(0, 10);
a.rotate(3);
return a.sum() - 45 + a[0] - 3; // sum unchanged by rotation; a[0] is now 3
}
std.math
Integer mathematics. WebAssembly has abs/min/max/sqrt instructions for floats only,
so the core library declares those on f32/f64 and this module
supplies the integer versions as real code.
abs min max min_u max_u clamp signum isqrt pow gcd lcm log2 log10
is_pow2 next_pow2 div_floor mod_floor sat_add sat_sub lerp is_prime
Nothing here traps. Where a result does not exist, the documented answer is a clamp or a
sentinel rather than a trap: isqrt of a negative is 0, log2(0) is -1.
Two of these are worth knowing about specifically:
use std.math.*;
export func check as "check";
func check() -> i32 {
// `/` truncates toward zero; `div_floor` rounds toward negative infinity.
let a: i32 = -7 / 2; // -3
let b: i32 = (-7).div_floor(2); // -4
// `%` takes the dividend's sign; `mod_floor` takes the divisor's, so it is
// usable directly as an array index.
let c: i32 = -7 % 3; // -1
let d: i32 = (-7).mod_floor(3); // 2
return a - b - 1 + c + 1 - d + 2;
}
std.bits
Bit manipulation above what the core library's single instructions cover.
mask_low bit_range set_bit_range bit with_bit reverse_bits bswap clo cto
parity lowest_bit clear_lowest_bit highest_bit shl_wide shr_wide sign_extend
pack_bytes byte
WebAssembly masks a shift count modulo the operand width, so x << 32 is x, not 0. That
is why mask_low(32) and shl_wide(32) exist and give the arithmetic answer:
use std.bits.*;
export func check as "check";
func check() -> i32 {
let all_ones: i32 = (32).mask_low(); // -1, where (1 << 32) - 1 gives 0
let zero: i32 = (1).shl_wide(32); // 0, where 1 << 32 gives 1
return all_ones + 1 + zero;
}
std.ascii
Character classification, on i32 values holding bytes.
is_digit is_alpha is_alnum is_space is_upper is_lower is_hex_digit is_punct
is_print is_control is_ascii to_lower to_upper digit_value digit_char
cmp_ignore_case
ASCII only, deliberately. Correct Unicode case conversion is locale-dependent, is not a
per-byte operation, and is not even length-preserving. Bytes above 127 pass through
unchanged rather than being mangled; use std.str's decode_utf8 for real code points.
std.str
Strings. A Str is array Str { mut i8 } — a garbage-collected byte array, not a pointer
into linear memory. A memory belongs to the module that declares it, and a library cannot
impose one on every importer.
Building one from source text uses a data segment, which is how a literal string reaches
a GC array:
use std.str.*;
data Greeting = "Hello, World";
export func check as "check";
func check() -> i32 {
let s: &Str = new Str[12] data<Greeting>(0);
let parts: &StrList = s.split_byte(44); // split on ','
let first: &Str = parts.at(0).trim();
return first.len() - 5 // "Hello"
+ (s.contains(new Str[12] data<Greeting>(0)) - 1)
+ (s.to_upper().byte(0) - 72); // 'H' is already uppercase
}
Searching len is_empty byte try_byte index_of last_index_of index_of_byte
contains starts_with ends_with count
Transforming clone concat slice substr to_lower to_upper trim trim_start
trim_end reverse repeat replace pad_start pad_end
Splitting split split_byte split_whitespace str_join str_concat_all
String lists StrList.at len is_empty index_of contains non_empty
Comparing equals equals_ignore_case compare hash
Numbers parse_int str_from_int
UTF-8 char_count decode_utf8 str_encode_utf8 is_valid_utf8
split and str_join are exact inverses: n separators always yield n + 1 pieces, so
str_join(s.split(sep), sep) reproduces s for every input. That contract is why split
keeps empty pieces; when they are noise rather than data, .non_empty() drops them.
StrList.at(i) returns a non-null &Str, so iterating a split result never needs its own
narrowing — the element type is ?Str only because a runtime-length GC array must be
default-initializable.
std.list
Growable lists. Capacity doubles, so n pushes cost O(n) copying in total.
use std.list.*;
export func check as "check";
func check() -> i32 {
let l: &IntList = int_list();
for i in 0..100 { l.push(i); }
l.insert(0, 999);
let (removed, _ok): (i32, i32) = l.remove(50);
return l.len() - 100 + removed - 49;
}
There are three list types because Reed has no type parameters. IntList and LongList
store their elements unboxed, which is the common case and avoids allocating an i31 per
element; RefList stores ?any for heterogeneous contents and its at returns a value you
narrow with is.
Note l.len(), not #l: a list's length differs from its backing array's capacity, and
that gap is the reason these are structs rather than bare arrays.
For an element type these three do not cover, see std.vec, which generates one.
std.vec
The same idea as std.list, minus the fixed element type: a
generator you instantiate per element type.
use std.vec.*;
comptime vec_of(f64, Samples, samples);
export func check as "check";
func check() -> i32 {
let s: &Samples = new_samples();
s.push(1.5);
s.push(2.5);
s.push(4.0);
var total: f64 = 0.0;
for i in 0..s.len() { total = total + s.at(i); }
if (total != 8.0) { return 1; }
return 0;
}
vec_of(T, Name, prefix) declares Name plus new_<prefix>(),
new_<prefix>_with_capacity(n), and the methods len, capacity, is_empty, reserve,
push, pop, at, get_or, set, insert, remove, clear, truncate, swap,
reverse, extend, clone, and to_array. It takes both a Name and a prefix because
a type must be PascalCase and a function snake_case.
vec_search_of(T, Name) adds index_of, contains, count_of, and equals to a vector
vec_of already declared. It is separate because == requires numeric operands, so those
four do not compile for a reference element type — folding them into vec_of would make
every reference instantiation an error.
use std.vec.*;
struct Point { x: mut i32, y: mut i32 }
comptime vec_of(?Point, PointVec, point_vec);
export func check as "check";
func check() -> i32 {
let v: &PointVec = new_point_vec();
v.push(new Point { x: 3, y: 4 });
v.push(null);
let first: ?Point = v.at(0);
if (first is &Point) {
return first->x + first->y - 7 + v.len() - 2;
}
return 1;
}
The element type must be defaultable — numeric, or a nullable ?Foo — because growth
allocates a default-initialized array. That is why a vector of structs is spelled
?Point and its at returns a value you narrow with is. Passing &Point reports that
&Point has no default value, at the instantiation.
std.sort
Sorting and searching, over &I32Values from std.array.
sort is an in-place heapsort: O(n log n) worst case, no allocation, no recursion.
sort_stable is a bottom-up merge sort: also O(n log n), stable, using one scratch
array. Quicksort is deliberately absent — its worst case is quadratic on already-sorted
input, which is common.
Ordering is a function reference, so any rule you can write works:
use std.sort.*;
use std.array.*;
func by_last_digit(a: i32, b: i32) -> i32 {
return (a % 10) - (b % 10);
}
export func check as "check";
func check() -> i32 {
let a: &I32Values = new I32Values[4]{};
a[0] = 25; a[1] = 13; a[2] = 47; a[3] = 31;
sort_by(a, &by_last_digit);
return a[0] - 31 + a[3] - 47; // 31, 13, 25, 47 by last digit
}
binary_search returns (index, found), where a miss gives the insertion point — so one
call serves both lookup and ordered insertion. lower_bound/upper_bound delimit a run of
equal values.
std.map
IntMap (i32 keys) and StrMap (string keys, compared by content). Both are
open-addressed with linear probing and grow at 75% load.
use std.map.*;
export func check as "check";
func check() -> i32 {
let counts: &IntMap = int_map();
for i in 0..100 { counts.increment(i % 7, 1); }
return counts.len() - 7 + counts.get_or(0, 0) - 15;
}
get returns (value, found) rather than a sentinel, because every i32 is a legitimate
stored value and no sentinel could distinguish the two cases.
Deletion leaves a tombstone rather than an empty slot: blanking a slot would cut the probe chain running through it and make later entries unfindable. The load factor counts tombstones, so a map churned by repeated insert/remove rehashes rather than degrading.
Iteration order is unspecified and changes when the map grows.
std.set
IntSet is a hash set built on IntMap. BitSet is a bit vector over 0..n: a thousand
members cost 128 bytes, and union/intersection/difference process 32 members per word.
use std.set.*;
export func check as "check";
func check() -> i32 {
let primes: &BitSet = bit_set(50);
primes.add(2); primes.add(3); primes.add(5); primes.add(7);
var total: i32 = 0;
var p: i32 = primes.next(0);
loop walk() {
if (p < 0) { break walk(); }
total = total + p;
p = primes.next(p + 1);
continue walk();
}
return total - 17 + primes.len() - 4;
}
BitSet.next is how you enumerate one: it skips empty words wholesale and uses ctz within
a word, so a sparse set costs time proportional to its words rather than its capacity.
std.rand
Seeded pseudorandom generation. Not cryptographic — every generator here has state recoverable from a few outputs. WebAssembly has no entropy source, so real randomness must come from a host import.
The generator is an object you create, not a hidden global, which is what makes a run reproducible:
use std.rand.*;
use std.array.*;
export func check as "check";
func check() -> i32 {
let r: &Random = random_seed(42);
let deck: &I32Values = array_range(0, 52);
r.shuffle(deck);
// A shuffle is a permutation, so the sum is unchanged.
return deck.sum() - 1326 + (r.below(6) < 6) - 1;
}
below(n) is uniform, not merely bounded: it rejects the unbalanced tail of the range
rather than taking next() % n, which over-represents low residues. unit() builds its
f64 from 53 bits, the full mantissa width, so every representable value in [0, 1) is
reachable.
Gotchas
A literal receiver needs a type when two types share a method name. pow is declared on
both i32 and i64, so (2).pow(10) cannot pick one
(section 7.1 forbids guessing, since the
choice would depend on declaration order). Bind it:
let two: i32 = 2;
let k: i32 = two.pow(10); // fine
// let k: i32 = (2).pow(10); // error: needs a contextual type
Methods declared on only one type, like isqrt, take a literal receiver fine.
Names are global. The library shares your program's flat namespace, so declaring your
own IntList while importing std.list is a duplicate-declaration error. Import only what
you need (use std.list.{IntList};) if you want to keep the rest of the namespace
clear.
The library allocates. Every Str and every list operation returns a fresh GC object.
That is the right default for correctness, but a hot loop that concatenates strings should
use str_concat_all (one allocation) rather than repeated concat (one per step).