Lyric Grammar
The formal grammar of the Lyric language. This document defines the syntax only; it says nothing about typing, name resolution or runtime behaviour.
Notation
EBNF with the following conventions:
| Form | Meaning |
|---|---|
'x' |
terminal |
X |
non-terminal |
{ X } |
zero or more |
[ X ] |
zero or one |
X \| Y |
alternative |
( X ) |
grouping |
X . |
end of production |
(* … *) |
note |
a'..'z denotes a character range. any-char denotes any character of the source encoding.
1. Lexical grammar
1.1 Source file
SourceFile = { Trivia | Token } EOF .
Trivia = Whitespace | LineComment | BlockComment .
Source is UTF-8. A byte-order mark at offset 0 is skipped. Line terminators are \n and \r\n.
1.2 Whitespace and comments
Whitespace = ' ' | '\t' | '\n' | '\r' .
LineComment = '//' { any-char-except-newline } .
BlockComment = '/*' { BlockComment | any-char } '*/' .
DocComment = '///' { any-char-except-newline } .
Block comments nest: /* /* */ */ is one comment. Doc comments are tokenized and carry no
grammatical meaning.
1.3 Identifiers
IdentStart = 'a'..'z' | 'A'..'Z' | '_' .
IdentCont = IdentStart | '0'..'9' .
IDENTIFIER = IdentStart { IdentCont } . (* unless it is a keyword *)
AT_IDENT = '@' IdentStart { IdentCont } .
1.4 Keywords
Reserved; never an identifier:
module import as pub static
struct class enum interface extend
fn mut let var params
if else while do for in match
break continue return yield resume defer
try catch throw
true false null
this
Contextual; reserved only in the position shown, an identifier everywhere else:
| Word | Position |
|---|---|
type |
first token of a top-level declaration, before an identifier |
throws |
after the return type of a function signature |
Built-in type names (int, string, …) are identifiers, not keywords.
1.5 Literals
IntLit = ( DecLit | HexLit | BinLit | OctLit ) [ IntSuffix ] .
DecLit = DecDigit { DecDigit | '_' } .
HexLit = '0' ( 'x' | 'X' ) HexDigit { HexDigit | '_' } .
BinLit = '0' ( 'b' | 'B' ) BinDigit { BinDigit | '_' } .
OctLit = '0' ( 'o' | 'O' ) OctDigit { OctDigit | '_' } .
IntSuffix = ( 'i' | 'u' ) DecDigit { DecDigit } . (* i8 … i64, u8 … u64 *)
FloatLit = DecLit ( '.' DecLit [ Exponent ] | Exponent ) [ FloatSuffix ]
| DecLit FloatSuffix .
Exponent = ( 'e' | 'E' ) [ '+' | '-' ] DecDigit { DecDigit | '_' } .
FloatSuffix = 'f' DecDigit { DecDigit } . (* f32, f64 *)
StringLit = '"' { StringChar | EscapeSeq } '"' .
InterpolatedStr = 'f' '"' { StringChar | EscapeSeq | Interpolation } '"' .
Interpolation = '{' Expr [ ':' FormatSpec ] '}' .
CharLit = "'" ( CharChar | EscapeSeq ) "'" .
EscapeSeq = '\' ( 'n' | 'r' | 't' | '\' | '"' | "'" | '0'
| 'x' HexDigit HexDigit
| 'u' '{' HexDigit { HexDigit } '}' ) .
BoolLit = 'true' | 'false' .
NullLit = 'null' .
Inside an interpolated string, {{ and }} produce a literal brace. FormatSpec runs to the
matching }, tracking nested braces, parentheses and brackets.
1.6 Operators and punctuation
( ) { } [ ]
, . ; : :: -> =>
? ?. ?? !
+ - * / %
& | ^ ~
<< >>
== != < <= > >=
&& || !
++ --
.. ..=
= += -= *= /= %=
&= |= ^= <<= >>=
&&= ||= ??=
The lexer takes the longest match: <<= before << before <.
:: introduces an interface list and never appears in a module path; . separates path
segments. ! is postfix force-unwrap and prefix logical not.
2. Module structure
Module = { Attribute } ModuleHeader { TopLevelDecl }
| { TopLevelDecl } .
ModuleHeader = 'module' ModulePath ';' .
ModulePath = IDENTIFIER { '.' IDENTIFIER } .
Attribute = AT_IDENT { '.' IDENTIFIER }
[ '{' [ AttrArg { ',' AttrArg } [ ',' ] ] '}' ] .
AttrArg = IDENTIFIER '=' Expr .
ImportDecl = 'import' ModulePath [ ImportClause ] ';' .
ImportClause = '{' IDENTIFIER { ',' IDENTIFIER } [ ',' ] '}'
| 'as' IDENTIFIER .
TopLevelDecl = ImportDecl
| { Attribute } [ 'pub' ] ( FunctionDecl
| StructDecl
| ClassDecl
| EnumDecl )
| [ 'pub' ] ( InterfaceDecl
| ExtendDecl
| GlobalBinding
| TypeAlias ) .
GlobalBinding = BindingStmt . (* 'let' only *)
TypeAlias = 'type' IDENTIFIER '=' TypeExpr ';' .
The module header is optional. In an entry file the name then comes from the file name; in a file
reached through an import, the name is the imported path, and a header that disagrees with it is
an error.
An attribute before the header describes the module; in a file without a header an attribute at the
top belongs to the first declaration. The path names a struct type, and an AttrArg value must be
a literal — an integer, float, string, char or bool, optionally sign-prefixed — which is a semantic
rule, not a syntactic one.
3. Declarations
3.1 Functions
FunctionDecl = [ 'pub' ] [ 'static' ] [ 'mut' ] 'fn' IDENTIFIER [ GenericParams ]
'(' [ ParamList ] ')' [ ':' TypeExpr ]
[ 'throws' [ TypeExpr ] ]
( Block | ';' ) .
GenericParams = '<' GenericParam { ',' GenericParam } '>' .
GenericParam = IDENTIFIER [ '::' '[' TypeExpr { ',' TypeExpr } ']' ] .
ParamList = Param { ',' Param } .
Param = [ 'params' ] IDENTIFIER ':' TypeExpr [ '=' Expr ] .
A body of ; declares a function without one. params may appear on the last parameter only,
whose type must be an array.
3.2 Structs
StructDecl = [ 'pub' ] 'struct' IDENTIFIER [ GenericParams ]
[ '::' InterfaceList ]
'{' [ StructBody ] '}' .
InterfaceList = '[' TypeExpr { ',' TypeExpr } ']' .
StructBody = { StructMember [ ',' ] } .
StructMember = Field | FunctionDecl | StaticBinding .
StaticBinding = [ 'pub' ] 'static' BindingStmt .
Field = IDENTIFIER ':' TypeExpr [ '=' Expr ] .
Members are separated by ,. Only a field requires it: a member that closes itself — a block body
ending in }, a bodiless method or a static let ending in ; — may omit it.
3.3 Classes
ClassDecl = [ 'pub' ] 'class' IDENTIFIER [ GenericParams ]
[ '::' InterfaceList ]
'{' [ ClassBody ] '}' .
ClassBody = { ClassMember [ ',' ] } .
ClassMember = Field | FunctionDecl | StaticBinding .
3.4 Enums
EnumDecl = [ 'pub' ] 'enum' IDENTIFIER [ GenericParams ]
[ '::' InterfaceList ]
'{' [ EnumBody ] '}' .
EnumBody = EnumVariant { ',' EnumVariant } [ ',' ]
[ ';' { FunctionDecl } ] .
EnumVariant = IDENTIFIER [ TupleVariant | StructVariant ] .
TupleVariant = '(' TypeExpr { ',' TypeExpr } ')' .
StructVariant = '{' Field { ',' Field } [ ',' ] '}' .
The ; separates the variant list from the method list.
3.5 Interfaces
InterfaceDecl = [ 'pub' ] 'interface' IDENTIFIER [ GenericParams ]
'{' { InterfaceMember } '}' .
InterfaceMember = FunctionDecl .
An interface member with a body is a default implementation. An interface declares no interface list; there is no interface inheritance.
A member carries no static: it is reached through a vtable slot, which takes a receiver, and a
static member has none. Declare it on the implementing type instead.
3.6 Extend blocks
ExtendDecl = 'extend' TypeExpr [ '::' InterfaceList ]
'{' { FunctionDecl } '}' .
4. Type expressions
TypeExpr = TypePrefix TypeAtom { TypeSuffix } .
TypePrefix = [ '?' ] .
TypeAtom = BuiltinType
| ModulePath [ '<' TypeExpr { ',' TypeExpr } '>' ]
| FunctionType
| TupleType
| GroupedType .
FunctionType = 'fn' '(' [ TypeExpr { ',' TypeExpr } ] ')' '->' TypeExpr .
TupleType = '(' TypeExpr ',' TypeExpr { ',' TypeExpr } ')' . (* arity >= 2 *)
GroupedType = '(' TypeExpr ')' .
TypeSuffix = '[' ']' .
BuiltinType = 'int' | 'uint' | 'float'
| 'int8' | 'int16' | 'int32' | 'int64'
| 'uint8' | 'uint16' | 'uint32' | 'uint64'
| 'float32' | 'float64'
| 'bool' | 'char' | 'string' | 'void' .
A function type extends as far to the right as possible: fn(int) -> void[] is a function
returning void[]. An array of function values is written (fn(int) -> void)[].
? binds to the atom together with its suffixes: ?T[] is an optional array. An array of
optionals is written (?T)[].
? does not nest: ??T is not a type.
5. Statements
Block = '{' { Statement } '}' .
Statement = Block
| BindingStmt
| DestructuringStmt
| IfStmt
| WhileStmt
| DoWhileStmt
| ForInStmt
| MatchStmt
| BreakStmt
| ContinueStmt
| ReturnStmt
| YieldStmt
| DeferStmt
| ThrowStmt
| TryStmt
| ExprStmt .
BindingStmt = ( 'let' | 'var' ) IDENTIFIER [ ':' TypeExpr ] [ '=' Expr ] ';' .
DestructuringStmt = ( 'let' | 'var' ) TuplePattern [ ':' TypeExpr ] '=' Expr ';' .
IfStmt = 'if' '(' Expr ')' Block [ 'else' ( Block | IfStmt ) ] .
WhileStmt = 'while' '(' Expr ')' Block .
DoWhileStmt = 'do' Block 'while' '(' Expr ')' ';' .
ForInStmt = 'for' '(' IDENTIFIER 'in' Expr ')' Block .
MatchStmt = 'match' '(' Expr ')' '{' { MatchArm } '}' .
MatchArm = Pattern [ 'if' Expr ] '=>' ( Expr | Block ) .
BreakStmt = 'break' ';' .
ContinueStmt = 'continue' ';' .
ReturnStmt = 'return' [ Expr ] ';' .
YieldStmt = 'yield' [ Expr ] ';' .
DeferStmt = 'defer' ( Block | Expr ';' ) .
ThrowStmt = 'throw' Expr ';' .
TryStmt = 'try' Block { CatchClause } .
CatchClause = 'catch' '(' CatchBinding ')' Block .
CatchBinding = '_'
| IDENTIFIER ':' TypeExpr
| IDENTIFIER .
ExprStmt = Expr ';' .
A destructuring binding requires an initializer. Its pattern admits names, _ and nested tuple
patterns; no form that can fail.
ExprStmt admits a call, an assignment or resume. A statement does not begin with a struct
initializer: at that position Name { … } is a name followed by a block.
An arm whose body is an expression is followed by ,, except for the last arm before }. An arm
whose body is a block may omit it.
6. Expressions
6.1 Precedence
Highest first. All levels are left-associative unless stated.
| # | Operators | Associativity |
|---|---|---|
| 1 | postfix . ?. [ ] ( ) ++ -- ! |
left |
| 2 | prefix ! - ~ ++ -- resume |
right |
| 3 | as |
left |
| 4 | * / % |
left |
| 5 | + - |
left |
| 6 | << >> |
left |
| 7 | .. ..= |
non-associative |
| 8 | & |
left |
| 9 | ^ |
left |
| 10 | \| |
left |
| 11 | < <= > >= |
left |
| 12 | == != |
left |
| 13 | && |
left |
| 14 | \|\| |
left |
| 15 | ?? |
right |
| 16 | assignment | right |
6.2 Grammar
Expr = Assign .
Assign = Coalesce [ AssignOp Assign ] .
AssignOp = '=' | '+=' | '-=' | '*=' | '/=' | '%='
| '&=' | '|=' | '^=' | '<<=' | '>>='
| '&&=' | '||=' | '??=' .
Primary = IntLit | FloatLit | StringLit | InterpolatedStr
| CharLit | BoolLit | NullLit
| 'this'
| IDENTIFIER
| TypePath
| '(' Expr ')'
| IfExpr
| MatchExpr
| StructInit
| ArrayLit
| TupleLit
| Lambda .
Lambda = '(' [ LambdaParam { ',' LambdaParam } ] ')' [ ':' TypeExpr ]
'=>' ( Expr | Block ) .
LambdaParam = IDENTIFIER [ ':' TypeExpr ] .
ResumeExpr = 'resume' UnaryExpr .
StructInit = TypePath '{' [ StructInitField { ',' StructInitField } [ ',' ] ] '}' .
StructInitField = IDENTIFIER '=' Expr .
TypePath = ModulePath [ '<' TypeExpr { ',' TypeExpr } '>' ] [ '.' IDENTIFIER ] .
CallArgs = [ '<' TypeExpr { ',' TypeExpr } '>' ] '(' [ Expr { ',' Expr } ] ')' .
ArrayLit = '[' [ Expr { ',' Expr } [ ',' ] ] ']' .
TupleLit = '(' Expr ',' Expr { ',' Expr } ')' .
IfExpr = 'if' '(' Expr ')' Expr 'else' Expr .
MatchExpr = 'match' '(' Expr ')' '{' { MatchArm } '}' .
else is mandatory in IfExpr; else if is a nested IfExpr.
In a TypePath, the trailing '.' IDENTIFIER names an enum variant: Opt<int>.Some. The type
arguments belong to the type and precede that segment.
6.3 Resolving <
A < after a name opens a type-argument list when it closes balanced and only tokens that can
occur in a type expression stand between the two, and the token that follows is one of:
| Follower | Form |
|---|---|
( |
call with explicit type arguments — f<int>() |
{ |
struct initializer — Pair<int> { … } |
. |
type path in value position — Pair<int>.of(3) |
In every other case < is the comparison operator.
7. Patterns
Pattern = '_'
| Literal
| IDENTIFIER
| TypePath [ '(' Pattern { ',' Pattern } ')' ]
| TypePath '{' [ FieldPattern { ',' FieldPattern } [ ',' ] ] '}'
| TuplePattern
| Pattern '|' Pattern
| RangePattern .
FieldPattern = IDENTIFIER [ '=' Pattern ] .
TuplePattern = '(' Pattern ',' Pattern { ',' Pattern } ')' .
RangePattern = Literal ( '..' | '..=' ) Literal .
Literal is an integer, float, string, char, bool or null literal. A FieldPattern without =
binds the field to its own name.