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

Evaluation

This specification owns what a checked program does when it is evaluated: bindings and blocks, functions and anonymous functions, local and top-level items, calls and method syntax, evaluation order, tail calls, return, if, match, guard, the meaning of patterns, building and updating records, and which functions can start a run.

It consumes source forms from grammar.md, types, constructors and their resolution from types.md, effects from effects.md, and values from data.md. What a checked program has been proved to satisfy before it runs, including exhaustiveness and the discard rules, is checking.md's. Result, ? and faults are errors.md's; Tool calls are tools.md's; tasks are concurrency.md's; what a run records and how replay and resume re-evaluate it are history.md's.

Values and bindings

Values are immutable. There is no var, no assignment and no mutable reference; state moves forward by passing a new value to the next call.

let pattern = e evaluates e and binds the names in the pattern for the rest of the enclosing block. A let may carry a type, let plan: Plan = e, which fixes the type of e for inference (types.md). A later let of the same name introduces a new binding that shadows the earlier one from that point on; the earlier value is unchanged, and anything that captured it keeps it.

let _ = e evaluates e and keeps nothing. It is the one way to drop a value, under checking.md. Dropping a value does not undo its evaluation: a Tool call whose result is dropped was still made and is still recorded.

let context = [System(brief)]
let context = context ++ [User(question), Assistant(answer)]
let (kept, dropped) = list.partition(findings, flow(f) = f.open)
let _ = notify.send(summary)
Open in playground →

Blocks

A block { ... } is a sequence of statements followed by an optional final expression. A statement is a let, a guard, a local item, or an expression of type Unit (checking.md). The block evaluates its statements in order; its value is the value of its final expression, or () when it has none.

A block is an expression and opens a scope. Names it binds are not visible after it. A block is not something return can leave; see return.

Functions

A function is declared with flow. Every top-level function writes its parameter types, result type and effect in full (effects.md). Its body follows = and is an expression, often a block.

flow describe(item: Item) -> View = render(item)

flow read_batch(refs: List<Text>) -> Batch !tool = {
    let documents = list.map(refs, flow(r) = catalog.read(r))
    collect(documents)
}
Open in playground →

Calling a function evaluates the callee and the arguments, binds each parameter to its argument, and evaluates the body. Arguments bind by position. There are no named arguments, no default arguments and no variadic functions; a call passes exactly the parameters declared. A record names whatever needs naming:

web.search(Query { text, deep: true, limit: 10 })
Open in playground →

A parameter may be written as a pattern. It must always match (checking.md), and its names are bound as a let would bind them.

Functions are values

A named function, a @tool function (tools.md) and a constructor (types.md) are each an ordinary function value: it can be passed, bound, returned and stored in a record or a list. x ? Rejected passes the Rejected constructor as a converter, and list.map(requests, workbench.run) passes a Tool. A function value has no equality, ordering or text (types.md).

Anonymous functions

An anonymous function is a flow without its name. Its parameter and result types and its effect may be left to inference (effects.md):

list.map(items, flow(item) = render(item))
task.spawn(flow() = generate(history))
list.fold(items, 0, flow(total, x) = total + x)
list.map(rows, flow(row) = {
    let parsed = parse(row)
    summarize(parsed)
})
Open in playground →

Evaluating an anonymous function creates a function value. It does not run the body. The value captures every binding its body mentions from the enclosing scopes, as the value that binding holds when the anonymous function is created; since values are immutable, that is the same value whenever the body later runs.

Calling a function stored in a field

p.run without parentheses reads a field. A function stored in a field is called by parenthesizing it, (p.run)(x), because p.run(x) is method syntax. In any call, the callee is evaluated before the arguments.

Method syntax

v.f(a, b) is a call to the function f in the module that declares the type of v, with v as its first argument: it means m.f(v, a, b), where m is that module, under the same visibility as the qualified call. The module is found from the type, so method syntax needs no import of it. Each built-in type is declared in its home module in std (stdlib.md), so items.map(f) calls std's list.map and page.click("#login") calls browser.click(page, "#login") when browser declares Page.

let recent = context.filter(flow(t) = t.important).take_last(20)
Open in playground →

There is no pipe operator, and a type gains no methods from any other module.

Local items

