Reussir Language Reference

Lexical and Grammar Rules

Lexical and Grammar Rules

This chapter is the compact reference for Reussir source text. It describes which characters form tokens and how those tokens form a complete source file. Earlier chapters introduce the same constructs through working programs; come here when punctuation, precedence, or an edge case needs an exact answer.

The grammar uses the following notation:

  • quoted text is written literally;
  • uppercase names denote lexical categories or helper productions;
  • x?x?, x*x*, and x+x+ mean zero or one, zero or more, and one or more;
  • x | yx | y chooses between alternatives; and
  • parentheses group grammar terms and do not necessarily appear in source.

The productions describe well-formed source to write. The parser also performs error recovery, but recovery behavior is not part of the language grammar.

Lexical structure

Reussir source files are UTF-8. Whitespace and comments may appear between any two tokens. They separate tokens but otherwise do not change the grammar; newlines are not statement terminators.

Whitespace and comments

WHITESPACE = (" " | "\\t" | "\\r" | "\\n" | U+000B | U+000C)+ ;
LINE-COMMENT = "//" (any character except "\\n")* ;
BLOCK-COMMENT = "/*" (any character sequence not containing "*/") "*/" ;
WHITESPACE = (" " | "\\t" | "\\r" | "\\n" | U+000B | U+000C)+ ;
LINE-COMMENT = "//" (any character except "\\n")* ;
BLOCK-COMMENT = "/*" (any character sequence not containing "*/") "*/" ;

A line comment stops immediately before the newline. A block comment stops at the first */*/ and therefore does not nest:

let total = left + right; // continues to the end of this line
let next = total /* one block comment */ + 1;
let total = left + right; // continues to the end of this line
let next = total /* one block comment */ + 1;

Identifiers and words

An identifier follows Unicode identifier rules:

IDENT = XID_Start XID_Continue* ;
IDENT = XID_Start XID_Continue* ;

Names such as countercounter, ListList, αα, and 变量变量 are identifiers. An underscore does not satisfy XID_StartXID_Start, so _item_item is tokenized as __ followed by itemitem; use a letter as the first character of a name. A lone __ is the wildcard token used in patterns and inferred type arguments.

The lexer recognizes these words specially because they introduce syntax:

fn struct enum pub mod let
if else match regional extern as
true false import
fn struct enum pub mod let
if else match regional extern as
true false import

They are not globally reserved names. When the grammar explicitly expects a name, a word from this list is accepted too. At an ambiguous item or expression boundary, however, its syntactic meaning wins; for example, an expression that starts with ifif is parsed as a conditional.

Several other words are contextual. transformtransform and trampolinetrampoline are special only in their item forms. Primitive type names and capability names are special only in type or capability position. Everywhere else they remain ordinary identifiers.

The grammar below uses NAMENAME wherever the parser expects an identifier-like name. It includes both an ordinary IDENTIDENT token and any hard-keyword token:

NAME = IDENT
| "fn" | "struct" | "enum" | "pub" | "mod" | "let"
| "if" | "else" | "match" | "regional" | "extern" | "as"
| "true" | "false" | "import" ;
NAME = IDENT
| "fn" | "struct" | "enum" | "pub" | "mod" | "let"
| "if" | "else" | "match" | "regional" | "extern" | "as"
| "true" | "false" | "import" ;

This distinction matters only after lexing. Contextual words such as sharedshared and i32i32 already arrive as IDENTIDENT tokens.

Numeric literals

Integer literals may be decimal, hexadecimal, octal, or binary. Floating-point literals are decimal and require a fractional part, an exponent, or both.

