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

Grammar

This specification owns how a Flow program is written: source text, the lexical rules, names and keywords, literals, operators and their precedence, where a newline ends a statement, the shape of items, types, expressions and patterns, and the canonical formatter. It says nothing about what a form means. Types belong to types.md, effects to effects.md, the meaning of literals and operators to data.md, evaluation and what a pattern matches to semantics.md, and imports to modules.md.

There is one way to write each form. The grammar has no semicolons, no block comments, no user-defined operators, no user-defined annotations and no form that evaluates source text.

Notation

Productions in this document are normative. They use this notation:

A ::= ...        A is defined as ...
'x'              the token x
A B              A followed by B
A | B            A or B
A?               zero or one A
A*               zero or more A
A+               one or more A
( ... )          grouping

A production Xs written as X (',' X)* ','? is a comma list: one or more X, separated by commas, with an optional trailing comma. A trailing comma is allowed in every comma list.

Source text

A source file is UTF-8. A file that is not valid UTF-8 is refused before anything else is read. A source character is a Unicode scalar value.

Invisible bidirectional control characters are refused anywhere in a file, including text literals and comments. These are the characters with the Unicode Bidi_Control property: U+061C, U+200E, U+200F, U+202A–U+202E and U+2066–U+2069. Each changes how a line is displayed without changing what the compiler reads, so a reader could approve a program that is not the one that runs. A text value may still hold one, written with a \u{...} escape, which is visible.

A line ends at U+000A (line feed). A carriage return directly before a line feed belongs to that line end; a carriage return anywhere else is refused. Whitespace is space (U+0020) and tab (U+0009). Whitespace separates tokens and has no other meaning; line ends are significant where Newlines says.

Unicode is welcome in text literals and comments. Names are ASCII.

Serves foundations: visible outside influence: what a reader sees is what the compiler reads.

Comments

// starts a comment that runs to the end of the line; it is only the comment marker, and there is no integer-division operator (data.md). /// starts a doc comment, which documents what stands directly below it: an item, or, inside a type body, a field, a constructor, or a field of a constructor's named payload. A run of doc comment lines documents one of them. A doc comment with none of them directly after it is refused. There are no block comments.

Comment     ::= '//' CommentText            CommentText not beginning with '/'
DocComment  ::= DocLine ( LF DocLine )*
DocLine     ::= '///' CommentText
CommentText ::= every character up to the line end

A comment ends before its line end, which is not part of it.

Comments carry no meaning. They are not part of the parsed program, so they take no part in program identity (history.md). Documentation tools read doc comments.

Names

Names are ASCII, and their case is part of the grammar:

LowerName ::= [a-z] [a-z0-9_]*        values, parameters, functions, fields, modules
UpperName ::= [A-Z] [A-Za-z0-9]*      types, constructors, type parameters

A lowercase name begins with a letter, so _ alone is never a name; it is the wildcard. A name in the wrong case for its position is refused: a function named Run, a type named report, a type parameter named t. Look-alike letters from other scripts cannot appear in a name.

The keywords are:

flow  type  pub  opaque  let  match  if  else  guard  return  use  as  true  false

Nothing else is reserved. A keyword is never a name. Int, Unit, Never, Result and the other built-in and prelude names are ordinary names that std declares (modules.md); true and false are typed through std's @lang(bool) declaration (stdlib.md). Words that follow @ or !, such as tool, lang, data or pure, are ordinary names in those positions.

A name is scanned whole and only then compared with the keywords, so iffy and user_id are names.

Numbers

