Syntax at a Glance
This chapter is a reference map of the active Rock syntax. It separates the notation used to describe syntax from programs that can be copied into a source file.
Reading grammar notation
Grammar descriptions in this chapter use a small notation:
::=means “is defined as”.- A word written in
UPPER_CASEis a metavariable supplied by the grammar. - Text in double quotes is a literal token.
|separates alternatives.- A suffix
?means zero or one occurrence. - A suffix
*means zero or more occurrences. - A suffix
+means one or more occurrences. INDENTandDEDENTare layout tokens produced by the lexer.
These marks describe grammar productions; they are not Rock source. Every
rock fence below is a complete example with its declarations in the same
fence. No grammar production uses an omission marker.
The condensed productions reuse a few base names throughout the chapter:
IDENTIFIERis a lowercase or underscore-led source name;TYPE_NAMEis an uppercase-led type name.DIGITis one decimal digit,CHARACTERis one source character accepted by the literal lexer, andOPERATORis one or more operator characters.PATHandTYPE_PATHare::-separated identifier or type segments.EXPRESSIONmeans any expression form described in this chapter;STATEMENTis an expression, binding, assignment,return,break, orcontinuein a block.BODYis either one expression after an arrow or an indentedBLOCK;DECLARATIONis any top-level declaration production.- A name ending in
_LISTis a comma-separated sequence of the named element.PARAMETER_LISTis a comma-separated pattern list,ARGUMENT_LISTis a comma-separated expression list, andTYPE_PARAMETER_LISTis a comma-separated type-name list. MACRO_PATTERN,MACRO_TEMPLATE, andMACRO_ARGUMENTare token sequences interpreted by the declarative macro matcher; their typed captures are defined in the macro production below.
These are metavariables, not words that appear literally in Rock source.
The top-level shape is:
PROGRAM ::= TOP_LEVEL_ITEM*
TOP_LEVEL_ITEM ::= IMPORT | MODULE | EXPORT | TYPE_DECLARATION | TRAIT_DECLARATION | IMPLEMENTATION | FUNCTION_DECLARATION | EXTERN_DECLARATION | MACRO_DECLARATION | OPERATOR_DECLARATION
Layout and comments
Rock uses indentation to delimit blocks. The first indented block establishes the file’s indentation step; use that step consistently. A block starts after a declaration or control-flow header and ends when the indentation returns to the surrounding level.
BLOCK ::= INDENT STATEMENT+ DEDENT
IF_EXPRESSION ::= "if" EXPRESSION BLOCK ("else" (BLOCK | IF_EXPRESSION))?
This is a complete layout example:
main = !->
if true
"the condition is true".println!
else
"the condition is false".println!
Line comments begin with //. Block comments begin with /* and end with
*/; both forms are ignored by the parser.
main = !->
// This comment ends at the line break.
/* This comment spans one complete comment token. */
"comments do not produce values".println!
Names and keywords
Identifiers name declarations and local bindings. Type names conventionally start with an uppercase letter, while functions and bindings conventionally start with a lowercase letter. The lexer reserves the following keywords:
KEYWORD ::= "struct" | "enum" | "trait" | "impl" | "if" | "then" | "else" | "for" | "in" | "while" | "loop" | "macro" | "mod" | "extern" | "match" | "unsafe" | "infix" | "return" | "continue" | "break" | "type" | "mut" | "where" | "as" | "true" | "false"
Declaration punctuation has its own meaning:
<exports a declaration, field, module, or path.>imports a path.@,^@, and~@describe shared, mutable, and consuming receivers.%nameinvokes a macro and$namenames a macro fragment.
Literals
The literal forms are:
INTEGER_LITERAL ::= DIGIT+
FLOAT_LITERAL ::= DIGIT+ "." DIGIT+
BOOLEAN_LITERAL ::= "true" | "false"
CHAR_LITERAL ::= "'" CHARACTER "'"
STRING_LITERAL ::= "\"" CHARACTER* "\""
ARRAY_LITERAL ::= "[" EXPRESSION_LIST? "]" | "[" EXPRESSION ";" INTEGER_LITERAL "]"
TUPLE_LITERAL ::= "(" EXPRESSION "," EXPRESSION_LIST? ")"
RANGE_EXPRESSION ::= EXPRESSION? (".." | "..=") EXPRESSION?
An inclusive range written with ..= requires an end expression. Native ranges are first-class values and can be used for slicing, for example &values[1..3], &values[..count], and &values[..].
This program constructs integer, floating-point, Boolean, character, string, array, and tuple literals, with explicit annotations for their types:
main = !->
count: I64 = 42
ratio: F64 = 3.5
enabled: Bool = true
initial: Char = 'R'
greeting: &Str = "Rock"
values: [I64; 3] = [1, 2, 3]
repeated: [I64; 3] = [7; 3]
pair: (I64, &Str) = (1, "one")
count.println!
ratio.println!
enabled.println!
initial.println!
greeting.println!
values.println!
repeated.println!
pair.0.println!
pair.1.println!
Strings and characters are byte-oriented in the current implementation. Use ordinary ASCII escapes only when targeting the current parser and verify non-ASCII behavior against the target compiler.
Bindings and places
The grammar distinguishes a binding from an assignment target:
BINDING ::= "mut"? IDENTIFIER (":" TYPE)? "=" EXPRESSION
ASSIGNMENT ::= PLACE "=" EXPRESSION
PLACE ::= IDENTIFIER | PLACE "." IDENTIFIER | PLACE "[" EXPRESSION "]" | "(" PLACE "," PLACE_LIST ")"
The following complete program declares every place before assigning through it:
main = !->
mut first: I64 = 1
second: I64 = 2
first = second + 3
mut values: [I64; 2] = [first, second]
values[1] = first
first.println!
values[1].println!
mut permits mutable borrowing and mutable receiver calls. Ordinary local
reassignment is available for a binding whose inferred type does not change.
Functions and calls
Function signatures and definitions use arrows. Multiple parameter arrows are curried type notation; a definition with comma-separated parameters receives those parameters in one call expression.
FUNCTION_SIGNATURE ::= IDENTIFIER ":" TYPE
FUNCTION_DEFINITION ::= IDENTIFIER "=" PARAMETER_LIST ARROW BODY
ARROW ::= "->" | "!->" | "~>"
CALL ::= EXPRESSION ARGUMENT_LIST | EXPRESSION "!"
This program shows value-returning, unit-arrow, and zero-argument calls:
add: I64 -> I64 -> I64
add = left, right -> left + right
announce: &Str -> ()
announce = message !->
message.println!
main = !->
total: I64 = add 2, 3
announce "five"
total.println!
Arguments are separated by commas, and each argument consumes a complete
expression. A nested call owns its comma-separated arguments without extra
grouping; parentheses do not turn a call into Rust-style
function(arguments) syntax.
In an inline argument list, a bare lambda has at most one parameter; commas
separate call arguments. Group a multi-parameter lambda explicitly:
apply (left, right -> left + right).
add: I64 -> I64 -> I64
add = left, right -> left + right
double: I64 -> I64
double = value -> value * 2
main = !->
result: I64 = double add 2, 3
result.println!
The postfix ! calls a function or method with no explicit arguments. A
method uses a value receiver and a type-associated function uses ::.
Structs and enums
The declaration forms are:
STRUCT_DECLARATION ::= "struct" TYPE_NAME TYPE_PARAMETER_LIST? FIELD_DECLARATION*
FIELD_DECLARATION ::= "<"? IDENTIFIER ":" TYPE
ENUM_DECLARATION ::= "enum" TYPE_NAME TYPE_PARAMETER_LIST? VARIANT_DECLARATION+
VARIANT_DECLARATION ::= IDENTIFIER TYPE_LIST?
A complete program can combine named fields, associated construction, and variant matching:
struct Point
< x: I64
< y: I64
enum Shape
Dot Point
Empty
impl Point
new: I64 -> I64 -> Point
new = x, y ->
Point
x: x
y: y
describe: Shape -> &Str
describe = shape ->
match shape
Shape::Dot point => "dot"
Shape::Empty => "empty"
main = !->
point: Point = Point::new 3, 4
shape: Shape = Shape::Dot point
describe shape .println!
All required fields must be initialized. An enum constructor is qualified by its enclosing type so that the variant’s payload type is unambiguous.
Traits, implementations, and receivers
Trait and implementation grammar is:
TRAIT_DECLARATION ::= "trait" TYPE_NAME TYPE_PARAMETER_LIST? TRAIT_MEMBER+
TRAIT_MEMBER ::= "type" IDENTIFIER | RECEIVER? IDENTIFIER ":" TYPE
IMPLEMENTATION ::= "impl" TYPE_NAME TYPE_ARGUMENT_LIST? ("for" TYPE)? IMPLEMENTATION_MEMBER+
RECEIVER ::= "@" | "^@" | "~@"
The receiver markers express the ownership contract. This complete example uses all three receiver modes without referring to a declaration outside the fence:
struct Counter
< value: I64
impl Counter
@read: I64
@read = -> @value
^@add: I64 -> ()
^@add = amount ->
self.value = self.value + amount
return
~@finish: I64
~@finish = -> self.value
main = !->
mut counter: Counter = Counter
value: 4
counter.add 3
current: I64 = counter.read!
total: I64 = counter.finish!
current.println!
total.println!
@read observes, ^@add mutates, and ~@finish consumes. A consuming call
ends the owner’s usable lifetime, so the example does not use counter after
finish!.
Bounds follow declarations and signatures:
WHERE_CLAUSE ::= "where" BOUND ("," BOUND)*
BOUND ::= TYPE_NAME ":" TRAIT_PATH_LIST | TYPE_NAME IDENTIFIER ":" TRAIT_PATH_LIST
The following program declares a generic trait with an associated type and implements it for a concrete type:
trait Combiner T
type Output
@combine: T -> Self::Output
struct AddOne
impl Combiner I64 for AddOne
type Output = I64
@combine = value -> value + 1
main = !->
()
Combiner T declares the argument type as a trait parameter, while each
implementation chooses Output. AddOne uses I64 for both and returns its
argument plus one. This main returns unit without calling combine; see
Generics for functions with where bounds.
Type forms
Type expressions use these productions:
TYPE ::= TYPE_NAME | TYPE_APPLICATION | TYPE_LAMBDA | TYPE_PROJECTION | TYPE_REFERENCE | TYPE_POINTER | TYPE_ARRAY | TYPE_SLICE | TYPE_TUPLE | TYPE_FUNCTION | "()" | "_"
TYPE_APPLICATION ::= TYPE_NAME TYPE_ARGUMENT ("," TYPE_ARGUMENT)*
TYPE_ARGUMENT ::= TYPE | "_"
TYPE_LAMBDA ::= "\\" TYPE_NAME "->" TYPE
TYPE_PROJECTION ::= TYPE_NAME "::" TYPE_NAME
TYPE_REFERENCE ::= "&" TYPE | "&mut" TYPE
TYPE_POINTER ::= "*" TYPE
TYPE_ARRAY ::= "[" TYPE ";" INTEGER_LITERAL "]"
TYPE_SLICE ::= "[" TYPE "]"
TYPE_TUPLE ::= "(" TYPE "," TYPE_LIST? ")"
TYPE_FUNCTION ::= TYPE "->" TYPE
The following program combines a generic struct, a generic function signature, a concrete type application, and a shared reference:
struct Pair A, B
< first: A
< second: B
identity: T -> T
identity = value -> value
borrow_first: &Pair I64, &Str -> &Str
borrow_first = pair -> pair.second
main = !->
pair: Pair I64, &Str = Pair
first: 7
second: "seven"
text: &Str = borrow_first &pair
text.println!
Pair I64, &Str fixes the two field types. Despite its name, borrow_first
reads pair.second through a shared reference, so the program prints seven.
The generic identity function is declared but not called.
Other forms in the grammar include _ for an inference hole, Self::Output
for an associated type projection, *T for a raw pointer, and &mut T for a
mutable borrowed view.
Control flow and patterns
The core control-flow productions are:
CONTROL_FLOW ::= IF_EXPRESSION | WHILE_LOOP | FOR_LOOP | LOOP_EXPRESSION | MATCH_EXPRESSION
WHILE_LOOP ::= "while" EXPRESSION BLOCK
FOR_LOOP ::= "for" PATTERN "in" EXPRESSION BLOCK
LOOP_EXPRESSION ::= "loop" BLOCK
MATCH_EXPRESSION ::= "match" EXPRESSION MATCH_ARM+
MATCH_ARM ::= PATTERN ("if" EXPRESSION)? "=>" EXPRESSION
This complete program uses a while, a for, break, and continue:
main = !->
mut number: I64 = 0
while number < 3
number = number + 1
if number == 2
continue
number.println!
for item in 3..6
item.println!
loop
break
Patterns include bindings, literals, tuple shapes, constructor payloads, and wildcards:
PATTERN ::= "_" | IDENTIFIER | "mut" IDENTIFIER | LITERAL | TUPLE_PATTERN | ARRAY_PATTERN | INSTANCE_PATTERN | "&" PATTERN | "&mut" PATTERN | IDENTIFIER "@" PATTERN
TUPLE_PATTERN ::= "(" PATTERN "," PATTERN_LIST ")"
ARRAY_PATTERN ::= "[" ARRAY_PATTERN_ELEMENT_LIST? "]"
ARRAY_PATTERN_ELEMENT ::= PATTERN | ".." IDENTIFIER
INSTANCE_PATTERN ::= TYPE_PATH | TYPE_PATH PATTERN_LIST | TYPE_PATH FIELD_PATTERN_LIST
FIELD_PATTERN ::= IDENTIFIER ":" PATTERN
Here is a complete constructor-pattern example:
enum Message
Quit
Number I64
label: Message -> &Str
label = message ->
match message
Message::Quit => "quit"
Message::Number number if number > 0 => "positive"
Message::Number _ => "nonpositive"
main = !->
first: &Str = label Message::Quit
second: &Str = label Message::Number 4
first.println!
second.println!
An arm guard runs only after its structural pattern matches. The current
compiler does not diagnose every incomplete enum match, so write every variant
or finish with _ deliberately.
Modules, imports, and visibility
The module grammar is:
MODULE ::= "mod" IDENTIFIER
IMPORT ::= ">" PATH
EXPORT ::= "<" (DECLARATION | PATH)
PATH ::= IDENTIFIER ("::" IDENTIFIER)* ("::*")?
mod name loads name.rk relative to the declaring file. > brings an item
into scope and < exposes an item to consumers. The following project listing
is a complete two-file source arrangement; each file is shown in its entirety
and the imported function is declared before its use in the consumer file.
/tmp/rock-syntax-project/math.rk
< square: I64 -> I64
< square = value -> value * value
/tmp/rock-syntax-project/main.rk
mod math
> math::square
main = !->
result: I64 = square 6
result.println!
Inline module syntax is parsed by some tools but is not a supported compiled project form; use this file-backed arrangement.
Operators and postfix forms
Operators are declarations in the current program or an explicit dependency:
OPERATOR_DECLARATION ::= "infix" INTEGER_LITERAL OPERATOR
INFIX_FUNCTION ::= OPERATOR "=" PARAMETER_LIST "->" BODY
PREFIX_EXPRESSION ::= OPERATOR EXPRESSION
POSTFIX_EXPRESSION ::= EXPRESSION "?" | EXPRESSION "!"
SPACED_DOT_EXPRESSION ::= EXPRESSION " ." IDENTIFIER ARGUMENTS?
Function application has precedence 8. An operator declared above 8 is
part of the final argument; an operator at or below 8 applies to the call
result. A spaced dot has lower precedence and applies to the complete
expression on its left:
double: I64 -> I64
double = value -> value * 2
main = !->
value: I64 = 3
double value + 1 .println!
Option::Some value <&> double .unwrap_or 0 .println!
Use parentheses when an operator above application precedence must instead
operate on the call result: (double value) + 1.
An adjacent & begins a shared-borrow argument: read &value parses as
read (&value), unlike a spaced infix &. Postfix ? after the final
argument propagates the whole call, including when that argument is borrowed:
read &value? means (read (&value))?. The
error-handling chapter
contains a complete example. ? is propagation syntax, not an infix operator
whose precedence is declared by the program.
This complete program defines a pipeline operator, converts its result with an associated string constructor, and prints it with a zero-argument method call:
infix 1 |>
|> = value, function -> function value
double: I64 -> I64
double = value -> value * 2
main = !->
value: I64 = 7 |> double
text: String = String::from value
text.println!
7 |> double produces 14, String::from value creates the text 14,
and text.println! prints it. The standard library supplies common arithmetic
and comparison meanings, but the compiler does not assign a meaning to an
operator spelling by itself.
Macros
Declarative macro grammar uses fragment captures:
MACRO_DECLARATION ::= "macro" IDENTIFIER MACRO_ARM+
MACRO_ARM ::= MACRO_PATTERN "=>" MACRO_TEMPLATE
MACRO_INVOCATION ::= "%" IDENTIFIER MACRO_ARGUMENT*
MACRO_CAPTURE ::= "$" IDENTIFIER ":" FRAGMENT_KIND
FRAGMENT_KIND ::= "ident" | "expr" | "ty"
This complete macro program captures an identifier and emits a function before
main calls it:
macro make_value
$name:ident =>
$name = -> 42
%make_value answer
main = !->
value: I64 = answer!
value.println!
Macro facilities are experimental. Keep macro definitions small, and test the expanded program through the normal compiler rather than treating expansion text as a stable interchange format.