Standard library
This specification owns the standard library's module and function lists, the
rules every std function follows, the declarations std marks @lang for the
compiler, the clock and runner modules of Tool declarations, and
json.encode and json.decode. Each module's exports are the declarations in std source for the
language version; this file fixes what every one of them must satisfy and lists
every function std v1 declares.
It creates no type, effect, concurrency, Tool or versioning semantics. The
built-in data types are data.md's, the data-type category and
generics types.md's, effects and pass-through
effects.md's, Result, ToolProblem and faults
errors.md's, the task functions and clock.timeout
concurrency.md's, Tool declarations and decoding
tools.md's, and imports, the prelude, the @lang annotation and
std versions modules.md's.
std is a versioned repository
std is imported like any repository, by path and version:
use "std@v1/list"
use "std@v1/task"
flow names(rows: List<Row>) -> List<Text> = list.map(rows, flow(r) = r.name)Open in playground →The std version is the language version: std@v1 is Flow v1, and a program
uses one std (modules.md). Adding, removing or changing a std
declaration is therefore a change of language version, and a std minor version
only adds.
Imported names are used qualified, list.map, task.spawn. Method syntax on a
built-in type needs no import (semantics.md).
The v1 modules
| Module | Declares |
|---|---|
list |
List<T> and functions over lists |
dict |
Dict<K, V> and functions over dictionaries |
set |
Set<T> and functions over sets |
text |
Text and functions over text |
int |
Int, its operators, and functions over integers |
decimal |
Decimal, its operators, and functions over decimals |
bytes |
Bytes and functions over bytes |
option |
functions over Option<T> |
result |
functions over Result<T, E> |
task |
Task<T>, Stop<T>, and the task functions |
json |
json.Value, json.encode and json.decode |
time |
Instant and functions over instants |
duration |
Duration and functions over durations |
clock |
Tool declarations for reading time and waiting |
runner |
Tool declarations for checking, running and resuming other Flow programs |
and the prelude module, marked @lang(prelude), whose exports are visible
without an import (modules.md). These fifteen modules and the
prelude are the whole of std v1.
Every std function is total and pure
Every function std declares, outside clock and runner, satisfies three
rules.
Total. It returns a value for every argument of its declared type. Absence,
a missing key, an out-of-range index and malformed input are values a program
matches on, usually None or Err. The only std operations that fault are the
ones a specification names: division by zero (data.md), and
waiting (task.await or task.first) on a stopped task and task.first([])
(concurrency.md). Exceeding a host limit faults anywhere
(errors.md). There is no unwrap, no fail and no other
function that turns a value into a fault.
Pure. It makes no Tool call of its own. A function that takes a callback
passes the callback's effect through, so list.map with a !tool callback is
a !tool call and with a pure one is pure (effects.md). Where
std needs a pure callback, it marks the parameter !pure, as list.sort_by
does for its key. The task functions are pure in this sense: their scheduling
choices are recorded, not effects (concurrency.md).
Closed. It reads no clock, randomness, file, network, process, environment, locale or host state, and nothing about the run itself. Its result depends only on its arguments and the language version. std exposes no hashing of values, no identity of a value beyond its contents, and no reading of the run's history (history.md).
std holds no policy. Credentials, approvals, budgets, retries, rate limits and
sandboxing belong to the host or the application around the run, which may
build them from Tools and ordinary Flow code. Retrying a call is an ordinary
program over ToolProblem (errors.md).
The clock module
std has no clock of its own. Its clock module is @tool declarations that
the host implements (tools.md), so reading the time and waiting are
Tool calls, recorded and replayed like any other:
@tool pub flow now() -> Result<time.Instant, Never>
@tool pub flow sleep(d: duration.Duration) -> Result<Unit, Never>
pub type Timed<T> { Done(T), TimedOut, ClockFailed(ToolProblem<Never>) }
pub flow timeout<T>(limit: duration.Duration, work: () -> T) -> Timed<T> !toolOpen in playground →now and sleep declare no errors of their own, so their error type is
Never; a call can still come back as NotRun, Unknown or BadReply
(errors.md). timeout is ordinary Flow over sleep and the
task functions, and its meaning is concurrency.md's.
ClockFailed keeps a failed clock from being read as a timeout.
The runner module
Checking, running and resuming other Flow programs is a Tool the host provides
(tools.md). std's runner module declares it:
check, run and resume, the host type Run for a paused run, and the
records and enums their arguments and replies are built from, Diagnostic
among them. Each is a Tool call like any other, recorded and replayed as one.
execution.md owns the
declarations, what each operation does and how its values cross.
@lang declarations
The compiler knows mechanisms and categories, never particular names.
Everything it treats specially is declared in std source and marked @lang,
so it can be read and navigated, and only std may use the annotation
(modules.md). std declares:
| Rule, stated generally | Declared in std |
|---|---|
| A bodyless type is implemented outside Flow, and is data only when declared so | @lang(int, data) type Int in int, @lang(list, data) type List<T> in list, and one such declaration for each built-in type in its home module; @lang(task) type Task<T> in task, which is not data |
| A bodyless function is implemented by the compiler | each @lang function, such as task.spawn |
| An operator is a function declared for its operand type | +, -, *, % and unary - in int; +, -, *, / and unary - in decimal; ++ in list and text; each marked @lang |
| Each literal form builds the type std marks for it | @lang(list_literal), @lang(dict_literal), @lang(text_literal), and the integer and decimal literal forms |
true and false are keywords of the boolean type |
@lang(bool) on Bool |
| A Tool call wraps its error in the Tool-problem type | @lang(result) on Result, @lang(tool_problem) on ToolProblem |
? works on the result type |
@lang(result) on Result |
A missing or null external field is allowed only for the optional type |
@lang(option) on Option |
| An expression that never produces a value has the never type | @lang(never) on Never |
| Names the prelude exports can't be declared again | the @lang(prelude) module's exports |
| A value of the raw-JSON enum encodes as the JSON value it stands for | @lang(json_value) on json.Value |
Each rule is stated by its owner; this table says only where std fills it in.
Adding, renaming or removing one of these declarations is a std change and so a
language version change, not a new compiler rule. A @lang declaration's
signature is an ordinary signature, checked as one, so a reader learns a
compiler-implemented function's type and effect the same way as any other's.
json
json.encode turns a value of a data type into text, and json.decode turns
text into a value of the type the caller expects, by exactly the rules a Tool
boundary uses: the same encoding
(runtime/spec/abi.md) and the same decoding table,
including that nothing decodes into a type that is or contains an opaque
type (tools.md). Decoding never faults; text that does not match
the expected type is an Err saying where and why:
@lang(json_decode) pub flow decode<T>(text: Text) -> Result<T, DecodeError>
pub type DecodeError { pub path: Text, pub problem: Text }Open in playground →path names the place in the value that failed, such as steps[2].owner, and
problem says what was wrong there.
let text = json.encode(report)
let plan: Plan = match json.decode(text) {
Ok(p) => p,
Err(problem) => return Unreadable(problem),
}Open in playground →json.Value is an enum std marks for raw JSON encoding: it describes any JSON
value, and encodes as that value
(runtime/spec/abi.md). It is how a
program passes arbitrary JSON through, for example to a generic Tool over an
open registry of operations; there is no dynamic type.
The v1 functions
std v1 declares exactly these functions, 98 in all; the signature and doc
comment of each are in its module's std source. A function marked @lang there
is implemented by the compiler; every other one is ordinary Flow over these.
| Module | Functions | Owner of the rule |
|---|---|---|
list |
concat (++), length, get, empty, fold, map, filter, filter_map, flat_map, enumerate, any, all, find, contains, sort_by (stable, pure key), dedupe, dedupe_by, partition, take, take_last, zip, join |
data.md, types.md |
dict |
get, returning an Option; insert, remove, keys, values, to_list, contains_key, length, empty |
data.md |
set |
from, building a set from a list; contains, to_list, length |
data.md |
text |
concat (++); length, get and slice in code points; code_points, graphemes, byte_length, to_bytes, normalize, lowercase, uppercase, regex, matches |
data.md |
int |
add, sub, mul, rem and neg (the operators); div, integer division rounding down; checked_div, returning an Option; to_decimal |
data.md |
decimal |
add, sub, mul, div and neg (the operators); floor, rounding down to an Int |
data.md |
bytes |
length; to_text, returning an Option |
data.md |
option |
unwrap_or, to_result, is_some, is_none, map |
errors.md |
result |
map, map_err, and_then, unwrap_or, is_ok, is_err, ok, all |
errors.md |
task |
spawn, await, stop, first, detach, all, map_all, map_limit, race, map |
concurrency.md |
json |
encode, decode |
this file |
time |
add, an Instant plus a Duration |
data.md |
duration |
millis, seconds |
data.md |
clock |
now, sleep, timeout |
this file, concurrency.md |
runner |
check, run, resume |
execution.md |
Besides the built-in types, the modules declare text.Regex, task.Stop,
json.Value, json.DecodeError, clock.Timed, and runner.Run,
runner.Pending, runner.RunState, runner.RunError, runner.Diagnostic,
runner.Location, runner.Note, runner.Fix and runner.Edit. A function or type not in
this section is not part of std v1, and adding one is a std change.
text.regex(pattern) -> Result<Regex, Text> compiles a pattern or returns why
it could not. Matching runs in time linear in the input and gives the same
result on every implementation; there is no backtracking construct whose cost
or result depends on the engine. A pattern is written in RE2's syntax as the
Rust regex crate defines it, with Unicode classes, so . is one code point
and there are no back-references or look-around. text.matches(r, t) is true
when r matches some part of t; ^ and $ anchor it to the whole.
None of the option or result helpers faults.
Adding to std
A new std function must satisfy the three rules above. A function that would
observe anything outside the run is a Tool, declared with @tool in a module
of its own, not a std function; clock and runner are the two such modules
in std, and they add no host access beyond what their declarations state. A new @lang
declaration needs a language reason: ordinary Flow cannot express it. That a
function is popular, convenient or faster natively is not one.
Any change to std is a change of language version (modules.md).
Related documents
- data.md owns the built-in data types and their operators.
- effects.md owns purity and pass-through.
- concurrency.md owns
taskandclock.timeout. - tools.md owns
@tooldeclarations and decoding. - modules.md owns imports, the prelude,
@langand versions. - errors.md owns faults,
ResultandToolProblem.
Serves foundations: Flow owns a closed evaluation model, Visible outside influence and A general core, bounded in scope.