IntLit     ::= Decimal | '0x' HexDigits | '0b' BinDigits
DecimalLit ::= Decimal '.' Digits
Decimal    ::= '0' | [1-9] ( '_'? [0-9] )*
Digits     ::= [0-9] ( '_'? [0-9] )*
HexDigits  ::= [0-9a-fA-F] ( '_'? [0-9a-fA-F] )*
BinDigits  ::= [01] ( '_'? [01] )*
  • 42 is an Int literal and 4.2 a Decimal literal. A numeral with a point is a Decimal; one without is an Int.
  • _ separates digits and means nothing: 1_000_000. It stands only between two digits, so _1, 1_, 1__0 and 0x_ff are refused.
  • 0x is hexadecimal and 0b binary, with lowercase prefixes. There is no octal form, and a decimal literal has no leading zero, so 010 is refused rather than read as either.
  • A decimal literal has digits on both sides of the point: 0.5, not .5 or 5.. There is no exponent form. A . is part of a number only when a digit follows it, so 2.max(n) is a method call on 2.
  • A literal has no sign. -5 is unary minus applied to 5.

No literal is too large. What a numeric literal denotes is data.md's.

Text literals

A text literal is written in one of four forms:

Form Escapes Interpolation Lines
"..." yes yes one
""" block yes yes many
r"..." no no one
r""" block no no many
TextLit      ::= '"' ( TextChar | Escape | Hole )* '"'
               | '"""' LF BlockLine* Margin '"""'
               | 'r"' RawChar* '"'
               | 'r"""' LF RawLine* Margin '"""'
BlockLine    ::= ( TextChar | Escape | Hole )* LF
RawLine      ::= RawChar* LF
Hole         ::= '${' Expr '}'
Escape       ::= '\n' | '\t' | '\\' | '\"' | '\${' | '\u{' HexDigit{1,6} '}'
TextChar     ::= any source character but '\' and a line end, and '$' only
                 when '{' does not follow it; '"' only inside a block
RawChar      ::= any source character but a line end; '"' only inside a block
Margin       ::= ( ' ' | '\t' )*
LF           ::= a line end

HexDigit{1,6} is one to six hexadecimal digits. A one-line form ends at its first unescaped ", and a block at its first """, which must stand on the closing line after nothing but its Margin. The rest of this section states what each part means.

Escapes

In "..." and """ text, a backslash begins one of these escapes:

Escape Stands for
\n line feed
\t tab
\\ backslash
\" quotation mark
\u{1F600} the code point with that hexadecimal value, one to six digits
\${ the two characters ${, starting no interpolation

Any other backslash sequence is refused, and so is a \u{...} that names a surrogate or a value above 10FFFF. The escape set is closed.

Interpolation

${ begins an interpolation hole holding one expression, and the matching } ends it. The hole's extent is decided over tokens, so a } inside a nested text literal, block or record literal does not end it. Inside a hole, line ends are ignored as inside parentheses. A $ not followed by {, and a { or } outside a hole, are ordinary text, so JSON in a prompt needs no escaping:

let request = "Reply as {\"score\": 1} for ${name}"
Open in playground →

Which values a hole accepts, and the text it inserts, are types.md's.

One-line text

A "..." literal ends on the line it starts. A line end inside one is refused; text over several lines is a """ block or a \n escape.

Multi-line text

A """ block opens with """ at the end of a line and closes with a line whose first non-whitespace characters are """:

let prompt = """
    You are reviewing ${pr.title}.
    Reply as JSON: {"verdict": "approve" | "reject", "reason": "..."}
    """
Open in playground →
  • The whitespace before the closing """ is the margin. Every non-blank line of content begins with exactly that whitespace, and the margin is removed from each. A non-blank line that does not begin with the margin is refused. A blank line may be shorter than the margin.
  • The value is the lines between the opening and closing lines, joined by line feeds. The line end after the opening """ and the one before the closing line are not part of it.
  • A carriage return before a line end inside the block is not part of the value, so a file's line endings never change what a literal holds.
  • " and "" may appear in the content unescaped. Escapes and interpolation work as in "...".
  • Tokens may follow the closing """ on its line, as in model.extract(""" ... """)?.

Raw text

