Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The prelude

Thirty-seven names are bound in every Praxis file before the first line runs. There is no import, no use, and no way to get more: a program is one file, and the prelude is the whole free-function surface of the language. Everything else is a method, and those are in the method catalog.

The list lives in crates/praxis-stdlib/src/prelude.rs, which is the same table the type checker and the editor read. It falls into five groups.

Output and control

NameSignatureWhat it does
out(T) -> UnitWrite one value to stdout, followed by a newline.
dbg(T) -> TWrite one value to stderr and return it unchanged.
panic(T) -> NeverStop with an explicit message and raise a fault.
assert(Bool) -> UnitStop if the condition is false.

out, dbg and panic take any type and render it through the value’s own formatter, so panic(candidate) says what the candidate was:

out(42)
out("a line")
out([1, 2, 3])
out((1, "x"))
out(Some(3))

var squares = Map()
squares.insert(2, 4)
squares.insert(3, 9)
out(squares)
42
a line
[1, 2, 3]
(1, x)
Some(3)
{2: 4, 3: 9}

dbg is the identity on types, which is what lets it wrap any subexpression without changing what the program computes — including one whose value you then panic on. Both halves of this go to stderr:

var doubled = dbg(21) * 2
panic(doubled)
21
error: program faulted: panic: 42

Backtrace:
#0   <entry>

  locals:
    doubled: Int = 42
  temps:
    <tmp#1: Int> @ "21" = 21
    <tmp#2: Int> @ "dbg(21)" = 21
    <tmp#3: Int> @ "2" = 2
    <tmp#4: Int> @ "dbg(21) * 2" = 42
    <tmp#6> @ "panic(doubled)" = <uninit>

panic’s result type is Never, so a function can end on one and still satisfy a declared result type: fn pick(v: Vec[Int]) -> Int { if v.is_empty() { panic("no candidates") }; v[0] } type-checks, and so does fn boom() -> Int { panic("x") }.

assert is the one name here that is monomorphic. It takes a Bool and nothing else, so assert(1) is a type error rather than a call that silently accepts anything, and it takes exactly one argument — a message parameter has no spelling, because a name in Praxis has exactly one signature.

assert(1 + 1 == 3)
error: program faulted: assertion failed

Backtrace:
#0   <entry>

  temps:
    <tmp#1: Int> @ "1" = 1
    <tmp#2: Int> @ "1" = 1
    <tmp#3: Int> @ "1 + 1" = 2
    <tmp#4: Int> @ "3" = 3
    <tmp#5: Bool> @ "1 + 1 == 3" = false
    <tmp#6: Unit> @ "assert(1 + 1 == 3)" = <uninit>

Both panic and assert raise ordinary faults, which is why the output above carries a backtrace and the locals. Under the default --debug auto — stdin and stdout both a terminal — they drop you into the crash debugger instead of printing. That is the reason they are faults rather than a write to stderr followed by an exit: a panic that bypassed the fault path is a panic you cannot debug.

Numeric helpers

Seven functions on Int, and two nullary Float functions.

NameSignatureWhat it does
abs(Int) -> IntAbsolute value. Faults on Int’s minimum, which has no positive counterpart.
sign(Int) -> Int-1, 0 or 1. Total.
min(Int, Int) -> IntThe smaller of two.
max(Int, Int) -> IntThe larger of two.
clamp(Int, Int, Int) -> Intclamp(value, low, high). Faults if low > high.
gcd(Int, Int) -> IntNon-negative greatest common divisor. gcd(0, 0) is 0.
lcm(Int, Int) -> IntNon-negative least common multiple; 0 if either operand is 0. Faults if the result leaves Int.
pi() -> Floatπ.
e() -> FloatEuler’s number.

pi and e are nullary functions, not bare constants: pi(), not pi.

out(abs(-7))
out(sign(-7))
out(min(3, 9))
out(max(3, 9))
out(clamp(12, 0, 10))
out(gcd(12, 18))
out(lcm(4, 6))
out(pi())
out(e())
7
-1
3
9
10
6
12
3.141592653589793
2.718281828459045

