Skip to main content

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.

Generated reference

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

ModuleProvides
std.arrayfixed-length arrays, and the shared element types
std.mathinteger math: abs, gcd, isqrt, pow, clamp, saturating add
std.bitsbit fields, reversal, byte swap, non-wrapping shifts
std.asciicharacter classification and case conversion
std.strstrings: search, split, join, trim, parse, format, UTF-8
std.listgrowable IntList, LongList, RefList
std.veca growable vector generator, for any element type
std.sortheapsort, stable merge sort, binary search
std.mapIntMap and StrMap hash maps
std.setIntSet hash set and BitSet bit vector
std.randseeded 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).