r"..." and r""" blocks have no escapes and no interpolation: a backslash and ${ are ordinary characters. They are for text, such as a prompt template or a program given to a Tool, that contains ${. An r"..." literal cannot contain ", and an r""" block cannot contain """. An r""" block follows the margin rules of a """ block.

let template = r"Fill in ${name} and \n stays as written"
Open in playground →

Other literals

ListLit ::= '[' ( Expr (',' Expr)* ','? )? ']'
DictLit ::= '[' ':' ']'
          | '[' Expr ':' Expr (',' Expr ':' Expr)* ','? ']'
UnitLit ::= '(' ')'
Tuple   ::= '(' Expr ',' Expr (',' Expr)* ','? ')'
  • [a, b, c] is a list and [] the empty list.
  • ["gpt": 3, "claude": 5] is a dict and [:] the empty dict. A bracket literal is a dict when its first element is followed by :, and every entry of a dict literal is key: value.
  • () is the Unit value.
  • A tuple has two or more elements. There is no one-element tuple: (e) and (e,) are both e in parentheses.
  • true and false are the Bool literals.
  • Set and Bytes have no literal; std builds them.

Which type each literal form builds is fixed by std's @lang declarations (stdlib.md).

Operators

The token inventory is closed:

(  )  [  ]  {  }  ,  :  .  ..  =  =>  ->  ?  !  @  |  _
+  -  *  /  %  ++  ==  !=  <  <=  >  >=  &&  ||

At each position the longest token wins, so .. is not two . and => is not = then >. A character that begins no token, no name, no literal and no comment is refused.

Operators, tightest first:

Level Operators Grouping
1 call f(x), field and method ., postfix ?, converter ? f left to right
2 unary !, unary - prefix
3 *, /, % left
4 +, -, ++ left
5 ==, !=, <, <=, >, >= none
6 && left
7 || left
  • Comparisons don't chain. a < b < c and a == b == c are refused, as is any expression with two comparisons at one level; parentheses say which is meant.
  • Unary operators bind tighter than every binary one. -7 % 2 is (-7) % 2. Postfix forms bind tighter still, so -x.size() is -(x.size()), -7.div(2) is -(7.div(2)), and !done? is !(done?).
  • ? has two forms. x? is postfix. x ? f takes a converter f, which is a name, a qualified name, a constructor or an anonymous function, and must begin on the same line as the ?. A ? followed on its line by a name or by flow is the converter form; otherwise it is postfix. The converter form ends a postfix chain, so continuing one after it takes parentheses: (x ? f).g().
  • There is no pipe, no indexing operator and no assignment operator. Indexing is a std function; values are never reassigned.

The grammar fixes the operators and their precedence. What each operator does is a function std declares for its operand type and marks @lang in that type's module (stdlib.md); data.md gives the meaning for the built-in types, and errors.md the meaning of ?.

Newlines

There are no semicolons. A line end ends a statement or an item, except where this section says it is ignored.

Where a line end counts. Whether a line end counts depends on the innermost bracket around it:

Innermost bracket Line ends
none: the top level of a file end an import or an item
block braces { ... } end a statement
parentheses ( ... ) ignored
square brackets [ ... ] ignored
an interpolation hole ${ ... } ignored
the braces of a type body, a record or constructor literal, a record or constructor pattern, or a match arm list ignored

So arguments, list elements, fields and match arms can be split across lines. A block inside parentheses, such as the body of an anonymous function passed as an argument, is block braces again, and line ends inside it end statements. The angle brackets of type parameters and type arguments are not brackets here: a line end inside < ... > counts as it does around them.

The productions write LineEnd where a line end that counts may stand:

LineEnd ::= one or more line ends that count, with any comments between them

Anywhere else, a line end that counts ends the form being read, so a form that is not complete there is refused.

Continuation. Where line ends count, a line end is still ignored when:

  • the next line's first token is ., so a method chain can be split, one call per line;
  • the line's last token is a binary operator (||, &&, a comparison, +, -, ++, *, /, %, or | between patterns), =, => or ->. A token counts by the role it plays there: a - that negates and a > that closes type arguments are not binary operators;
  • the line's last token is an annotation, so @lang(spawn) may stand on the line above the item it marks.

A line end in any other place ends the statement. In particular else must stand on the line where its if block or guard condition ends (} else {), and a line starting with - or ? begins a new statement.

flow work(ticket: Ticket) -> Report !tool = {
    let plan = planner.next(ticket)?
    let recent = context
        .filter(flow(t) = t.important)
        .take_last(20)
    let report = Report {
        id: ticket.id,
        notes: notes ++
            [summary],
    }
    run(plan, recent, report)
}
Open in playground →

Files and items

File        ::= LineEnd? ( Entries LineEnd? )?
Entries     ::= Import ( LineEnd Import )* ( LineEnd Item ( LineEnd Item )* )?
              | Item ( LineEnd Item )*
Import      ::= 'use' LowerName? TextLit
Item        ::= DocComment? Annotation* 'pub'? ( FlowItem | TypeItem | LetItem )
Annotation  ::= '@' LowerName ( '(' AnnotationArg (',' AnnotationArg)* ','? ')' )?
AnnotationArg ::= LowerName ( '.' LowerName )*
  • A file is a sequence of imports followed by items. Imports come first. Each import and each item begins on a new line.
  • An import's path is a one-line "..." literal with no interpolation hole. use "path" binds the path's last segment; use name "path" binds name instead. What a path may name and what an import binds are modules.md's.
  • Items at the top level may appear in any order (semantics.md).

Annotations

There are four annotations, and no others can be declared: @lang, @tool, @test and @fake. An annotation precedes the item it marks, before pub. Its arguments, when it has any, are names or dotted names:

@lang(int, data) pub type Int
@tool pub flow chat(prompt: Text) -> Result<Text, ChatError>
@fake(openai.chat) flow chat(prompt: Text) -> Result<Text, ToolProblem<ChatError>> = Ok("hi")
Open in playground →

@lang is accepted only in std (modules.md); the declarations std makes with it are stdlib.md's. @tool is tools.md's. @test and @fake mark test functions and fakes for flow test (execution.md).

Functions

FlowItem   ::= 'flow' LowerName TypeParams? '(' Params? ')' '->' Type Effect? ( '=' Expr )?
Params     ::= Param (',' Param)* ','?
Param      ::= Pattern ':' Type
TypeParams ::= '<' UpperName (',' UpperName)* ','? '>'
Effect     ::= '!' LowerName

A top-level function writes its parameter types, its result type and its effect in full; effects.md owns which effect marks exist and where each may appear. The body follows = and is one expression, usually a block. A function with no body is implemented outside Flow and must carry @lang or @tool; one with either annotation has no body (tools.md).

flow describe(item: Item) -> View = render(item)
flow read_batch(refs: List<Text>) -> Batch !tool =
    refs.map(flow(r) = catalog.read(r))
pub flow map<A, B>(items: List<A>, operation: (A) -> B) -> List<B> = ...
Open in playground →

Types

TypeItem     ::= 'opaque'? 'type' UpperName TypeParams? TypeBody?
TypeBody     ::= '{' Fields '}' | '{' Constructors '}' | '=' Type
Fields       ::= Field (',' Field)* ','?
Field        ::= 'pub'? LowerName ':' Type
Constructors ::= Constructor (',' Constructor)* ','?
Constructor  ::= UpperName Payload?
Payload      ::= '(' Type (',' Type)* ','? ')'
               | '{' PayloadField (',' PayloadField)* ','? '}'
PayloadField ::= LowerName ':' Type
  • Braces holding fields declare a record; braces holding constructors declare an enum. A type body holds at least one field or constructor, and never both.
  • = Type declares a transparent alias.
  • A type with no body is implemented outside Flow and must carry @lang or @tool.
  • pub may mark a record's fields one by one. A constructor and its payload fields carry no pub of their own.
  • opaque applies only to a record or enum, and is written after pub when both appear: pub opaque type Session { ... }.
pub type Row { pub id: Text, pub quantity: Text }
pub type Outcome {
    Picked(Text),
    StepFailed { step: Int, last: Text },
    Skipped,
}
type Reply<T> = Result<T, ToolProblem<ChatError>>
type UserId { UserId(Text) }
Open in playground →

What records, enums, aliases, visibility and opaque mean is types.md's.

Constants

LetItem ::= 'let' Pattern ( ':' Type )? '=' Expr

A top-level let declares a constant; its type may be inferred. What a constant may compute is semantics.md's.

Types

Type     ::= FnType | TupleType | '(' Type ','? ')' | TypeRef
FnType   ::= '(' ( Type (',' Type)* ','? )? ')' '->' Type Effect?
TupleType ::= '(' Type ',' Type (',' Type)* ','? ')'
TypeRef  ::= ( LowerName '.' )? UpperName TypeArgs?
TypeArgs ::= '<' Type (',' Type)* ','? '>'
  • List<Text>, catalog.Document, Result<T, ToolProblem<E>>.
  • (A, B) is a tuple type, and (A, B) -> C a function type: a parenthesized list followed by -> is a parameter list. () -> T takes no arguments.
  • -> groups to the right, and an effect mark belongs to the arrow directly before it: (A) -> (B) -> C !tool is (A) -> ((B) -> C !tool).
  • (T) and (T,) are both T in parentheses; there is no one-element tuple type.
  • Unit and Never are written by name. () is a value, not a type.
  • Type arguments are written only in types, never at a call. < and > therefore never need telling apart from comparisons.

Blocks and statements

Block     ::= '{' LineEnd? ( Element ( LineEnd Element )* LineEnd? )? '}'
Element   ::= LetStmt | GuardStmt | LocalItem | Expr
LocalItem ::= DocComment? ( LocalFlow | LocalType )
LetStmt   ::= 'let' Pattern ( ':' Type )? '=' Expr
GuardStmt ::= 'guard' Head 'else' Block
            | 'guard' 'let' Pattern '=' Head 'else' Block
LocalFlow ::= 'flow' LowerName TypeParams? '(' LocalParams? ')' ( '->' Type Effect? )? '=' Expr
LocalParams ::= LocalParam (',' LocalParam)* ','?
LocalParam  ::= Pattern ( ':' Type )?
LocalType ::= 'type' UpperName TypeParams? TypeBody

A block holds one element per line. The last element, when it is an expression, gives the block its value (semantics.md). A local function may leave its parameter types, result type and effect to inference (effects.md). Local items carry no pub, no opaque and no annotation.

let _ = e is the only spelling for dropping a value (checking.md).

Expressions

Expr      ::= 'return' Expr | AnonFn | OrExpr
OrExpr    ::= AndExpr ( '||' AndExpr )*
AndExpr   ::= CmpExpr ( '&&' CmpExpr )*
CmpExpr   ::= AddExpr ( CmpOp AddExpr )?
CmpOp     ::= '==' | '!=' | '<' | '<=' | '>' | '>='
AddExpr   ::= MulExpr ( ( '+' | '-' | '++' ) MulExpr )*
MulExpr   ::= Unary ( ( '*' | '/' | '%' ) Unary )*
Unary     ::= ( '!' | '-' ) Unary | Postfix
Postfix   ::= Primary ( '(' Args? ')' | '.' Name | '?' )* ( '?' Converter )?
Converter ::= Name ( '.' Name )* | AnonFn
Args      ::= Expr (',' Expr)* ','?
Name      ::= LowerName | UpperName
Primary   ::= Literal | Name | RecordLit | ListLit | DictLit | UnitLit
            | '(' Expr ','? ')' | Tuple | Block | IfExpr | MatchExpr
Literal   ::= IntLit | DecimalLit | TextLit | 'true' | 'false'
  • Dotted names are read by what their first name binds: task.spawn(w) calls a function from an imported module, v.f(x) is a method call, v.name reads a field, and catalog.Answer.Dismissed names a constructor. The grammar reads all of them as one postfix chain; semantics.md and modules.md resolve them. A function stored in a field is called as (p.run)(x).
  • return e extends as far right as an expression can: return a + b returns the sum. It may stand wherever an expression can, such as a match arm (semantics.md).
  • No type arguments at a call. list.empty() is complete; the type comes from inference or an annotation (types.md).
  • No named or default arguments. Every argument is positional.

Record and constructor literals

RecordLit    ::= CtorPath '{' RecordEntries? '}'
CtorPath     ::= ( LowerName '.' )? ( UpperName '.' )? UpperName
RecordEntries ::= Entry (',' Entry)* ( ',' '..' Expr )? ','?
               | '..' Expr ','?
Entry        ::= LowerName ( '.' LowerName )* ':' Expr
               | LowerName

A brace after a type or constructor name builds a record or a named payload: Report { id, notes: [] }, StepFailed { step: 2, last }, catalog.Document { ... }. A name written alone is field shorthand: id means id: id. ..base comes last and supplies the fields not written. A dotted path, config.limit: 3, updates a field inside a field and is written only in a literal with ..base:

let next = State { rounds: state.rounds + 1, config.limit: 3, ..state }
Open in playground →

A positional payload is built with a call: Picked(text). What building and updating mean is semantics.md's.

Anonymous functions

AnonFn ::= 'flow' '(' LocalParams? ')' ( '->' Type Effect? )? '=' Expr

An anonymous function is a flow without its name. Its body extends as far right as an expression can, so fold(items, 0, flow(total, x) = total + x) ends at the closing parenthesis.

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

if, match and guard

IfExpr    ::= 'if' Head Block ( 'else' ( Block | IfExpr ) )?
MatchExpr ::= 'match' Head '{' Arm ( ',' Arm )* ','? '}'
Arm       ::= Pattern ( 'if' Expr )? '=>' Expr
Head      ::= Expr, with no brace outside brackets

if takes a block in each branch; else if chains. Match arms are separated by commas, a block-bodied arm included. if cond after a pattern is a match guard.

No brace-built value directly in a head. In the head of if, match or guard, a { that is not inside parentheses or square brackets always opens the form's body. A record literal, a block, or any expression that ends in braces must be bound with let first or wrapped in parentheses:

match model.extract(prompt) ? Model { ... }        // the braces are the match body
let reply = model.extract(prompt) ? Model
match reply { ... }                                  // bound first
match (Point { x: 0, y: 0 }) { ... }                 // or parenthesized
Open in playground →

What if, match and guard do, and when else may be left out, are semantics.md's.

Patterns

One pattern syntax serves match arms, let, guard let and parameters.

Pattern        ::= AltPattern ( 'as' LowerName )?
AltPattern     ::= SinglePattern ( '|' SinglePattern )*
SinglePattern  ::= '_' | LowerName | LiteralPattern | '(' ')' | '(' Pattern ','? ')'
                 | TuplePattern | ListPattern | CtorPattern
LiteralPattern ::= '-'? ( IntLit | DecimalLit ) | TextLit | 'true' | 'false'
TuplePattern   ::= '(' Pattern ',' Pattern (',' Pattern)* ','? ')'
ListPattern    ::= '[' ']'
                 | '[' ( Rest ',' )? Pattern (',' Pattern)* ( ',' Rest )? ','? ']'
                 | '[' Rest ','? ']'
Rest           ::= '..' LowerName?
CtorPattern    ::= CtorPath ( '(' Pattern (',' Pattern)* ','? ')'
                            | '{' FieldPatterns '}' )?
FieldPatterns  ::= FieldPattern (',' FieldPattern)* ( ',' '..' )? ','?
                 | '..'
FieldPattern   ::= LowerName ( ':' Pattern )?
  • A lowercase name binds; an uppercase name, alone or qualified, is a constructor. x binds, None matches, ToolProblem.Unknown(reason) matches a qualified constructor.
  • A text literal pattern has no interpolation hole.
  • A record or named payload pattern names fields; a field written alone, Answered { value, .. }, matches the field and binds its name. .. ends the list when fields are left out.
  • A list pattern has at most one rest, first or last: [], [x], [x, ..rest], [..init, last]. .. with no name leaves the rest unbound.
  • as binds the whole value and applies to everything to its left, so A | B as x is (A | B) as x. Each alternative of an or-pattern binds the same names.
  • A match guard is written after the pattern, Ok(n) if n > limit => ..., and belongs to match arms only.
match output.exit { 0 => Passed, code => Failed(code) }
Some(Finding { severity: Critical, .. } as finding) => ...
Critical | Important => true
[first, ..rest] => ...
let (kept, dropped) = list.partition(findings, flow(f) = f.open)
Open in playground →

What each pattern matches and binds is semantics.md's; which patterns must always match and exhaustiveness are checking.md's.

Canonical form

There is one canonical formatter, flow fmt, and it has no options. Every conforming formatter satisfies these invariants:

  • Same program. Parsing the formatted text gives the same parsed program as the original. Formatting changes layout, comments' placement and redundant parentheses, and nothing that program identity covers.
  • Comments kept. Every comment in the input appears in the output.
  • Idempotent. Formatting formatted text reproduces it byte for byte.
  • One file at a time. Formatting needs no imports resolved; a file whose imports are missing formats like any other.

Source that is not canonical is still accepted: formatting is presentation, not an acceptance rule.

The layout is fixed by the rules below. The formatter moves only whitespace and line ends, and adds or drops only trailing commas: it reorders no token, adds and removes no parenthesis, and changes nothing inside a token.

  • No line width. No line is broken or joined because of its length. Where a form may span lines, the author's choice between its two layouts below decides.
  • Lists. Parentheses, square brackets, and the braces of a type body, a record or constructor literal, a record or constructor pattern and a match arm list hold a list. A list is broken when a line end or a comment directly follows its opening bracket, or a comment stands directly inside it, and flat otherwise. A flat list is written on one line with no trailing comma: line ends after its commas and before its closing bracket are removed. A broken list puts each element on a line of its own, one level deeper than the line the list opens on, writes a comma after every element including the last, and puts its closing bracket on a line of its own at the indentation of the line it opened on. An empty list is written closed: (), [].
  • Blocks. A block is broken when a line end or a comment stands directly inside it or anything inside it is broken, and is then laid out like a broken list without commas. A flat block is written { e }, and an empty one {}.
  • Other line ends are kept: those that end a statement, an item or an import, and those inside an element, where the grammar ignores them or a line continues. A continued line is indented one level deeper than the line its element began on; after a line ending in = or =>, the line holding the value begins the element further lines continue from. A line end after an annotation keeps the item at the annotation's indentation. In an interpolation hole, line ends after ${ and before its } are removed, except one that ends a comment.
  • Blank lines. At most one blank line stands between two lines, and none at the start or end of the file, after an opening bracket or before a closing one.
  • Indentation is four spaces a level, and the line ends written are line feeds. A file that holds anything ends with one line end; one that holds nothing stays empty.
  • Spacing. Two tokens on one line are separated by one space, except that there is none after (, [, ${, ., .., @ or a prefix - or !, including the ! of an effect; none before ), ], the } of a hole, ,, :, . or a postfix ?; none on either side of the < and > of type parameters and arguments; and none before a ( that follows a name, a closing bracket, a text literal, a postfix ? or flow. A flat block or brace list has one space inside each brace: Item { id }.
  • Comments stay on their lines. A comment after code is separated from it by one space; a comment on a line of its own is indented like the code that follows it in its bracket, or like that bracket's elements when the closing bracket follows. Trailing whitespace is removed from a comment.
  • Text literals are written as they are. The content and closing lines of a multi-line literal keep their indentation, since its margin is part of what it holds; only the expressions in its holes are laid out.

Serves foundations: stable meaning: a formatting-only change cannot change what a program means.

  • types.md owns what types, constructors and visibility mean.
  • effects.md owns the effect marks.
  • data.md owns what literals and operators on built-in types mean.
  • semantics.md owns evaluation, name lookup in blocks, method syntax and patterns' meaning.
  • checking.md owns acceptance and discard rules, and runtime/spec/diagnostics.md the diagnostic shape.
  • modules.md owns import paths and what a name from a module resolves to.
  • stdlib.md owns the @lang declarations that give operators and literal forms their types.
  • errors.md owns what ? does.