// Jetro v2 grammar

WHITESPACE = _{ " " | "\t" | "\n" | "\r" }

// ── Character classes ─────────────────────────────────────────────────────────
ident_char = _{ ASCII_ALPHANUMERIC | "_" }
field_char = _{ ASCII_ALPHANUMERIC | "_" | "-" }

// ── Atomic keyword rules ──────────────────────────────────────────────────────
kw_and    = @{ "and"    ~ !ident_char }
kw_or     = @{ "or"     ~ !ident_char }
kw_not    = @{ "not"    ~ !ident_char }
kw_for    = @{ "for"    ~ !ident_char }
kw_in     = @{ "in"     ~ !ident_char }
kw_if     = @{ "if"     ~ !ident_char }
kw_else   = @{ "else"   ~ !ident_char }
kw_let    = @{ "let"    ~ !ident_char }
kw_lambda = @{ "lambda" ~ !ident_char }
kw_kind   = @{ "kind"   ~ !ident_char }
kw_is     = @{ "is"     ~ !ident_char }
kw_as     = @{ "as"     ~ !ident_char }
kw_when   = @{ "when"   ~ !ident_char }
kw_patch  = @{ "patch"  ~ !ident_char }
kw_delete = @{ "DELETE" ~ !ident_char }
kw_try    = @{ "try"    ~ !ident_char }
kw_has    = @{ "has"    ~ !ident_char }
kw_match  = @{ "match"  ~ !ident_char }
kw_with   = @{ "with"   ~ !ident_char }

reserved = _{
    ("true" | "false" | "null" | "and" | "or" | "not"
     | "for" | "in" | "if" | "else" | "let" | "lambda" | "kind"
     | "is" | "as" | "when" | "patch" | "DELETE" | "try" | "has"
     | "match" | "with") ~ !ident_char
}

ident      = @{ !reserved ~ (ASCII_ALPHA | "_") ~ ident_char* }
// `loose_ident` allows reserved keywords in *key* positions (object
// literal keys, object-pattern field names) where the trailing `:` or
// `?` disambiguates from operator usage. Lets `{kind: "click"}` and
// pattern arms like `{kind: "click", ...}` parse — `kind` is reserved
// only because of the `is kind` operator, not because it's an unsafe
// identifier in key position.
loose_ident = @{ (ASCII_ALPHA | "_") ~ ident_char* }
field_name = @{ (ASCII_ALPHA | "_") ~ field_char* }

// ── Literals ──────────────────────────────────────────────────────────────────
lit_null      = @{ "null"  ~ !ident_char }
lit_true      = @{ "true"  ~ !ident_char }
lit_false     = @{ "false" ~ !ident_char }
lit_float     = @{ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ }
lit_int       = @{ ASCII_DIGIT+ }
// Backslash escapes are recognised greedily so embedded `\"` does not
// terminate a double-quoted literal. The semantic layer (`unescape_str_lit`)
// decodes the actual character; the grammar only needs to cover the bracket.
lit_str_dq    = @{ "\"" ~ (("\\" ~ ANY) | (!"\"" ~ ANY))* ~ "\"" }
lit_str_sq    = @{ "'"  ~ (("\\" ~ ANY) | (!"'"  ~ ANY))* ~ "'" }
lit_str       = { lit_str_dq | lit_str_sq }
// f-strings captured raw; inner expressions parsed in Rust
lit_fstring   = @{ "f\"" ~ (!"\"" ~ ANY)* ~ "\"" }
literal       = { lit_null | lit_true | lit_false | lit_float | lit_int | lit_fstring | lit_str }

root    = { "$" }
current = { "@" }

// ── Kind types (only valid in kind_expr position) ─────────────────────────────
kind_type = @{ ("number" | "string" | "array" | "object" | "bool" | "null") ~ !ident_char }

// ── Cast types (only valid after `as`) — superset of kind_type incl int/float
cast_type = @{ ("int" | "float" | "number" | "string" | "array" | "object" | "bool" | "null") ~ !ident_char }

// ── Operators ─────────────────────────────────────────────────────────────────
cmp_op = { "~=" | "==" | "!=" | ">=" | "<=" | ">" | "<" }
add_op = { "+" | "-" }
mul_op = { "*" | "/" | "%" }

