// ----- Trivia -----
WHITESPACE = _{ " " | "\t" }
COMMENT    = _{ "#" ~ (!NEWLINE ~ ANY)* }
NEWLINE    = _{ "\r\n" | "\n" | "\r" }

// Statement separator: newline or `;` (both interchangeable, repeatable).
stmt_sep = _{ (NEWLINE | ";")+ }

// ----- Keywords (excluded from idents) -----
keyword = @{
    ("if" | "then" | "else" | "match" | "true" | "false" | "import" | "export")
    ~ !(ASCII_ALPHANUMERIC | "_")
}

// ----- Literals & identifiers -----
// `!"."` lookahead on the fractional dot keeps `0..` from being read as `0.`
// followed by `.` — the range syntax `[a..b]` needs the bare `..` to survive.
number   = @{ ASCII_DIGIT+ ~ ("." ~ !"." ~ ASCII_DIGIT*)? }
string   = @{ "\"" ~ (!"\"" ~ ANY)* ~ "\"" }
bool     = @{ ("true" | "false") ~ !(ASCII_ALPHANUMERIC | "_") }
ident    = @{ !keyword ~ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }
wildcard = @{ "_" ~ !(ASCII_ALPHANUMERIC | "_") }

// Elixir-style atom: `:name`. Self-evaluating constant, equal by name.
// `:` is not an `op_char`, so atoms can't collide with custom operators or
// with the `::` cons token (which would fail the `ASCII_ALPHA | "_"` start).
atom = @{ ":" ~ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* }

// ----- Operator tokens -----
// Operator characters. Built-in op tokens forbid an op_char tail so that a
// longer sequence (e.g. `++`, `+>`) is never consumed as the built-in prefix.
op_char = _{ "!" | "$" | "%" | "&" | "*" | "+" | "-" | "/"
           | "<" | "=" | ">" | "?" | "@" | "^" | "|" | "~" }

power    = @{ ("**" | "^") ~ !op_char }
multiply = @{ "*" ~ !op_char }
divide   = @{ "/" ~ !op_char }
modulo   = @{ "%" ~ !op_char }
add      = @{ "+" ~ !op_char }
subtract = @{ "-" ~ !op_char }

eq        = @{ "==" ~ !op_char }
ne        = @{ "!=" ~ !op_char }
le        = @{ "<=" ~ !op_char }
ge        = @{ ">=" ~ !op_char }
lt        = @{ "<" ~ !op_char }
gt        = @{ ">" ~ !op_char }
not_match = @{ "!~" ~ !op_char }
match_op  = @{ "~" ~ !op_char }

logical_and = @{ "&&" ~ !op_char }
logical_or  = @{ "||" ~ !op_char }

// Prefix `-` and `!` are intentionally single-char (no `!op_char` lookahead)
// so `--5` and `!!true` keep stacking. `(--)` and `(!!)` can still be defined
// as custom infix operators since the infix builders look for a leading
// non-prefix context.
neg         = @{ "-" }
logical_not = @{ "!" }

assign = { "=" }

// User-defined operators. Each variant is bucketed by first character so the
// Pratt parser can assign OCaml-style precedence. `=>` and `->` are reserved
// for function and match arrows so they're excluded from the `=`/`-` buckets.
custom_op_pow  = @{ "**" ~ op_char+ }
custom_op_mult = @{ ("*" | "/" | "%") ~ op_char+ }
custom_op_add  = @{ "+" ~ op_char+ | "-" ~ !">" ~ op_char+ }
custom_op_cat  = @{ ("^" | "@") ~ op_char* }
custom_op_comp = @{ ("<" | ">" | "!" | "$") ~ op_char+ | "=" ~ !">" ~ op_char+ }
custom_op_and  = @{ "&" ~ op_char+ }
custom_op_or   = @{ "|" ~ op_char+ }

// Prefix operators must start with `?` or `~` and be at least two chars.
// `!` is excluded so the built-in `!` keeps stacking (`!!true`); custom
// prefixes named with `!` would be unreachable in prefix position anyway.
custom_prefix  = @{ ("?" | "~") ~ op_char+ }

// Parenthesized operator name. Acts as an identifier in expressions and
// patterns so users can bind and reference operators as plain functions:
// `(++) = (x, y) => x + y`, then `1 ++ 2` or `(++) 1 2` or `(++)(1, 2)`.
// `::` is the built-in cons operator; included so `(::)` references it like
// any other op even though `:` isn't an `op_char`.
op_paren_name = @{ "::" | op_char+ }
op_paren      = { "(" ~ op_paren_name ~ ")" }

prefix_op  = _{ neg | logical_not | custom_prefix }
postfix_op = _{ call_args | dot_access | juxt_arg }