DIGIT = "0" ... "9" ;
HEX-DIGIT = DIGIT | "a" ... "f" | "A" ... "F" ;
DEC-RUN = DIGIT (DIGIT | "_")* ;
DEC-INT = DEC-RUN ;
HEX-INT = "0" ("x" | "X") "_"* HEX-DIGIT (HEX-DIGIT | "_")* ;
OCT-INT = "0" ("o" | "O") "_"* ("0" ... "7")
("0" ... "7" | "_")* ;
BIN-INT = "0" ("b" | "B") "_"* ("0" | "1")
("0" | "1" | "_")* ;
INT = DEC-INT | HEX-INT | OCT-INT | BIN-INT ;
EXPONENT = ("e" | "E") ("+" | "-")? DEC-RUN ;
FLOAT = DEC-RUN "." DEC-RUN EXPONENT? | DEC-RUN EXPONENT ;
DIGIT = "0" ... "9" ;
HEX-DIGIT = DIGIT | "a" ... "f" | "A" ... "F" ;
DEC-RUN = DIGIT (DIGIT | "_")* ;
DEC-INT = DEC-RUN ;
HEX-INT = "0" ("x" | "X") "_"* HEX-DIGIT (HEX-DIGIT | "_")* ;
OCT-INT = "0" ("o" | "O") "_"* ("0" ... "7")
("0" ... "7" | "_")* ;
BIN-INT = "0" ("b" | "B") "_"* ("0" | "1")
("0" | "1" | "_")* ;
INT = DEC-INT | HEX-INT | OCT-INT | BIN-INT ;
EXPONENT = ("e" | "E") ("+" | "-")? DEC-RUN ;
FLOAT = DEC-RUN "." DEC-RUN EXPONENT? | DEC-RUN EXPONENT ;

Examples include 4242, 1_000_0001_000_000, 0xff_200xff_20, 0o7550o755, 0b10100b1010, 3.14153.1415, 1e61e6, and 2.5e-72.5e-7. Underscores are ignored when interpreting the value. The current lexical grammar also permits a trailing underscore in a digit run and underscores immediately after a radix prefix, but every radix literal must contain at least one digit of that radix.

-- is always an operator rather than part of a number. Consequently, -12-12 is a prefix expression applied to the integer 1212. Leading-dot and trailing-dot floats such as .5.5 and 1.1. are not literals.

Strings and characters

Strings use double quotes, while characters use single quotes:

"Reussir\n"
'R'
'λ'
'\u{1f680}'
"Reussir\n"
'R'
'λ'
'\u{1f680}'

Both use Rust’s cooked-literal escape grammar. This includes escapes such as \\\\, \n\n, \r\r, \t\t, \0\0, escaped quotes, \xNN\xNN, and \u{...}\u{...}. A character literal must decode to exactly one Unicode scalar value. A string may span physical lines; a character literal may not.

Opaque MLIR and foreign-source literals

[{[{ begins one opaque raw literal and the matching }]}] ends it:

RAW-MLIR-LITERAL = "[{" balanced-brace-content "}]" ;
RAW-MLIR-LITERAL = "[{" balanced-brace-content "}]" ;

Braces inside the block are balanced. Braces inside double-quoted strings, //// comments, and /* ... *//* ... */ comments do not affect the balance. Reussir does not tokenize the contents further; the complete block is passed to the appropriate foreign-code or MLIR consumer. Raw literals occur only in the item productions listed below.

Punctuation and operators

The lexer recognizes the longest applicable punctuation token:

:: -> => := == != <= >= && || ..
( ) { } [ ] < > : ; ,
. = + - * / % ! | # _
:: -> => := == != <= >= && || ..
( ) { } [ ] < > : ; ,
. = + - * / % ! | # _

No other punctuation is currently part of the language. In particular, a standalone &&, ??, or backslash is a lexical error.

Source files and items

A source file contains only items. A free-standing expression is valid in a function body or the REPL, but not at file scope.

source-file = item* ;
item = attribute* "pub"? item-core ;
item-core = function-item
| struct-item
| enum-item
| module-item
| import-item
| extern-item
| transform-item ;
source-file = item* ;
item = attribute* "pub"? item-core ;
item-core = function-item
| struct-item
| enum-item
| module-item
| import-item
| extern-item
| transform-item ;

On items that expose visibility, pubpub makes the item public. Attributes always precede it.

Attributes

attribute = "#[" NAME ("(" attribute-arguments? ")")? "]" ;
attribute-arguments = attribute-argument
("," attribute-argument)* ","? ;
attribute-argument = NAME ("=" STRING)? ;
attribute = "#[" NAME ("(" attribute-arguments? ")")? "]" ;
attribute-arguments = attribute-argument
("," attribute-argument)* ","? ;
attribute-argument = NAME ("=" STRING)? ;

An argument is either a bare word or a word paired with a string. Common forms include #[main]#[main], #[ffi(import)]#[ffi(import)], #[ffi(rust = "path")]#[ffi(rust = "path")], and #[repr(fixed)]#[repr(fixed)]. The meaning and allowed placement of each attribute are checked after parsing.

Functions

