Reference
The Standard Library
The library is built into qoqi. There is nothing to install and no project file is needed: any program can import its modules.
import std.dict;
fn main() -> Int {
let counts = dict.empty<Int>();
for word in "to be or not to be".words() {
counts.set(word, (counts.get(word) ?? 0) + 1);
}
println(counts.keys(), counts.values()); // ["to", "be", "or", "not"] [2, 2, 1, 1]
return 0;
}
Whole programs built on it: a report on a web server's log, a sales report from a CSV file, invoices from packages, and the flows of a shop and of subscriptions.
| Module | For |
|---|---|
std.math |
Numbers, conversion between Int and Float |
std.strings |
Taking text apart, searching, changing, parsing numbers |
std.arrays |
Functions on arrays: slice, map, filter, sort, ... |
std.list |
List<T>: an array with methods |
std.dict |
Dict<V>: values found by a text key |
std.map |
Map<K, V>: values found by a key of any type |
std.option |
Chaining steps on values that may be null |
std.result |
Result<T, E>, surviving a failure with attempt, traces |
std.io |
Reading input, files, the environment |
std.time |
The clock, dates, waiting, measuring |
std.random |
Random numbers, identifiers |
std.path |
Paths taken apart and put together |
std.fs |
Folders: listing, walking, creating, copying |
std.os |
Program arguments, exit, running other programs |
std.log |
Messages about what a program does, with levels and values, as text or JSON |
std.format |
Numbers, sizes, durations and tables written for people |
std.json |
Reading, searching and writing JSON |
std.regex |
Regular expressions |
std.csv |
Tables as text |
std.set |
Set<T>: values without repeats |
std.semver |
Versions, 1.4.2, and the constraints on them, ^1.2 || ^2.0 |
std.encoding, std.hash |
Base64, hex, URL encoding; SHA, MD5, HMAC |
std.http |
Asking web servers, and being one |
std.url |
Addresses taken apart, changed and resolved |
std.sql |
Databases: SQLite, Postgres and MySQL, rows and transactions |
std.task |
Doing several things at once: tasks and channels |
std.compiler |
Asking the compiler about a program: its mistakes, its declarations, what a name is |
std.test |
Tests and checks; what qoqi test runs |
std.layout |
Text laid out to fit a width |
std.formatter |
The formatter of Qoqi: what qoqi fmt runs |
std.ops |
The traits behind operators, loops and brackets; sorted, max, join for any type that has them |
The modules are imported and used like any other (modules.md): import std.path; then path.dir(file), or import std.path.{dir}; then dir(file). The methods of arrays, texts and numbers need no import (see below).
print, println, toString, push and panic are not part of the library: they are builtins, available without an import.
Conventions
Methods, not functions. What the library does to an array, a text or a number is a method of it: items.filter(wanted), name.trim(), x.sqrt(). The tables of std.arrays and std.strings below list them as name(receiver, arguments), and that of std.math as the methods they are: the receiver is what stands before the dot, so take(items, count) is called as items.take(count). Nothing is imported for a method: these three modules are part of any program that calls one of their methods. An import is only for the few functions that have nothing to stand on: arrays.range, arrays.fill, arrays.flatten, arrays.unzip, strings.fromCode, and the constants math.pi and math.e. A method is handed to another function as any function is, by its name: texts.map(String.parseInt) calls parseInt on each text, and keys.map(users.get) calls get of that dictionary.
Nothing instead of failure. A function that may have no answer returns an optional, and null means "none": the position of something not found, the first item of an empty array, a text that is not a number. Where there is a reason worth knowing, as for a file that cannot be read, it returns a Result with the reason in it. The type checker then makes sure the program deals with it:
let n = line.parseInt(); // Int?
if n != null { total += n; } // an Int in here
let port = text.parseInt() ?? 8080;
A failure is for a mistake in the program. Asking for characters 2 to 9 of a three-letter text, the square root of a negative number, or a random number below zero stops the program, like an index out of bounds does. The message names the function as the program called it and the line that called it:
[ERROR] R0007: std.strings.substring: range 0..5 is outside a text of 3 characters
--> main.qi:8:19 in function 'shorten'
|
8 | return name.substring(0, 5) + "...";
| ^^^^^^^^^^
called from main.qi:13:13 in function 'main'
That holds however deep in the library the failure happened: the lines of the library are left out, and what is named is the function the program called, std.csv.parseWith for a separator it cannot use, not the private one that found out.
Arrays are not changed by the functions of std.arrays; they return new ones. The exceptions say so (pop, removeAt, insertAt, clear, swap, random.shuffle). A List is the reverse: its methods change it, unless they return a new list.
Ranges are given as from, to: from is included, to is not. Positions count from 0. In texts they count characters, not bytes, like text[i] and text.len().
std.math
An Int is taken for a Float where one is expected, and the two work together as Floats (1 + 2.5 is 3.5), so toFloat is rarely needed: it is there for when the conversion should be seen. Going to an Int is never implicit, and there are four ways to lose the fraction.
What there is to do with a number is a method of it: n.abs(),
x.sqrt(), price.round(). Only the constants math.pi and math.e need
import std.math;.
| Method | Result |
|---|---|
n.toFloat() -> Float |
The same number, as a Float |
x.floor() -> Int |
The largest whole number not above x: 2.7.floor() is 2, (-2.7).floor() is -3 |
x.ceil() -> Int |
The smallest whole number not below x: 2.1.ceil() is 3 |
x.round() -> Int |
The nearest whole number; halves go away from zero: 2.5.round() is 3 |
x.trunc() -> Int |
x without its fraction: (-2.7).trunc() is -2 |
The four fail if the number does not fit in an Int.
Whole numbers
| Method | Result |
|---|---|
n.abs() |
n without its sign |
n.min(m), n.max(m) |
The smaller, the larger |
n.clamp(low, high) |
n, or the nearest limit if it is outside them |
n.sign() |
-1, 0 or 1 |
n.pow(exponent) |
n multiplied by itself exponent times. A negative exponent gives 0, except for 1 and -1. Fails past what an Int holds, as * does: 2.pow(47) |
n.gcd(m) |
Greatest common divisor, never negative: 12.gcd(18) is 6 |
n.lcm(m) |
Least common multiple: 4.lcm(6) is 12 |
n.isEven(), n.isOdd() |
|
n.sqrt() -> Float |
Square root. Fails for a negative number |
n.toString() |
The number as a text |
Numbers with a fraction
| Method | Result |
|---|---|
math.pi, math.e |
The constants |
x.abs(), x.min(y), x.max(y) |
As for whole numbers |
x.clamp(low, high) |
x, or the nearest limit if it is outside them |
x.sign() -> Int |
-1, 0 or 1; 0 for zero and for a Float that is not a number |
x.sqrt() |
Square root. Fails for a negative number |
x.pow(y) |
x raised to y, which need not be whole: 2.0.pow(0.5) |
x.exp(), x.log() |
e raised to x; the natural logarithm, which fails for zero and negative numbers |
x.sin(), x.cos(), x.tan() |
Angles in radians |
y.atan2(x) |
The angle of the point (x, y), from -π to π |
x.radians(), x.degrees() |
From degrees to radians, and back |
x.toString() |
The number as a text |
// The length of the diagonal of a 3 by 4 rectangle
let a = 3;
let b = 4;
let diagonal = (a * a + b * b).sqrt(); // 5.0
std.strings
| Function | Result |
|---|---|
len(text) -> Int |
How many characters the text has. This is the only way to ask: a text has no .length |
byteLength(text) -> Int |
How many bytes it takes in UTF-8: "é" is one character and two bytes |
Taking apart
| Function | Result |
|---|---|
substring(text, from, to) -> String |
The characters from from up to, not including, to. Fails if the range is not inside the text |
from(text, from) |
From a position to the end |
first(text, count), last(text, count) |
The first or last count characters; the whole text if it is shorter |
split(text, separator) -> [String] |
The pieces between occurrences of the separator: split("a,b", ",") is ["a", "b"]. An empty separator gives the characters |
chars(text) -> [String] |
The characters |
lines(text) -> [String] |
The lines, without their line ends |
words(text) -> [String] |
What is between spaces, tabs and line ends |
Putting together
| Function | Result |
|---|---|
join(parts, separator) -> String |
The parts with the separator between them. [Int] and [Float] have it too: [1, 2, 3].join(", ") is "1, 2, 3" |
repeat(text, count) |
The text count times over. Fails for a negative count |
padLeft(text, width, fill = " "), padRight(text, width, fill = " ") |
The text with fill before or after it, up to width characters: "7".padLeft(3, "0") is "007" |
reverse(text) |
The characters in the opposite order |
Building a long text piece by piece with += copies everything so far at each step. Collect the pieces in an array and join them once.
Searching
| Function | Result |
|---|---|
indexOf(text, part) -> Int? |
Where part first occurs, or null |
cut(text, separator) -> (String, String)? |
What is before the first separator, and what is after it: let (name, value) = "port=80".cut("=") ?? ("", ""); |
lastIndexOf(text, part) -> Int? |
Where it last occurs, or null |
contains(text, part), startsWith(text, start), endsWith(text, end) |
|
count(text, part) -> Int |
How many times part occurs, not counting overlaps |
Changing
| Function | Result |
|---|---|
replace(text, old, with) |
Every occurrence of old replaced. Fails if old is empty |
upper(text), lower(text) |
|
capitalize(text) |
The first character in upper case |
trim(text) |
Without the spaces, tabs and line ends at both ends |
trimStart(text), trimEnd(text) |
Without them at the start only, or at the end only: " a b ".trimStart() is "a b " |
Asking
| Function | Result |
|---|---|
isEmpty(text) |
No characters at all |
isBlank(text) |
Empty, or only spaces, tabs and line ends |
isDigits(text) |
Every character is a digit, and there is at least one |
isLetters(text), isAlphanumeric(text) |
Every character is a letter of any alphabet ("été"), or a letter or a digit; false for "" |
isSpaces(text) |
Every character is a space, a tab or a line end; false for "", which isBlank accepts |
isUpper(text), isLower(text) |
There are letters, all capitals or none: "HTTP/2".isUpper() |
What a lexer asks of the character it is at: if c.isLetters() || c == "_" { ... }.
Numbers and characters
| Function | Result |
|---|---|
parseInt(text) -> Int? |
The whole number the text spells, or null: parseInt("42"), parseInt(" -7 "). Spaces around it are ignored |
parseFloat(text) -> Float? |
Likewise, for a number written the plain way: a sign or none, digits with a point among them or not, an exponent or none. "2.5", "-1e3", ".5" and "5." are numbers; "nan", "inf", "1_000" and "0x1p4" are not |
code(text) -> Int |
The number of the first character: code("A") is 65. Fails for "" |
fromCode(code) -> String |
The character with that number: fromCode(65) is "A" |
hash(text) -> Int |
A number that is the same for equal texts and almost always different otherwise. Never negative |
Going the other way, any value becomes text with the builtin toString.
let line = "name = Ada Lovelace";
let at = line.indexOf("=");
if at != null {
let key = line.first(at).trim(); // "name"
let value = line.from(at + 1).trim(); // "Ada Lovelace"
}
std.arrays
Generic functions on plain arrays. Type arguments are inferred: [1, 2, 3].reversed(). Where there is nothing to infer from, write them: [].first().
| Function | Result |
|---|---|
len<T>(items) -> Int |
How many items the array holds. This is the only way to ask: an array has no .length |
Making
| Function | Result |
|---|---|
range(from, to) -> [Int] |
The whole numbers from from up to, not including, to |
fill<T>(count, value) -> [T] |
count times the same value. This is how to get an array of a given size |
copy<T>(items) |
A new array with the same items |
slice<T>(items, from, to) |
The items from from up to, not including, to. Positions outside the array are brought back to its ends |
concat<T>(first, second) |
One after the other |
reversed<T>(items) |
|
without<T>(items, index) |
Without the item at index. An index outside the array changes nothing |
inserted<T>(items, index, value) |
With value at index, the rest moved up. An index at or past the end puts it at the end; a negative one changes nothing |
Changing the array itself
| Function | Does |
|---|---|
pop<T>(items) -> T? |
Takes the last item out and returns it; null for an empty array. Takes the same time however long the array is |
removeAt<T>(items, index) -> T? |
Takes the item at index out and returns it, the items after it moving down. null, and nothing changed, if there is none there |
insertAt<T>(items, index, value) |
Puts value at index, the items from there on moving up. An index at or past the end puts it at the end; a negative one changes nothing, as with inserted |
clear<T>(items) |
Takes every item out |
With push, these make an array a stack (push, pop) or a queue (push, removeAt(items, 0)). They change the array they are given, so everything that holds it sees the change, and they make no new array: one made shorter keeps the room it had, to grow into again.
let stack = [1, 2];
stack.push(3);
let top = stack.pop(); // 3, an Int?; stack is [1, 2]
stack.insertAt(0, 0); // [0, 1, 2]
let first = stack.removeAt(0);
while !stack.isEmpty() { // until nothing is left
let item = stack.pop();
}
Two things to know:
nullfrom an array of optionals. In a[Int?]an item can itself benull, andpopthen givesnullfor that item as it does for an empty array. AskisEmpty(orlen) before, as the loop above does, instead of waiting fornull.- Not inside
for ... inover the same array. That loop counts the items once, before it starts; an array made shorter under it is read past its end, which stops the program (index out of bounds). Usewhilewith an index that only moves on when nothing was removed, go down from the end, or loop overitems.copy():
let i = 0;
while i < tasks.len() {
if tasks[i].done { tasks.removeAt(i); } else { i++; }
}
One item
| Function | Result |
|---|---|
first<T>(items) -> T?, last<T>(items) -> T? |
null for an empty array |
at<T>(items, index) -> T? |
The item there, or null. items[index] fails instead |
find<T>(items, wanted) -> T? |
The first item the function accepts |
findIndex<T>(items, wanted) -> Int? |
Its position |
indexOf<T>(items, wanted) -> Int? |
The position of the first item equal to wanted, or null |
Asking
| Function | Result |
|---|---|
isEmpty<T>(items) |
|
contains<T>(items, wanted) |
An item is equal to wanted |
any<T>(items, wanted) |
The function accepts at least one item |
all<T>(items, wanted) |
The function accepts every item. True for an empty array |
count<T>(items, wanted) -> Int |
How many it accepts |
Every item
| Function | Result |
|---|---|
map<T, U>(items, convert) -> [U] |
What the function makes of each item |
filter<T>(items, wanted) -> [T] |
The items the function accepts |
fold<T, A>(items, start, combine) -> A |
One value from all the items: starts with start, then combine(value so far, item) for each |
reduce<T>(items, combine) -> T? |
The items combined two by two from the first, combine(so far, item); null for none |
each<T>(items, action) |
Calls the function with each item |
Order
| Function | Result |
|---|---|
sorted<T>(items, before) -> [T] |
In order, where before(a, b) says whether a must come first. Items equal in that order keep the order they had |
swap<T>(items, a, b) |
Changes the array: the two items change places |
Numbers and texts
| Function | Result |
|---|---|
sum(numbers) -> Int |
|
smallest(numbers) -> Int?, largest(numbers) -> Int? |
null for an empty array |
sortedNumbers(numbers), sortedTexts(texts) |
Smallest first; alphabetical |
These are methods of [Int], and all but sortedTexts of [Float] as well, giving a Float: prices.sum(), areas.largest(). [Int], [Float] and [String] also take sorted() without a function, smallest first or alphabetical, and with one like any array: scores.sorted((a, b) -> a > b) for the largest first.
Pieces and groups
| Function | Result |
|---|---|
take(items, count), drop(items, count) |
The first count items; all but those |
chunks(items, size) -> [[T]] |
Pieces of size, the last one shorter if it does not divide |
flatten(arrays) -> [T], flatMap(items, convert) |
|
groupBy(items, name) -> Dict<[T]> |
The items sorted into groups by a text worked out from each |
countBy(items, name) -> Dict<Int> |
How many items give each text |
unique(items), uniqueBy(items, name) |
Without repeats; the first of each is kept. unique is for [String], [Int] and [Float] |
partition(items, wanted) -> ([T], [T]) |
The items a function accepts, and the others: let (even, odd) = numbers.partition(n -> n.isEven()); |
zip(first, second) -> [(A, B)] |
The items of two arrays side by side, as many as the shorter has |
unzip(pairs) -> ([A], [B]) |
The other way round |
sortedBy<R: Ordered>(items, rank: fn(T) -> R) |
In the order of a value worked out from each, smallest first: a number, a text, anything Ordered. people.sortedBy(p -> p.name), people.sortedBy(p -> p.height) |
sortedByText(items, name) |
The same, for a text |
maxBy<R: Ordered>(items, rank) -> T?, minBy<R: Ordered>(items, rank) -> T? |
The item ranked highest, or lowest; the first of several ranked the same. null for an empty array |
sumOf(items, amount) -> Int |
Equal, for indexOf and contains, means what == means: numbers, texts and booleans are equal when they have the same value, arrays and structs when what they hold is, and a type that implements Equal when its equals says so.
let people = [Person { name: "Ada", age: 36 }, Person { name: "Linus", age: 21 }];
let adults = people.filter(p -> p.age >= 18);
let names = adults.map(p -> p.name);
let oldestFirst = people.sorted((a, b) -> a.age > b.age);
let totalAge = people.fold(0, (sum, p) -> sum + p.age);
std.list
List<T> is an array in a struct, with the functions of std.arrays as methods. Calls read left to right and can be chained.
import std.list;
let names = list.of(["ada", "linus"]);
names.add("grace");
let long = names.filter(n -> n.len() > 3).sorted((a, b) -> a < b);
println(long); // ["grace", "linus"]: a list prints as an array does
for name in names { println(name); } // a list goes where an array does
names[0] = "Ada"; // as with an array, a position that is not there stops the program
println(names[0]);
| Function | Result |
|---|---|
list.empty<T>() -> List<T> |
An empty list. The type has to be written |
list.of<T>(items) -> List<T> |
A list of the items of an array. The list has its own copy |
Methods that change the list
They change the array the list holds, as the functions of std.arrays that change an array do. As there, do not take items out of a list inside a for ... in over that same list: the loop counted them before it started. Loop over names.toArray(), or use while with names.len().
| Method | Does |
|---|---|
add(value), addAll(values) |
Adds at the end |
set(index, value) |
Replaces the item there. Fails if there is none |
insert(index, value) |
Puts value at index, moving the rest up. An index at or past the end adds it at the end; a negative one changes nothing |
removeAt(index) -> T? |
Takes the item out and returns it; null if there is none there |
pop() -> T? |
Takes out the last item; null for an empty list |
clear() |
|
sort(before) |
Puts the list itself in order |
Methods that read
| Method | Result |
|---|---|
len(), isEmpty() |
|
get(index) -> T? |
The item there, or null |
first() -> T?, last() -> T? |
|
find(wanted) -> T?, findIndex(wanted) -> Int? |
|
contains(value), indexOf(value) -> Int? |
By equality; see std.arrays |
any(wanted), all(wanted), count(wanted) |
|
maxBy<R: Ordered>(rank) -> T?, minBy<R: Ordered>(rank) -> T? |
As for arrays |
fold<A>(start, combine) -> A, reduce(combine) -> T? |
|
sumOf(amount) -> Int |
|
countBy(name) -> Dict<Int> |
How many items give each text |
each(action) |
|
toArray() -> [T] |
An array of the items; changing it does not change the list |
Methods that return a new list
map<U>(convert), filter(wanted), sorted(before), sortedBy(rank), reversed(), slice(from, to), take(count), drop(count), flatMap<U>(convert), uniqueBy(name), concat(other: List<T>), copy(). They do what the methods of arrays of the same names do, and return a List; so do those that give several: chunks(size) -> List<List<T>>, partition(wanted) -> (List<T>, List<T>), groupBy(name) -> Dict<List<T>>.
map and fold have a type parameter of their own, for what they produce. It is worked out from the function passed, or can be written: names.map<Int>(n -> n.len()).
A list prints as an array does, [1, 2, 3], alone, inside another value and in "${...}".
std.dict
Dict<V> (a dictionary) finds values by a text key: finding a key takes about the same time however many there are.
Its keys and values are two arrays, in the order the keys were set. Up to eight keys are simply looked through, so a small dictionary is three objects and costs next to nothing to make; a program can have one per record, per scope, per request. Past eight the machine keeps an index of where each key is.
let ages = {"ada": 36, "linus": 21}; // a literal, or:
let more = dict.empty<Int>();
more.set("grace", 85);
let age = ages.get("ada"); // Int?
if age != null { println(age + 1); }
println(ages.get("nobody") ?? 0);
| Function / method | Result |
|---|---|
{"ada": 36}, dict.of(pairs) -> Dict<V> |
A dictionary of entries; a literal with texts for keys is a Dict, and let d: Dict<Int> = {}; an empty one |
dict.empty<V>() -> Dict<V> |
An empty dictionary. The type of the values has to be written |
set(key, value) |
Sets the value of a key, replacing the one it had |
get(key) -> V? |
The value, or null if the key has none |
getOr(key, fallback) -> V |
The value, or the fallback |
update(key, start, change) |
Sets the key to what a function makes of its value, or of start if it has none. Counting: counts.update(word, 0, n -> n + 1) |
getOrSet(key, make) -> V |
The value, set first to what make() gives if the key has none. Grouping: groups.getOrSet(letter, () -> []).push(word) |
has(key) -> Bool |
|
remove(key) -> Bool |
Removes a key; whether it was there |
len(), isEmpty() |
|
keys() -> [String] |
The keys, in the order they were first set |
sortedKeys() -> [String] |
In alphabetical order |
entries() -> [(String, V)] |
Each key with its value: for (name, age) in ages.entries() { ... } |
values() -> [V] |
The values, in the order of their keys |
each(action) |
Calls action(key, value) for each |
copy() -> Dict<V> |
A dictionary of its own with the same entries: setting a key of one leaves the other alone |
A dictionary takes brackets and loops: ages["ada"] is what get gives, a value or null; ages["ada"] = 36; is set; and for name in ages { ... } goes through the keys. Counting reads counts[word] = (counts[word] ?? 0) + 1;. Two dictionaries are == when they have the same keys with equal values, and one prints as its literal: {"ada": 36}.
Keys are compared exactly: "a" and "A" are two keys, and "" is a key.
The values can be of any type, including arrays, structs and other dictionaries (dict.empty<Dict<Int>>()). For keys that are not texts, there is Map.
// Group words by their first letter
let groups = dict.empty<[String]>();
for word in words {
groups.getOrSet(word.first(1), () -> []).push(word);
}
std.map
Map<K, V> finds values by a key of any type: a number, a value of an enum, a struct, a tuple. Two keys are the same key when they are written the same, as toString writes them, and are ==: so 1 and "1" are two keys of a Map<Int | String, V>. It takes brackets and loops as a Dict does, and prints as its literal: {(1, 1): "ada"}.
let seats = {(1, 1): "ada", (1, 2): "alan"}; // keys that are not texts: a Map
seats[(2, 1)] = "grace";
let counts = map.empty<Fruit, Int>();
for fruit in basket { counts.update(fruit, 0, n -> n + 1); }
let scores: Map<Player, Int> = {};
| Function / method | Result |
|---|---|
{key: value}, map.of(pairs) -> Map<K, V> |
A map of entries; a key given twice has the value given last |
map.empty<K, V>() -> Map<K, V> |
An empty map |
set, get, getOr, getOrSet, update, has, remove, len, isEmpty |
As for Dict, with K for String |
keys() -> [K], values() -> [V], entries() -> [(K, V)], each(action) |
In the order the keys were first set |
copy() -> Map<K, V> |
A map of its own with the same entries |
A Map has no sortedKeys, which is a Dict's: keys of any type have no alphabetical order. keys().sortedBy(...) puts them in one.
A key is found by how it is written, then by == among the keys written the same way: finding one takes about the same time however many there are.
std.option
Functions on optionals: T?, a value or null. They carry "or nothing" through a series of steps, so that each step does not need its own null test.
import std.option;
// Without: one test per step
let line = io.readLine();
if line != null {
let n = line.parseInt();
if n != null {
println(n * 2);
}
}
// With: each step runs only if the one before gave a value
let doubled = option.map(option.then(io.readLine(), text -> text.parseInt()), n -> n * 2);
| Function | Result |
|---|---|
map<T, U>(value: T?, convert: fn(T) -> U) -> U? |
The value turned into another, or null if there was none |
then<T, U>(value: T?, step: fn(T) -> U?) -> U? |
The same, for a step that may itself have no answer |
filter<T>(value: T?, wanted) -> T? |
The value if the function accepts it, null otherwise |
mapOr<T, U>(value: T?, fallback: U, convert) -> U |
The value turned into another, or fallback |
or<T>(first: T?, second: T?) -> T? |
The first value if there is one, otherwise the second |
orElse<T>(value: T?, make: fn() -> T) -> T |
The value, or what the function makes. Unlike value ?? fallback, the fallback is only worked out when needed |
each<T>(value: T?, action) |
Calls the function with the value, if there is one |
isSome<T>(value), isNone<T>(value) |
|
expect<T>(value: T?, message: String) -> T |
The value, or the program stops with the message |
okOr<T, E>(value: T?, failure: E) -> Result<T, E> |
Ok with the value, or Err with the failure when there is none |
values<T>(items: [T?]) -> [T] |
The items of an array that are not null |
All of them but values are also methods of every optional, with no import: users.get(key).map(u -> u.name), io.env("HOME").expect("HOME is not set"). They are written impl<T> T? { ... }, which a program can use for methods of its own.
map is for a step that always has an answer (fn(Int) -> String), then for one that may not (fn(String) -> Int?). Using map with the second kind would give an optional of an optional; then keeps it flat.
What comes out is an ordinary optional: ?? gives it a fallback, and a test against null makes it a plain value inside the test.
// The name of a user's manager, through two lookups and a field that may be null
let manager = option.then(users.get(key), u -> u.manager);
let found = option.then(manager, users.get);
println(option.mapOr(found, "no manager", u -> u.name));
Type arguments are worked out from the values and functions passed. A function is passed by its name, toString, and so is a method: of a type, String.parseInt or String.trim, which takes the text as its argument, or of a value, users.get, which looks in that dictionary. option.then(io.readLine(), String.parseInt) reads as the steps it takes.
There is no separate Option type with Some and None: T? is that type, and the type checker already refuses to use one as a plain value. These functions add the chaining.
std.result
For failures: returning one with its reason, and surviving one that would stop the program. The whole subject is in errors.md; this is the reference.
Result<T, E>: enum Result<T, E> { Ok(T), Err(E) }, the value a function produced or the reason there is none. A Result<Void, E> is for an action that has nothing to give back: Ok() is its success. The name Result needs no import: every file has it, unless it declares a Result of its own. The rest of the module (attempt, Failure, Kind) is imported as usual.
| Method | Gives |
|---|---|
isOk(), isErr() |
|
value() -> T?, reason() -> E? |
One or the other, or null |
or(fallback) -> T |
The value, or the fallback |
orElse(make: fn(E) -> T) -> T |
The value, or what the function makes of the reason: io.readFile(path).orElse(error -> "(${error.describe()})"). Unlike or, worked out only when needed |
map<U>(convert), mapErr<F>(convert) |
The value, or the reason, turned into another |
then<U>(step) -> Result<U, E> |
A next step that can fail too |
expect(message) -> T |
The value; stops the program if there is none |
or and orElse are named as those of an optional are: or takes the fallback itself, orElse a function that makes it.
| Function | Gives |
|---|---|
collect<T, E>(items: [Result<T, E>]) -> Result<[T], E> |
Ok with the values of all the results, in their order, or the first Err among them: result.collect(lines.map(parseOrder))? |
Failures that stop the program
| Function | Does |
|---|---|
attempt<T>(action: fn() -> T) -> Result<T, Failure> |
Runs the function; if it stops, the program goes on and the failure is returned |
guard(action: fn() -> Void) -> Failure? |
The same, for a function run for what it does |
stack(skip: Int) -> [Location] |
The calls in progress, innermost first, leaving out skip of them: for an error of your own that says where it comes from |
trace(message, stack) -> String |
The message and the calls, over several lines |
| Type | Holds |
|---|---|
Failure |
kind: Kind, message, at: Location, stack: [Location]; describe(), trace(). It prints as describe() writes it |
Kind |
Panic, Bounds, Cast, Arithmetic, Depth, Memory, Library, Other |
Location |
function, file, line, column; describe() |
The builtin panic(values...), which stops the program, needs no import.
std.io
Output goes through the builtins print and println. Reading and writing a file can fail for reasons outside the program; those functions return a Result and never stop the program.
| Function | Result |
|---|---|
readLine() -> String? |
A line of input, without its line end. null at the end of the input |
readBytes(count) -> String? |
The next count bytes of the input, as a text: for input that says first how long it is. null if the input ends before |
ask(question) -> String? |
Prints the question, then reads the answer |
readLines() -> [String] |
Every remaining line of the input |
readFile(path) -> Result<String, Error> |
The whole content of a file, or why it cannot be read |
readFileLines(path) -> Result<[String], Error> |
Its lines |
writeFile(path, text) -> Result<Int, Error> |
Writes the text, replacing what was in the file. How many characters were written |
appendFile(path, text) -> Result<Int, Error> |
Adds to the end of the file, creating it if needed |
exists(path) -> Bool |
|
remove(path) -> Result<String, Error> |
Deletes a file, or a folder that is empty. Gives the path. A folder with all it holds: fs.removeAll |
printError(text) |
Writes to the error output. No line end is added |
env(name) -> String? |
An environment variable, or null if it is not set |
An io.Error says what kind of failure it was, for the program to act on, and the rest for telling a person:
| Field | |
|---|---|
kind: ErrorKind |
NotFound, PermissionDenied, IsDirectory, NotDirectory, AlreadyExists, Other |
operation |
What was being done: "read", "write", "append", "remove"; from std.fs "inspect" (info), "list", "walk" (walk, find), "create" (makeDir, makeTempDir), "remove" (removeAll), "rename", "copy"; from std.os "run" |
path |
As it was given |
message |
As the operating system puts it: no such file or directory |
stack: [Location] |
Where the program asked for the file |
describe() gives one sentence (cannot read notes.txt: no such file or directory), trace() adds where. An Error is Show: println(error) and "${error}" write the sentence, and so does expect when it stops.
Input is what the user types, or what is piped in: qoqi run sum.qi < numbers.txt.
import std.io;
import std.io.{ErrorKind};
import std.strings;
fn main() -> Int {
match io.readFile("notes.txt") {
case Ok(content): {
for i, line in content.lines() {
println(toString(i + 1).padLeft(4), line);
}
}
case Err(error): {
match error.kind {
case ErrorKind.NotFound: { println("no notes yet"); }
default: { io.printError(error.describe() + "\n"); return 1; }
}
}
}
return 0;
}
When the reason does not matter: io.readFile(path).or("").
Paths are relative to the directory the program is run from, not to the program's file. For a file that comes with the program, start from os.program().
std.time
| Function | Result |
|---|---|
now() -> Int |
The time now, a moment: milliseconds since 1 January 1970 |
seconds() -> Int |
The same, in whole seconds |
ticks() -> Int |
Microseconds since the program started. For measuring: it never jumps when the clock is set |
after(length) -> After |
That long from now, to wait for: wait time.after(2.seconds()) |
until(moment) -> After |
A moment, to wait for: wait time.until(renewal). One that has passed is ready at once |
since(moment) -> Duration |
How long ago a moment was |
stopwatch() -> Stopwatch |
Starts measuring |
A Stopwatch has microseconds(), milliseconds() and restart().
let watch = time.stopwatch();
work();
println("took", watch.milliseconds(), "ms");
Lengths of time
A length of time is a Duration, written as what it is: a number and its unit. A number alone is not one, so that seconds are never taken for milliseconds: time.after(500) does not compile, and says to write 500.milliseconds().
wait time.after(2.days());
let renewal = 30.days().fromNow();
wait time.until(renewal);
println(90.minutes(), 1.5.hours() > 80.minutes()); // 1 h 30 min true
| Method | Result |
|---|---|
n.milliseconds(), seconds(), minutes(), hours(), days(), weeks() |
A Duration of that many: 2.days(). On a Float, seconds() to days(), to the nearest millisecond: 1.5.hours() |
inMilliseconds(), inSeconds(), inMinutes(), inHours(), inDays() |
How long it is in a unit, the rest dropped: 90.minutes().inHours() is 1 |
fromNow() -> Int |
The moment that long from now |
after(moment) -> Int, before(moment) -> Int |
The moment that long after or before another: 3.days().after(started) |
times(count) -> Duration |
So many of it: self.day.times(30) |
isZero() |
|
+, -, <, ==, toString |
Duration is Add, Subtract, Ordered, Equal and Show: 1.days() + 3.hours(), and one prints as people say it, 1 day 3 h |
A moment and a length are not added with +: + adds two values of one type. 30.days().fromNow() and 3.days().after(started) say which is which.
In a flow's fields, from the command line or the pages, a Duration is a text with its unit: "90s", "1h30m", "2d", "250ms".
Dates
A moment is a number, which is what now gives. A Date is that moment told the way people do, in some time zone.
| Function / method | Result |
|---|---|
time.today() -> Date |
Now, where the program runs |
time.utc(moment), time.local(moment) |
A moment as told in UTC, or where the program runs |
time.date(year, month, day), dateTime(year, month, day, hour, minute, second) |
In UTC. Numbers out of range carry over: the 32nd of January is the 1st of February |
time.parse(text) -> Date? |
Reads 2026-10-13, 2026-10-13 14:30, 2026-10-13T14:30:00.250+02:00 (or +0200), and the dates of e-mail and of web server logs. The date is told in UTC, whatever zone the text names |
timestamp() -> Int |
The moment |
atOffset(minutes) -> Date |
The same moment told at a fixed offset from UTC: atOffset(120) is +02:00, atOffset(-330) is -05:30. It stays at that offset when moved, across the dates summer time begins or ends |
format(pattern), iso(), isoTime() |
See below; 2026-10-13; 2026-10-13T14:30:00Z, with the milliseconds when there are some, 2026-10-13T14:30:00.250+02:00 |
zone() -> String |
The offset as written in a date: Z for UTC, +02:00, -05:30 |
plus(length), plusDays(n), plusMonths(n), startOfDay() |
New dates: plus(36.hours()), a negative length going back |
daysUntil(other) -> Int |
Days of the calendar, not lengths of 24 hours |
isBefore(other), isAfter(other), isSameDay(other) |
|
time.daysIn(year, month), isLeapYear(year), monthName(month) |
|
<, ==, compare, toString |
Date is Ordered, Equal and Show: dates compare as the moments they are, whatever zone each is told in, so ops.sorted(dates) and ops.largest(dates) work, and one prints as isoTime() writes it |
A Date has the fields year, month (1 for January), day, hour, minute, second, millisecond, weekday (an enum, Weekday.Monday to Weekday.Sunday, with name() and isWeekend()), dayOfYear and offset (minutes ahead of UTC).
In a pattern for format:
YYYY 2026 |
YY 26 |
MMMM October |
MMM Oct |
MM 10 |
M 10 |
DD 03 |
D 3 |
dddd Tuesday |
ddd Tue |
HH 09 |
H 9 |
hh 09 (of 12) |
h 9 |
A AM |
mm 05 |
ss 07 |
SSS 042 |
Z +02:00 |
[text] kept as it is |
let today = time.today();
println(today.format("dddd D MMMM YYYY")); // Tuesday 13 October 2026
println(today.plusDays(30).iso(), today.daysUntil(time.date(2027, 1, 1)));
std.random
| Function | Result |
|---|---|
seed(n) |
Makes the numbers that follow the same on every run, for the same n |
below(limit) -> Int |
From 0 up to, not including, limit. Fails if the limit is below 1 |
between(low, high) -> Int |
From low to high, both included. Fails if low is above high |
float() -> Float |
From 0.0 up to, not including, 1.0 |
chance(probability) -> Bool |
True with that probability: chance(0.25) one time in four |
pick<T>(items) -> T? |
One of the items; null for an empty array |
shuffle<T>(items) |
Changes the array: puts its items in a random order |
uuid() -> String |
A random identifier: 3f2b8c1e-9d4a-4f6b-8a1c-0e5d7b9f2a4c, from the secure generator: seed does not repeat it |
token(bytes) -> String |
A secret nobody can guess, in hexadecimal: token(16) for a session |
Without seed the numbers differ on every run. With it they repeat, which is what a test or a replay needs.
These numbers are for games and simulations. They are not fit for passwords or keys: uuid and token are, coming from the secure generator of the system.
std.path
Paths taken apart and put together. Nothing here touches the disk. The separator is that of the system the program runs on.
| Function | Result |
|---|---|
join(parts) -> String |
The parts put together and tidied: join(["a", "b/", "../c.txt"]) is a/c.txt |
inside(folder, name) -> String |
inside("notes", "todo.txt") is notes/todo.txt |
base(path), dir(path) |
The last part; all but the last part |
extension(path) |
With its dot: .txt, .gz for archive.tar.gz; "" if there is none. The dots a name starts with are not one: .bashrc has none |
stem(path) |
The last part without its extension: .bashrc for .bashrc |
withExtension(path, extension) |
withExtension("a/b.md", ".html") is a/b.html; withExtension(".bashrc", ".bak") is .bashrc.bak |
parts(path) -> [String] |
"a/b/c.txt" gives ["a", "b", "c.txt"] |
clean(path) |
The shortest path that means the same |
isAbsolute(path), absolute(path) |
|
matches(pattern, name) -> Bool |
* stands for any characters, ? for one: matches("*.txt", "todo.txt") |
relative(from, to) -> String? |
The path from a folder to another path: relative("app/src", "app/lib") is ../lib; null where there is none, as between two drives |
separator() |
/, or \ on Windows |
std.fs
Folders and the files in them. Reading and writing the content of a file is in std.io. Like there, what can fail returns a Result whose reason is an io.Error.
| Function | Result |
|---|---|
list(folder) -> Result<[Entry], Error> |
What a folder holds, sorted by name; one level |
walk(folder) -> Result<[String], Error> |
The path of every file under it, however deep |
find(folder, pattern) -> Result<[String], Error> |
Those whose name fits: fs.find("src", "*.qi") |
info(path) -> Result<Entry, Error> |
What is known about one file or folder |
isDir(path), isFile(path) |
|
makeDir(folder) -> Result<String, Error> |
Creates it and the folders above it. One already there is fine |
removeAll(path) -> Result<String, Error> |
Deletes a file, or a folder with all it holds. Nothing there is fine |
rename(from, to), copy(from, to) |
Give the new path |
currentDir(), tempDir() |
|
makeTempDir(prefix) -> Result<String, Error> |
A new, empty folder for the program's own use |
An Entry has name, path, kind (Kind.File or Kind.Dir), size in bytes (0 for a folder), modified (a moment, as time.now), and isFile(), isDir().
match fs.list("notes") {
case Ok(entries): {
for entry in entries { println(entry.name, format.bytes(entry.size)); }
}
case Err(error): { println(error.describe()); } // cannot list notes: no such file or directory
}
std.os
| Function | Result |
|---|---|
args() -> [String] |
What followed the program on the command line: qoqi run notes.qi add milk gives ["add", "milk"] |
program() -> String |
The file the program was run from. Files that come with a program are found from it: path.inside(path.dir(os.program()), "data.csv") |
exit(status) |
Ends the program now. Nothing after it runs, not even an attempt around it |
platform() |
"linux", "darwin", "windows" |
hostname() |
|
environment() -> Dict<String> |
Every environment variable. One alone: io.env |
run(program, arguments) -> Result<Output, Error> |
Runs another program and waits for it |
runWithInput(program, arguments, input) |
The same, with a text for it to read |
command(program, arguments) -> Command |
A program to run, told a part at a time: .inDir(folder), .env(name, value), .withInput(text), then .run(), which gives what run gives |
An Output has status, out, err, and succeeded(), lines(). A program that ran and reports a failure is not an error: look at output.status. The error is for a program that could not be started.
let status = os.command("git", ["status", "--short"])
.inDir("checkout")
.env("GIT_TERMINAL_PROMPT", "0") // added to the environment it inherits
.run()?;
Arguments on the command line. What follows the file is for the program, except the options of qoqi itself (--heap-mb, --max-depth). After a lone -- everything is for the program:
qoqi run todo.qi add "Buy milk" --due=2026-10-20
qoqi run --heap-mb=64 report.qi -- --heap-mb=this-one-is-mine
qoqi run . list # a project: name its folder, then the arguments
Being asked to stop. os.signals() -> Channel<String> gives the signals the program is sent, as they come: "interrupt" for Ctrl-C, "terminate" for kill or a service manager stopping it. Once a program listens, they no longer stop it: it stops its own way, and a second Ctrl-C stops it at once. Waiting for a signal does not keep a program alive whose other tasks are done.
let stop = os.signals();
task.run(fn() {
let name = wait stop.receive();
log.info("stopping", {"signal": name});
server.close(); // what is being answered is answered first
});
std.log
import std.log;
log.info("list saved", {"items": 12, "file": path});
log.warn("slow answer", {"ms": 950});
log.error("cannot reach the database", {"reason": error.describe()});
2026-10-16T09:41:07.120Z INFO list saved items=12 file=todo.json
The messages go to the error output, so that what the program prints stays its result. A value with a space, a quote or an = in it is written in quotes, so the line reads back.
| Function | |
|---|---|
debug, info, warn, error (message, fields = {}) |
A message at that level, with the values that go with it: a Dict<Any> |
setLevel(level) |
Messages below it are not written: log.Level.Debug, Info (at first), Warn, Error |
useJson(), useText() |
One line of JSON per message, {"time": ..., "level": "info", "message": ..., and the values}, for messages that are collected and searched; or text, as at first |
writeTo(write) |
The lines go to a function instead: a file, a list a test looks at |
with(fields) -> Logger |
A logger that adds these values to every message: let request = log.with({"id": id}); in a handler, then request.info("answered") |
A Logger has debug, info, warn, error, with and write(level, message, fields); the level, the form and where the lines go are those of the module, shared by every logger.
std.format
Numbers and tables written for people.
Every function rounds the same way, to the nearest with a half going away
from zero, as Float.round() does: fixed(2.5, 0) is 3, fixed(-2.5, 0)
is -3, money(0.125) is 0.13. The number is rounded as it is written,
so fixed(1.005, 2) is 1.01, though the Float nearest 1.005 is a little
below it. A number that rounds to zero has no minus sign. The rounding
comes before the unit is chosen: duration(59999) is 1m 00s, not
60.0s, and bytes(1048575) is 1.0 MB, not 1024.0 KB.
| Function | Result |
|---|---|
fixed(x, digits) |
fixed(3.14159, 2) is 3.14, fixed(1.0, 2) is 1.00 |
grouped(n), groupedWith(n, separator) |
1,234,567 |
money(amount) |
1,234,567.50 |
percent(share, digits) |
percent(0.125, 1) is 12.5% |
padded(n, width) |
padded(7, 3) is 007 |
hex(n), binary(n), inBase(n, base), fromBase(text, base) -> Int? |
|
bytes(size) |
512 B, 1.5 MB |
duration(milliseconds) |
350ms, 2.5s, 1m 05s, 1h 02m 03s, 3d 04h; the last unit written is rounded |
ordinal(n) |
1st, 2nd, 11th, 22nd |
plural(count, one, many) |
1 file, 3 files |
pad(text, width, align) |
Align.Left, Align.Right, Align.Center |
table(titles, rows) -> String |
Columns as wide as their widest value; a column of numbers is set to the right, numbers as this module writes them included (-1,234, 5,862.69, 12.5%) |
tableWith(titles, aligns, rows) |
The same, saying how each column is set |
bar(value, largest, width) |
A bar of a chart, in # |
print(format.table(["name", "age"], [["ada", "36"], ["grace", "45"]]));
name age
----- ---
ada 36
grace 45
std.json
A document is a value of an enum, so a match takes it apart and the compiler checks every kind is handled:
pub enum Json { Null, Bool(Bool), Number(Float), String(String), Array([Json]), Object(Dict<Json>) }
| Function / method | Result |
|---|---|
json.parse(text) -> Result<Json, ParseError> |
The error has message, line, column, describe(), which is also how it prints. A number no Float holds, 1e400, is an error too |
get(key) -> Json?, at(index) -> Json? |
Into an object, into an array |
find(path) -> Json? |
Keys and positions separated by dots: find("user.tags.0"). A key with a dot in it is out of its reach: get("a.b") |
text(), number(), int(), bool(), items(), fields() |
The value if it is of that kind, else null |
isNull() |
|
a == b, a != b |
By content: the same kind of value, equal items in the same order, equal values under the same keys |
stringify(), pretty() |
On one line; over several, indented by two spaces. println(document) and "${document}" write it as stringify does |
prettyWith(indent) |
Over several lines, each level indented by indent: " ", "\t" |
json.text(value), json.int(n), json.number(n), json.bool(value), json.texts(items), json.ints(items), json.array(items, convert) |
Making documents: json.array(prices, json.number) |
json.object() |
An object built a key at a time: .text(k, v), .int, .number, .bool, .none, .set(k, json), then .done(). What done gives stays as it is when the builder goes on |
json.textOf, intOf, numberOf, boolOf, itemsOf |
The same accessors as functions, to hand to map and the like |
let document = json.parse(text).expect("a document");
let name = document.get("name")?.text() ?? "nobody";
let firstTag = document.find("user.tags.0")?.text();
let reply = json.object().text("name", name).int("visits", 3).set("tags", json.texts(tags)).done();
println(reply.stringify()); // {"name":"ada","visits":3,"tags":["a","b"]}
The keys of an object stay in the order they were written. Numbers are Floats; int() gives one that is whole and that an Int holds, up to 140737488355327 either side of 0, and null for 1e15.
std.regex
Regular expressions, in the syntax of RE2: no look-behind and no back-references, and a search that always takes time proportional to the text. \w, \d and \b know the letters and digits of English only.
| Function / method | Result |
|---|---|
regex.compile(pattern) -> Result<Regex, PatternError> |
Checks a pattern. The error has pattern, message and describe(), which is also how it prints |
regex.must(pattern) -> Regex |
The same for a pattern written in the program: a wrong one stops it |
matches(text) -> Bool |
Whether it fits somewhere |
find(text) -> Match?, findAll(text) -> [Match] |
|
capture(text) -> [String]? |
What the groups of the first match took |
all(text) -> [String] |
The text of every match |
replace(text, with) |
$1 is what the first group took, $0 the whole match, ${name} a named group |
replaceWith(text, convert) |
Each match replaced by what a function makes of it |
split(text) -> [String] |
The pieces between the matches |
regex.escape(text) |
A text as a pattern that matches exactly it |
A Match has text, start, end (positions in characters), groups (what each group took, the first at 0), and group(n), which counts as $n does in a replacement: group(0) is the whole match, group(1) the first group; named(name) is for a group written (?P<name>...).
In a text of a program a backslash is written twice, and ${ would start a value to insert: write "\\d+" and "\${name}". In a replacement a name runs on as far as letters, digits and _ go, so $1_x is the group named 1_x, which there is none of: the first group followed by _x is written "\${1}_x".
let date = regex.must("(\\d{4})-(\\d{2})-(\\d{2})");
date.replace("on 2026-10-13", "$3/$2/$1") // on 13/10/2026
std.csv
| Function / method | Result |
|---|---|
csv.parse(text) -> Result<[[String]], ParseError> |
The rows, each an array of its values |
csv.parseWith(text, separator) |
With ";" or "\t" |
csv.write(rows), csv.writeWith(rows, separator) |
Rows as text, quoting what needs it |
csv.table(text) -> Result<Table, ParseError> |
The first row names the columns; every row must have a value for each |
table.column(name) -> [String]?, cell(row, name) -> String? |
|
table.records() -> [Dict<String>] |
Each row as a dictionary from column to value |
table.where(wanted) -> Table |
The rows a function accepts |
table.write(), len(), header, rows |
A ParseError has message, line and column, the line being the one the row starts on in the text, whatever values over several lines or blank lines come before it, and describe(), which is also how it prints. A byte-order mark in front of the text, which some programs write, is passed over.
std.set
Values without repeats, of any type. A set is the keys of a Map, and tells its items apart as a Map tells its keys: two items are the same when they are written the same and are ==. One made with set.by tells them apart by the text its function gives alone. It prints as its items in braces: {"a", "b"}.
| Function / method | Result |
|---|---|
set.empty<T>(), set.texts(), set.numbers() |
Empty sets |
set.of(items) |
From an array of anything: set.of(["a", "b"]), set.of([3, 1, 3]), set.of(points) |
set.by(key) |
Of anything, given how to tell two apart; two items are the same when key gives the same text: set.by(fn(name: String) -> String { return name.lower(); }) |
add(item) -> Bool, addAll(items), remove(item) -> Bool, has(item) |
add says whether the item was new |
len(), isEmpty(), items(), copy() |
Items come in the order they were first added |
union(other), intersection(other), difference(other), filter(wanted) |
New sets |
isSubsetOf(other), a == b |
Two sets are equal when they hold the same items, in whatever order |
for item in set |
Goes through the items |
std.semver
Versions numbered the semantic way, major.minor.patch, and the constraints written on them in the syntax of Composer and npm. The package manager chooses versions with it.
import std.semver;
let installed = semver.parse("v1.4.2").expect("a version");
let wanted = semver.constraint("^1.2 || ^2.0").expect("a constraint");
wanted.allows(installed) // true
semver.highest(available, wanted) // the newest that fits, or null
installed < semver.parse("1.10.0").expect("") // true: by the numbers, not the letters
| Function / method | Result |
|---|---|
semver.parse(text) -> Result<Version, ParseError> |
1.4.2, v2, 1.4 (2.0.0, 1.4.0), 2.0.0-beta.3, 1.0.0+build.7 |
semver.constraint(text) -> Result<Constraint, ParseError> |
See the table below |
semver.highest(versions, constraint) -> Version? |
The newest the constraint accepts |
semver.sorted(versions) |
Oldest first |
semver.caret(version) -> String |
The constraint to require a version with: ^1.4 for 1.4.2, ^0.3.1 for 0.3.1 |
version.major, minor, patch, pre, build |
pre is the parts after -: ["beta", "3"] |
version.isPrerelease(), release() |
release() leaves out what follows the patch |
version.nextMajor(), nextMinor(), nextPatch() |
|
<, ==, toString, ... |
Version is Ordered, Equal and Show. A pre-release comes before its release; the build plays no part |
constraint.allows(version), isAny() |
| Constraint | Accepts |
|---|---|
1.2.3, =1.2.3 |
That version |
^1.2.3, ^0.3.1, ^0.0.3 |
Up to the next change of the first number that is not 0: <2.0.0, <0.4.0, <0.0.4 |
~1.2, ~1.2.3 |
The last number written may grow: <2.0.0; <1.3.0 |
1.2.*, 1.2, 1.x, * |
Every version that starts so |
>=1.2 <2.0, >=1.2, <2.0, !=1.5.0 |
Comparisons that all hold |
1.0 - 2.1 |
>=1.0.0 <2.2.0 |
^1.0 || ^2.0, ^1.0 | ^2.0 |
Either |
A pre-release is accepted only by a range that names a pre-release of the same version, so that ^1.0 never takes 2.0.0-beta.1, nor 1.3.0-rc.1.
A number is written without zeros in front, as semver.org has it: 01.2.3 and 1.0.0-alpha.01 are not versions, 0.1.0 and 1.0.0-0a are. A ParseError says why, with describe(), which is also how it prints.
std.encoding and std.hash
| Function | Result |
|---|---|
encoding.toBase64(text), fromBase64(encoded) -> String? |
Reading back gives null for a text not in that encoding; with its padding of = or without |
toBase64Url, fromBase64Url |
The variant that is safe in an address, written without padding and read with it or without |
toHex, fromHex |
Two hexadecimal digits per byte |
toUrl, fromUrl |
As a value in the query of an address |
toHtml, fromHtml |
Safe in a page: <, >, &, ' and " as < and the like. Whatever a visitor typed goes through toHtml before http.html |
bytes(text) -> [Int], fromBytes(bytes) -> String |
The bytes of a text in UTF-8, and the text made of some bytes. Bytes that are not UTF-8 stay as they are, as in a text fromHex gives |
hash.sha256(text), sha1, md5 |
Hexadecimal digits |
hash.crc32(text) -> Int |
|
hash.hmacSha256(key, text) |
A signature |
hash.matches(text, sha256Sum) -> Bool |
Whether the text has that sha256: for checking a download against its published sum |
These hashes are made to be fast: they are for telling contents apart and for signing, not for storing passwords.
std.url
import std.url;
let page = url.parse("https://shop.example:8443/search?q=red+shoes&page=2#top").expect("an address");
page.host // "shop.example"
page.port // 8443, an Int?
page.param("q") // "red shoes": decoded
page.withParam("page", "3") // a new Url; println writes it out encoded
page.resolve("../help") // a link found on that page, made whole: Result<Url, ParseError>
A Url is a struct with every part: scheme, user: String?, host, port: Int?, path (decoded), query: [(String, String)] in the order written, fragment: String?. One can be written as a literal, url.Url { scheme: "https", host: "api.example", path: "/v1/users" }, and the parts that need it are encoded when it is written out.
| Function | Result |
|---|---|
url.parse(text) -> Result<Url, ParseError> |
A relative address (/a?b=c, ../d) is one too, with no scheme and no host |
url.queryOf(text) -> [(String, String)] |
"a=1&b=x+y" taken apart and decoded |
u.param(name) -> String?, u.params(name) -> [String] |
The first value of a name in the query; all of them |
u.withParam(name, value), u.withoutParam(name), u.withPath(path) |
A new Url |
u.resolve(link) -> Result<Url, ParseError> |
A link against this address, as a browser does |
u.origin() |
"https://shop.example:8443" |
u.isAbsolute() |
It has a scheme |
u.queryText(), u.toString() |
Written out, encoded |
std.http
Asking a server.
match http.get("https://example.com/api/users/1") {
case Ok(response): { println(response.status, response.body); }
case Err(error): { println(error.describe()); }
}
let created = http.to(Method.Post, url).header("Authorization", token).json(document).timeout(5000).send();
A server that answers "not found" has answered: that is a Response with status 404. An Error is for when there was no answer at all, and its kind says why: Timeout, Refused, UnknownHost, BadUrl, Other, and for a server of your own that cannot listen, InUse when the port is taken. error.describe() says it in a sentence, which is also how an Error prints: no answer from https://example.com/x: ..., or for a server cannot listen on port 8080: the port is taken.
| Function / method | Result |
|---|---|
http.get(url), post(url, body, contentType), postJson(url, document) |
Result<Response, Error> |
http.to(method, url) |
A request built a part at a time: .header, .query, .body, .json, .timeout, then .send() |
response.status, body, headers |
Header names are in lower case |
response.ok(), header(name), json() |
Being one. A handler is a function from a Request to a Response. A Router chooses one from the method and the path:
let app = http.router();
app.get("/hello/:name", fn(request: Request) -> Response {
return http.text("hello, ${request.params.get("name") ?? "you"}");
});
app.post("/notes", createNote);
http.serve(8080, app.handler()).expect("a free port");
| Function / method | Result |
|---|---|
http.serve(port, handler) |
Answers every request, each in a task of its own, for as long as the program runs. It returns only if the port cannot be listened on: nothing can close the server it makes |
http.listen(port) -> Result<Server, Error> |
Port 0 lets the system choose: server.port |
server.handleAll(handler) |
Answers every request, each in a task of its own, until the server is closed |
server.handle(count, handler), next(), reply(response) |
That many requests one after the other, or one at a time by hand. next() is null once the server is closed |
server.nextWithin(milliseconds) -> Request? |
next, giving up after that long: null if no request came |
server.answer(request, response) |
Answers a request next gave, whichever it was: several can be in progress |
server.close() |
Stops taking requests, waits for those in progress to be answered, ten seconds at most, then closes |
router.get, post, put, patch, delete, on(method, path, handler) |
:name takes one part of the path into request.params; a last * takes the rest, as "rest" |
router.answer(request) -> Response |
The answer, without a server: how routes are tested |
http.request(method, address, body = "") -> Request |
A request as a server hands it over, to give router.answer: http.request(Method.Get, "/notes?tag=work"). No headers; its sender is "test" |
http.methodOf(name) -> Method? |
methodOf("post") is Method.Post, whatever the case; method.name() is "POST" |
http.queryOf(text) -> Dict<String> |
"a=1&b=x+y" as a dictionary, decoded |
http.text(body), html(body), sendJson(status, document), respond(status, body), notFound(), redirect(to) |
Answers |
http.files(folder) |
A handler that serves the files of a folder, named by what a route's * takes: app.get("/static/*", http.files("public")). A folder gives its index.html; the content type comes from the extension; what is not there, or tries to leave the folder with .., is 404 |
response.with(name, value) |
One more header |
request.method, path, query, headers, body, params, sender, header(name), json() |
A path nobody takes is answered with 404, one taken only for other methods with 405. A HEAD request that no route takes for HEAD is answered by the GET route of its path, with that route's status and headers and no body. A handler that stops, by a panic or any other failure, does not stop the server: its request is answered with 500 and the failure goes to the error output.
request.query holds one value for a name: in ?a=1&a=2, the last, "2". For all of them, read the address with std.url: url.queryOf(text) keeps every pair in order, and u.params("a") gives ["1", "2"].
Stopping a server. serve answers for as long as the program runs. A server that is to stop is listened on, answered with handleAll in a task, and closed:
let server = http.listen(8080).expect("a free port");
let serving = task.run(fn() { server.handleAll(app.handler()); });
// ... until it is time to stop
server.close(); // the requests in progress are answered first
wait serving; // handleAll has returned
While it closes, a request that comes in, or one still unanswered after ten seconds, is answered 503, the server is closing. Once close returns, the port is free.
With serve and handleAll a handler that waits, for another server or for the clock, does not keep the other requests waiting; handlers still run one at a time between their waits (tasks.md). handle answers one request after the other. The server listens on this machine only (127.0.0.1).
std.sql
import std.sql;
import std.sql.{Value};
let db = sql.open("shop.db").expect("the database");
defer db.close();
db.exec("CREATE TABLE IF NOT EXISTS parcels (id TEXT PRIMARY KEY, city TEXT, grams INTEGER)")?;
db.exec("INSERT INTO parcels VALUES (?, ?, ?)", [Value.Text("p-1"), Value.Text("Lyon"), Value.Int(1200)])?;
for row in db.query("SELECT id, grams FROM parcels WHERE city = ?", [Value.Text("Lyon")])? {
println(row.text("id"), row.int("grams")); // p-1 1200
}
A database is a file of SQLite (a path, or :memory:), or a server, by its address: postgres://user:secret@host:5432/shop, mysql://user:secret@host:3306/shop, what follows ? passed to the server (?sslmode=require, ?tls=true). A program is the same on all three: a parameter is written ? everywhere, and Postgres is given $1, $2. Waiting for the database lets the other tasks go on.
sql.open(address) -> Result<Db, Error> |
A file that is not there is made |
db.exec(statement, params = []) -> Result<Int, Error> |
A statement that gives no rows: how many rows it changed |
db.query(statement, params = []) -> Result<[Row], Error> |
The rows a statement gives |
db.one(statement, params = []) -> Result<Row?, Error> |
The first row, null when there is none |
db.begin() -> Result<Tx, Error> |
A transaction: tx.exec, tx.query, tx.one as on db, then tx.commit() -> Result<Void, Error> or tx.rollback() |
db.transaction(fn(tx: Tx) -> Result<T, Error>) -> Result<T, Error> |
Committed when the function gives Ok, rolled back when it gives Err |
db.kind(), db.close() |
"sqlite", "postgres" or "mysql"; closing twice is closing once |
row.text(column) -> String?, int, float, bool |
null when the column is NULL, of another kind, or not there; bool takes 0 and 1, as SQLite and MySQL keep a truth |
row.get(column) -> Value?, row.isNull(column), row.columns() |
The value as it came; println(row) shows {id: "p-1", grams: 1200} |
A Value is Null, Int(Int), Float(Float), Text(String) or Bool(Bool), given as parameters and found in rows. A decimal comes as an Int when it is whole (SUM, COUNT on Postgres and MySQL), a Float otherwise; a number beyond an Int as its digits, a Text; a moment as a text of ISO 8601; bytes that are not text as \x and their hex.
In a transaction, a statement that fails leaves the transaction as it was, on every database (Postgres is given a savepoint around each). A table made, changed or dropped in a transaction is so in it on SQLite and Postgres; MySQL, which would commit the transaction for it, has it done beside, at once. Error has a kind: Unreachable, Query (the words, a table or a column), Constraint (a key taken, a NULL, a reference), Conflict, Closed or Other, and a message. Conflict is the database ending a transaction for another that ran at the same time: nothing more runs in it, its commit fails, and the work is to be done again in a new one.
In a flow, a database is the world: it is reached from a step. A step that writes only to the database the flows are kept in asks for it with flow.store(), and what it writes is kept with what it returns, in one transaction (flow.md).
A depot's parcels, run on each of the three: test/e2e/cases/sql_depot.qi.
std.ops
The traits that give a struct or an enum the syntax of the builtin types, and what a generic function asks of its type parameters. They are explained, with how to implement them, in traits.md.
| Trait | Method to write | Gives |
|---|---|---|
Equal |
equals(other: Self) -> Bool |
==, != |
Ordered |
compare(other: Self) -> Int |
<, <=, >, >=; isBefore, isAfter |
Add, Subtract, Multiply, Divide |
plus, minus, times, dividedBy |
+ - * /, += ... |
Show |
show() -> String |
What println, toString and "${...}" write, for the value alone and wherever it is held: in an array, a field, an enum, a tuple |
Iterable<T> |
iterate() -> [T] |
for item in value |
Indexed<K, V>, IndexedSet<K, V> |
at(key) -> V, put(key, value) |
value[key], value[key] = v; |
Int, Float, String and Bool implement Equal and Show; all but Bool implement Ordered.
Range
What 0..10 is when it is kept instead of looped over: let teens = 13..20;. A struct, made by writing a range (no import is needed for that; import std.ops.{Range}; to name the type). It knows its numbers without holding them, and a loop over one counts.
| Field or method | |
|---|---|
from, to, inclusive, by |
The ends as written, whether to is one of the numbers (..=), and the step, 1 unless step said otherwise |
step(by: Int) -> Range |
One number every by: (0..10).step(3) is 0, 3, 6, 9. Below 0 it goes down from from: (10..0).step(-2) is 10, 8, 6, 4, 2. A step of 0 stops the program |
reversed() -> Range |
The same numbers, from the last to the first |
len() -> Int, isEmpty() -> Bool |
How many numbers. A range that cannot move towards its end (10..0) holds none |
first() -> Int?, last() -> Int? |
null when there is none |
contains(n: Int) -> Bool |
Whether n is one of them, the step counted |
toArray() -> [Int], map<U>(f) -> [U], filter(wanted) -> [Int] |
The numbers, or what a function makes or keeps of them |
It is written as its range: 0..10, (0..=10).step(5).
For any type that implements the trait named:
| Function | Result |
|---|---|
max<T: Ordered>(a, b), min(a, b) |
The first when neither comes first |
largest<T: Ordered>(items) -> T?, smallest(items) -> T? |
null for an empty array |
sorted<T: Ordered>(items) -> [T] |
From first to last; equal items keep their order |
contains<T: Equal>(items, wanted), indexOf(items, wanted) -> Int? |
|
same<T: Equal>(first, second) -> Bool |
Equal items in the same order |
join<T: Show>(items, separator) -> String |
The texts of the items |
import std.ops;
import std.ops.{Ordered};
struct Version { major: Int; minor: Int; }
impl Ordered for Version {
fn compare(other: Version) -> Int {
if self.major != other.major { return self.major - other.major; }
return self.minor - other.minor;
}
}
let newest = ops.largest(versions);
if newest != null && newest > installed { ... }
Templates
A Template is a text written with ${...} in it, kept in its pieces: parts, what was written around the values (one more than there are values), and values, what each ${...} gave, as text. text() puts them together. A function whose parameter is a Template gets those, where one taking a String would get the text:
fn shout(t: Template) -> String { ... }
shout("hello ${name}, ${count} times"); // parts ["hello ", ", ", " times"], values [name, count]
The function can then tell what the program wrote from what was put in it, which is how a query or a page can escape what is put in them.
A text written in the program becomes a Template wherever that is the type it must have: an argument, a variable or a field declared Template, a return from a function that returns one, an item of a [Template]. So a command can be kept, built by a function or carried in a struct:
struct Job { name: String; command: Template; }
fn deploy(target: String) -> Template { return "make deploy ENV=${target}"; }
let jobs = [Job { name: "lint", command: "composer pint-test" }, Job { name: "deploy", command: deploy(env) }];
for job in jobs { sh.run(job.command); }
Like Result, the name Template needs no import. A String made elsewhere is refused where a Template is expected, since nothing would then say which of it was written and which was put in. Everywhere else, "${...}" is a String as always, and t.text() turns a Template into one.
std.task
Functions running at the same time, and channels between them. All of it, with how a program of several tasks ends, is in tasks.md.
let pages = wait task.map(urls, url -> http.get(url)); // all asked at once
let lines = task.channel<String>(16);
task.run(fn() {
for name in files { wait lines.send(io.readFile(name).or("")); }
lines.close();
});
lines.each(fn(text: String) { println(text.len()); });
One task runs at a time, until it waits: for a task, a channel, the clock, the network, a line of input, another program. What a task waits for in the program is written wait: wait job, wait lines.receive(), wait time.after(500.milliseconds()), and wait match { ... } for the first of several. Tasks share everything and need no locks, as long as a change does not have a wait in its middle.
| Function / method | Result |
|---|---|
task.start(work) -> Task<T> |
Runs work on its own; returns at once |
task.run(action) -> Task<Bool> |
The same for a function that returns nothing |
wait task -> Result<T, Failure> |
What it returned, or the failure that stopped it |
task.isDone() |
|
wait task.all(tasks) -> Result<[T], Failure> |
Every value in order, or the first failure |
wait task.settle(tasks) -> [Result<T, Failure>] |
How each ended |
wait task.map(items, work) -> Result<[U], Failure> |
One task per item |
task.pause(), task.current() |
Lets the others have a turn; the number of the task that asks |
task.channel<T>(capacity) -> Channel<T> |
Capacity 0: a send waits for its receive |
wait channel.send(value), wait channel.receive() -> T?, close() |
receive gives null once closed and empty |
channel.each(action), drain(), len(), isClosed() |
|
wait match { case let v = channel.receive(): ...; case time.after(2.seconds()): ... } |
Whichever comes first: tasks.md |
Waitable<T> |
What wait waits for; a type of the program can implement it: tasks.md |
A task that stops does not stop the program: whoever waits for it gets the failure. If nobody ever does, the failure ends the program once the other tasks are done. Tasks that all wait for each other end it with error R0009.
std.compiler
The front of the compiler, asked about a program instead of told to run it. Every function takes the path of a file and its text: the text is what is looked at, the path says where its modules are and how places are named.
let source = io.readFile("main.qi").expect("a file");
for problem in compiler.check("main.qi", source) {
println(problem.describe()); // main.qi:4:9: error E1001: undefined identifier 'totl'
}
| Function | Result |
|---|---|
compiler.check(path, source) -> [Problem] |
Errors and warnings: severity, code, message, at, endColumn, suggestion |
compiler.symbols(path, source) -> [Symbol] |
What the file declares: kind, name, detail, container, line, column |
compiler.describe(path, source, line, column) -> Description? |
The name at that place: its signature, the doc above its declaration, where it is declared |
compiler.complete(path, source, line, column) -> [Suggestion] |
What could be written there: after a dot, what the value before it has. The line being typed need not make sense |
compiler.open(path, text), close(path) |
A file being edited: importing it gives this text, not what is on disk |
Lines and columns count from 1, columns in characters. describe answers null for a program with errors that keep it from being understood; complete then works from the file without the line being typed.
It is what the language server is written with, and serves as well for a linter or a documentation generator: internal/lsp/server.qi is the longest example.
std.test
import std.test;
fn main() -> Int {
return test.suite("prices")
.test("a total adds up", fn() { test.equal(total([1, 2, 3]), 6); })
.test("an empty basket costs nothing", fn() { test.equal(total([]), 0); })
.run().exitCode();
}
prices
ok a total adds up
FAIL an empty basket costs nothing
expected 0, got 1
at prices.qi:6
1 passed, 1 failed
A check that does not hold stops its test; the suite goes on with the next and reports at the end. So does any other failure in a test: an index out of bounds is a failed test, not the end of the run.
| Function / method | |
|---|---|
test.suite(name), .test(name, body) |
|
.run() -> Report |
Runs and prints. .check() runs without printing |
report.ok(), passed(), failed(), exitCode(), lines(), results |
|
equal(got, want), notEqual |
As == has it: by what the values hold, arrays and structs too |
same(got, want) |
Two arrays with equal items in the same order; a failure says which item differs |
near(got, want, tolerance) |
For numbers with a point |
isTrue(condition, what), isFalse |
|
present(value, what) -> T, absent(value, what) |
For optionals; present gives the value |
contains(text, part) |
|
fails(kind, action) -> Failure |
The function must stop with a failure of that kind |
fail(message) |
For a check of your own |
Test files: qoqi test
A suite needs a main that builds it. qoqi test writes that main for you: put the tests in a file whose name ends in _test.qi, one function each, named test....
// prices_test.qi
import std.test;
import shop.prices;
fn testATotalAddsUp() {
test.equal(prices.total([1, 2, 3]), 6);
}
fn testAnEmptyBasketCostsNothing() {
test.equal(prices.total([]), 0);
}
$ qoqi test
shop/prices_test.qi
ok a total adds up
ok an empty basket costs nothing
2 passed
qoqi test |
Every test file under the current folder, however deep. Folders whose name starts with a dot are left out |
qoqi test shop prices_test.qi |
The test files under these folders, and these files |
qoqi test --filter=basket |
Only the tests whose name holds the text, whatever its case |
A test is a function at the top of a test file whose name starts with test, that takes nothing and gives nothing back. It is reported by the words of its name: testReadsJSONText is "reads JSON text". The other functions of the file are helpers, and so is a testData() that returns something.
Each test file is a program of its own, with the modules of its project in reach, and no main. Without a project file, module paths start at the folder qoqi test runs in: from the folder that holds shop, shop/prices_test.qi imports shop.prices. The tests of a file run in the order they are written; one that fails does not stop the next. The status of qoqi test is 0 when every test passed, and 1 when one failed or a file could not be run: it does not compile, or a test ended the program with os.exit.
The example flows have test files: qoqi test examples/flows.
std.layout
Text laid out to fit a width. What is to be written is described once, as a document: pieces of text, and the places where a line may end. Writing it out decides which of those places are used.
import std.layout;
import std.layout.{Doc};
fn list(items: [Doc]) -> Doc {
return layout.group(layout.concat([
layout.text("["),
layout.indent(layout.concat([Doc.Soft, layout.join(items, layout.concat([layout.text(","), Doc.Line]))])),
Doc.Soft,
layout.text("]"),
]));
}
print(layout.render(list(words), 80, 4)); // [one, two, three], or an item to a line
A group is written on one line if it fits, and otherwise every line end directly in it is taken: all or none. Groups inside it decide for themselves.
layout.text(text), concat(parts), join(items, separator) |
Text, and documents one after the other |
layout.group(doc) |
On one line if it fits. layout.broken(doc) takes its line ends whether it fits or not |
layout.indent(doc) |
The lines that start inside it start one step further in |
Doc.Line, Doc.Soft |
Where a line may end: a space, or nothing, in a group that fits |
Doc.Hard |
Where a line ends. Two in a row leave an empty line; more leave no more than that |
Doc.IfBroken(taken, fitting) |
One document or the other, by whether the group around it fits: the comma after a last item |
layout.fill(items) |
Items separated by commas that fill each line before starting the next |
Doc.Trailing(comment) |
A comment for the end of the line, wherever that comes to be; those of lines that follow each other start in one column |
Doc.Item(doc, open), Doc.Gap |
Things written under each other: one that comes to take several lines gets an empty line around it |
Doc.Verbatim(text, indent) |
Text over several lines, written as it is |
layout.render(doc, width, step) -> String |
The text: lines of width characters at most where it can be helped, step spaces a level |
layout.forces(doc) -> Bool |
Whether it holds a line end that is always taken |
It is what the formatter is written with, and serves as well for printing JSON, a query, or a program in another language.
std.formatter
The formatter of Qoqi as a function: the one qoqi fmt and the language server run.
import std.formatter;
match formatter.format(source) {
case Ok(tidy): { io.writeFile("main.qi", tidy); }
case Err(problem): { println(problem.describe()); } // 12:5: expected ';', found 'let'
}
formatter.format(source) -> Result<String, Problem> |
The text of a program, formatted. A text that is formatted already comes back as it is |
Problem |
Where the text stops reading as a program: line, column, message, describe() |
formatter.width, formatter.step |
100 and 4: how long a line may be, how far a level is indented |
What it decides is in formatting.md. It needs a text whose brackets close and whose statements end; names and types it does not look at.
How It Is Built
The library has two halves.
Qoqi source. Each module is a .qi file in internal/stdlib/lib/, compiled into the qoqi binary. Most of the library is written here, in the language itself: gcd, sorted, all of List and Dict. These are ordinary modules; a program importing one gets its functions compiled along with its own.
Native functions. What cannot be written in Qoqi, because it needs the machine (files, the clock) or would be too slow (searching a text), is declared in the module without a body:
pub extern fn sqrt(x: Float) -> Float;
and provided by a Go function in the virtual machine (internal/vm/native_*.go) registered under the function's full name, std.math.sqrt. See extern fn.
The package std is found like any package; the only difference is that its folder is inside the binary. In messages its files appear as <std>/strings.qi. A project that lists a package named std in its qoqi.json gets that one instead, whole.
Natives hand back simple things. A native function cannot make a struct or a value of an enum: those are made by compiled code, which knows what each must hold. So a native returns a number, a text, or arrays of them, and the Qoqi half of the module builds the types the program sees. json.parse is the clearest case: Go checks the text and lists its tokens, and thirty lines of Qoqi build the tree of Json values from them. An operation that fails gives null and keeps the reason, which the Qoqi half fetches to build its error value.
The web server is split the same way, for another reason: only one task of a program runs at a time. Go accepts connections and reads requests, each in its own goroutine, and queues them; the program takes them one at a time, works out each answer in Qoqi, in a task of its own if it wants several in progress, and hands it back under the number of its request.
Tasks are mostly Qoqi as well. The machine provides tasks by number and three things to do with them: suspend the one that asks, resume another, wait for one to end. Task<T> and Channel<T> are written on those, which is how they come to be typed and collected like any other struct (tasks.md).
A folder called std among a program's own modules is not reachable as std.something: the name belongs to the library.