This compiler-facing EBNF summarizes the admitted P-374/S-205 surface. Static restrictions called out below are normative even where EBNF alone cannot express them. The authoritative syntax rationale is retained in the S-numbered decision record.
Notation: quoted text is a terminal; ?, *, and + mean optional,
zero-or-more, and one-or-more; parentheses group; | separates alternatives;
comma-list(X) means zero or more X separated by exactly one comma with one
optional trailing comma.
source-file = shebang? package-declaration import-declaration* declaration* EOF
package-declaration = "package" identifier
import-declaration = "import" import-path import-tail?
import-path = identifier ("." identifier)*
import-tail = "as" identifier | "{" comma-list(identifier) "}"
The package header is mandatory. Imports precede other declarations. as is
valid only for a whole-package import. Package and import declarations have no
semicolon.
declaration = const-declaration | struct-declaration | enum-declaration
| resource-declaration | type-declaration | function-declaration
| native-function-declaration | native-type-declaration
| protocol-declaration | conformance-declaration
visibility = "pub"
generic-parameters = "<" comma-list1(generic-parameter) ">"
generic-parameter = identifier (":" bound-list)?
bound-list = type ("+" type)*
const-declaration = visibility? "const" identifier (":" type)?
"=" constant-expression ";"
struct-declaration = visibility? "struct" identifier generic-parameters?
directive-group? "{" comma-list(struct-field) "}"
struct-field = visibility? identifier ":" type directive-group?
enum-declaration = visibility? "enum" identifier generic-parameters?
directive-group? "{" comma-list(enum-variant) "}"
enum-variant = identifier ("(" comma-list1(type) ")")? directive-group?
resource-declaration = visibility? "resource" identifier generic-parameters?
"{" resource-field-list cleanup-clause "}"
resource-field-list = (resource-field ",")*
resource-field = identifier ":" type
cleanup-clause = "cleanup" "(" "mut" "self" ")" block
type-declaration = visibility? "type" "opaque"? identifier type ";"
| visibility? "type" identifier generic-parameters?
"=" type ";"
function-declaration = visibility? "fn" function-head generic-parameters?
parameter-list result-type? block
function-head = identifier | owner-head "::" identifier
owner-head = type-path | type-path "<" comma-list1(owner-parameter) ">"
owner-parameter = identifier (":" bound-list)?
parameter-list = "(" comma-list(parameter) ")"
parameter = "self" | "mut" "self"
| "mut"? identifier ":" type
result-type = type
native-function-declaration = visibility? "native" "offload"? "fn" identifier
parameter-list result-type? ";"
native-type-declaration = visibility? "native" "type" identifier ";"
protocol-declaration = visibility? "protocol" identifier generic-parameters?
"{" comma-list(protocol-requirement) "}"
protocol-requirement = "fn" identifier parameter-list result-type?
conformance-declaration = "conform" owner-head ":" named-type ";"
const exists only at package scope. A receiver, when present, occupies the
first parameter position. Generic owner heads repeat the owner's complete
parameter vector; P-363 permits only positive applicability bounds there.
Resources do not admit pub fields or structural directives. Native functions
have no owner, receiver, body, or generic parameters. A native declaration may
have at most one writable parameter, normalized exactly to Array<byte>.
directive-group = "#[" comma-list1(directive) "]"
directive = directive-path ("(" comma-list(directive-option) ")")?
directive-path = identifier ("::" identifier)*
directive-option = identifier | identifier ":" constant-expression
Directive groups are trailing attachments on struct/enum heads, struct fields, and enum variants only. The admitted paths and option vocabularies are closed; this grammar is not an open attribute system.
For Debug, an ordinary struct field admits exactly Debug(redact) or
Debug(skip). They are marker directives, are mutually exclusive, and cannot
be duplicated. Debug(deny) is invalid at every attachment position. No
positive Debug, derives, visibility-based, enum-variant, whole-type, or
resource-field Debug directive exists.
type = named-type | callable-type | "(" type ")"
named-type = type-path type-arguments?
type-path = identifier ("." identifier)*
type-arguments = "<" comma-list1(type) ">"
callable-type = "fn" "(" comma-list(callable-parameter-type) ")" result-type?
callable-parameter-type = "mut"? type
Primitive, predeclared, imported, and declared names share named-type syntax.
Angle-bracket token splitting in nested types is parser-owned. A callable's
mut parameter effect is part of its exact type.
block = "{" statement* tail-expression? "}"
statement = let-statement | assignment-statement | return-statement
| expression-statement | if-statement | if-let-statement
| for-statement | arena-statement | "break" ";" | "continue" ";"
let-statement = "let" "mut"? identifier (":" type)?
("=" expression)? ";"
| "let" struct-let-pattern "=" expression ";"
assignment-statement = assignment-target assignment-operator expression ";"
assignment-target = "_" | place
assignment-operator = "=" | "+=" | "-=" | "*=" | "/=" | "%="
| "&=" | "|=" | "^=" | "&^=" | "<<=" | ">>="
return-statement = "return" expression? ";"
expression-statement = expression ";"
Only value-producing blocks owned by an expression admit a final expression without a semicolon. There are no empty statements or standalone bare blocks. Assignment is statement-only.
if-statement = "if" expression block ("else" (if-statement | block))?
if-let-statement = "if" "let" pattern "=" expression block
("else" block)?
for-statement = "for" for-header? block
for-header = expression | for-pattern "in" expression
| map-for-binding "," map-for-binding "in" expression
map-for-binding = "mut"? identifier | "_"
arena-statement = "arena" block
if-expression = "if" expression block "else" (if-expression | block)
match-expression = "match" expression "{" comma-list1(match-arm) "}"
match-arm = pattern "=>" (expression | block)
select-expression = "select" "{" comma-list1(select-branch) "}"
select-branch = "case" select-target? "="? expression "=>" (expression | block)
| "default" "=>" (expression | block)
recover-expression = "recover" block "with" identifier block
The three loop forms are infinite for {}, condition for condition {}, and
iteration. Arrays, sets, receivers, ranges, and user iterators use
for pattern in source {}. A map uses exactly
for key_binding, value_binding in source {}; each binding is one identifier,
mut plus one identifier, or _. A map rejects the single-binding form and
every non-map rejects the two-binding form. There is no MapEntry type, C-style
three-clause loop, loop value, label, while, named arena, match guard, or
select guard. Select
target punctuation is constrained by the selected operation's result and the
rules in the consolidated specification.
Primary expressions include literals, paths, self, collection literals,
anonymous functions, struct construction, and the control-flow expressions
above. Postfix operations bind more tightly than prefix or binary operators.
expression = primary-expression
| expression postfix
| prefix-operator expression
| expression binary-operator expression
| expression (".." | "..=") expression
postfix = "(" comma-list(call-argument) ")"
| "<" comma-list1(type) ">" "(" comma-list(call-argument) ")"
| "." identifier
| "::" identifier
| "[" expression "]"
| "?"
call-argument = expression | "mut" place
place = identifier (("." identifier) | ("[" expression "]"))*
array-literal = "[" comma-list(expression) "]"
map-literal = "map" "{" comma-list(expression ":" expression) "}"
set-literal = "set" "{" comma-list(expression) "}"
struct-construction = type-path type-arguments?
"{" comma-list(identifier ":" expression) "}"
anonymous-function = "fn" anonymous-parameter-list result-type? block
spawn-expression = "spawn" call-expression
mut place is a complete call argument, not a prefix expression. spawn
selects a call-shaped operation, looking through grouping only. Generic struct
and enum construction requires a complete explicit owner argument vector when
the owner is generic; pattern heads do not write owner arguments.
From tightest to loosest:
- Member/type-associated access, indexing, calls, and postfix
? - Prefix
!,-, and^; call-onlyspawn *,/,%,<<,>>,&,&^+,-,|,^==,!=,<,<=,>,>=&&||- finite range
..and..=
Binary arithmetic, bitwise, shift, and logical operators are left-associative. Comparisons do not create a general chaining facility. Range expressions require both endpoints and cannot chain.
pattern = alternative-pattern | bound-pattern
alternative-pattern = "|"? bound-pattern ("|" bound-pattern)+
bound-pattern = ("mut"? identifier "@")? atomic-pattern
atomic-pattern = "_" | identifier | qualified-value | literal
| prelude-variant-pattern | enum-variant-pattern
| struct-pattern | array-pattern | range-pattern
| "(" pattern ")"
enum-variant-pattern = type-path "::" identifier
("(" comma-list(pattern) ")")?
struct-pattern = type-path "{" comma-list(struct-pattern-field | "..") "}"
struct-pattern-field = identifier | identifier ":" pattern
array-pattern = "[" comma-list(pattern | "..") "]"
range-pattern = range-bound? (".." | "..=") range-bound?
Alternative branches must bind identical names, types, and mutability. An array
pattern has at most one rest element. Struct rest is final. Range-pattern bounds
use the restricted literal/constant grammar. let and for admit only their
specified irrefutable subsets.
interpolation = "${" interpolation-path "}"
interpolation-path = (identifier | "self") ("." identifier)*
Calls, indexing, conversion, operators, blocks, and nested interpreted strings are not admitted in a slot.
A conforming parser must preserve original byte spans, recover with bounded work on invalid input, and never use name or type lookup to change the parse. Ambiguous call/member/type-owned forms remain lookup-neutral syntax until resolution. The formatter and editor grammar must consume this same admitted surface; a permissive highlighting grammar is not normative.