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 method catalog

Every method in Praxis is a row in one table. There are 149 of them, they live in crates/praxis-stdlib/src/builtins.rs, and there is no way to add a 150th from a program: the language has no impl, no traits, no extension methods, and a record carries fields but no methods. This chapter is that table.

The closedness is load-bearing rather than a limitation the compiler tolerates. Because the catalog is the complete method universe, a name it does not carry at that arity can never resolve against any receiver — so fn f(x) { x.nope() } is refused at check time, before anything has said what x is.

How to read these tables

Method is the row’s name and the type pattern of each parameter; Result is its result pattern. Both are rendered the way the compiler prints a type: T, U, K, V and Acc are type variables, two occurrences of one name in a row are the same type, (Int, Int) is a tuple, and (T) -> Bool is a closure parameter. A nullary collection prints bare, so it is BitSet and not BitSet[].

Mutates is the row’s purity flag. The impure rows are exactly the ones that change the receiver, and the flag is visible from a program: the crash debugger’s p expression evaluator refuses a call to an impure method, because a debugger that mutates a faulted state cannot resume it.

var v = [1, 2, 3]
out(v[9])

That faults, and the debugger it drops into will evaluate one of these two calls and not the other:

error: program faulted: index out of bounds

Backtrace:
#0   <entry>

  locals:
    v: Vec[Int] = [1, 2, 3]
  temps:
    <tmp#1: Vec[Int]> @ "[1, 2, 3]" = [1, 2, 3]
    <tmp#2: Int> @ "1" = 1
    <tmp#3: Unit> = Unit
    <tmp#4: Int> @ "2" = 2
    <tmp#5: Unit> = Unit
    <tmp#6: Int> @ "3" = 3
    <tmp#7: Unit> = Unit
    <tmp#9: Int> @ "9" = 9
    <tmp#10: Int> @ "v[9]" = <uninit>
    <tmp#11: Unit> @ "out(v[9])" = <uninit>
Entered crash debugger. 1 frame(s). Type `help` for commands.
Praxis crash> p v.len()
3
Praxis crash> p v.push(4)
error: method `push` is impure (may mutate state) — `p` rejects mutating expressions
Praxis crash> quit

Faults is whether the call can raise a runtime fault. For a row backed by a runtime wrapper this is the wrapper’s own declaration in the ABI manifest, which is what puts a fault check after the call — so “yes” means the check is emitted, not that you are likely to trigger it. Vec.push says yes for a type-mismatch case a well-typed program cannot reach; Vec.get says yes because indexing off the end is the everyday one.

Allocates is whether the call may allocate, and therefore whether its call site is a garbage-collection safepoint. It is derived from the same manifest row, which is why len() says yes on every collection: the count comes back as a freshly boxed Int.

Neither flag is restated per method — both are read off the wrapper the row lowers to, so a row cannot disagree with the code it calls. Thirty-one of the thirty-five pipeline rows have no wrapper to read: the compiler fuses them into the loop, so their tables below carry no Allocates column and their Faults column is what the fused code does rather than a manifest row.

Sequence collections

Vec[T]

MethodResultMutatesFaultsAllocatesWhat it does
get(Int)TnoyesnoThe element at index; faults IndexOutOfBounds if out of range.
is_empty()BoolnononoTrue iff the vector has no elements.
len()IntnonoyesNumber of elements in the vector.
push(T)UnityesyesyesAppend a value to the end; returns Unit.
to_text()TextnoyesyesThese Chars as one Text, with nothing between them; the element type must be Char.
var v = [10, 20, 30]

out(v.len())
out(v.is_empty())
out(v.get(1))

v.push(40)
out(v)
3
false
20
[10, 20, 30, 40]

push is the only way a vector grows. v[v.len()] = x is an IndexOutOfBounds fault and not an append.

Deque[T]

MethodResultMutatesFaultsAllocatesWhat it does
get(Int)TnoyesnoThe element at index (0-based from the front); faults if out of range.
is_empty()BoolnononoTrue iff the deque has no elements.
len()IntnonoyesNumber of elements in the deque.
pop_back()TyesyesnoRemove and return the back element; faults if empty.
pop_front()TyesyesnoRemove and return the front element; faults if empty.
push_back(T)UnityesyesyesAppend a value to the back; returns Unit.
push_front(T)UnityesyesyesPrepend a value to the front; returns Unit.
var d = Deque()
d.push_back(2)
d.push_back(3)
d.push_front(1)

out(d)
out(d.len())
out(d.get(0))
out(d.pop_front())
out(d.pop_back())
out(d.is_empty())
[1, 2, 3]
3
1
1
3
false

Index 0 is the front, whichever end you have been pushing to.

Keyed collections

Map[K, V]