// ── Slice / Index ─────────────────────────────────────────────────────────────
// `idx_val` matches an integer (with optional sign). `slice_step` adds a
// Python-style step (`[start:end:step]` or any subset). `wildcard` is the
// `[*]` form — sugar for "every element of the array, kept as-is in read
// context".
//
// IMPORTANT: longer alternatives must come first (PEG rules are ordered):
// `slice_step` before `slice_full` before `slice_from` etc., otherwise the
// shorter rule consumes the input and the optional `:step` part never
// parses.
idx_val       = @{ "-"? ~ ASCII_DIGIT+ }
slice_step    = { idx_val? ~ ":" ~ idx_val? ~ ":" ~ idx_val? }
slice_full    = { idx_val ~ ":" ~ idx_val }
slice_from    = { idx_val ~ ":" }
slice_to      = { ":" ~ idx_val }
wildcard_filter = { "*" ~ kw_if ~ expr }
wildcard      = { "*" }
idx_only      = { idx_val }
bracket_inner = { slice_step | slice_full | slice_from | slice_to | wildcard_filter | wildcard | idx_only | expr }

// ── Arguments ─────────────────────────────────────────────────────────────────
named_arg = { ident ~ ":" ~ expr }
pos_arg   = { expr }
arg       = { named_arg | pos_arg }
arg_list  = { arg ~ ("," ~ arg)* }

// ── Postfix operations ────────────────────────────────────────────────────────
// Optional (null-propagation / first-of-array) is now a POSTFIX `?` after
// the step it guards — `.field?`, `.method()?`, `..field?` — not a prefix
// `?.field`.  The prefix form is no longer accepted.
deep_method    = { ".." ~ field_name ~ "(" ~ arg_list? ~ ")" }
descendant     = { ".." ~ field_name? }
method_call    = { "." ~ field_name ~ "(" ~ arg_list? ~ ")" }
dyn_field      = { ".{" ~ expr ~ "}" }
map_into_shape = { "[" ~ "*" ~ (kw_if ~ expr)? ~ "]" ~ "=>" ~ expr }
field_access   = { "." ~ field_name }
index_access   = { "[" ~ bracket_inner ~ "]" }
inline_filter  = { "{" ~ expr ~ "}" }
// Quantifier:
//   `?`  → first-of-array / null-propagate (postfix optional marker)
//   `!`  → exactly-one (error if 0 or >1)
// Allow `.` / `?` to follow so postfix `?` can be chained with more steps
// (e.g. `..services?.name`, `.a?.b?`).  Still reject `?|` to avoid
// conflict with pipe, and `??` which is coalesce.
quantifier     = { ("?" ~ !("|" | "?")) | ("!" ~ !"=") }
postfix = { deep_match | deep_method | descendant | method_call | dyn_field | map_into_shape | field_access | index_access | inline_filter | quantifier }

// ── Expression hierarchy ──────────────────────────────────────────────────────
expr = { cond_expr }

// Python-style ternary: `a if cond else b` — right-associative, lower precedence
// than pipeline. When `if` is present it MUST have `else`; Pest's `?` backtracks
// on partial match so plain `pipe_expr` still matches.
//
// `try BODY else DEFAULT` shares precedence with the ternary.  BODY is either
// a parenthesised arbitrary expression (for cases like a ternary inside the
// body) or a bare `pipe_expr`.  DEFAULT is a `cond_expr` so chained
// `try a else try b else c` parses right-associative.
try_body  = { ("(" ~ expr ~ ")") | pipe_expr }
try_expr  = { kw_try ~ try_body ~ kw_else ~ cond_expr }
cond_expr = { try_expr | (pipe_expr ~ (kw_if ~ pipe_expr ~ kw_else ~ cond_expr)?) }

// Pipeline: base followed by | steps and -> bind steps
pipe_step         = { pipe_bind | pipe_forward }
pipe_method_call  = { ident ~ "(" ~ arg_list? ~ ")" }
pipe_forward      = { ("|>" | ("|" ~ !"|")) ~ (pipe_method_call | coalesce_expr) }
pipe_bind    = { "->" ~ bind_target }
bind_target  = { bind_obj | bind_arr | ident }
bind_rest    = { "..." ~ ident }
bind_obj     = { "{" ~ ident ~ ("," ~ ident)* ~ ("," ~ bind_rest)? ~ ","? ~ "}" }
bind_arr     = { "[" ~ ident ~ ("," ~ ident)* ~ "]" }
pipe_expr    = { coalesce_expr ~ pipe_step* }