function-item = "regional"? "fn" NAME generic-parameters?
parameter-list return-type? function-body ;
parameter-list = "(" (parameter ("," parameter)* ","?)? ")" ;
parameter = NAME ":" flex-flag? type ;
return-type = "->" flex-flag? type ;
flex-flag = "[" "flex" "]" ;
function-body = block
| ";"
| RAW-MLIR-LITERAL ";" ;
function-item = "regional"? "fn" NAME generic-parameters?
parameter-list return-type? function-body ;
parameter-list = "(" (parameter ("," parameter)* ","?)? ")" ;
parameter = NAME ":" flex-flag? type ;
return-type = "->" flex-flag? type ;
flex-flag = "[" "flex" "]" ;
function-body = block
| ";"
| RAW-MLIR-LITERAL ";" ;

A block defines an ordinary function. A semicolon declares a function without a surface-language body. An opaque foreign body requires its following semicolon. regionalregional may appear immediately before fnfn; [flex][flex] may annotate parameters and return types.

Generic parameter declarations are shared by functions and records:

generic-parameters = "<"
(generic-parameter
("," generic-parameter)* ","?)?
">" ;
generic-parameter = NAME (":" path ("+" path)*)? ;
generic-parameters = "<"
(generic-parameter
("," generic-parameter)* ","?)?
">" ;
generic-parameter = NAME (":" path ("+" path)*)? ;

Each bound is a path. Bound checking belongs to type elaboration rather than the surface grammar.

Structs and enums

struct-item = "struct" capability? NAME generic-parameters?
struct-body ;
capability = "[" ("shared" | "value" | "flex" | "rigid"
| "field" | "regional") "]" ;
struct-body = named-fields | unnamed-fields | ";" ;
named-fields = "{" (named-field ("," named-field)* ","?)? "}" ;
named-field = NAME ":" field-flag? type ;
unnamed-fields = "(" (unnamed-field ("," unnamed-field)* ","?)? ")" ;
unnamed-field = field-flag? type ;
field-flag = "[" "field" "]" ;
struct-item = "struct" capability? NAME generic-parameters?
struct-body ;
capability = "[" ("shared" | "value" | "flex" | "rigid"
| "field" | "regional") "]" ;
struct-body = named-fields | unnamed-fields | ";" ;
named-fields = "{" (named-field ("," named-field)* ","?)? "}" ;
named-field = NAME ":" field-flag? type ;
unnamed-fields = "(" (unnamed-field ("," unnamed-field)* ","?)? ")" ;
unnamed-field = field-flag? type ;
field-flag = "[" "field" "]" ;

The semicolon form declares an opaque, field-less struct. Named and unnamed struct definitions do not take a semicolon after their closing delimiter.

enum-item = "enum" capability? NAME generic-parameters?
"{" (variant ("," variant)* ","?)? "}" ;
variant = NAME variant-payload? ;
variant-payload = "(" (type ("," type)* ","?)? ")" ;
enum-item = "enum" capability? NAME generic-parameters?
"{" (variant ("," variant)* ","?)? "}" ;
variant = NAME variant-payload? ;
variant-payload = "(" (type ("," type)* ","?)? ")" ;

Enum variant payloads are positional. Record capabilities and [field][field] are listed here only as syntax; their ownership meaning is introduced with records and regional mutability.

Modules and imports

module-item = "mod" NAME ";" ;
import-item = "import" path ("as" NAME)? ";" ;
module-item = "mod" NAME ";" ;
import-item = "import" path ("as" NAME)? ";" ;

Without asas, an import binds the final segment of its path. Both declarations require a semicolon.

Foreign and transform items

extern-item = "extern" STRING
( RAW-MLIR-LITERAL ";"
| "trampoline" STRING "=" path
trampoline-type-arguments? ";" ) ;
trampoline-type-arguments = "<"
(type ("," type)* ","?)?
">" ;
transform-item = "transform" RAW-MLIR-LITERAL ";" ;
extern-item = "extern" STRING
( RAW-MLIR-LITERAL ";"
| "trampoline" STRING "=" path
trampoline-type-arguments? ";" ) ;
trampoline-type-arguments = "<"
(type ("," type)* ","?)?
">" ;
transform-item = "transform" RAW-MLIR-LITERAL ";" ;

The first string after externextern names the ABI. In a trampoline item, the second string names the exported symbol and the path selects its Reussir function. The other externextern form carries opaque foreign source. A transformtransform item carries an opaque MLIR transform sequence. Every raw-literal item ends in a semicolon.

Paths and types