A block may declare functions and types. They are visible only inside that block.

  • Items declared in one block refer to each other in any order, as top-level items do, so local functions may be mutually recursive.
  • A local function may capture only bindings declared above it in the enclosing scopes. A binding declared later in the block is not yet bound when the function could first be called. The rule follows calls: a local function may be called, or used as a value, only where every binding it captures, directly or through the local functions it calls, is already bound. Once it is a value it can be called at any time.
  • A local function's types and effect are inferred (effects.md).
  • A local type does not use the enclosing function's type parameters. It declares its own.
flow run(input: Input) -> Outcome !tool = {
    type Step { name: Text, tries: Int }
    flow plan(s: State) -> Outcome !tool = ... execute(next) ...
    flow execute(s: State) -> Outcome !tool = ... plan(next) ...
    plan(start(input))
}
Open in playground →

Top-level items

Top-level items of a module refer to each other in any order. Two top-level items of one module may not have the same name, and neither may two items declared in the same block. A top-level let constant is pure: it may call pure functions and makes no Tool call (effects.md), it uses no task operation, and constants do not depend on each other in a cycle. A constant therefore has one value, the same whenever and however often it is computed.

Visibility of items is types.md's; imports and module boundaries are modules.md's.

Evaluation order

Evaluation is left to right, in source order, everywhere. This covers:

  • a call's callee, then its arguments, the receiver of method syntax first;
  • the elements of list, dict and tuple literals;
  • record and constructor fields as written, then a ..base;
  • the operands of binary operators;
  • the holes of text interpolation.

&& and || evaluate their right operand only when the left does not already decide the result. Every other operator evaluates both operands first.

Tool calls make this order visible: each call is recorded as it is made (history.md), so a reordering would show as a different history. Ordinary computation is deterministic: for the same program, input and outside answers, it produces the same values and makes the same Tool calls in the same order. Which outside answers exist, and how concurrent work interleaves, are tools.md's and concurrency.md's; every choice that is not derivable from the program is recorded.

Tail calls

A tail position is one whose value is the value of the whole function. Exactly these are tail positions:

  • the body of a function, named, local or anonymous;
  • the final expression of a block in tail position;
  • each branch of an if in tail position;
  • each arm body of a match in tail position;
  • the operand of return, wherever the return stands.

No other position is one: not an argument, an operand, the operand of ?, the subject of a match, the condition of an if or guard, the right side of a let, or an element of a literal. Parentheses do not change a position.

A call in tail position is a tail call, whatever it calls: the function itself, another top-level function, a local or anonymous function, or any function value. A tail call ends the calling function's activation before the callee's begins, and the callee continues the same call: its result is that call's result. A tail call never grows the stack. A chain of tail calls of any length holds one activation, so a loop written as tail recursion runs in constant stack however many rounds it makes. Tail calls change nothing else: arguments, evaluation order and every recorded fact are those of an ordinary call.

flow count_to(n: Int, total: Int) -> Int =
    if n == 0 { total } else { count_to(n - 1, total + 1) }    // tail call

flow length(items: List<Item>) -> Int = match items {
    [] => 0,
    [_, ..rest] => 1 + length(rest),                            // not a tail call
}
Open in playground →

Loops are recursion, together with std functions such as list.map, list.fold and list.filter. There is no loop statement. The language sets no bound on recursion; a call depth beyond what the host supports is a fault (errors.md), and which calls count toward it is execution.md's.

That a tail call continues the same call is what keeps a loop's tasks alive from one round to the next (concurrency.md), and a tail call to a top-level function from the root task is where a checkpoint can be taken (history.md).

return

return e evaluates e and leaves the nearest enclosing function, named, local or anonymous, with that value. Blocks, if and match are not return targets. return e is an expression of type Never, so it can stand anywhere an expression can, such as a match arm:

let plan = match reply {
    Ok(p) => p,
    Err(problem) => return Failed(problem),
}
Open in playground →

Inside an anonymous function, return leaves that anonymous function, not the function that created it. return outside any function body, such as in a top-level constant, is an error.

if

if c { a } else { b } is an expression. It evaluates the condition, a Bool, then exactly one branch, and its value is that branch's value. Both branches have the same type, except that a branch of type Never fits any type (types.md). else if chains.

else may be left out only when the block's type is Unit or Never. The if then has type Unit, and a false condition produces (). No value is silently dropped:

