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
( ... ) groupingA 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 endA 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 parametersA 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 falseNothing 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] )*42is anIntliteral and4.2aDecimalliteral. A numeral with a point is aDecimal; one without is anInt._separates digits and means nothing:1_000_000. It stands only between two digits, so_1,1_,1__0and0x_ffare refused.0xis hexadecimal and0bbinary, with lowercase prefixes. There is no octal form, and a decimal literal has no leading zero, so010is refused rather than read as either.- A decimal literal has digits on both sides of the point:
0.5, not.5or5.. There is no exponent form. A.is part of a number only when a digit follows it, so2.max(n)is a method call on2. - A literal has no sign.
-5is unary minus applied to5.
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 endHexDigit{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 inmodel.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 iskey: value.()is theUnitvalue.- A tuple has two or more elements. There is no one-element tuple:
(e)and(e,)are bothein parentheses. trueandfalseare theBoolliterals.SetandByteshave 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 < canda == b == care 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 % 2is(-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 ? ftakes a converterf, 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 byflowis 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 themAnywhere 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"bindsnameinstead. 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 ::= '!' LowerNameA 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.
= Typedeclares a transparent alias.- A type with no body is implemented outside Flow and must carry
@langor@tool. pubmay mark a record's fields one by one. A constructor and its payload fields carry nopubof their own.opaqueapplies only to a record or enum, and is written afterpubwhen 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 )? '=' ExprA 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) -> Ca function type: a parenthesized list followed by->is a parameter list.() -> Ttakes no arguments.->groups to the right, and an effect mark belongs to the arrow directly before it:(A) -> (B) -> C !toolis(A) -> ((B) -> C !tool).(T)and(T,)are bothTin parentheses; there is no one-element tuple type.UnitandNeverare 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? TypeBodyA 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.namereads a field, andcatalog.Answer.Dismissednames 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 eextends as far right as an expression can:return a + breturns 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
| LowerNameA 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? )? '=' ExprAn 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 bracketsif 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 parenthesizedOpen 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.
xbinds,Nonematches,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. asbinds the whole value and applies to everything to its left, soA | B as xis(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 tomatcharms 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
matcharm 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?orflow. 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.
Related documents
- 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
@langdeclarations that give operators and literal forms their types. - errors.md owns what
?does.