All seven are Int functions and none of them is generic. Float carries its own abs, sign, min and max as methods — x.abs(), x.min(y) — so the free function never has to choose a lowering per instantiation. clamp, gcd and lcm have no Float counterpart at all; (2.5).clamp(0.0, 1.0) is a Y110. Handing a Float to one of the free functions is an ordinary type error:

$ praxis check prelude-min-is-int.px
error[Y001]: expected (Int, Int) -> Int, found (Float, Float) -> ?T

  prelude-min-is-int.px:1:5
  1 | out(min(1.0, 2.0))
    |     ^^^^^^^^^^^^^ expected (Int, Int) -> Int, found (Float, Float) -> ?T

praxis: 1 error(s)

Collection constructors

Nine names. Called with no arguments, each builds an empty collection and the element type comes from what you then put in. Two of them — Vec and Grid — also take a size and a fill, and the argument count is what chooses between the two shapes.

NameSignatureNotes
Vec() -> Vec[T]Ordered, growable, indexed from 0.
Vec(Int, T) -> Vec[T]n slots, every one the fill.
Deque() -> Deque[T]Double-ended queue.
Map() -> Map[K, V]Hash map.
Set() -> Set[T]Hash set.
Counter() -> Counter[T]A map whose absent values read as zero.
MinHeap() -> MinHeap[T]Priority queue, smallest first.
MaxHeap() -> MaxHeap[T]Priority queue, largest first.
Grid() -> Grid[T]2D grid. Grid() is the empty 0 × 0 one.
Grid(Int, Int, T) -> Grid[T]A w × h board, every cell the fill.
BitSet() -> BitSetCompact set of non-negative integers. Takes no type argument.

Only those two are sized, and the rest of the language has no arity overloading at all — Set(3, 0) is an error that says the function takes zero arguments. The two exceptions are the collections whose contents are addressed by position, which is what makes “n of them” mean something: a sized Set is n copies of one element in a set, which is one element, and a sized Map has no answer at all for what its keys would be. This narrows “a name has one signature” without reopening it — the shape is chosen by counting arguments, a syntactic fact available before any argument is typed, and never by looking at their types.

var v = Vec()
v.push(1)

var d = Deque()
d.push_front("front")

var m = Map()
m.insert("k", 1)

var s = Set()
s.insert(3)

var c = Counter()
c.inc("x")

var lo = MinHeap()
lo.push(5)

var hi = MaxHeap()
hi.push(5)

var g = Grid()

var sized = Vec(3, 0)
var board = Grid(3, 2, '.')

var b = BitSet()
b.insert(3)

out(v)
out(d)
out(m)
out(s)
out(c)
out(lo.peek())
out(hi.peek())
out(g.width())
out(b)
out(sized)
out(board)
out(board.width())
[1]
[front]
{k: 1}
{3}
{x: 1}
5
5
0
{3}
[0, 0, 0]
[., ., ., ., ., .]
3

[1, 2, 3] is a Vec literal, so Vec() is only needed when you want an empty one. The tenth collection, Range, has no constructor: a range is written 0..n or 0..=n, and Range is a type name rather than a value — Range() is N001: 'Range' is not defined.

A Map key, a Set element and a Counter key have to be usable as keys, and a heap element has to be orderable — but the constructor is not where that is asked. The requirement is checked at the first method call on the collection, because that is where a program actually puts a value into one, so the construction below is accepted and the len() on the next line is what is refused:

var seen: Set[Vec[Int]] = Set()
out(seen.len())
$ praxis check prelude-key-bound.px
error[Y014]: a value of type `Vec[Int]` can change after it is stored, so it cannot be used as a key

  prelude-key-bound.px:2:10
  2 | out(seen.len())
    |          ^^^ a value of type `Vec[Int]` can change after it is stored, so it cannot be used as a key

help: use a value that cannot change — a number, `Text`, or a tuple of those

praxis: 1 error(s)

See capabilities.

Optionality

NameSignatureWhat it is
OptionThe type name. Option[T] is a legal annotation.
Some(T) -> Option[T]Wrap a value.
NoneOption[T]The absent value. Not a call — None, never None().