// Null-coalescing: lower precedence than logical, higher than pipe
coalesce_expr = { or_expr ~ (("??" | "?|") ~ or_expr)* }

or_expr   = { and_expr  ~ (kw_or  ~ and_expr)* }
and_expr  = { not_expr  ~ (kw_and ~ not_expr)* }
not_expr  = { kw_not ~ not_expr | kind_expr }
kind_expr     = { contains_expr ~ ((kw_kind | kw_is) ~ kw_not? ~ kind_type)? }
// `lhs has rhs` is syntactic sugar for a membership / containment
// check: `arr has x` ⇔ `arr.includes(x)`.  Works on arrays (element
// equality), objects (key existence), and strings (substring).
//
// (The `in` keyword would be ambiguous with `let X = Y in Z` and
//  `for x in xs`, so membership uses `has` only — or the dot-method
//  forms `.includes(x)` / `.contains(x)` / `.exists(pred)`.)
contains_expr = { cmp_expr ~ (kw_has ~ cmp_expr)? }
cmp_expr      = { add_expr ~ (cmp_op ~ add_expr)? }
add_expr  = { mul_expr ~ (add_op ~ mul_expr)* }
mul_expr  = { cast_expr ~ (mul_op ~ cast_expr)* }
// `as` cast — tighter than arithmetic, looser than postfix
cast_expr = { unary_expr ~ (kw_as ~ cast_type)* }

unary_neg  = { "-" }
unary_expr = { unary_neg ~ unary_expr | postfix_expr }

postfix_expr = { primary ~ postfix* }

// ── Comprehension variables ────────────────────────────────────────────────────
// Bare `a` (single var), `a, b` (pair destructure), or `[a, b]`
// (explicit array-pattern destructure). The pattern form is sugar for the
// 2-var form and is bound the same way at runtime.
comp_vars = { ("[" ~ ident ~ ("," ~ ident)+ ~ "]") | (ident ~ ("," ~ ident)?) }

// ── Comprehensions ─────────────────────────────────────────────────────────────
// Multiple `if` clauses are conjoined with logical `and` at parse time so the
// runtime sees a single combined predicate.
list_comp = { "[" ~ expr ~ kw_for ~ comp_vars ~ kw_in ~ expr ~ (kw_if ~ expr)* ~ "]" }
dict_comp = { "{" ~ expr ~ ":" ~ expr ~ kw_for ~ comp_vars ~ kw_in ~ expr ~ (kw_if ~ expr)* ~ "}" }
set_comp  = { "{" ~ expr ~ kw_for ~ comp_vars ~ kw_in ~ expr ~ (kw_if ~ expr)* ~ "}" }
gen_comp  = { "(" ~ expr ~ kw_for ~ comp_vars ~ kw_in ~ expr ~ (kw_if ~ expr)* ~ ")" }

// ── Lambda ────────────────────────────────────────────────────────────────────
// Each parameter is either a bare identifier or an `[a, b, ...]` array-pattern
// destructure. Array patterns are sugar: `([k, v]) => body` desugars to
// `__lampat_N => let k = __lampat_N[0] in let v = __lampat_N[1] in body`.
// This lets `pairwise().map(([a, b]) => b - a)` and similar idioms work
// without any runtime support for destructuring beyond array indexing.
// Optional trailing `...rest` captures the remaining tail of the array
// past the fixed prefix, mirroring `[a, ...rest]` patterns inside `match`.
lambda_array_rest = { "..." ~ ident }
lambda_array_pat  = { "[" ~ ident ~ ("," ~ ident)* ~ ("," ~ lambda_array_rest)? ~ "]" }
lambda_param     = { lambda_array_pat | ident }
lambda_params    = { lambda_param ~ ("," ~ lambda_param)* }
lambda_expr      = { kw_lambda ~ lambda_params ~ ":" ~ expr }

// Arrow-form lambda: `x => body` or `(x, y) => body` or `([a, b]) => body`.
arrow_params  = { "(" ~ (lambda_param ~ ("," ~ lambda_param)*)? ~ ")" | lambda_param }
arrow_lambda  = { arrow_params ~ "=>" ~ expr }