// `list.0` for list index, `object.foo` for object atom-key lookup
// (the `:` prefix is implicit after `.`), `module.foo` for module member.
// `dot_index` is digits-only so `xs.0.1` chains as `(xs.0).1` rather than
// being read as a single decimal index.
dot_index  = @{ ASCII_DIGIT+ }
dot_access = { "." ~ (dot_index | ident) }

// Function application by juxtaposition: `f x` is `f(x)`. Each juxt_arg
// consumes one primary, so `f g h` chains left-associatively as `(f g) h`.
juxt_arg = { primary }
infix_op   = _{
      power
    | multiply | divide | modulo
    | add | subtract
    | eq | ne | le | ge | lt | gt
    | logical_and | logical_or
    | not_match | match_op
    | cons_op
    | custom_op_pow
    | custom_op_mult
    | custom_op_add
    | custom_op_cat
    | custom_op_comp
    | custom_op_and
    | custom_op_or
}

// ----- Collection separators -----
// Inside `[]`, `{}`, object/set patterns and pattern-lists, items can be
// separated by any non-empty run of commas and/or newlines. This makes layouts
// like `[1,\n 2]`, `[\n 1\n 2\n]`, and `[1,\n  ,2]` all parse the same.
coll_sep  = _{ (NEWLINE | ",")+ }

// ----- Patterns -----
// `..` matches zero-or-more remaining elements; `..name` binds them.
pattern_rest = { ".." ~ ident? }
pattern_item = _{ pattern_rest | pattern }

pattern_list = { "[" ~ coll_sep? ~ (pattern_item ~ (coll_sep ~ pattern_item)* ~ coll_sep?)? ~ "]" }

// `()` matches the Unit value.
pattern_unit = { "(" ~ NEWLINE* ~ ")" }

// Object-pattern entries: full `key: pat` (key is an expression, value a sub-pattern)
// or shorthand `name`, which desugars to `"name": name` — handy for destructuring
// modules: `{Left, Right} = import "lib.ff"`.
pattern_object_entry = { (expr ~ NEWLINE* ~ ":" ~ NEWLINE* ~ pattern) | ident }
pattern_object       = { "{" ~ coll_sep? ~ (pattern_object_entry ~ (coll_sep ~ pattern_object_entry)* ~ coll_sep?)? ~ "}" }
pattern_set        = { "{" ~ coll_sep? ~ pattern ~ (coll_sep ~ pattern)* ~ coll_sep? ~ "}" }

// `wildcard` and `bool` precede `ident` so they aren't shadowed. `op_paren`
// precedes `pattern_unit` so `(++)` binds the operator name rather than
// being attempted as the unit pattern.
pattern_atom = _{
      pattern_list
    | op_paren
    | pattern_unit
    | pattern_object
    | pattern_set
    | number
    | string
    | bool
    | atom
    | wildcard
    | ident
}

// `::` — the built-in cons operator. `:` isn't an `op_char` so this can't be
// expressed via the `custom_op_*` buckets; it gets its own token and its own
// precedence slot in the Pratt table.
cons_op = @{ "::" }

// `h :: t` — type-polymorphic cons-pattern. Only wraps when there's at least
// one `::`, so single-atom patterns flow through untouched. Right-associativity
// is applied at build time, not in the grammar.
pattern_cons = { pattern_atom ~ (cons_op ~ pattern_atom)+ }
pattern = _{ pattern_cons | pattern_atom }

// ----- Ranges -----
// `[a..]` infinite, `[a..b]` exclusive, `[a..=b]` inclusive. Lazy at runtime —
// no expansion into a list. PEG order: try `..=` first; the bare-`..` variants
// use `!"="` so they don't also consume an inclusive marker.
range_incl = { expr ~ "..=" ~ expr }
range_excl = { expr ~ ".." ~ !"=" ~ expr }
range_open = { expr ~ ".." ~ !"=" }
range      = _{ range_incl | range_excl | range_open }

// ----- Collections (expressions) -----
list  = { "[" ~ NEWLINE* ~ range ~ NEWLINE* ~ "]"
        | "[" ~ coll_sep? ~ (expr ~ (coll_sep ~ expr)* ~ coll_sep?)? ~ "]" }
// `()` is the Unit literal.
unit = { "(" ~ NEWLINE* ~ ")" }
object_entry = { expr ~ NEWLINE* ~ ":" ~ NEWLINE* ~ expr }
object       = { "{" ~ coll_sep? ~ (object_entry ~ (coll_sep ~ object_entry)* ~ coll_sep?)? ~ "}" }
set        = { "{" ~ coll_sep? ~ expr ~ (coll_sep ~ expr)* ~ coll_sep? ~ "}" }

