Statement Grammar
Statements form the imperative core of Grove declaration bodies. While Grove is a declarative language at the top level, the bodies of actions, functions, workflows, activities, validates, and tests contain ordered sequences of statements that execute step by step.
Prerequisites: Lexical Grammar, Expression Grammar. What you'll learn: The EBNF grammar and semantics for every statement form in Grove.
Statement Overview
Statement = LetStmt
| IfStmt
| ForStmt
| ReturnStmt
| EmitStmt
| FailStmt
| PublishStmt
| ExprStmt .
Statements do not require semicolons. Newlines serve as implicit statement separators.
Let Statement
LetStmt = "let" [ "mut" ] Identifier [ ":" TypeExpr ] "=" Expression .
Declares a local variable. Variables are immutable by default; use mut for mutable bindings.
let order_id = Id.generate()
let total: Decimal = 0.0
let mut count = 0
Type Inference
When the type annotation is omitted, the type is inferred from the right-hand expression:
let name = "Grove" // inferred as String
let items = [1, 2, 3] // inferred as List<Int>
let active = true // inferred as Bool
Mutability
Mutable variables can be reassigned using let with the same name (shadowing) or via direct assignment in for loop contexts:
let mut total = 0.0
// ... later in a for loop:
total = total + item.price
Only variables declared with mut can appear as assignment targets.
If Statement
IfStmt = "if" Expression "{" { Statement } "}" { ElseIf } [ Else ] .
ElseIf = "else" "if" Expression "{" { Statement } "}" .
Else = "else" "{" { Statement } "}" .
The condition expression must evaluate to Bool. Braces are always required; there is no braceless single-statement form.
if order.total > 1000 {
emit HighValueOrder { order_id: order.id }
} else if order.total > 500 {
emit MediumValueOrder { order_id: order.id }
} else {
emit StandardOrder { order_id: order.id }
}
Null-Narrowing with If
When an if condition uses is not null, the variable is narrowed to its non-optional type within the if body:
let email = customer.email // type: String?
if email is not null {
// email is String here, not String?
send_notification(email)
}
For Statement
ForStmt = "for" Identifier "in" Expression "{" { Statement } "}" .
Iterates over a List<T>. The loop variable is immutable and scoped to the loop body.
for item in order.items {
let line_total = item.price * item.quantity
emit LineItemProcessed { product_id: item.product_id, total: line_total }
}
For Loop Restrictions
For loops are subject to semantic constraints that preserve determinism:
- No nested
emitinforloops withinvalidateblocks. - Mutable variables declared outside the loop may be modified inside the loop body.
returninside aforloop exits the enclosing declaration, not just the loop.- There is no
breakorcontinuestatement in Grove.
See Semantic Constraints for the full list of restrictions.
Return Statement
ReturnStmt = "return" [ Expression ] .
Exits the current declaration body with an optional value. The return type must match the declaration's return type annotation.
function double(n: Int) -> Int {
return n * 2
}
action delete_order(order_id: Id) {
emit OrderDeleted { order_id: order_id }
return
}
- In declarations with a
-> TypeExprreturn type, thereturnmust include an expression of that type. - In declarations without a return type,
return(without an expression) may be used for early exit. - The last expression in a block is not implicitly returned; an explicit
returnis always required.
Emit Statement
EmitStmt = "emit" EventName "{" [ FieldInit { "," FieldInit } [ "," ] ] "}" .
Emits an event, appending it to the event stream. The event is validated (if a validate block exists) before being persisted.
emit OrderCreated {
customer_id: customer_id,
items: items,
total: calculate_total(items),
}
Emit Restrictions
emitis permitted only inaction,workflow, andtestbodies.emitis not permitted infunction,query, orvalidatebodies.emitis not permitted insideactivitybodies (activities interact with external systems; events must be emitted by the calling workflow).
See Semantic Constraints for the full list of restrictions.
Fail Statement
FailStmt = "fail" Expression .
Aborts the current operation with an error. The expression must evaluate to String. In action and workflow contexts, a fail rolls back any events emitted in the current transaction.
if order.total <= 0 {
fail "Order total must be positive"
}
if not customer.is_active {
fail "Cannot create order for inactive customer: ${customer.id}"
}
In validate bodies, fail is the primary mechanism for rejecting invalid events:
validate OrderCreated {
if items.is_empty() {
fail "Order must have at least one item"
}
}
Publish Statement
PublishStmt = "publish" TopicName "{" [ FieldInit { "," FieldInit } [ "," ] ] "}" .
TopicName = Identifier .
Publishes a message to an external topic or message bus. This is used for cross-module or cross-system communication.
publish order_events {
type: "order.created",
order_id: order_id,
total: total,
}
publishis permitted inactionandworkflowbodies.- Published messages are delivered after the transaction commits (at-least-once semantics).
Expression Statements
ExprStmt = Expression .
An expression may appear as a statement when it has a side effect, typically a function or method call:
logger.info("Processing order ${order_id}")
notify_customer(customer_id)
The result of an expression statement is discarded.
Statement Blocks
Several declaration forms and control-flow constructs use statement blocks:
Block = "{" { Statement } "}" .
Variables declared within a block are scoped to that block:
if condition {
let x = 42 // x is scoped to this block
}
// x is not accessible here
See Also
- Expression Grammar -- expressions used within statements
- Declaration Grammar -- the declaration contexts that contain statements
- Semantic Constraints -- restrictions on where specific statements may appear
- Lexical Grammar -- token-level syntax