// ── Let ───────────────────────────────────────────────────────────────────────
// Multi-binding: `let a=x, b=y in body` desugars to nested Let in parser.
// Tuple binding is parser sugar: `let (a, b) = expr in body` lowers to one
// synthetic binding plus indexed scalar lets; no runtime tuple-binding opcode.
let_tuple_target = { "(" ~ ident ~ ("," ~ ident)+ ~ ","? ~ ")" }
let_target  = { let_tuple_target | ident }
let_binding = { let_target ~ "=" ~ expr }
let_expr    = { kw_let ~ let_binding ~ ("," ~ let_binding)* ~ kw_in ~ expr }

// ── Object construction ────────────────────────────────────────────────────────
obj_key_expr     = { loose_ident | lit_str }
obj_field_dyn    = { "[" ~ expr ~ "]" ~ ":" ~ expr }
obj_field_opt_v  = { obj_key_expr ~ "?" ~ ":" ~ expr }
obj_field_opt    = { obj_key_expr ~ "?" }
obj_field_spread_deep = { "...**" ~ expr }
// `...*expr` is a shallow spread synonym used by object rest patterns
// for symmetry with the `{...*rest}` capture form. Same semantics as
// `...expr`. Listed before `obj_field_spread` so it wins the leading
// `...*` prefix without backtracking.
obj_field_spread_star = { "...*" ~ expr }
obj_field_spread      = { "..." ~ expr }
obj_field_kv          = { obj_key_expr ~ ":" ~ expr ~ (kw_when ~ expr)? }
obj_field_short       = { ident }
obj_field             = { obj_field_dyn | obj_field_opt_v | obj_field_opt | obj_field_spread_deep | obj_field_spread_star | obj_field_spread | obj_field_kv | obj_field_short }
obj_construct    = { "{" ~ (obj_field ~ ("," ~ obj_field)* ~ ","?)? ~ "}" }

// ── Array construction ─────────────────────────────────────────────────────────
arr_spread    = { "..." ~ expr }
arr_elem      = { arr_spread | expr }
arr_construct = { "[" ~ (arr_elem ~ ("," ~ arr_elem)* ~ ","?)? ~ "]" }

// ── Global function calls ─────────────────────────────────────────────────────
global_call = { ident ~ "(" ~ arg_list? ~ ")" }

// ── Match expression ──────────────────────────────────────────────────────────
// Pattern grammar.  Object/array patterns are recognised inside arm heads only,
// so `{k: pat}` does not collide with `{k: expr}` object literals (those parse
// as `obj_construct` in expression position).
//
// Pattern precedence: `pat_or` > `pat_atom`.  Or-patterns at the top of an
// arm head; everything else is atom or object/array.
pat_lit_null  = @{ "null"  ~ !ident_char }
pat_lit_true  = @{ "true"  ~ !ident_char }
pat_lit_false = @{ "false" ~ !ident_char }
pat_lit_float = @{ "-"? ~ ASCII_DIGIT+ ~ "." ~ ASCII_DIGIT+ }
pat_lit_int   = @{ "-"? ~ ASCII_DIGIT+ }
pat_lit_str   = { lit_str_dq | lit_str_sq }
pat_literal   = { pat_lit_null | pat_lit_true | pat_lit_false | pat_lit_float | pat_lit_int | pat_lit_str }

// Numeric range patterns. `1..10` is half-open `[1, 10)`; `1..=10` is
// closed `[1, 10]`. Bounds are integer or float literals; mixing the two
// is allowed (compared as `f64` at runtime). Listed before `pat_literal`
// in `pat_atom` so the parser does not commit to the leading numeric
// literal before checking for a range operator.
pat_range_num    = @{ "-"? ~ ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT+)? }
pat_range_op     = @{ "..=" | ".." }
pat_range        = { pat_range_num ~ pat_range_op ~ pat_range_num }

pat_wild      = @{ "_" ~ !ident_char }
pat_kind_bind = { ident ~ ":" ~ kind_type }
pat_kind_only = { kind_type }
pat_bind      = { ident }