// ----- Scope (block expression) -----
// `( stmts )`. Evaluates statements in order in a child environment that
// captures the enclosing lexical scope; new bindings are local to the block.
// The block's value is the value of the last statement (Unit if that statement
// is an assignment). To disambiguate from a parenthesized expression, a scope
// must contain either a single assignment or two-plus statements separated by
// `;` or newline. `(x)` remains a parenthesized expression; `(x = 1)` is a scope.
scope = {
      "(" ~ stmt_sep? ~ statement ~ (stmt_sep ~ statement)+ ~ stmt_sep? ~ ")"
    | "(" ~ stmt_sep? ~ assignment ~ stmt_sep? ~ ")"
}

// ----- Function literal -----
// Parameters are space-separated; commas are optional. Parens are optional for
// one or more parameters but required for zero: `() => 1`, `x => x + 1`,
// `a b c => a + b + c`, `(a, b, c) => ...`, `(a b c) => ...` all work.
// Each parameter is a pattern, so functions can destructure their arguments:
// `foo = [a, ..rest] => ...`, `foo = a :: rest => ...`,
// `foo = {:key: value, ..} => ...`.
params = {
      "(" ~ (pattern ~ (","? ~ pattern)* ~ ","?)? ~ ")"
    | pattern ~ (","? ~ pattern)*
}
function = { params ~ "=>" ~ expr }

// ----- If expression -----
// Newlines are allowed between any tokens, so the expression can be laid
// out across multiple lines:
//   if cond
//   then x
//   else y
if_expr = {
    "if" ~ NEWLINE* ~ expr
    ~ NEWLINE* ~ "then" ~ NEWLINE* ~ expr
    ~ NEWLINE* ~ "else" ~ NEWLINE* ~ expr
}

// ----- Match expression -----
// Arms are separated by `,`. Two forms terminate the scrutinee so
// juxtaposition can't eat the first pattern as a function arg:
//   - multi-line: a newline after the scrutinee (`match value\n  pat -> ...`)
//   - single-line: a `:` after the scrutinee (`match value: pat -> ..., ...`)
// Newlines around the comma are allowed so arms can be laid out across
// multiple lines. The scrutinee is optional: a match without one evaluates
// to a 1-arg function whose argument becomes the scrutinee.
// An optional `if <expr>` guard runs after the pattern binds; arms whose
// guard is false fall through to the next arm.
match_guard = { "if" ~ expr }
match_arm  = { pattern ~ match_guard? ~ "->" ~ expr }
match_expr = {
      "match" ~ expr? ~ NEWLINE+ ~ match_arm ~ ("," ~ NEWLINE* ~ match_arm)* ~ ","?
    | "match" ~ (expr ~ ":")? ~ NEWLINE* ~ match_arm ~ ("," ~ NEWLINE* ~ match_arm)* ~ ","?
}

// ----- Function call (postfix) -----
// Newlines allowed around commas and at the boundaries, but commas remain the
// argument separator (so `f(a b)` is still juxtaposed application, not two args).
call_args = { "(" ~ NEWLINE* ~ (expr ~ (NEWLINE* ~ "," ~ NEWLINE* ~ expr)* ~ NEWLINE* ~ ","? ~ NEWLINE*)? ~ ")" }

// ----- Import expression -----
// `import <primary>` loads a module and returns it as a value. As a bare
// statement (not the RHS of an assignment) it also splats the module's exports
// into the current scope — see eval_statement.
import_expr = { "import" ~ NEWLINE* ~ primary }

// ----- Primary expression -----
// `scope` precedes `unit` / `(expr)` so `(x = 1)` and multi-statement blocks
// bind as blocks rather than failing the other paren rules. `function` is first
// so `(x) => body` is read as a function rather than a parenthesized `x`.
primary = _{
      function
    | match_expr
    | if_expr
    | import_expr
    | number
    | string
    | bool
    | atom
    | list
    | object
    | set
    | op_paren
    | scope
    | unit
    | "(" ~ expr ~ ")"
    | ident
}

// ----- Expression -----
expr = {
    prefix_op* ~ primary ~ postfix_op*
    ~ (infix_op ~ prefix_op* ~ primary ~ postfix_op*)*
}

// ----- Statements -----
// LHS is a pattern, so `a = 1`, `[b, ..] = a`, `(x, y) = pair`, and
// `{ "k": v } = some_object` all work.
assignment = { pattern ~ assign ~ expr }

// `export Left, Right` exports the named bindings from the current module.
// `export *` snapshots every local binding visible at the call site.
export_all   = { "*" ~ !op_char }
export_name  = { op_paren | ident }
export_names = { export_name ~ (NEWLINE* ~ "," ~ NEWLINE* ~ export_name)* ~ (NEWLINE* ~ ",")? }
export_stmt  = { "export" ~ NEWLINE* ~ (export_all | export_names) }

statement = _{ export_stmt | assignment | expr }

program = {
    SOI
    ~ stmt_sep?
    ~ (statement ~ (stmt_sep ~ statement)*)?
    ~ stmt_sep?
    ~ EOI
}