Paths are always relative sequences; their meaning is resolved later:

path = NAME ("::" NAME)* ;
path = NAME ("::" NAME)* ;

The primitive types recognized directly by the grammar are:

primitive-type = "i8" | "i16" | "i32" | "i64"
| "u8" | "u16" | "u32" | "u64"
| "f16" | "f32" | "f64"
| "bfloat16" | "float8"
| "bool" | "str" | "char" | "unit" ;
primitive-type = "i8" | "i16" | "i32" | "i64"
| "u8" | "u16" | "u32" | "u64"
| "f16" | "f32" | "f64"
| "bfloat16" | "float8"
| "bool" | "str" | "char" | "unit" ;

All other names form path types. Type arguments in type position are non-empty and do not take a trailing comma.

type = type-segment ("->" type)? ;
type-segment = primitive-type
| path-type
| array-type
| "(" type ("," type)* ")" ;
path-type = path ("<" type ("," type)* ">")? ;
array-type = "[" type ";" expression
("," expression)* "]" ;
type = type-segment ("->" type)? ;
type-segment = primitive-type
| path-type
| array-type
| "(" type ("," type)* ")" ;
path-type = path ("<" type ("," type)* ">")? ;
array-type = "[" type ";" expression
("," expression)* "]" ;

Arrow types associate to the right: A -> B -> CA -> B -> C means A -> (B -> C)A -> (B -> C). A parenthesized list of two or more types is valid only immediately to the left of an arrow, where it denotes multiple arguments:

fn apply(f: (i32, bool) -> i64, x: i32, flag: bool) -> i64 {
f(x, flag)
}
fn apply(f: (i32, bool) -> i64, x: i32, flag: bool) -> i64 {
f(x, flag)
}

(T)(T) is ordinary grouping. Reussir has no separate tuple type in this grammar; use unitunit for the unit type. An array type has one or more extent expressions, as in [f64; 512][f64; 512] or [i32; 5, 16, 8][i32; 5, 16, 8]; a trailing comma is not accepted in the extent list.

Expressions

The expression grammar is easiest to read in layers. The detailed atom forms appear immediately afterward.

expression = lambda-expression | assignment-expression ;
assignment-expression = logical-or-expression
("->" access-target ":="
logical-or-expression)? ;
logical-or-expression = logical-and-expression
("||" logical-and-expression)* ;
logical-and-expression = comparison-expression
("&&" comparison-expression)* ;
comparison-expression = additive-expression
(comparison-op additive-expression)? ;
additive-expression = multiplicative-expression
(("+" | "-") multiplicative-expression)* ;
multiplicative-expression = term (("*" | "/" | "%") term)* ;
comparison-op = "==" | "!=" | "<" | ">" | "<=" | ">=" ;
access-target = NAME | INT ;
expression = lambda-expression | assignment-expression ;
assignment-expression = logical-or-expression
("->" access-target ":="
logical-or-expression)? ;
logical-or-expression = logical-and-expression
("||" logical-and-expression)* ;
logical-and-expression = comparison-expression
("&&" comparison-expression)* ;
comparison-expression = additive-expression
(comparison-op additive-expression)? ;
additive-expression = multiplicative-expression
(("+" | "-") multiplicative-expression)* ;
multiplicative-expression = term (("*" | "/" | "%") term)* ;
comparison-op = "==" | "!=" | "<" | ">" | "<=" | ">=" ;
access-target = NAME | INT ;

Comparisons and assignments are non-associative. Write a < b && b < ca < b && b < c, not a < b < ca < b < c, and split multiple updates into separate block expressions.

Precedence and associativity

From tightest to loosest:

6 prefix - !; postfix calls/accesses; cast as T see notes below
5 * / % left associative
4 + - left associative
3 == != < > <= >= non-associative
2 && left associative
1 || left associative
0 lhs -> field := rhs non-associative
6 prefix - !; postfix calls/accesses; cast as T see notes below
5 * / % left associative
4 + - left associative
3 == != < > <= >= non-associative
2 && left associative
1 || left associative
0 lhs -> field := rhs non-associative

One term accepts at most one prefix operator. It then accepts either one cast or a repeating chain of call and field-access suffixes:

term = prefix? atom (cast | suffix*) ;
prefix = "-" | "!" ;
cast = "as" type ;
suffix = argument-list | access-chain ;
access-chain = ("." (NAME | INT))+ ;
argument-list = "(" (expression ("," expression)* ","?)? ")" ;
term = prefix? atom (cast | suffix*) ;
prefix = "-" | "!" ;
cast = "as" type ;
suffix = argument-list | access-chain ;
access-chain = ("." (NAME | INT))+ ;
argument-list = "(" (expression ("," expression)* ","?)? ")" ;

The postfix operation wraps the prefixed atom, so -x as f64-x as f64 means (-x) as f64(-x) as f64. A cast and a suffix chain are alternatives at this level: write (value.field) as i64(value.field) as i64 rather than value.field as i64value.field as i64. A direct path call is part of the path atom, so f(x) as i64f(x) as i64 is valid. Calls and accesses can otherwise alternate freely, as in factory()(input).resultfactory()(input).result.

Numeric accesses select unnamed fields. pair.0.1pair.0.1 is valid even though the lexer may combine 0.10.1 into one numeric token after the first dot; the parser interprets it as the two accesses .0.0 and .1.1.

Atoms, blocks, and binding

atom = literal
| path-expression
| "(" expression ")"
| block
| if-expression
| let-expression
| match-expression
| regional-expression ;
literal = INT | FLOAT | STRING | CHAR | "true" | "false" ;
block = "{" (expression (";" expression)* ";"?)? "}" ;
let-expression = "let" NAME
(":" flex-flag? type)? "=" expression ;
if-expression = "if" expression block ("else" block)? ;
regional-expression = "regional" block ;
atom = literal
| path-expression
| "(" expression ")"
| block
| if-expression
| let-expression
| match-expression
| regional-expression ;
literal = INT | FLOAT | STRING | CHAR | "true" | "false" ;
block = "{" (expression (";" expression)* ";"?)? "}" ;
let-expression = "let" NAME
(":" flex-flag? type)? "=" expression ;
if-expression = "if" expression block ("else" block)? ;
regional-expression = "regional" block ;

A block is a sequence of expressions. Semicolons separate them and may follow the final expression. letlet is itself an expression; its binding is available to the expressions that follow it in the surrounding sequence.

An ifif branch is always a block. else ifelse if is not a distinct grammar form; nest another ifif inside the elseelse block. Omitting elseelse supplies an empty unit block, so the then-branch must also have unit type.

Lambdas

lambda-expression = lambda-parameters lambda-return-type? expression ;
lambda-parameters = "||"
| "|" (lambda-parameter
("," lambda-parameter)* ","?)? "|" ;
lambda-parameter = NAME (":" type)? ;
lambda-return-type = "->" type ;
lambda-expression = lambda-parameters lambda-return-type? expression ;
lambda-parameters = "||"
| "|" (lambda-parameter
("," lambda-parameter)* ","?)? "|" ;
lambda-parameter = NAME (":" type)? ;
lambda-return-type = "->" type ;

Adjacent empty pipes are lexed as the single |||| token, but in expression-head position they introduce an empty lambda rather than logical OR. A lambda is recognized at the start of a complete expression. Parenthesize it when it must act as an operand, then apply normal suffixes:

let increment = |x: i64| -> i64 x + 1;
let answer = (|x| x * 2)(21);
1 + (|x| x)(2)
let increment = |x: i64| -> i64 x + 1;
let answer = (|x| x * 2)(21);
1 + (|x| x)(2)

Paths, calls, and constructors

path-expression = expression-path
(constructor-arguments | argument-list)? ;
expression-path = path
(expression-type-arguments ("::" path)?)? ;
expression-type-arguments = "<" type-or-hole
("," type-or-hole)* ">" ;
type-or-hole = type | "_" ;
constructor-arguments = "{"
(constructor-argument
("," constructor-argument)* ","?)?
"}" ;
constructor-argument = (NAME ":")? expression ;
path-expression = expression-path
(constructor-arguments | argument-list)? ;
expression-path = path
(expression-type-arguments ("::" path)?)? ;
expression-type-arguments = "<" type-or-hole
("," type-or-hole)* ">" ;
type-or-hole = type | "_" ;
constructor-arguments = "{"
(constructor-argument
("," constructor-argument)* ","?)?
"}" ;
constructor-argument = (NAME ":")? expression ;

Type arguments on expressions are non-empty and have no trailing comma. __ asks elaboration to infer one argument. A type-argument list can occur before a final path suffix, as in Arc<List<i32>>::ConsArc<List<i32>>::Cons.

