WHITESPACE = _{ " " | "\t" | NEWLINE }
program = { SOI ~ expr ~ EOI }
expr = { prefix* ~ primary ~ (infix ~ prefix* ~ primary )* }
infix = _{ and | or }
and = { "&&" | ^"and" }
or = { "||" | ^"or" }
prefix = _{ neg }
neg = { "!" | "\\!" | ^"not" }
primary = _{ predicate | single_word | "(" ~ expr ~ ")" }
predicate = { selector ~ operator ~ value }
selector = @{ (ASCII_ALPHANUMERIC | "." | "_" | "-" | "/" | "[" | "]" | "*" | ":")+ }
// Parse operators flexibly - validate in typechecker
// Start with symbols or letters, but not mix arbitrarily
operator = @{
// Symbol-based operators (can combine symbols)
("=" | "!" | ">" | "<" | "~")+ |
// Word-based operators (alphanumeric with underscores)
// But NOT the reserved infix/prefix operators
!(^"and" | ^"or" | ^"not") ~ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")*
}
value = { value_content ~ trailing_quote? }
value_content = _{ quoted_string | unterminated_string | raw_token }
// Error detection: unterminated string literals
// Matches opening quote without closing quote before EOI/newline
unterminated_string = @{
("\"" ~ (!"\"" ~ (escaped | ANY))* ~ (EOI | &NEWLINE)) |
("'" ~ (!"'" ~ (escaped | ANY))* ~ (EOI | &NEWLINE))
}
// Error detection: trailing quote after value content
trailing_quote = @{ "\"" | "'" }
// Raw tokens with recursive balanced delimiter matching
// Supports nested structures like ((pub|async)\s+)* or [[a-z]]
raw_token = @{ raw_char+ }
raw_char = _{
"\\" ~ ANY | // Escaped character
balanced_paren | // Recursive paren matching
balanced_bracket | // Recursive bracket matching
balanced_curly | // Recursive curly matching
!WHITESPACE ~ !"&&" ~ !"||" ~ !")" ~ !"\"" ~ !"'" ~ ANY // Regular character
}
balanced_paren = { "(" ~ ( "\\" ~ ANY | balanced_paren | balanced_bracket | balanced_curly | !")" ~ ANY )* ~ ")" }
balanced_bracket = { "[" ~ ( "\\" ~ ANY | balanced_paren | balanced_bracket | balanced_curly | !"]" ~ ANY )* ~ "]" }
balanced_curly = { "{" ~ ( "\\" ~ ANY | balanced_paren | balanced_bracket | balanced_curly | !"}" ~ ANY )* ~ "}" }
quoted_string = ${ "\"" ~ inner_double ~ "\"" | "'" ~ inner_single ~ "'" }
inner_double = @{ (!"\"" ~ (escaped | ANY))* }
inner_single = @{ (!"'" ~ (escaped | ANY))* }
escaped = { "\\" ~ ("\"" | "'" | "\\" | "n" | "t" | "r") }
single_word = @{
// Structured data selectors: word:.path (validated at typecheck)
(ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* ~ ":" ~ (ASCII_ALPHANUMERIC | "." | "_" | "-" | "/" | "[" | "]" | "*")+ |
// Regular word aliases
(ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")*
}
// Separate entry point for parsing set contents
// Used by typechecker when operator is 'in'
set_contents = { SOI ~ set_items? ~ EOI }
set_items = { set_item? ~ ("," ~ set_item?)* }
set_item = { quoted_string | bare_set_item }
// Bare items in sets: stop at comma, whitespace, or quotes
bare_set_item = @{ (!("," | "\"" | "'" | WHITESPACE) ~ ANY)+ }