Syntax
Syntax
This page defines the formal grammar of Zirric. It covers every syntactic form the parser accepts. For the meaning of each construct, see the corresponding semantic pages: Declarations, Expressions, and Type System.
The grammar is written in EBNF notation. Terminal symbols are quoted. {x} means zero or more repetitions of x, and [x] means x is optional.
Tokens
Identifiers
An identifier starts with a letter or underscore and continues with letters, digits, or underscores. Keywords take precedence over identifiers.
IDENT = (letter | "_"), {letter | digit | "_"};
Keywords
The following words are reserved and cannot be used as identifiers:
mod import fn const var
data union extern type attr
if else for switch case
is return break continue
true false void _
Literal Tokens
INT = "0", {octal_digit}
| ("1"..."9"), {digit}
| "0x", hex_digit, {hex_digit}
| "0b", ("0" | "1"), {("0" | "1")};
FLOAT = digits, ".", digits, [("e" | "E"), ["-"], digits]
| digits, ("e" | "E"), ["-"], digits;
STRING = '"', {string_char}, '"';
CHAR = "'", char_char, "'";
string_char = ? any character except '"' or backslash ? | escape | backslash, '"';
char_char = ? any character except "'" or backslash ? | escape | backslash, "'";
escape = backslash, ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | backslash
| "x", 2 * hex_digit
| "u", 4 * hex_digit
| "U", 8 * hex_digit
| 3 * octal_digit );
backslash = ? the "\" character ?;
BOOL = "true" | "false";
An integer written with a leading 0 is octal — 0777 is 511. There is no 0o prefix.
String and char literals accept the same escapes, except that each escapes only its own delimiter: \" belongs to a string and \' to a char. \u and \U name a code point, which a STRING holds UTF-8 encoded, while \x and the three-digit octal form name a single byte. Any other escape is an error rather than literal text.
Operator and Punctuation Tokens
PLUS = "+"; MINUS = "-"; ASTERISK = "*";
SLASH = "/"; PERCENT = "%"; BANG = "!";
EQ = "=="; NEQ = "!=";
LT = "<"; GT = ">";
LTE = "<="; GTE = ">=";
AND = "&&"; OR = "||";
ASSIGN = "="; ARROW = "->"; LARROW = "<-";
PLUS_ASSIGN = "+="; MINUS_ASSIGN = "-=";
STAR_ASSIGN = "*="; SLASH_ASSIGN = "/=";
PERCENT_ASSIGN = "%=";
COLON = ":"; DOT = "."; COMMA = ",";
AT = "@";
QDOT = "?."; BANGDOT = "!.";
QQ = "??"; BANGBANG = "!!";
QUESTION = "?";
LPAREN = "("; RPAREN = ")";
LBRACE = "{"; RBRACE = "}";
LBRACKET = "["; RBRACKET = "]";
Comments
line_comment = ("//" | "#"), {any_char}, newline;
Comments run to the end of the line. Both markers are equivalent to the compiler; // is conventional, and # additionally allows a #! shebang on the first line of an executable script.
A comment immediately preceding a declaration documents it, by convention written with //. The compiler keeps that comment and hands it back through reflect.docs — see Declarations § Documentation comments.
There are no block comments.
Operator Precedence
Operators are listed from lowest to highest precedence.
| Precedence | Operators | Associativity |
|---|---|---|
| Logical OR | || |
Left |
| Logical AND | && |
Left |
| Comparison | ==, !=, <, <=, >, >=, is Type |
Left |
| Fallback | ??, !! |
Right |
| Sum | +, - |
Left |
| Product | *, /, % |
Left |
| Prefix | -x, !x |
Right |
| Call | f(args) |
Left |
| Member | expr.field, expr?.field, expr!.field |
Left |
See Expressions § Operators for semantic details.
Source File Structure
A source file begins with an optional shebang line, then the module declaration, followed by top-level declarations.
SourceFile = [shebang], Module, {TopLevelDeclaration};
shebang = "#!", {any_inline_char}, newline;
Module = "mod", [Identifier, "="], ModulePath;
ModulePath = Identifier, {".", Identifier};
Declarations
See Declarations for semantic rules.
Variable and Constant Declarations
Const = "const", Identifier, [":", TypeExpr], "=", Expression;
Var = "var", Identifier, [":", TypeExpr], "=", Expression;
Function Declaration
Function = "fn", Identifier, "(", [Parameters], ")", ["->", TypeExpr], "{", Block, "}";
Data Declaration
Data = "data", Identifier, ["{", {DataField}, "}"];
DataField = {Attribute}, Identifier, ([":", TypeExpr] | "(", [Parameters], ")", ["->", TypeExpr]), [","];
A data declaration without a body (no braces) declares a zero-field type. Fields are either simple values with an optional type annotation, or function-style fields with parameters and an optional return type.
Union Declaration
Union = "union", Identifier, "{", {UnionMember}, "}";
UnionMember = {Attribute}, (Data | StaticReference), [","];
Union members are either inline data declarations or references to existing types. See Type System § Union Types for membership semantics.
Attribute Declaration
AttributeDecl = "attr", Identifier, ["{", {DataField}, "}"];
Attribute declarations follow the same field syntax as data. See Declarations § Attribute Declarations for application rules.
Extern Declarations
ExternType = "extern", "type", Identifier, ["{", {DataField}, "}"];
ExternFunc = "extern", "fn", Identifier, "(", [Parameters], ")", ["->", TypeExpr];
ExternConst = "extern", "const", Identifier, [":", TypeExpr];
Import Declaration
Import = "import", [Identifier, "="], ImportPath, ["{", {Identifier, [","]}, "}"];
ImportPath = Identifier, {".", Identifier};
Attribute Application
Attribute = "@", StaticReference, "(", [Arguments], ")";
Attributes precede the declaration or field they annotate. Parentheses are always required, even when empty. Multiple attributes can be stacked.
TopLevelDeclaration = Import
| {Attribute}, (Union | Data | ExternType | ExternFunc | ExternConst | AttributeDecl | Function | Const | Var)
| Expression;
Parameters
Parameters = Parameter, {",", Parameter};
Parameter = {Attribute}, (Identifier | "_"), [":", TypeExpr];
Parameters appear in fn declarations, closures, extern fn, and function-style data fields. See Declarations § Parameters.
Type Expressions
Type expressions annotate declarations and parameters with type information. They appear after : on fields, parameters, and variables, and after -> on function return types.
TypeExpr = TypeExprPrimary, {"?" | "!"};
TypeExprPrimary = TypeExprRef | TypeExprArray | TypeExprDict | TypeExprFunc | TypeExprAttrs;
TypeExprRef = StaticReference;
TypeExprArray = "[", TypeExpr, "]";
TypeExprDict = "[", TypeExpr, ":", TypeExpr, "]";
TypeExprFunc = "fn", "(", [TypeExprParams], ")", ["->", TypeExpr];
TypeExprAttrs = "@", StaticReference, {"@", StaticReference};
TypeExprParams = TypeExprParam, {",", TypeExprParam};
TypeExprParam = [{Attribute}, Identifier, ":"], TypeExpr;
A ? or ! suffix must sit directly on the type it qualifies, with no whitespace or comment between them. That is what keeps a return type from running on into the next line: after extern fn ready() -> Bool, a following statement that begins with ! is a statement of its own. See Type System § Optional and Result Shorthands.
See Type System § Type Hints for what type expressions express and how they interact with type checking.
Expressions
See Expressions for semantic details.
Literals
IntLiteral = INT;
FloatLiteral = FLOAT;
StringLiteral = STRING;
BoolLiteral = "true" | "false";
ArrayLiteral = "[", [Expression, {",", Expression}], "]";
DictLiteral = "[", [DictEntry, {",", DictEntry}], "]";
DictEntry = Expression, ":", Expression;
Identifiers and References
Identifier = IDENT;
StaticReference = Identifier, {".", Identifier};
A StaticReference is a dot-separated path used for qualified names in imports, attributes, and type hints.
Operators
InfixExpr = Expression, operator, Expression;
PrefixExpr = ("-" | "!"), Expression;
IsExpr = Expression, "is", TypeExpr;
FallbackExpr = Expression, ("??" | "!!"), Expression;
A ! directly before an expression negates it; one directly after a type is the T! shorthand. See Type Expressions.
Calls and Member Access
CallExpr = Expression, "(", [Arguments], ")";
MemberExpr = Expression, ("." | "?." | "!."), Identifier;
IndexExpr = Expression, "[", Expression, "]";
Arguments = Expression, {",", Expression};
?. and !. read a field the same way . does, but only once the value they sit on turns out to be readable. See Expressions § Guarded Member Access.
Closures
Closure = "fn", "(", [Parameters], ")", ["->", TypeExpr], "{", Block, "}";
Closures are anonymous functions. They share syntax with fn declarations but omit the name. See Expressions § Closures.
Switch Expression
SwitchExpr = "switch", Expression, "{", {SwitchCase}, "}";
SwitchCase = "case", ("is", TypeExpr | "_" | Expression), ":", Expression;
Type matching requires the is keyword (case is Type:). Value matching uses a plain expression (case value:). The default case uses _. Switch expressions require a _ fallback case, including where the is cases appear to cover every member of a union. See Expressions § Switch.
Assignment
Assignment = Identifier, AssignOp, Expression;
FieldAssignment = Expression, ".", Identifier, AssignOp, Expression;
IndexAssignment = Expression, "[", Expression, "]", AssignOp, Expression;
AssignOp = "=" | "+=" | "-=" | "*=" | "/=" | "%=";
Only var bindings and mutable locations accept assignment. A guarded access (?. or !.) is not an assignment target, since it may produce no field to assign to. The augmented forms read the location, apply the operator, and write the result back. See Expressions § Assignment.
Statements
Statements appear inside blocks. Most expressions can also be used as statements.
Block = {Statement};
Statement = ScopeLevelDeclaration | Return | If | For | SwitchStmt | Expression;
ScopeLevelDeclaration = Function | Const | Var;
data, union, attr, extern and import are top-level forms only. The parser shares one routine across both positions, so writing one inside a block is rejected after parsing rather than at it. See Declarations for the position each form is valid in.
Return
Return = "return", [Expression];
If
If = "if", Expression, "{", Block, "}", {"else", "if", Expression, "{", Block, "}"}, ["else", "{", Block, "}"];
See Expressions § If for the distinction between if expressions and if statements.
For
For = "for", [ForBinding], "{", Block, "}";
ForBinding = Identifier, "<-", Expression (* collection iteration *)
| Expression; (* boolean or infinite loop *)
When the binding and expression are both omitted, the loop runs indefinitely. See Expressions § For.
Switch Statement
SwitchStmt = "switch", Expression, "{", {StmtSwitchCase}, "}";
StmtSwitchCase = "case", ("is", TypeExpr | "_" | Expression), ":", Block;
Switch statements do not require a _ fallback.