Option[T] is domain-level absence and not an error channel. It is what Map.get, Grid.find, the sequence find/position, the checked_* arithmetic methods and the goal-directed graph walks answer with.

var seen = Map()
seen.insert("a", 1)

var found: Option[Int] = seen.get("a")
var missing = seen.get("z")

out(found)
out(missing)

match missing {
  Some(n) => out(n)
  None => out("nothing there")
}
Some(1)
None
nothing there

Some and None are the only enum variants the prelude declares; everything else about them is what enums says about any variant.

Graph helpers

Twelve closure-driven walks. None of them takes a graph object — there is no graph type — so a program describes its graph by giving a start state and a function from a state to its neighbours.

The name says which of three questions a helper answers. The bare name is the whole walk, _distance is what the route to a goal came to, and _path is that route. So knowing one family is knowing all of them, and a family with no whole-walk meaning has no bare name — which is why A* is only a_star_distance and a_star_path. Which route depends on the search, and each row says which; only depth-first declines to promise a cheapest one.

NameSignatureAnswers
bfs(T, (T) -> Vec[T]) -> Vec[T]Every state reached, in breadth-first order.
bfs_distance(T, (T) -> Vec[T], (T) -> Bool) -> Option[Int]Steps to the first goal state, or None.
bfs_path(T, (T) -> Vec[T], (T) -> Bool) -> Option[Vec[T]]A shortest route to the first goal state, start to goal inclusive, or None.
dfs(T, (T) -> Vec[T]) -> Vec[T]Every state reached, in depth-first order.
dfs_distance(T, (T) -> Vec[T], (T) -> Bool) -> Option[Int]Steps along the route depth-first search reached a goal by, which need not be the fewest, or None.
dfs_path(T, (T) -> Vec[T], (T) -> Bool) -> Option[Vec[T]]The route depth-first search reached a goal by, which need not be a shortest one, start to goal inclusive, or None.
dijkstra(T, (T) -> Vec[T], (T, T) -> Int) -> Map[T, Int]Least cost to each reachable state. An unreachable state is simply absent.
dijkstra_distance(T, (T) -> Vec[T], (T, T) -> Int, (T) -> Bool) -> Option[Int]Cost of the cheapest route to a goal, or None.
dijkstra_path(T, (T) -> Vec[T], (T, T) -> Int, (T) -> Bool) -> Option[Vec[T]]The cheapest route to a goal, start to goal inclusive, or None.
a_star_distance(T, (T) -> Vec[T], (T, T) -> Int, (T) -> Int, (T) -> Bool) -> Option[Int]Cost of the cheapest route to a goal, or None.
a_star_path(T, (T) -> Vec[T], (T, T) -> Int, (T) -> Int, (T) -> Bool) -> Option[Vec[T]]The cheapest route to a goal, start to goal inclusive, or None.
flood_fill(T, (T) -> Vec[T]) -> Set[T]Every state reached, unordered.

The first parameter is always the start state and every other parameter is a function of it. The weight function takes two adjacent states, the heuristic takes one state and estimates the remaining cost, and the goal is a predicate rather than a value, so a search can stop on a property.

Only the goal-directed helpers answer with an Option, and that pairing is the rule: a walk that always reaches at least its own start cannot fail, and dijkstra needs no Option because “unreachable” is absence from its table. An Option[Vec[T]] is for the same reason an Option[Int] is, and an empty Vec could not stand in for it: a route that was found always holds at least its own start, so “no route” and “a route of nothing” would be the same value.

flood_fill is the unordered twin of bfs and takes no goal at all, so it has neither a _distance nor a _path form — a Set has no route.

fn steps(n: Int) -> Vec[Int] {
  var next = Vec()
  if n * 2 <= 20 { next.push(n * 2) }
  if n + 1 <= 20 { next.push(n + 1) }
  next
}

out(bfs(1, |n| steps(n)).len())
out(dfs(1, |n| steps(n)).len())
out(flood_fill(1, |n| steps(n)).len())
out(dijkstra(1, |n| steps(n), |a, b| b - a).len())

