// === Whitespace & Comments ===
WHITESPACE = _{ " " | "\t" | "\r" | "\n" }
COMMENT = _{
block_comment
| line_comment
}
line_comment = _{ "#" ~ (!"\n" ~ ANY)* }
block_comment = _{ "#-" ~ (!"-#" ~ ANY)* ~ "-#" }
// === Entry point ===
file = {
SOI
~ top_level_item*
~ EOI
}
top_level_item = {
import_stmt
| import_lib_stmt
| component_def
| fn_def
| node_invocation // e.g. Window "name" { ... } at the top level
| stmt
}
// === Import ===
as_kw = { "as" }
import_layout_block = { "{" ~ (layout_list | import_all_wildcard) ~ "}" }
layout_list = { pascal_ident ~ ("," ~ pascal_ident)* ~ ","? }
import_all_wildcard = { "*" }
import_stmt = {
"import"
~ !ASCII_ALPHANUMERIC
~ string_lit
~ as_kw
~ snake_ident
~ import_layout_block?
}
import_lib_stmt = { "import" ~ snake_ident ~ dot ~ snake_ident }
// === Identifiers ===
//
// snake_case: variables, function names, prop keys
// PascalCase: node type names (Rust-registered)
// Reserved keywords cannot be used as identifiers
keyword = _{
"any" | "set" | "let" | "const" | "fn" | "for" | "in" | "while" | "if"
| "else" | "match" | "return" | "import" | "true" | "false" | "null"
}
// snake_ident must not be a reserved keyword
// The !keyword prevents "let" from parsing as an ident even though
// it matches the character pattern.
keyword_boundary = _{ !(ASCII_ALPHANUMERIC | "_") }
snake_ident = @{
!(keyword ~ keyword_boundary)
~ (
ASCII_ALPHA_LOWER ~ (ASCII_ALPHANUMERIC | "_")*
| ASCII_ALPHA_UPPER ~ (ASCII_ALPHA_UPPER | ASCII_DIGIT | "_")+
)
~ keyword_boundary
}
pascal_ident = @{
ASCII_ALPHA_UPPER
~ ASCII_ALPHA_LOWER ~ (ASCII_ALPHANUMERIC | "_")*
}
// A prop key is always snake_case
prop_key = { snake_ident }
// === Literals ===
literal = {
float_lit
| int_lit
| bool_lit
| null_lit
| string_lit
| list_lit
| map_lit
}
// numbers
int_lit = @{ "-"? ~ ASCII_DIGIT+ }
float_lit = @{ "-"? ~ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ }
// boolean
bool_lit = { "true" | "false" }
// null
null_lit = { "null" }
// string
// "hello" or 'hello'
string_lit = ${
"\"" ~ double_quoted_inner ~ "\""
| "'" ~ single_quoted_inner ~ "'"
}
double_quoted_inner = @{ (escape_seq | !"\"" ~ ANY)* }
single_quoted_inner = @{ (escape_seq | !"'" ~ ANY)* }
escape_seq = @{ "\\" ~ ("n" | "t" | "r" | "\\" | "\"" | "'" | "u" ~ ASCII_HEX_DIGIT{4}) }
// lists
list_lit = { "[" ~ (expr ~ ("," ~ expr)*)? ~ ","? ~ "]" }
// map literals (key = value inside a prop value position)
map_lit = { "{" ~ map_entry* ~ "}" }
map_entry = { prop_key ~ equal ~ expr }
// === Ranges (used in for loops) ===
range_expr = { expr ~ range_op ~ expr }
range_op = { "..=" | ".." }
// === Expressions ===
expr = { or_expr }
or_expr = { and_expr ~ (or_op ~ and_expr)* }
or_op = { "||" }
and_expr = { cmp_expr ~ (and_op ~ cmp_expr)* }
and_op = { "&&" }
cmp_expr = { add_expr ~ (cmp_op ~ add_expr)? }
add_expr = { mul_expr ~ (add_op ~ mul_expr)* }
add_op = { "+" | "-" }
mul_expr = { unary_expr ~ (mul_op ~ unary_expr)* }
mul_op = { "*" | "/" | "%" }
cmp_op = { "==" | "!=" | "<=" | ">=" | "<" | ">" }
unary_expr = {
(not_expr | neg_expr) ~ unary_expr
| postfix_expr
}
not_expr = { "!" }
neg_expr = { "-" }
postfix_expr = {
primary_expr
~ (
accessor ~ snake_ident // field access: foo.bar or foo?.bar
| "[" ~ expr ~ "]" // index: foo[0]
| call_args // call: foo(a, b)
)*
}
accessor = {
safe_dot | dot
}
dot = { "." }
safe_dot = { "?." }
call_args = { "(" ~ (expr ~ ("," ~ expr)*)? ~ ")" }
primary_expr = {
literal
| lambda_expr
| if_expr
| match_expr
| "(" ~ expr ~ ")"
| snake_ident // variable reference or function name
}
// === Lambda ===
// |x, y| expr single expression
// |x, y| { stmts } block body
lambda_expr = {
"|" ~ (lambda_param ~ ("," ~ lambda_param)*)? ~ "|"
~ lambda_body
}
lambda_param = { snake_ident }
lambda_body = {
fn_body // { stmt* }
| expr // implicit return
}
block_body = { "{" ~ (stmt | node_invocation)* ~ expr? ~ "}" }
// === Statements ===
stmt = {
assign_stmt
| let_stmt
| const_stmt
| for_stmt
| while_stmt
| return_stmt
| expr_stmt
}
// Statements allowed inside a Node Block
node_stmt = {
assign_stmt
| let_stmt
| for_stmt
| while_stmt
| if_expr
| expr_stmt
}
// Assign, let, and consts statements
assignable_lhs = {
primary_expr
~ (
accessor ~ snake_ident // field access
| "[" ~ expr ~ "]" // indexing
)*
}
equal = { "=" }
plus_equal = { "+=" }
min_equal = { "-=" }
mult_equal = { "*=" }
div_equal = { "/=" }
assignment_op = { plus_equal | min_equal | mult_equal | div_equal | equal }
assign_stmt = { "set" ~ assignable_lhs ~ assignment_op ~ expr }
let_stmt = { "let" ~ snake_ident ~ "=" ~ expr }
const_stmt = { "const" ~ snake_ident ~ "=" ~ expr }
// for x in range/list { stmts }
in_kw = { "in" }
for_stmt = {
"for" ~ for_pattern ~ in_kw ~ (range_expr | expr) ~ block_body
}
for_pattern = {
"(" ~ snake_ident ~ ("," ~ snake_ident)+ ~ ")" // destructure: (k, v)
| snake_ident
}
// while cond { stmts }
while_stmt = { "while" ~ expr ~ block_body }
// return expr
return_stmt = { "return" ~ expr? }
// bare expression used as statement (e.g. a function call)
expr_stmt = { expr }
if_expr = {
"if" ~ expr ~ block_body
~ else_if_branch*
~ else_branch?
}
else_if_branch = { "else" ~ "if" ~ expr ~ block_body }
else_branch = { "else" ~ block_body }
match_expr = {
"match" ~ expr ~ "{"
~ match_arm+
~ "}"
}
match_arm = {
match_pattern ~ "=>" ~ (block_body | expr ~ ","?)
}
match_pattern = {
match_underscore // wildcard
| literal
| snake_ident
}
match_underscore = { "_" }
// === Function definitions ===
fn_def = {
"fn" ~ snake_ident
~ "(" ~ (fn_param ~ ("," ~ fn_param)*)? ~ ")"
~ fn_body
}
fn_param = {
snake_ident
}
// Function body can contain statements AND node invocations
// (a fn can build and return a tree of nodes)
fn_body = { "{" ~ fn_item* ~ "}" }
fn_item = {
node_invocation // emit a node into the output tree
| stmt
}
// === Component Definition ===
component_def = {
"component" ~ pascal_ident ~ component_params ~ node_block
}
component_params = {
"(" ~ (any_params | named_params)? ~ ")"
}
// Case: (any: props)
any_params = {
"any" ~ ":" ~ snake_ident
}
// Case: (bg_color, width?)
named_params = {
param_item ~ ("," ~ param_item)*
}
param_item = {
snake_ident ~ optional_key?
}
optional_key = { "?" }
// === NODES ===
//
// Syntax: TypeName "optional_id" { node_body }
// TypeName { node_body }
//
// node_body contains:
// - prop assignments: key = value
// - child nodes: TypeName { ... }
// - control flow: for / if (bodies are node bodies)
node_invocation = {
pascal_ident
~ (id_expression)? // optional ID: Window "main" { ... }
~ node_block
}
id_expression = {
!node_block ~ (string_lit | postfix_expr | snake_ident)
}
node_block = { "{" ~ node_item* ~ "}" }
node_item = {
node_prop // key = value
| node_stmt // let, for, while, etc.
| node_invocation // child node
}
// prop = value (value is an expression)
node_prop = { prop_key ~ "=" ~ prop_value }
prop_value = {
expr // literals, variables, fn calls, lambdas, map { }
}