The standard library
The standard library is written in Lyric and ships as source alongside the toolchain.
| Module | Contents |
|---|---|
std.core |
panic, assert, todo, unreachable, Exception, Display, Hashable, Equatable, Ordered |
std.string |
inspection, search, split, join, trim, pad, parsing, StringBuilder |
std.fmt |
number formatting, padding, alignment, tables |
std.math |
sqrt, pi, abs, min, max, rounding, trigonometry |
std.collections |
List<T>, Map<K, V>, Set<T>, Indexable<T>, sorting |
std.iter |
Iterator<T>, Iterable<T>, adapters, sum |
std.option |
map, andThen, filter, zip, contains, toArray, iter, expect |
std.io.console |
print, println, readLine — the writers take any Display value: println(42) |
std.io.file |
reading and writing files — requires fileAccess |
std.os |
environment, process, exit — requires osAccess |
std.random |
Random.seeded, shuffle, choice, nextGaussian — deterministic, no capability |
std.time |
Instant, Duration, ISO 8601 — requires osAccess |
std.build |
addExecutable — only a build.lyr run by lyric build can use it |
Collections
List<T> grows; T[] does not.
import std.io.console { println };
import std.collections { List };
fn main(): int {
let items = List<string>.empty();
items.push("first");
items.push("second");
for (item in items) {
println(item);
}
let copy = items.toArray();
return copy.length;
}
Iteration
Anything implementing Iterable<T> works with for-in, including your own types.
import std.io.console { println };
import std.iter { Iterator, Iterable };
class Countdown :: [Iterable<int>] {
from: int,
fn iter(): Iterator<int> {
return CountdownIter { remaining = this.from };
}
}
class CountdownIter :: [Iterator<int>] {
remaining: int,
mut fn next(): ?int {
if (this.remaining <= 0) { return null; }
let value = this.remaining;
this.remaining = this.remaining - 1;
return value;
}
}
fn main(): int {
var total = 0;
for (n in Countdown { from = 3 }) {
total = total + n;
}
println(f"{total}");
return total;
}
An Iterator<T> yields ?T and signals the end with null. Iterable<T> hands out a fresh
iterator per call, so two loops over the same collection do not interfere.
One name per type, not overloads
Lyric has no overloading, so the library distinguishes by the type in the NAME. std.math carries
both families side by side:
| float | int |
|---|---|
min, max |
minInt, maxInt |
clamp |
clampInt |
abs, sign |
absInt, signInt |
clamp(index, 0, count - 1) on three ints is therefore cannot assign 'int' to 'float', and
the answer is clampInt rather than a conversion there and back. The same rule shapes
fromInt/fromFloat and parseInt/parseFloat in std.string.
Iterators chain
Since 2.17 the adapters are METHODS, so a pipeline reads left to right instead of inside out:
import std.iter { collectArray };
import std.collections { List };
fn main(): int {
let xs = List<int>.empty();
xs.push(1);
xs.push(2);
xs.push(3);
let out = collectArray<int>(
xs.iter().map<int>((n: int) => n * 2).filter((n: int) => n > 2).take(2));
return out.length;
}
map, filter, take, skip, takeWhile, zip, chain and flatMap are methods on
Iterator<T>; the free forms still work, warn, and go with 3.0.
Two families stay free, each for a reason worth knowing:
sum,sumFloat,minValue,maxValueask something of the ELEMENT type — that it is a number, that it is ordered — and an interface cannot require that of its own parameter.enumerateandchunkschange the element type without being generic, and a method like that would ask the compiler to buildIterator<(int, T)>, thenIterator<(int, (int, T))>, without end.mapandflatMapchange it safely because they are generic: they are built per use rather than per instance.
Files answer two ways, and each says which
Since 2.14 std.io.file has exactly two shapes, and the choice between them is not a taste:
import std.io.file as file;
fn main(): int {
let path = "notes.txt";
let content = file.text(path) ?? ""; // a READ answers ?T
let saved = file.writeText(path, content); // an OPERATION answers bool
return if (saved) 0 else 1;
}
A read answers ?T: null means the file could not be read, and an empty result means an
empty file. A write, a remove, a copy answers bool — whether it happened; it carries no
value, so nothing is lost by saying only that. A predicate (exists, isFile,
isDirectory) answers bool too, and there false is an answer rather than a failure.
Before 2.14 there were three shapes, and the third one lied: readBytes and readLines answered
an empty array both for an empty file and for a file that is not there. No caller could tell
those apart, and the advice was to ask exists first — which is a race, not an answer. The three
old names still work and warn (readText, readBytes, readLines); they go with 3.0, and the
compiler will say so on the day.
What none of the shapes carries is a REASON: a missing file and a permission denied look the
same. That gap is known and left open on purpose — carrying reasons means an error type and a
decision about throws that this module should not make on its own.
Capabilities
std.io.file, std.io.net and std.os require a capability. A standalone run grants
everything; a host grants explicitly, and a module that requires more than it is granted is
rejected before it runs.
Strings have methods
Since v1.15 the string API is method-shaped: s.trim(), s.split(","), s.contains(x),
s.length(). The deprecated free forms (trim(s), …) went with 2.0. concat and repeat
stay free — they back + and * — and the type-directed families (fromInt, parseInt, …)
keep their names.
The methods come with the module: any import std.string { … } makes them visible, and a file
that needs no free name imports the module under an alias, which avoids shadowing the builtin
type name:
import std.string as strings;
fn main(): int {
return " Grüße ".trim().length();
}
Two things worth knowing: s.length() is a call BECAUSE it costs O(n) — a property would
promise a stored answer this type does not have — and every method returns a NEW string;
s.trim(); as a statement does nothing, because a Lyric string has nothing to mutate.
Constructors live on the types
A container comes from its type: List<int>.empty(), Map<string, int>.empty(),
Set<int>.empty(), StringBuilder.new(), Random.seeded(42) — the latter from std.random
since v1.14, where shuffle, choice and nextGaussian live beside it. The old free
functions (emptyList, emptyMap, emptySet, newRandom) and the std.math.Random twin
went with 2.0.
Editing the standard library
The toolchain reads the standard library from beside its own binary — a copy made at build
time — or from LYRIC_STDLIB when that is set. Editing the sources in the repository therefore
changes nothing until the next dotnet build refreshes the copies, and a published toolchain
froze its copy when it was packaged. When an edit "is not found", it is almost always this:
point LYRIC_STDLIB at the repository's stdlib/ while working on it.
The library also holds itself accountable: every public item is documented (a test pins
completeness), every file is formatted and compiles without a single diagnostic, and
stdlib-tests/ carries its behavioral tests — written in Lyric, run by lyric test.