MethodResultMutatesFaultsAllocatesWhat it does
contains(K)BoolnononoTrue iff key is present in the map.
get(K)Option[V]nonoyesThe value for key as Some(value), or None if absent.
insert(K, V)UnityesnoyesSet key to value, replacing any prior value; returns Unit.
is_empty()BoolnononoTrue iff the map has no entries.
keys()Vec[K]nonoyesEvery key, as a Vec[K], ordered with values().
len()IntnonoyesNumber of entries in the map.
remove(K)UnityesnonoRemove key if present; returns Unit.
values()Vec[V]nonoyesEvery value, as a Vec[V], ordered with keys().
var m = Map()
m.insert("a", 1)
m.insert("b", 2)

out(m.len())
out(m.contains("a"))
out(m.get("a"))
out(m.get("z"))
out(m.keys())
out(m.values())

m.remove("a")
out(m)
out(m.is_empty())
2
true
Some(1)
None
[a, b]
[1, 2]
{b: 2}
false

get answers an Option; m[key] faults on a missing key. Those are the two halves of one question and the spelling picks which you meant.

keys() and values() answer Vecs in a fixed, deterministic order — by the key’s own order, the same one sorted() uses, so an Int key comes out numeric — and the two are index-aligned, so keys()[i] and values()[i] belong together. To get both at once, walk the map: for kv in m and the pipeline rows below both yield (K, V) pairs, and m.to_vec() is the Vec[(K, V)].

Set[T]

MethodResultMutatesFaultsAllocatesWhat it does
contains(T)BoolnononoTrue iff value is in the set.
insert(T)UnityesnoyesAdd value to the set; returns Unit.
is_empty()BoolnononoTrue iff the set has no elements.
len()IntnonoyesNumber of elements in the set.
remove(T)UnityesnonoRemove value if present; returns Unit.
var s = Set()
s.insert(1)
s.insert(1)
s.insert(2)

out(s.len())
out(s.contains(2))

s.remove(2)
out(s)
out(s.is_empty())
2
true
{1}
false

Counter[T]

MethodResultMutatesFaultsAllocatesWhat it does
get(T)IntnonoyesThe count for key, or zero if absent (never faults).
inc(T)UnityesyesyesIncrement the count for key by one; returns Unit.
is_empty()BoolnononoTrue iff the counter has no keys.
keys()Vec[T]nonoyesEvery key, as a Vec[T], ordered with values().
len()IntnonoyesNumber of distinct keys in the counter.
values()Vec[Int]nonoyesEvery count, as a Vec[Int], ordered with keys().
var c = Counter()
c.inc("x")
c.inc("x")
c.inc("y")

out(c.get("x"))
out(c.get("never seen"))
out(c.len())
out(c.keys())
out(c.values())
out(c.is_empty())
2
0
2
[x, y]
[2, 1]
false

A Counter is the collection whose absent values read as zero, so get and c[key] never fault and len() counts the keys that were actually touched.

Priority queues and bit sets

MinHeap[T]

MethodResultMutatesFaultsAllocatesWhat it does
is_empty()BoolnononoTrue iff the min-heap has no elements.
len()IntnonoyesNumber of elements in the min-heap.
peek()TnoyesnoThe smallest element without removing it; faults if empty.
pop()TyesyesnoRemove and return the smallest element; faults if empty.
push(T)UnityesnoyesPush a value onto the min-heap; returns Unit.

MaxHeap[T]

MethodResultMutatesFaultsAllocatesWhat it does
is_empty()BoolnononoTrue iff the max-heap has no elements.
len()IntnonoyesNumber of elements in the max-heap.
peek()TnoyesnoThe largest element without removing it; faults if empty.
pop()TyesyesnoRemove and return the largest element; faults if empty.
push(T)UnityesnoyesPush a value onto the max-heap; returns Unit.
var lo = MinHeap()
lo.push(5)
lo.push(1)
lo.push(3)

out(lo.len())
out(lo.peek())
out(lo.pop())
out(lo.is_empty())

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

out(hi.peek())
out(hi.pop())
3
1
1
false
5
5

peek is pure and pop is not, which is the only difference between them in this table and the whole difference at the call site.

BitSet

MethodResultMutatesFaultsAllocatesWhat it does
contains(Int)BoolnononoTrue iff the bit for the integer is set.
insert(Int)UnityesyesyesSet the bit for a non-negative integer; returns Unit.
is_empty()BoolnononoTrue iff no bits are set.
len()IntnonoyesNumber of set bits (popcount).
remove(Int)UnityesnonoClear the bit for an integer; returns Unit.
var b = BitSet()
b.insert(3)
b.insert(70)

out(b.contains(3))
out(b.contains(4))
out(b.len())

