nbcl 0.4.4

Configuration language designed to be easy and understandable.
Documentation
// === 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.
snake_ident = @{
    !(keyword ~ !ASCII_ALPHANUMERIC)
    ~ (
        ASCII_ALPHA_LOWER ~ (ASCII_ALPHANUMERIC | "_")*
      | ASCII_ALPHA_UPPER ~ (ASCII_ALPHA_UPPER | ASCII_DIGIT | "_")+
    )
    ~ !(ASCII_ALPHANUMERIC | "_")
}
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 = @{ (!"\"" ~ !"\\" ~ ANY | escape_seq)* }
single_quoted_inner = @{ (!"'"  ~ !"\\" ~ ANY | escape_seq)* }

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 ~ "?"?
}

// === 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 { }
}