out(bfs_distance(1, |n| steps(n), |n| n == 20))
out(bfs_path(1, |n| steps(n), |n| n == 20))
out(dfs_distance(1, |n| steps(n), |n| n == 20))
out(dfs_path(1, |n| steps(n), |n| n == 20))
out(dijkstra_distance(1, |n| steps(n), |a, b| b - a, |n| n == 20))
out(dijkstra_path(1, |n| steps(n), |a, b| b - a, |n| n == 20))
out(a_star_distance(1, |n| steps(n), |a, b| b - a, |n| 20 - n, |n| n == 20))
out(a_star_path(1, |n| steps(n), |a, b| b - a, |n| 20 - n, |n| n == 20))

out(bfs_distance(1, |n| steps(n), |n| n == 21))
out(bfs_path(1, |n| steps(n), |n| n == 21))
20
20
20
20
Some(5)
Some([1, 2, 4, 5, 10, 20])
Some(8)
Some([1, 2, 4, 8, 16, 17, 18, 19, 20])
Some(19)
Some([1, 2, 4, 5, 10, 20])
Some(19)
Some([1, 2, 4, 5, 10, 20])
None
None

All twenty states are reachable, so the four whole-walk answers all hold twenty entries and differ only in shape — a Vec in visit order twice, a Set, and a Map of costs. The goal-directed pairs are where the names earn themselves: bfs_distance says five steps and bfs_path names the six states they pass through, while dfs_distance says eight, because depth-first reaches 20 by the branch it happened to descend rather than by a shortest route. dijkstra and A* agree with each other on both the cost and the route, since an admissible heuristic changes how much of the graph gets opened and not what is found. The last two calls ask for a goal the graph does not contain, and both forms answer None rather than a sentinel.

Every walk remembers where it has been, so the state type has to be usable as a key: a number, a Text, a Char, a tuple of those, or a record or enum of those. A Vec state is refused at the call site, with the reason rather than the rule:

$ praxis check prelude-graph-state.px
error[Y014]: a value of type `Vec[Int]` can change after it is stored, so it cannot be used as a key

  prelude-graph-state.px:7:5
  7 | out(bfs([1], |s| step(s)).len())
    |     ^^^^^^^^^^^^^^^^^^^^^ a value of type `Vec[Int]` can change after it is stored, so it cannot be used as a key

help: use a value that cannot change — a number, `Text`, or a tuple of those

praxis: 1 error(s)

Which of these can fault

Most prelude names cannot fail. The ones that can:

NameFaultWhen
panicpanicAlways. That is the point.
assertassertion failedThe condition was false.
absinteger overflowThe argument is Int’s minimum.
clampempty rangelow > high. There is no value to answer with, and inventing one would be a guess.
gcdinteger overflowOnly for Int’s minimum with itself, whose answer is 2⁶³.
lcminteger overflowThe multiple does not fit an Int, which happens easily.
the twelve graph walkswhatever the closures raiseYour neighbour, weight, heuristic or goal function faulted.
Vec(n, fill)size or extent out of rangen is negative, or larger than the runtime will allocate at a stroke (2²⁸).
Grid(w, h, fill)size or extent out of rangeAn extent is negative, or w × h is past 2²⁸ cells.

out, dbg, sign, min, max, pi, e and Some cannot fault, and neither can any collection constructor called with no arguments — there is nothing you gave it for it to refuse. The two sized forms are the exception, and the size is the reason: it is an ordinary Int computed at run time, so a negative or absurd one cannot be caught at praxis check and is a fault instead. See the fault model for what happens after a fault.

They are ordinary bindings

A prelude name is a normal binding in the file’s root scope, so a var of the same name shadows it for the rest of the file, exactly as any other shadow works:

var max = 10
out(max + 1)
11

Worth knowing mostly so that “max is not a function” stops being mysterious once you have used the name for something else.

What is not here

The type names Int, Text, Bool, Char, Float, Unit and Never are also in scope, as annotations, and they are the whole list: any other name in type position is N002: unknown type.

There is nothing else. No I/O beyond out and dbg, no clock, no randomness, no file access, and no import that would add one — a program’s input arrives through read, and its answer leaves through out.