The parser distinguishes a < ba < b from f<i32>(b)f<i32>(b) by accepting <...><...> as type arguments only when the complete list parses and closes with >>. A path with type arguments but no parentheses or braces is treated as a constructor reference.

Constructor braces accept positional expressions, named fields, or a mixture:

Point { x: 3, y: 4 }
List::Cons<i64> { 1, List::Nil<i64> }
Point { x: 3, y: 4 }
List::Cons<i64> { 1, List::Nil<i64> }

In an ifif condition or matchmatch scrutinee, an unparenthesized path {path { begins the following control-flow block rather than a constructor. Parenthesize a constructor expression there:

if (Flag { enabled: true }.enabled) { 1 } else { 0 }
if (Flag { enabled: true }.enabled) { 1 } else { 0 }

Match expressions and patterns

match-expression = "match" expression "{" match-arms? "}" ;
match-arms = match-arm ("," match-arm)* ;
match-arm = pattern "=>" expression ;
pattern = pattern-kind ("if" expression)? ;
pattern-kind = "_"
| literal
| bind-pattern
| constructor-pattern ;
bind-pattern = NAME ;
match-expression = "match" expression "{" match-arms? "}" ;
match-arms = match-arm ("," match-arm)* ;
match-arm = pattern "=>" expression ;
pattern = pattern-kind ("if" expression)? ;
pattern-kind = "_"
| literal
| bind-pattern
| constructor-pattern ;
bind-pattern = NAME ;

A match may be empty syntactically. A non-empty match does not permit a trailing comma after its final arm.

A bare, single-segment name that does not begin with an uppercase character is a binding pattern. A qualified path or a name whose first character is uppercase is a constructor pattern, even without arguments:

match option {
Option::Some(value) => value,
Option::None => 0
}
match option {
Option::Some(value) => value,
Option::None => 0
}

Constructor patterns have positional or named arguments:

constructor-pattern = path
(positional-patterns | named-patterns)? ;
positional-patterns = "(" pattern-items? ")" ;
named-patterns = "{" named-pattern-items? "}" ;
pattern-items = ".."
| pattern-kind ("," pattern-kind)*
("," ".." | ","?) ;
named-pattern-items = ".."
| named-pattern ("," named-pattern)*
("," ".." | ","?) ;
named-pattern = NAME (":" pattern-kind)? ;
constructor-pattern = path
(positional-patterns | named-patterns)? ;
positional-patterns = "(" pattern-items? ")" ;
named-patterns = "{" named-pattern-items? "}" ;
pattern-items = ".."
| pattern-kind ("," pattern-kind)*
("," ".." | ","?) ;
named-pattern-items = ".."
| named-pattern ("," named-pattern)*
("," ".." | ","?) ;
named-pattern = NAME (":" pattern-kind)? ;

.... must be last and ignores the remaining constructor fields. Without a colon, a named field is shorthand for binding a variable of the same name. Trailing commas are accepted when .... is absent. A guard belongs to the whole arm pattern, not to a nested pattern:

enum Packet {
Data(i64, i64, i64),
Empty
}
fn classify(packet: Packet) -> i64 {
match packet {
Packet::Data { first: 0, .. } => 0,
Packet::Data { first, second, .. } if first > second => 1,
Packet::Data { .. } => 2,
Packet::Empty => 3
}
}
enum Packet {
Data(i64, i64, i64),
Empty
}
fn classify(packet: Packet) -> i64 {
match packet {
Packet::Data { first: 0, .. } => 0,
Packet::Data { first, second, .. } if first > second => 1,
Packet::Data { .. } => 2,
Packet::Empty => 3
}
}

Literal patterns use the same literal tokens as literal expressions. A minus sign is a prefix operator rather than part of a numeric token, so a negative number is not one constant pattern; bind the value and test it in a guard when that distinction matters.

Delimiter and comma summary

For quick reference, trailing commas are accepted in function parameter lists, generic parameter lists, struct fields, enum variants and payloads, lambda parameters, call arguments, constructor arguments, and ordinary constructor pattern lists. Trampoline type-argument lists also accept one. A comma cannot follow a pattern’s final ..... Trailing commas are not accepted in ordinary type-argument lists, array extent lists, or after the final match arm.

Semicolons separate expressions inside a block. They are mandatory after module and import declarations, function declarations without a body, opaque struct declarations, foreign-bodied functions, extern items, and transform items. They do not follow ordinary function blocks, struct field bodies, or enum bodies.