b.remove(3)
out(b)
out(b.is_empty())
true
false
2
{70}
false

insert faults on a negative or oversized member; remove does not, because clearing a bit that was never in range is not a question the set has to answer. contains is the one row in the catalog that lowers to a dedicated scalar-producing instruction rather than a call, which is why it is not a safepoint.

Grid[T]

MethodResultMutatesFaultsAllocatesWhat it does
around4((Int, Int))Around4nonoyesThe 4 orthogonal neighbors by name: { up, left, right, down }, each Some((x, y)) or None off the grid.
around8((Int, Int))Around8nonoyesThe 8 neighbors by name, in reading order: { up_left, up, up_right, left, right, down_left, down, down_right }, each Some((x, y)) or None.
cells()Vec[T]nonoyesAll cells in row-major order, as a Vec.
column(Int)Vec[T]noyesyesColumn x as a Vec; faults if out of range.
contains(Int, Int)BoolnononoTrue iff (x, y) is within the grid.
count4((Int, Int), T)IntnonoyesHow many of the 4 orthogonal neighbors hold value. A neighbor off the grid has no cell and is not counted.
count4_where((Int, Int), (T) -> Bool)IntnoyesyesHow many of the 4 orthogonal neighbors hold a cell the closure accepts. A neighbor off the grid has no cell, so the closure never sees one.
count8((Int, Int), T)IntnonoyesHow many of the 8 neighbors hold value. A neighbor off the grid has no cell and is not counted.
count8_where((Int, Int), (T) -> Bool)IntnoyesyesHow many of the 8 neighbors hold a cell the closure accepts. A neighbor off the grid has no cell, so the closure never sees one.
find(T)Option[(Int, Int)]nonoyesThe first (x, y) whose cell equals value as Some((x, y)), or None.
find_all(T)Vec[(Int, Int)]nonoyesAll (x, y) positions whose cell equals value, as a Vec.
get(Int, Int)TnoyesnoThe cell at (x, y); faults if out of range.
height()IntnonoyesThe number of rows.
neighbors4((Int, Int))Vec[(Int, Int)]nonoyesThe 4 orthogonal in-bounds neighbors of a point, as a Vec of (x, y).
neighbors8((Int, Int))Vec[(Int, Int)]nonoyesThe 8 in-bounds neighbors of a point, as a Vec of (x, y).
positions()Vec[(Int, Int)]nonoyesAll (x, y) positions in row-major order, as a Vec.
rotate_left()Grid[T]nonoyesA copy rotated 90° counter-clockwise.
rotate_right()Grid[T]nonoyesA copy rotated 90° clockwise.
row(Int)Vec[T]noyesyesRow y as a Vec; faults if out of range.
set(Int, Int, T)UnityesyesnoSet the cell at (x, y); faults if out of range.
transpose()Grid[T]nonoyesA transposed copy (rows ↔ columns).
width()IntnonoyesThe number of columns.

A grid is indexed (x, y) with x the column and y the row, and positions(), cells() and find_all() walk it in row-major order.

Around4 and Around8 are the catalog’s only named result types, and they are records, not collections: read the field with a dot, a.up, and every one of them is an Option[(Int, Int)]. Around4 is the plus with the centre skipped; Around8 is the whole 3×3 block in reading order, again without the centre. Neither has a constructor — a grid is the only thing that makes one — and the order the rows above list the fields in is the order the runtime lays the value out in, which is why it is written that way and not alphabetically.

var g = read grid(one_of(".#"))
var wall = '#'

out(g.width())
out(g.height())
out(g.get(1, 0))
out(g.contains(3, 0))
out(g.row(0))
out(g.column(1))
out(g.cells())
out(g.positions().len())
out(g.neighbors4((1, 1)))
out(g.neighbors8((0, 0)))
out(g.around4((1, 1)))
out(g.around8((0, 0)))
out(g.around4((1, 1)).right)
out(g.count4((1, 1), wall))
out(g.count8((1, 1), wall))
out(g.count4_where((1, 1), |c| c != wall))
out(g.count8_where((1, 1), |c| c != wall))
out(g.find(wall))
out(g.find_all(wall))
out(g.transpose().row(0))
out(g.rotate_left().row(0))
out(g.rotate_right().row(0))

g.set(0, 0, wall)
out(g.row(0))

with input