let label = if open { "open" } else { "closed" }
if retries >= 3 { return GaveUp(reason) }
Open in playground →

match

match s { arms } evaluates its subject once, then tries the arms in source order. An arm is taken when its pattern matches the subject and, if the arm has a guard if cond, the guard then evaluates to true with the pattern's bindings in scope. The first arm taken is the only one evaluated; its body's value is the value of the match. A guard that evaluates to false passes on to the next arm.

Every match covers its subject's type, and every arm can be taken by some value (checking.md), so a checked match always takes an arm. In every arm, constructors resolve against the subject's type (types.md).

match (state, event) {
    (Idle, Start(job)) => Running(job),
    (Running(_), Cancel) => Idle,
    (_, _) => state,
}
Open in playground →

guard

guard states a precondition for the rest of its block.

  • guard c else { ... } evaluates c. If it is true, evaluation continues after the guard; if it is false, the else block runs.
  • guard let pattern = e else { ... } evaluates e and matches it against the pattern. If it matches, the pattern's names are bound for the rest of the block; if not, the else block runs and none of them is bound.

The else block has type Never: it must leave, through return or another expression of type Never, and the checker proves it cannot fall through. A call in a return there is still a tail call.

guard state.rounds < state.max_rounds else { return OutOfRounds(state.rounds) }
guard let Some(owner) = dict.get(owners, plan.owner) else { return UnknownOwner(plan.owner) }
run(owner, plan)
Open in playground →

Patterns

One pattern language serves match arms, let, guard let and parameters. grammar.md fixes how each form is written; this section fixes what each matches and binds. Where a pattern must always match is checking.md's; in match and guard let a pattern may fail, and the form says what happens then.

Pattern Matches Binds
_ any value nothing
a name, x any value x to the value
a literal, 0, "done", true a value equal to the literal under == (data.md) nothing
a constructor, Some(p), Failed { step, .. } a value built by that constructor whose payload matches what the payload patterns bind
a record, Finding { severity: Critical, .. } a value of that record type whose named fields match what the field patterns bind
a tuple, (a, b) a tuple whose elements match, position by position what the element patterns bind
a list, [], [x], [x, ..rest], [..init, last] a list with exactly as many elements as the fixed patterns, or, with a rest, at least as many, whose fixed elements match from the front or the back the fixed elements' bindings, and the rest as a List
p as name what p matches name to the whole value, and what p binds
p | q what p or q matches, tried left to right the same names, with the same types, from whichever matched

A record or named payload pattern lists every field, or ends with .. to ignore the rest. A field written alone, Answered { value, .. }, matches the field value and binds it to the name value. Outside the type's module only public fields can be named, so a pattern there ends with .. when any field is private (types.md).

Matching a pattern only inspects the value. It evaluates nothing else, and a match that fails binds nothing.

Building and updating records

A record or a constructor with named fields is built by naming every field. A field whose value is a binding of the same name may be written alone, mirroring the pattern shorthand:

Loop { contract, rounds }          // Loop { contract: contract, rounds: rounds }
Open in playground →

T { f: e, ..base } builds a new T from base, with the fields written replacing those of base. A field may be a dotted path into nested records:

State { config.limit: 3, ..state }
// State { config: Config { limit: 3, ..state.config }, ..state }
Open in playground →

base is evaluated once, after the written fields. Who may build a record raw and who may rebuild one with ..base is types.md's.

Entry functions

Any top-level pub flow whose parameter types and result type are all data types (types.md) can start a run. Since those types are concrete, an entry has no type parameters. It may be pure or !tool. The language names no default entry; which function a run starts with is chosen by whoever starts it (execution.md).

A run's input is one value per parameter, keyed by parameter name and decoded by tools.md's decoding rules, so an entry's parameter types must be ones decoding can build, with no opaque part (tools.md); an input that does not decode refuses the run before any evaluation. The entry is then called as an ordinary function in the run's root task, and its result is the run's result, encoded by the same rules. How a run ends is errors.md's, and what the run records is history.md's.

pub flow main(ticket: Text, max_rounds: Int) -> Report !tool =
    work(ticket, [], 1, max_rounds)
Open in playground →

Serves foundations: Flow owns a closed evaluation model, Deterministic reconstruction, and A general core, bounded in scope.