pat_obj_rest_named = { "...*" ~ ident }
pat_obj_rest_anon  = { "...*" }
pat_obj_rest       = { pat_obj_rest_named | pat_obj_rest_anon }
// `{id, name}` is shorthand for `{id: id, name: name}` — binds each key
// to a same-named local. Listed before the `key: pat` form so the parser
// commits to shorthand only when the trailing `:` is absent.
pat_obj_field_short = { loose_ident ~ !":" }
pat_obj_field_kv    = { loose_ident ~ ":" ~ pat_or }
pat_obj_field = { pat_obj_field_kv | pat_obj_field_short }
pat_obj       = { "{" ~ (pat_obj_field ~ ("," ~ pat_obj_field)* ~ ("," ~ pat_obj_rest)? ~ ","?)? ~ "}" }

pat_arr_rest_named = { "..." ~ ident }
pat_arr_rest_anon  = { "..." }
pat_arr_rest       = { pat_arr_rest_named | pat_arr_rest_anon }
pat_arr            = { "[" ~ (pat_or ~ ("," ~ pat_or)* ~ ("," ~ pat_arr_rest)? ~ ","?)? ~ "]" }

pat_atom = {
    pat_wild     |
    pat_range    |
    pat_literal  |
    pat_kind_bind|
    pat_kind_only|
    pat_obj      |
    pat_arr      |
    pat_bind     |
    "(" ~ pat_or ~ ")"
}
pat_or  = { pat_atom ~ ("|" ~ pat_atom)* }

// Guard expression sits below pipe precedence so a trailing `->` belongs to
// the arm separator rather than being eaten as a `pipe_bind` step inside
// the guard. Arm bodies use the full `expr` grammar.
match_arm = { pat_or ~ (kw_when ~ coalesce_expr)? ~ "->" ~ expr }
// `match scrutinee with { arms }` — `with` separator avoids ambiguity with
// `expr { inline_filter }` postfix. Without it, `match $.x { v -> v }` parses
// the `{ v -> v }` as inline_filter (since `v -> v` is a valid pipe-bind expr).
match_expr = { kw_match ~ expr ~ kw_with ~ "{" ~ match_arm ~ ("," ~ match_arm)* ~ ","? ~ "}" }
// `..match { arms }` walks every descendant in DFS pre-order, runs the
// match against each, and collects truthy arm-body results into an
// array. Falsy bodies and unmatched values are silently dropped — the
// shape is "find all descendants whose pattern fires non-falsy".
//
// The `!` suffix (`..match! { arms }`) selects the early-stop variant:
// the runtime returns the first truthy arm-body result without walking
// the rest of the tree. Useful for "find any descendant satisfying the
// pattern" queries.
deep_match_op = @{ "..match!" | "..match" }
deep_match = { deep_match_op ~ "{" ~ match_arm ~ ("," ~ match_arm)* ~ ","? ~ "}" }

// ── Patch block ───────────────────────────────────────────────────────────────
pp_wild_filter = { "[" ~ "*" ~ kw_if ~ expr ~ "]" }
pp_wild        = { "[" ~ "*" ~ "]" }
pp_index       = { "[" ~ idx_val ~ "]" }
pp_descendant  = { ".." ~ field_name }
pp_dot_field   = { "." ~ field_name }
patch_step     = { pp_wild_filter | pp_wild | pp_index | pp_descendant | pp_dot_field }
patch_key      = { ident ~ patch_step* }
patch_field    = { patch_key ~ ":" ~ expr ~ (kw_when ~ expr)? }
patch_block    = { kw_patch ~ coalesce_expr ~ "{" ~ (patch_field ~ ("," ~ patch_field)* ~ ","?)? ~ "}" }

// ── Primary ───────────────────────────────────────────────────────────────────
// `.field` at expression start desugars to `@.field`. Useful inside
// method arguments where the receiver is implicit (`$.users.filter(.active)`
// ≡ `$.users.filter(@.active)`). Listed before `current` to avoid leading
// `.` being eaten by the postfix machinery on a missing primary; the rule
// emits an `Expr::Chain(Current, [Field(name), …])` synthetic expression at
// parse time.
bare_leading_field = { "." ~ field_name }

primary = {
    match_expr    |
    patch_block   |
    let_expr      |
    lambda_expr   |
    arrow_lambda  |
    list_comp     |
    gen_comp      |
    dict_comp     |
    set_comp      |
    obj_construct |
    arr_construct |
    global_call   |
    "(" ~ expr ~ ")" |
    kw_delete     |
    root          |
    current       |
    bare_leading_field |
    literal       |
    ident
}

// ── Program ───────────────────────────────────────────────────────────────────
program = { SOI ~ expr ~ EOI }
