Grammar
The syntax of Lyric 5 as one EBNF: what a file may hold, token by token. Source of the decisions: 08 Syntax.
Scope
The canonical EBNF of Lyric 5 and the rules around it: the notation, the declarations, types, statements, expressions and patterns, the choices the parser makes where the grammar leaves two readings, and how the grammar is checked against the suite.
1. Notation and reading
- The grammar is EBNF.
Rule = … .defines a rule;'x'is a terminal;A Bis a sequence,A | Ba choice,[ A ]an option,{ A }any number ofA,( … )a group;(* … *)is a note. A rule's name begins with a capital; a name in capitals is a class of tokens of 01. - A quoted word is the reserved word, or, for a contextual word (01 §3 rule 3), a name
spelled that way:
'type'isIDENTIFIERwrittentype. Everywhere anIDENTIFIERstands, a contextual word may stand too; a reserved word never does. - A quoted sign is the token of 01 §5. The
signs
'>>','>='and'>>='are also the'>'that closes a list of type arguments or type parameters followed by the rest, written against it (01 §1 rule 2):List<List<int>>closes two lists,type Pair<T>= (T, T);closes one before its=. - The grammar takes a superset of the programs a compiler accepts. A program it takes may still be refused by a rule beside it — a name that resolves to nothing, a type that does not fit, a pattern where none may bind — and where the grammar leaves two readings of one text, §7 says which one the parser takes.
- The list rule (08 D5/D6): a list of one element may stand without its brackets —
struct S :: I,throws E,catch (e in E),<T :: Display>,@Test—; a list of several stands in brackets —:: [I, J],throws [A, B],catch (e in [A, B]),@[A, B]. A list may end in a comma (08 Y9) wherever it stands between brackets, parentheses, braces or angle brackets; a tuple keeps its two elements before it.
The token classes:
| Class | Chapter 01 |
|---|---|
IDENTIFIER |
a name, §3 rule 1; _ is one |
INT_LITERAL, FLOAT_LITERAL |
§4.1, §4.2 |
CHAR_LITERAL |
§4.3 |
STRING_LITERAL |
§4.4: one line, raw, multi-line |
BYTE_STRING |
§4.5 |
FSTRING_START, FSTRING_TEXT, HOLE_START, FORMAT_SPEC, HOLE_END, FSTRING_END |
§4.6: an f-string's opening (f", fr", f""", fr"""), a run of its text, the { and } around a hole, the text after a hole's : at the hole's top level, and its closing quotes |
AT_NAME |
@ and a name written against it, §5 rule 1 |
2. Files and declarations
Module = { Attribute } [ 'module' ';' ] { TopLevelDecl } .
TopLevelDecl = ImportDecl
| { Attribute } [ Visibility ] Declaration .
Visibility = 'pub' | 'internal' | 'private' .
Declaration = FunctionDecl | StructDecl | ClassDecl | EnumDecl | InterfaceDecl | ExtendDecl
| GlobalBinding | TypeAlias | ExternDecl | ExternBlock .
Name = IDENTIFIER | Splice .
ImportDecl = [ 'pub' ] 'import' Path [ ImportList | 'as' IDENTIFIER ] ';' .
ImportList = '{' ImportItem { ',' ImportItem } [ ',' ] '}' .
ImportItem = IDENTIFIER [ 'as' IDENTIFIER ] .
Path = IDENTIFIER { '.' IDENTIFIER } .
Attribute = AT_NAME AttributeRest
| '@[' IDENTIFIER AttributeRest { ',' IDENTIFIER AttributeRest } [ ',' ] ']' .
AttributeRest = { '.' IDENTIFIER } [ '{' [ FieldValue { ',' FieldValue } [ ',' ] ] '}'
| '(' Expr ')' ] .
FieldValue = IDENTIFIER '=' Expr .
- A file is a module (04 §2).
module;alone is its header, which names nothing and carries the module's attributes; attributes at the top of a file without it belong to the first declaration. - An import takes no attribute, and of the visibility words only
pub(LYR-PAR0053).
FunctionDecl = FunctionHead ( Block | ';' ) .
FunctionHead = [ 'inline' ] [ 'mut' ] ( 'fn' | 'macro' ) Name [ GenericParams ]
'(' [ Params ] ')' [ ':' ReturnType ] [ ThrowsClause ] .
Params = Param { ',' Param } [ ',' ] .
Param = { Attribute } [ '&' ] Name ':' Type [ '...' ] [ '=' Expr ] .
ThrowsClause = 'throws' [ Type | '[' Type { ',' Type } [ ',' ] ']' ] .
ExternDecl = 'extern' STRING_LITERAL ExternFunction .
ExternBlock = 'extern' STRING_LITERAL '{' ExternMember { ExternMember } '}' .
ExternMember = { Attribute } [ Visibility ] ExternFunction .
ExternFunction = [ 'inline' ] [ 'mut' ] 'fn' Name [ GenericParams ] '(' [ ExternParams ] ')'
[ ':' ReturnType ] [ ThrowsClause ] [ '=' STRING_LITERAL ] ';' .
ExternParams = '...' | Param { ',' Param } [ ',' [ '...' ] ] .
GlobalBinding = ( 'let' | 'var' ) IDENTIFIER [ ':' Type ] [ '=' Expr ] ';' .
TypeAlias = 'type' Name [ GenericParams ] '=' Type ';' .
GenericParams = '<' GenericParam { ',' GenericParam } [ ',' ] '>' .
GenericParam = IDENTIFIER [ '::' Bounds ] [ '=' Type ] .
Bounds = Type | '[' Type { ',' Type } [ ',' ] ']' .
- A function's
throwsis the function's own, written after its return type;throwsalone isError(06). In a member,staticstands beforeinlineandmut(LYR-PAR0032). - A parameter's
&marks a place (03 §8);T...makes the last parameter variadic. A bare...is C's variadic arguments and closes the parameters of anexternfunction alone (13).
StructDecl = 'struct' Name [ GenericParams ] [ '::' Conformances ] TypeBody .
ClassDecl = 'class' Name [ GenericParams ] [ '::' Conformances ] TypeBody .
Conformances = Conformance | '[' Conformance { ',' Conformance } [ ',' ] ']' .
Conformance = Type [ 'by' IDENTIFIER ] .
TypeBody = '{' { TypeMember } [ { Attribute } FieldDecl ] '}' .
TypeMember = { Attribute } ( FieldDecl ',' | MemberBody [ ',' ] ) .
FieldDecl = [ Visibility ] [ 'var' ] Name ':' Type [ '=' Expr ] .
EnumDecl = 'enum' Name [ GenericParams ] [ '::' Bounds ]
'{' [ Variant { ',' Variant } [ ',' ] ] [ ';' { MemberDecl } ] '}' .
Variant = { Attribute } Name [ '(' [ Type { ',' Type } [ ',' ] ] ')'
| '{' [ VariantField { ',' VariantField } [ ',' ] ] '}'
| '=' Expr ] .
VariantField = { Attribute } Name ':' Type [ '=' Expr ] .
InterfaceDecl = [ 'sealed' ] 'interface' Name [ GenericParams ] [ '::' Bounds ]
'{' { MemberDecl } '}' .
ExtendDecl = 'extend' [ GenericParams ] Type [ '::' Bounds ] '{' { MemberDecl } '}' .
MemberDecl = { Attribute } MemberBody .
MemberBody = AssociatedType
| [ Visibility ] ( 'static' StaticBinding | [ 'static' ] FunctionDecl ) .
StaticBinding = ( 'let' | 'var' ) IDENTIFIER [ ':' Type ] [ '=' Expr ] ';' .
AssociatedType = 'type' Name [ '::' Bounds ] [ '=' Type ] ';' .
- In a struct or a class, a field is followed by
,unless it stands last; a member that closes itself — a body's}, a;— needs none and may have one (08 D1). In an enum the variants are separated by,and end with;before the members (08 D2); an interface, anextendblock and the members of an enum take no separator. bydelegates a conformance of a struct or a class to one of its fields (08 D7); after an enum's, an interface's or anextendblock's::it has no place (LYR-PAR0047).
3. Types
Type = { '?' | '??' } TypeCore [ 'throws' [ Type ] ] .
ReturnType = { '?' | '??' } TypeCore .
TypeCore = TypeAtom [ '.' '[' IDENTIFIER ']' ] { '[' [ INT_LITERAL ] ']' } .
TypeAtom = Path [ TypeArgs ]
| FunctionType
| 'extern' STRING_LITERAL FunctionType
| '(' Type ')'
| TupleType
| Splice .
TypeArgs = '<' TypeArg { ',' TypeArg } [ ',' ] '>' .
TypeArg = [ IDENTIFIER '=' ] Type .
FunctionType = 'fn' '(' [ FnTypeParam { ',' FnTypeParam } [ ',' ] ] ')' '->' ReturnType
[ 'throws' [ ReturnType | '[' Type { ',' Type } [ ',' ] ']' ] ] .
FnTypeParam = [ '&' ] Type .
TupleType = '(' TupleElement ',' TupleElement { ',' TupleElement } [ ',' ] ')' .
TupleElement = [ IDENTIFIER ':' ] Type .
?makes an optional of what follows it with its suffixes:?T[]is an optional array, an array of optionals is(?T)[];??Tis an optional of an optional (03 §3).T[]is an array,T[4]an inline array of four.- A function type reaches as far right as it can:
fn(int) -> void[]returnsvoid[], and an array of functions is(fn(int) -> void)[]. throwsafter a type is the type's own only where a type stands for a value — a field, a parameter, a binding, a type argument:Coroutine<int> throws IoError. After the return type of a function, a lambda or a function type, thethrowsbelongs to the function; that is why a return type is read asReturnType. Where a function type is itself a return type, athrowsbehind it says neither whose it is, and parentheses must (LYR-PAR0056).T.[f]is the type of the field acomptime forround names (11).
4. Statements
Block = '{' { Statement } '}' .
ValueBlock = '{' { Statement } [ Expr | 'throw' Expr ] '}' .
Statement = ';'
| Block
| 'unsafe' Block
| LetStmt
| 'using' 'let' IDENTIFIER [ ':' Type ] '=' Expr ';'
| FunctionDecl
| IfStmt
| 'comptime' ( IfStmt | ForStmt )
| [ IDENTIFIER ':' ] ( WhileStmt | DoStmt | ForStmt )
| LoopExpr
| MatchExpr
| 'break' [ IDENTIFIER ] [ Expr ] ';'
| 'continue' [ IDENTIFIER ] ';'
| 'return' [ Expr ] ';'
| 'yield' [ Expr ] ';'
| 'defer' ( Block | Expr ';' )
| 'throw' Expr ';'
| 'try' Block { CatchClause }
| ExprStmt .
LetStmt = ( 'let' | 'var' ) ( IDENTIFIER [ ':' Type ] [ '=' Expr [ 'else' Block ] ]
| Pattern [ ':' Type ] '=' Expr [ 'else' Block ] ) ';' .
IfStmt = 'if' '(' Condition ')' Block [ 'else' ( IfStmt | Block ) ] .
Condition = Expr | 'let' Pattern '=' Expr .
WhileStmt = 'while' '(' Condition ')' Block .
DoStmt = 'do' Block 'while' '(' Expr ')' ';' .
ForStmt = 'for' '(' ( IDENTIFIER | TuplePattern ) 'in' Expr ')' Block .
CatchClause = 'catch' '(' CatchBinding ')' Block .
CatchBinding = IDENTIFIER [ ':' Type | 'in' Bounds ] .
ExprStmt = Expr ';'
| BlockEnded [ ';' ] .
BlockEnded = Postfix TrailingLambda
| MacroCall
| Splice .
- A statement's body is a block, always (08 S1):
if (c) x;is refused (LYR-PAR0017). - A value block — a match arm, a lambda's body, a branch of an
ifexpression, a clause of atryexpression, the right of??, the block ofcomptime— may end in an expression without;, its value (07 §3). Anifwith its finalelse, amatchand aloopstanding last there are that value; elsewhere they are statements. Every other block ends with its statements. - A statement that ends in a block — a call with a trailing block, a macro site with a block —
needs no
;and may have one. An expression statement is a call, an assignment, an increment or an expression a rule of 07 names; what else stands there is the checker's to refuse. - A local function is a
fnin a body: no type parameters, no default, no variadic parameter, a body of its own (LYR-PAR0060).usingbinds withletonly (LYR-PAR0052). - A label names the loop that follows it and only a loop (
LYR-PAR0046);loopis a word only before its{,unsafeonly before its block,usingonly beforelet. - A
;alone is an empty statement (08 S10): it stands wherever a statement may, a value block's included, does nothing, andlyric fmtdrops it.
5. Expressions
| Level | Operators | Associativity |
|---|---|---|
| 1 | postfix . ?. [ ] ( ) ++ -- ! with { }, a trailing block |
left |
| 2 | prefix ! - ~ ++ -- ^, try, throw, comptime |
right |
| 3 | as |
left |
| 4 | * / % *% |
left |
| 5 | + - +% -% |
left |
| 6 | << >> |
left |
| 7 | .. ..= |
none |
| 8 | & |
left |
| 9 | ^ |
left |
| 10 | \| |
left |
| 11 | < <= > >= is in !in |
none |
| 12 | == != |
none |
| 13 | && |
left |
| 14 | \|\| |
left |
| 15 | ?? |
right |
| 16 | = and the compound assignments |
right |
Expr = 'try' Expr { CatchExpr }
| 'try' ( '?' | '!' ) Expr
| Assignment .
CatchExpr = 'catch' '(' CatchBinding ')' ( ValueBlock | Expr ) .
Assignment = Coalesce [ AssignOp Expr ] .
AssignOp = '=' | '+=' | '-=' | '*=' | '/=' | '%=' | '+%=' | '-%=' | '*%=' | '&=' | '|='
| '^=' | '<<=' | '>>=' | '&&=' | '||=' | '??=' .
Coalesce = Or [ '??' ( ValueBlock | Coalesce ) ] .
Or = And { '||' And } .
And = Equality { '&&' Equality } .
Equality = Comparison [ ( '==' | '!=' ) Comparison ] .
Comparison = BitOr [ CompareOp BitOr | 'is' Type ] .
CompareOp = '<' | '<=' | '>' | '>=' | 'in' | '!in' .
BitOr = BitXor { '|' BitXor } .
BitXor = BitAnd { '^' BitAnd } .
BitAnd = Range { '&' Range } .
Range = Shift [ ( '..' | '..=' ) Shift ] .
Shift = Additive { ( '<<' | '>>' ) Additive } .
Additive = Multiplicative { ( '+' | '-' | '+%' | '-%' ) Multiplicative } .
Multiplicative = Cast { ( '*' | '/' | '%' | '*%' ) Cast } .
Cast = Prefix { 'as' Type } .
Prefix = ( '!' | '-' | '~' | '++' | '--' | '^' | 'try' | 'throw' ) Prefix
| 'comptime' ( ValueBlock | Prefix )
| 'comptime' 'if' '(' Expr ')' Expr 'else' Expr
| Postfix .
Postfix = Primary { PostfixOp } .
PostfixOp = '.' ( IDENTIFIER | INT_LITERAL | FLOAT_LITERAL )
| '.' '[' Expr ']' [ InitBody ]
| '.' '[' Expr ',' Expr ']'
| '?.' IDENTIFIER
| '[' IndexElement ']'
| [ TypeArgs ] '(' [ Args ] ')'
| '++' | '--' | '!'
| 'with' '{' [ WithField { ',' WithField } [ ',' ] ] '}'
| TrailingLambda .
IndexElement = Expr
| [ Expr ] ( '..' | '..=' ) [ Expr ] .
Args = Arg { ',' Arg } [ ',' ] .
Arg = [ IDENTIFIER ':' ] [ '&' ] Expr [ '...' ] .
WithField = IDENTIFIER { '.' IDENTIFIER } '=' Expr .
TrailingLambda = '{' [ BlockParam { ',' BlockParam } '=>' ] { Statement } [ Expr | 'throw' Expr ] '}' .
BlockParam = [ '&' ] IDENTIFIER | TuplePattern .
Primary = Literal
| FString
| 'this'
| IDENTIFIER
| Path TypeArgs
| Path [ TypeArgs { '.' IDENTIFIER } ] InitBody
| '.' IDENTIFIER [ InitBody ]
| IDENTIFIER '=>' LambdaBody
| '(' [ LambdaParam { ',' LambdaParam } [ ',' ] ] ')'
[ ':' ReturnType [ ThrowsClause ] ] '=>' LambdaBody
| '(' Expr ')'
| '(' Expr ',' Expr { ',' Expr } [ ',' ] ')'
| '[' [ Expr { ',' Expr } [ ',' ] ] ']'
| IfExpr
| MatchExpr
| LoopExpr
| MacroCall
| 'quote' ( '{' { TopLevelDecl } '}' | ValueBlock )
| Splice
| AT_NAME [ '(' [ Args ] ')' ] .
Literal = INT_LITERAL | FLOAT_LITERAL | CHAR_LITERAL | STRING_LITERAL | BYTE_STRING
| 'true' | 'false' | 'null' .
FString = FSTRING_START { FSTRING_TEXT | HOLE_START Expr [ FORMAT_SPEC ] HOLE_END }
FSTRING_END .
InitBody = '{' [ InitItem { ',' InitItem } [ ',' ] ] '}' .
InitItem = IDENTIFIER [ '=' Expr ]
| '[' Expr ']' '=' Expr
| 'comptime' 'for' '(' IDENTIFIER 'in' Expr ')' '[' Expr ']' '=' Expr .
LambdaParam = [ '&' ] IDENTIFIER [ ':' Type ]
| TuplePattern [ ':' Type ] .
LambdaBody = ValueBlock | Expr .
IfExpr = 'if' '(' Expr ')' ( ValueBlock | Expr ) 'else' ( ValueBlock | Expr ) .
MatchExpr = 'match' '(' Expr ')' '{' { MatchArm ',' | BlockArm [ ',' ] } [ MatchArm ] '}' .
MatchArm = Pattern [ 'if' Expr ] '=>' Expr .
BlockArm = Pattern [ 'if' Expr ] '=>' ValueBlock .
LoopExpr = [ IDENTIFIER ':' ] 'loop' Block .
MacroCall = IDENTIFIER '!' ( '(' [ Expr { ',' Expr } [ ',' ] ] ')' [ ValueBlock ]
| ValueBlock ) .
Splice = '#' '{' Expr '}' .
- Assignment is an expression (08 S8), the lowest level, right-associative. A comparison and
an equality do not chain:
a < b < cis refused with the form to write (LYR-PAR0059), andis, a test at the comparisons' level (08 §4), chains with none of them. A range does not chain either (LYR-PAR0005). trycovers everything to its right (06 §4):try a + bistry (a + b). At the start of an expression it covers that expression and takes itscatchclauses; to the right of an operator the plaintrycovers from where it stands, andtry?,try!and atrywith clauses stand at the start only (LYR-PAR0050,LYR-PAR0051).throw eis a prefix:x ?? throw e.a..bis a value (03); in an index the bounds may be left open —xs[..b],xs[a..],xs[..]— and^ncounts from the end.- A lambda is written in three forms (08 F1):
(n: int): int => …,x => …, and a trailing block after a callee or a call,xs.map { it * 2 },fold(0) { acc, x => acc + x }, its parameters before=>or the implicitit. ifas an expression has itselse(LYR-PAR0036).'comptime' 'if'in an expression chooses between two (LYR-PAR0061).- A match arm whose body is an expression is followed by
,unless it stands last; a block arm may omit it (08 S9). name!(…)andname! { … }are a macro's site (11 §5): the!written against the name, before(or{.quote { … }holds declarations when its first token begins one, statements otherwise;#{ e }splices inside it, where an expression, a type or a declaration's name stands.e.[f],e.[v, p],E.[v] { … }and the items[f] = eandcomptime for (f in …) [f] = eof an initializer reach fields and variants by a compile-time info (11).
6. Patterns
Pattern = PatternTerm { '|' PatternTerm } .
PatternTerm = PatternLiteral [ ( '..' | '..=' ) PatternLiteral ]
| IDENTIFIER ( ':' Type | 'in' Bounds )
| Path [ Payload ]
| '.' IDENTIFIER [ Payload ]
| '(' Pattern { ',' Pattern } [ ',' ] ')'
| '[' [ ArrayElement { ',' ArrayElement } [ ',' ] ] ']' .
PatternLiteral = [ '-' ] ( INT_LITERAL | FLOAT_LITERAL ) | STRING_LITERAL | CHAR_LITERAL
| 'true' | 'false' | 'null' .
Payload = '(' [ Pattern { ',' Pattern } [ ',' ] ] ')'
| '{' [ FieldPattern { ',' FieldPattern } [ ',' ] ] '}' .
FieldPattern = IDENTIFIER [ '=' Pattern ] .
ArrayElement = Pattern | '..' [ IDENTIFIER ] .
TuplePattern = '(' Pattern { ',' Pattern } [ ',' ] ')' .
- A name alone binds, always; a variant is written
.RedorSignal.Red(08 Y6, 09)._binds nothing.c: Circletests a type ands in [Circle, Rect]a set of them. - A pattern's literal may be negative; a range's bounds are literals. One parenthesized pattern
is a group, two or more a tuple. An array pattern holds at most one
..(LYR-PAR0033).
7. Where the grammar leaves two readings
The EBNF is ambiguous at a few places, on purpose: each is settled by looking ahead at the tokens, never by meaning. In order of how often a program meets them:
<after a name opens type arguments when it closes balanced over tokens a type may hold and is followed by(— a call,f<int>()—, by{that opens an initializer, or, for a path in value position, by.,),],},,,;or the end of the text (map(xs, ident<int>)). Everywhere else it is the comparison.- A
{after a name opens an initializer when the braces are empty, holdname =whose value ends at,or}, hold names separated by,— the shorthandS { v, w }—, or open with[orcomptime for. Otherwise the braces are a trailing block:run { total = 5; }, whose;makes it a statement's. No statement begins with an initializer — its value would be dropped (LYR-PAR0055); in a value position, inside any parentheses, brackets or braces, and after=, one may stand. - A trailing block follows a name, a member or a call; it holds statements when it begins
with a statement's word or holds a
;at its own level, and one expression otherwise.{ a, b => … }names its parameters. - A
(begins a lambda when its matching)is followed by=>or by:and a return type and=>; a tuple when a,follows its first element; a group otherwise. In a match arm's guard, the first(is a group and a name before=>is no lambda. - A name after
breakis a label when a loop around thebreakcarries it, the beginning of the value otherwise (07 §1 rule 3). - An
ifat the start of a statement in a value block is the block's value when its chain ends in anelseand nothing follows it; otherwise, and in every other block, it is the statement. The same holds formatchandloop. try?andtry!: the sign is written against the word; with a space between,!negates what follows —try !done().!after a name before(or{, written against the name, is a macro's site; anywhere else it unwraps:x! in xs,x!.f()..and a digit:t.0is a tuple's element,t.0.1two of them;3.show()calls on3;1..3is a range.
8. The check
tools/grammar.pyreads the EBNF of this chapter, cuts a program into the tokens of 01 and asks whether it is aModule. Every program the suite expects to compile — each case that leads withrunorcheckand expects noerror:, every file of a package case — must be one; the check runs in this repository's CI.- The check runs one way. A program the grammar takes may be refused by a rule beside it (§1 rule 4), so the cases that expect an error are not asked.