.#.
..#
3
2
#
false
[., #, .]
[#, .]
[., #, ., ., ., #]
6
[(1, 0), (0, 1), (2, 1)]
[(1, 0), (0, 1), (1, 1)]
{ up: Some((1, 0)), left: Some((0, 1)), right: Some((2, 1)), down: None }
{ up_left: None, up: None, up_right: None, left: None, right: Some((1, 0)), down_left: None, down: Some((0, 1)), down_right: Some((1, 1)) }
Some((2, 1))
2
2
1
3
Some((1, 0))
[(1, 0), (2, 1)]
[., .]
[., #]
[., .]
[#, #, .]

neighbors4 and neighbors8 return only the in-bounds neighbours, so they are already the neighbour function a graph walk wants: bfs(start, |p| g.neighbors4(p)) type-checks because the walk’s neighbours closure is (T) -> Vec[T].

around4 and around8 are not those rows spelled differently. A clipped Vec cannot say which direction each neighbour was, and at the edge of the grid it cannot say there was a direction at all — (1, 1) above has three orthogonal neighbours and the Vec has three entries, with nothing to mark the missing down. Every field of an Around4 is an Option, so both survive, at the cost of no longer being a Vec a walk can consume. Take the Vec for a search and the record when the direction is the answer.

count4 and count8 compare each in-bounds neighbouring cell to a value, where neighbors4 and neighbors8 answer positions; count4_where and count8_where take a predicate on the cell instead. A neighbour off the grid has no cell, so it is never counted and the closure never sees one — which is why the _where pair is the only Grid row besides column, get, row and set that can fault, and it faults for the predicate’s reasons rather than for any of its own.

transpose, rotate_left and rotate_right answer copies and leave the receiver alone.

A Grid is deliberately not a pipeline receiver. for cell in g walks it in row-major order, but g.map(f) is a Y110: a grid enters a pipeline through cells() or positions(), which already answer Vecs. A grid’s shape is part of its value, and a stage that flattened it would be answering about something else.

The pipeline

Thirty-five rows — thirty-four names, because count has two arities — sit on one generic receiver that stands for ten different receivers: Vec, Deque, Set, MinHeap, MaxHeap, Range, BitSet, Map, Counter and Text. That is the for loop’s list minus Grid, and what a receiver yields here is exactly what the for loop’s variable would bind — a Char from a Text, a (K, V) pair from a Map or a Counter, an element from everything else.

var s = Set()
s.insert(2)
s.insert(1)
out(s.sorted())

var m = Map()
m.insert("a", 1)
m.insert("b", 2)
out(m.map(|pair| pair.0))
out(m.to_vec())

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

var d = Deque()
d.push_back(7)
out(d.sum())

var lo = MinHeap()
lo.push(2)
lo.push(1)
out(lo.to_vec())

var b = BitSet()
b.insert(4)
b.insert(9)
out(b.sum())

out((0..5).sum())
out((0..=5).sum())
out("abc".map(|ch| ch.to_int()))
[1, 2]
[a, b]
[(a, 1), (b, 2)]
[(x, 1)]
7
[1, 2]
13
10
15
[97, 98, 99]

A pipeline’s currency is Vec. Every stage answers one whatever the receiver was, which is what makes “what does filter return” answerable without knowing what you started from. A program that wants a different collection back says which one, with a to_* row.

The Requires column below is the row’s own constraint on the item type. Its wording is the compiler’s: an unorderable element gets Y006 values of type ... cannot be ordered, and one that cannot be a key gets Y014 a value of type ... can change after it is stored.

Stages

MethodResultRequiresFaultsWhat it does
enumerate()Vec[(Int, T)]noPair each element with its index.
filter((T) -> Bool)Vec[T]noKeep elements satisfying a predicate, collecting into a Vec.
filter_map((T) -> Option[U])Vec[U]noMap each element to an Option and keep the Some payloads.
flat_map((T) -> Vec[U])Vec[U]noMap each element to a Vec and concatenate the results.
frequencies()Counter[T]items usable as keysnoA Counter holding how many times each element occurs.
map((T) -> U)Vec[U]noApply a function to each element, collecting into a Vec.
reversed()Vec[T]noA new Vec holding these elements in reverse order.
skip(Int)Vec[T]noDrop the first n elements.
sorted()Vec[T]items orderableyesA new Vec holding these elements in ascending order.
sorted_by_key((T) -> K)Vec[T]the extracted key is orderableyesA new Vec ordered by the key the closure extracts.
take(Int)Vec[T]noKeep at most the first n elements.
take_while((T) -> Bool)Vec[T]noKeep elements until the predicate is false.
unique()Vec[T]items usable as keysnoA new Vec with duplicate elements removed, keeping first occurrences.
zip(Vec[U])Vec[(T, U)]noPair elements with another sequence, stopping at the shorter length.

sorted, sorted_by_key, unique, reversed, frequencies and join are barriers: each needs the whole sequence before it can answer anything, so each is a call into the runtime rather than a stage the compiler folds into the loop. reversed is the clearest case of the definition — it cannot answer its first element until it has seen the last. Being a barrier is invisible from a program except in what it costs — the other stages are fused into a single pass over the source, which is also what the “Faults” column is measuring here: a fused stage has no wrapper of its own to fault, while sorted and sorted_by_key do — and sorted_by_key‘s also propagates whatever the key closure raised. reversed is the barrier that does not fault, and the two facts are the same fact: it reads no descriptor callback, which is also why its Requires column is empty where its neighbours’ are not.

join is in the sinks table below rather than here, because it answers a Text rather than a sequence.

Sinks

MethodResultRequiresFaultsWhat it does
all((T) -> Bool)BoolnoTrue if all elements satisfy the predicate (short-circuits).
any((T) -> Bool)BoolnoTrue if any element satisfies the predicate (short-circuits).
count()IntnoNumber of elements.
count((T) -> Bool)IntnoNumber of elements satisfying the predicate.
find((T) -> Bool)Option[T]noThe first matching element, or None.
fold(Acc, (Acc, T) -> Acc)AccnoReduce elements left-to-right with an accumulator and combining closure.
join(Text)Textitems are TextyesThese Text items concatenated with the separator between them.
max()Intitems are IntyesLargest (Int) element. Faults on an empty sequence.
max_by((T, T) -> Bool)TyesLargest element per a (T, T) -> Bool “less-than” comparator.
min()Intitems are IntyesSmallest (Int) element. Faults on an empty sequence.
min_by((T, T) -> Bool)TyesSmallest element per a (T, T) -> Bool “less-than” comparator.
position((T) -> Bool)Option[Int]noThe index of the first matching element, or None.
product()Intitems are IntyesMultiply the (Int) elements.
reduce((T, T) -> T)TyesReduce left-to-right, seeded with the first element.
sum()Intitems are IntyesSum the (Int) elements.

count is the one name in the catalog that carries two arities on a single receiver — count() is the element count, count(pred) the matching-element count — which the table’s (receiver, name, arity) key has always allowed. (get, contains and [] also appear at two arities, but split across receivers: one argument on a Vec, two on a Grid.)

min/max are Int sinks and min_by/max_by take a “less-than” comparator and work on anything. find answers the element, position the index, and both answer an Option.

The seven faulting sinks fault for two reasons and no others. min, max, min_by, max_by and reduce raise empty collection on an empty sequence: each has to answer with an element and there is none. sum and product raise integer overflow, because the running total is checked arithmetic like every other + and *. fold is the sink that does not fault on an empty sequence — it answers its seed — which is the reason to reach for it over reduce.

var v: Vec[Int] = Vec()
out(v.min())
error: program faulted: empty collection

Backtrace:
#0   <entry>

  locals:
    v: Vec[Int] = []
  temps:
    <tmp#1: Vec[Int]> = []
    <tmp#3: Int> = 0
    <tmp#4: Int> = 0
    <tmp#8: Unit> @ "out(v.min())" = <uninit>

Conversions

MethodResultRequiresFaultsWhat it does
to_bitset()BitSetitems are IntyesA BitSet holding these (Int) items. Faults on a negative or oversized member.
to_counter()Counter[T]items are (T, Int) pairs; T usable as a keynoA Counter built from (key, count) pairs. Duplicate keys: last wins.
to_deque()Deque[T]noA Deque holding these items, in order.
to_map()Map[K, V]items are (K, V) pairs; K usable as a keynoA Map built from (key, value) pairs. Duplicate keys: last wins.
to_max_heap()MaxHeap[T]items orderablenoA MaxHeap holding these items.
to_min_heap()MinHeap[T]items orderablenoA MinHeap holding these items.
to_set()Set[T]items usable as keysnoA Set holding these items, duplicates dropped.
to_vec()Vec[T]noThe items as a Vec. On a Vec receiver this is the receiver itself.

There is a conversion for every collection that has a constructor, and exactly one that has none: to_grid does not exist, because a grid needs a width and a flat item sequence does not carry one.

to_map and to_counter say “my item is a pair” in the receiver pattern rather than in prose, so [1, 2].to_map() fails at the method name with expected (?T, ?U), found Int instead of resolving and then faulting.

The whole pipeline, run

var v = [3, 1, 4, 1, 5]

out(v.map(|n| n * 2))
out(v.filter(|n| n > 2))
out(v.filter_map(|n| if n > 3 { Some(n) } else { None }))
out(v.flat_map(|n| [n, n]))
out(v.take(2))
out(v.skip(3))
out(v.take_while(|n| n < 4))
out(v.enumerate())
out(v.zip(["a", "b"]))

out(v.fold(0, |acc, n| acc + n))
out(v.reduce(|acc, n| acc + n))
out(v.sum())
out(v.product())
out(v.count())
out(v.count(|n| n == 1))
out(v.min())
out(v.max())
out(v.min_by(|a, b| a < b))
out(v.max_by(|a, b| a < b))
out(v.any(|n| n == 4))
out(v.all(|n| n > 0))
out(v.find(|n| n > 3))
out(v.position(|n| n > 3))

out(v.sorted())
out(v.sorted_by_key(|n| 0 - n))
out(v.unique())
out(v.reversed())
out(v.frequencies())
out(["a", "b", "c"].join(", "))

out(v.to_vec())
out(v.to_set())
out(v.to_deque())
out(v.to_min_heap().peek())
out(v.to_max_heap().peek())
out(v.to_bitset())
out([(1, "a"), (2, "b")].to_map())
out([("x", 3)].to_counter())
[6, 2, 8, 2, 10]
[3, 4, 5]
[4, 5]
[3, 3, 1, 1, 4, 4, 1, 1, 5, 5]
[3, 1]
[1, 5]
[3, 1]
[(0, 3), (1, 1), (2, 4), (3, 1), (4, 5)]
[(3, a), (1, b)]
14
14
14
60
5
2
1
5
1
5
true
true
Some(4)
Some(2)
[1, 1, 3, 4, 5]
[5, 4, 3, 1, 1]
[3, 1, 4, 5]
[5, 1, 4, 1, 3]
{1: 2, 3: 1, 4: 1, 5: 1}
a, b, c
[3, 1, 4, 1, 5]
{1, 3, 4, 5}
[3, 1, 4, 1, 5]
1
5
{1, 3, 4, 5}
{1: a, 2: b}
{x: 3}

What the Requires column refuses

A tuple can be a key but cannot be ordered — no composite in this language can, because ordering goes through one scalar comparison. That is a statement about < and sorted(), not about a container: a Map[(Int, Int), V] still walks and prints its keys element-wise, because it has to walk them in some reproducible order. A record behaves exactly the same way: a fine key, not orderable. A Vec is neither one nor the other: not orderable, for the same composite reason, and not a key, because it can change after it has been stored. Below, the tuple fails the first column and the Vec fails the second.

var pairs = [(2, "b"), (1, "a")]
out(pairs.sorted())

var groups = [[1], [2]]
out(groups.to_set())
$ praxis check catalog-bounds.px
error[Y006]: values of type `(Int, Text)` cannot be ordered

  catalog-bounds.px:2:11
  2 | out(pairs.sorted())
    |           ^^^^^^ values of type `(Int, Text)` cannot be ordered

error[Y014]: a value of type `Vec[Int]` can change after it is stored, so it cannot be used as a key

  catalog-bounds.px:5:12
  5 | out(groups.to_set())
    |            ^^^^^^ 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: 2 error(s)

sorted_by_key is the answer to the first half: the ordering requirement moves to the key the closure extracts, so the elements themselves need not be orderable.

var pairs = [(2, "b"), (1, "a")]
out(pairs.sorted_by_key(|pair| pair.0))
[(1, a), (2, b)]

Scalars

Text is the one scalar with members, and Int, Float and Char have the conversions and the explicit-overflow family.

Text

MethodResultMutatesFaultsAllocatesWhat it does
get(Int)CharnoyesyesThe Char at index; faults if out of range. t[index] is the same row and the same answer.
float()Option[Float]nonoyesThe Float this text spells as Some(x), or None if it spells none.
int()Option[Int]nonoyesThe Int this text spells as Some(n), or None if it spells none.
is_empty()BoolnononoTrue iff the text has no chars.
len()IntnonoyesNumber of Unicode scalar values (chars) in the text.
var line = "héllo"

out(line.len())
out(line.is_empty())
out(line.get(1))
out(line.get(1).to_int())
out((233).to_char())
out(" 42 ".int())
out("héllo".int())
out("1.5".float())
out("inf".float())
5
false
é
233
é
Some(42)
None
Some(1.5)
None

len() counts Unicode scalar values and get/t[i] index by them, not by bytes — which is why "héllo".len() is 5 and line.get(1) is é. Char.to_int and Int.to_char are the round trip out of and back into a character, and they are Int and Char rows rather than Text ones.

int() and float() trim the text and then read exactly what the input parser’s int and float atomics read, over the whole of what is left. They share the parser’s own scanner, so t.int() and parse(t, int) cannot disagree about what a number is. Anything the run does not cover is None"1 2", "12abc", "1." — because a text that is not a number is absence rather than a fault.

Two answers surprise people, and both follow from that rule:

  • "+5".int() is None. The int atomic takes a leading - and not a +. "+5.0".float() is a value, because the float atomic does take one.
  • "inf".float() and "nan".float() are None. Float has those values — 1.0 / 0.0 is one, and to_text() prints them — but no text spells one.

A value past Int’s range is None too. The input parser is the other way to get a number out of text, and the one to reach for when the text came from input in the first place: it reports where the parse broke instead of answering None.

There is no split, no chars and no to_upper: all three are Y110. for ch in text is how a Text is walked, and the pipeline rows above apply to it directly.

Int

MethodResultMutatesFaultsAllocatesWhat it does
checked_add(Int)Option[Int]nonoyesAdd, answering None where the checked + would fault.
checked_mul(Int)Option[Int]nonoyesMultiply, answering None where the checked * would fault.
checked_sub(Int)Option[Int]nonoyesSubtract, answering None where the checked - would fault.
saturating_add(Int)IntnonoyesAdd, clamping to Int’s ends instead of faulting.
saturating_mul(Int)IntnonoyesMultiply, clamping to Int’s ends instead of faulting.
saturating_sub(Int)IntnonoyesSubtract, clamping to Int’s ends instead of faulting.
to_char()CharnoyesyesThe Char with this Unicode scalar value; faults (InvalidChar) if it is negative, above 0x10FFFF, or a surrogate. The narrowing half of the pair, as Float.to_int is.
to_float()FloatnonoyesWiden to Float; the explicit Int→Float conversion.
to_text()TextnonoyesFormat as Text — the same digits out writes.
wrapping_add(Int)IntnonoyesAdd with two’s-complement wraparound instead of a fault.
wrapping_mul(Int)IntnonoyesMultiply with two’s-complement wraparound instead of a fault. The one row here a program could not write for itself: every arithmetic operator is checked and the language has no bitwise operators.
wrapping_sub(Int)IntnonoyesSubtract with two’s-complement wraparound instead of a fault.

Float

MethodResultMutatesFaultsAllocatesWhat it does
abs()FloatnonoyesAbsolute value.
ceil()FloatnonoyesRound toward positive infinity.
floor()FloatnonoyesRound toward negative infinity.
is_infinite()BoolnononoTrue iff ±infinity.
is_nan()BoolnononoTrue iff NaN.
max(Float)FloatnonoyesThe larger of two floats. If either is NaN, returns the other.
min(Float)FloatnonoyesThe smaller of two floats. If either is NaN, returns the other.
round()FloatnonoyesRound half away from zero.
sign()FloatnonoyesSign as -1.0 / 0.0 / 1.0. NaN yields NaN.
sqrt()FloatnonoyesSquare root. Negative inputs yield NaN (IEEE-754).
to_int()IntnoyesyesTruncate toward zero to an Int. Faults on NaN, ±inf, or out of i64 range.
to_text()TextnonoyesFormat as Text (shortest round-trip form; inf/-inf/NaN as literals).

Char

MethodResultMutatesFaultsAllocatesWhat it does
to_int()IntnonoyesThe Unicode scalar value, as an Int. Never faults.
to_text()TextnonoyesThe one-character Text holding this scalar — the same character out writes. Never faults.

All three receivers at once:

var x = -2.5

out(x.abs())
out(x.sign())
out(x.floor())
out(x.ceil())
out(x.round())
out(x.to_int())
out(x.to_text())
out(x.min(1.0))
out(x.max(1.0))
out((2.0).sqrt())
out((0.0 / 0.0).is_nan())
out((1.0 / 0.0).is_infinite())

out((7).to_float())
out((9223372036854775807).wrapping_add(1))
out((9223372036854775807).saturating_add(1))
out((9223372036854775807).checked_add(1))
out((5).checked_sub(1))
out((3).wrapping_mul(4))
out((1660).to_text())
out('A'.to_text())
2.5
-1.0
-3.0
-2.0
-3.0
-2
-2.5
-2.5
1.0
1.4142135623730951
true
true
7.0
-9223372036854775808
9223372036854775807
None
Some(4)
12
1660
A

Integer arithmetic is checked by default, and the nine wrapping_/saturating_ /checked_ rows are how a program opts out of the fault for one operation.

The to_text family is Int, Float and Char, and it is closed at three: Bool has no row and there is no universal T.to_text(). Each of the three answers exactly the characters out writes — the method and the printer share one renderer per scalar, so they cannot drift apart. A labelled line does not need any of them: "n = {n}" renders a value of any type through the same printer.

Subscripts

m[key], v[i] = x and grid[x, y] are catalog rows too, dispatched on the receiver’s shape and the index count exactly as a method call is. Their names — [], []=, []min=, []max= — are not identifiers, so no program can call them by name; the subscript grammar is their only caller.

Six receivers read. Five of the six also store: every one but Text, which is immutable.

ReceiverSpellingResultMutatesFaultsAllocatesWhat it does
Counter[T]c[key]Intnonoyesc[key] — the count for key, or zero if absent; never faults.
Deque[T]d[i]Tnoyesnod[i] — the element at i (0-based from the front); faults if out of range.
Grid[T]g[x, y]Tnoyesnog[x, y] — the cell at (x, y); faults if out of range.
Map[K, V]m[key]Vnoyesnom[key] — the value for key; faults if absent.
Textt[i]Charnoyesyest[i] — the Char at i, indexing by Unicode scalar value and not by byte; faults if out of range.
Vec[T]v[i]Tnoyesnov[i] — the element at i; faults if out of range.
Counter[T]c[key] = nUnityesnoyesc[key] = n — set the count for key.
Deque[T]d[i] = valueUnityesyesnod[i] = value — replace the element at i (0-based from the front); faults if out of range (it never inserts).
Grid[T]g[x, y] = valueUnityesyesnog[x, y] = value — set the cell at (x, y); faults if out of range.
Map[K, V]m[key] = valueUnityesnoyesm[key] = value — set key, replacing any prior value.
Vec[T]v[i] = valueUnityesyesnov[i] = value — replace the element at i; faults if out of range (it never appends — push is the spelling that grows a vector).
Map[K, Int]m[key] max= nUnityesnoyesm[key] max= n — keep the larger value; an absent entry accepts the first value.
Map[K, Int]m[key] min= nUnityesnoyesm[key] min= n — keep the smaller value; an absent entry accepts the first value.
var v = [10, 20, 30]
v[0] = 11
out(v[0])

var d = Deque()
d.push_back("a")
d[0] = "b"
out(d[0])

out("praxis"[2])

var m = Map()
m["k"] = 1
out(m["k"])

var c = Counter()
c["x"] = 4
out(c["x"])
out(c["never seen"])

var best = Map()
best["r"] min= 5
best["r"] min= 3
best["r"] max= 4
out(best)
11
b
a
1
4
0
{r: 4}

min= and max= exist as their own rows rather than as read-modify-write over the other two, because they give an absent entry a meaning no read can express: the first value is accepted as-is. A subscript read of an absent Map key faults, so there would be nothing to compare against.

var m = Map()
m.insert("a", 1)
out(m["b"])
error: program faulted: index out of bounds

Backtrace:
#0   <entry>

  locals:
    m: Map[Text, Int] = {"a": 1}
  temps:
    <tmp#1: Map[Text, Int]> = {"a": 1}
    <tmp#3: Text> @ ""a"" = "a"
    <tmp#4: Int> @ "1" = 1
    <tmp#5: Unit> @ "m.insert("a", 1)" = Unit
    <tmp#6: Text> @ ""b"" = "b"
    <tmp#7: Int> @ "m["b"]" = <uninit>
    <tmp#8: Unit> @ "out(m["b"])" = <uninit>

When a method does not resolve

Two diagnostics cover almost everything. Y110 is “this table has no such row” — including the wrong argument count, since arity is part of the key, so [1, 2].get() is no method 'get' on type 'Vec[Int]' taking 0 argument(s). Y001 is “the row exists and your types do not fit it”, which is what the item shapes produce; the two requirement columns above produce Y006 and Y014 instead.

out("a,b".split(","))
out([1.5, 2.5].sum())
out([1, 2].to_map())
out(['a'].join(""))
out([1, 2].to_text())
$ praxis check catalog-refusals.px
error[Y110]: no method `split` on type `Text` taking 1 argument(s)

  catalog-refusals.px:1:11
  1 | out("a,b".split(","))
    |           ^^^^^ no method `split` on type `Text` taking 1 argument(s)

error[Y001]: expected Int, found Float

  catalog-refusals.px:2:16
  2 | out([1.5, 2.5].sum())
    |                ^^^ expected Int, found Float

error[Y001]: expected (?T, ?U), found Int

  catalog-refusals.px:3:12
  3 | out([1, 2].to_map())
    |            ^^^^^^ expected (?T, ?U), found Int

error[Y001]: expected Text, found Char

  catalog-refusals.px:4:11
  4 | out(['a'].join(""))
    |           ^^^^ expected Text, found Char

error[Y001]: expected Char, found Int

  catalog-refusals.px:5:12
  5 | out([1, 2].to_text())
    |            ^^^^^^^ expected Char, found Int

praxis: 5 error(s)

The second is why sum is spelled as a bound rather than a literal Vec[Int] receiver: the row still matches a Vec[Float], so the report is about the element type you have rather than “no method sum on this type”. The last two are the same shape, and the same reason join and to_text bound their item rather than naming a concrete receiver.

See method resolution for how a call finds its row, and diagnostic codes for the full list.