Browse docs

Start here

examplesGetting started with Flowdocumentation

Design

FoundationsLanguage architecturePhilosophy

Language specification

Program checkingConcurrencyDataEffectsResults, Tool problems, and faultsGrammarHistoryModules and importsLanguage specificationEvaluationStandard libraryToolsTypes

Runtime

Runtime architectureThe host boundaryDiagnosticsRunning a programThe history format

Guides

Writing programs that reach checkpointsImplementing Tools with a toolkitLoops that never returnRecursive delegationSharing types between Tool modulesHarnesses over tool registries

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> !tool
Open 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).

  • data.md owns the built-in data types and their operators.
  • effects.md owns purity and pass-through.
  • concurrency.md owns task and clock.timeout.
  • tools.md owns @tool declarations and decoding.
  • modules.md owns imports, the prelude, @lang and versions.
  • errors.md owns faults, Result and ToolProblem.

Serves foundations: Flow owns a closed evaluation model, Visible outside influence and A general core, bounded in scope.