# `Ichor.Toolkit.Pratt`
[🔗](https://github.com/joetjen/ichor_runtime/blob/v0.5.0/lib/ichor/toolkit/pratt.ex#L1)

Precedence-climbing (Pratt) expression parsing over a runtime-mutable
operator table -- extracted from a pattern already hand-rolled once,
`OpExprTest.Operators.climb/6` (`test/support/opexpr_operators.ex`),
the worked example proving `Grammar.IR.Custom`/`@native(...)` rule-
position dispatch works at all. That fixture only ever needed infix
chains; this generalizes to prefix and postfix too, since those are
exactly what Track 1's own motivating scenarios need (Prolog's
`op/3` supports `fy`/`fx` prefix and `xf`/`yf` postfix operators
alongside infix ones, Haskell fixity declarations are infix-only but
mixfix notation is not).

Completely IR-agnostic, like `Ichor.Toolkit.Codegen`/`Result`: `pos`,
the stream, and the parsed value are all opaque to this module.
`peek_op` returning `{op_name, pos_after_op}` rather than just
`op_name` is deliberate, not incidental -- an operator token may sit
behind skippable trivia (whitespace, comments; `@native(...)` nodes
are opaque to Aether's own `@skip` splicing, so a caller has to handle
this itself, same as `climb/6` already did), so *where the operator
actually ends* can't be assumed to be a fixed offset from where the
search for it started. `parse_primary` is trusted to do its own
leading-trivia handling for the same reason.

## The infix/postfix ambiguity

An operator registered as *both* infix and postfix at once is
genuinely ambiguous: after parsing a left operand and seeing that
token, nothing short of looking further ahead can tell whether an
infix use (expecting a right operand next) or a postfix use (expecting
nothing) was meant. This is exactly why ISO Prolog's own `op/3`
forbids defining an atom as both infix and postfix -- the same
operator serving as prefix *and* infix (unary vs. binary `-`) or
prefix *and* postfix has no such problem, since prefix is only ever
attempted before a left operand exists and infix/postfix only after,
two mutually exclusive positions.

The table itself doesn't reject the combination -- it has no way to
know at build time whether a caller will later supply the means to
resolve it, since callbacks are only ever known at `parse/4` call
time, not baked into the table. Instead, `parse/4` only raises when it
actually encounters a token where *both* fixities are simultaneously
viable at the current precedence: if `callbacks` includes
`can_start_operand?`, it's consulted (does a valid operand start right
after the operator? infix if so, postfix if not); if it's absent,
`parse/4` raises `ArgumentError` naming the operator, rather than
silently misparsing one way or the other.

# `assoc`

```elixir
@type assoc() :: :left | :right | :none
```

# `callbacks`

```elixir
@type callbacks() :: %{
  :peek_op =&gt; (term() -&gt; {op_name(), term()} | nil),
  :parse_primary =&gt; (term() -&gt; {:ok, term(), term()} | :fail),
  :build =&gt; (fixity(), op_name(), [term()] -&gt; term()),
  optional(:can_start_operand?) =&gt; (term() -&gt; boolean())
}
```

# `fixity`

```elixir
@type fixity() :: :prefix | :infix | :postfix
```

# `op_name`

```elixir
@type op_name() :: term()
```

# `table`

```elixir
@type table() :: %{
  optional(op_name()) =&gt; %{
    optional(:prefix) =&gt; non_neg_integer(),
    optional(:infix) =&gt; {non_neg_integer(), assoc()},
    optional(:postfix) =&gt; non_neg_integer()
  }
}
```

# `infix`

```elixir
@spec infix(table(), op_name(), non_neg_integer(), assoc()) :: table()
```

Registers `op_name` as an infix operator at `prec` with associativity `assoc` (default `:left`).

# `new`

```elixir
@spec new() :: table()
```

An empty operator table.

# `parse`

```elixir
@spec parse(table(), term(), callbacks(), non_neg_integer()) ::
  {:ok, term(), term()} | :fail
```

Parses one expression starting at `pos`, only accepting an infix/postfix
operator whose precedence is at least `min_prec` (default `0` -- pass
a table entry's own precedence, bumped per associativity, on the
recursive call that parses an operator's own operand).

# `postfix`

```elixir
@spec postfix(table(), op_name(), non_neg_integer()) :: table()
```

Registers `op_name` as a postfix operator at `prec`.

# `prefix`

```elixir
@spec prefix(table(), op_name(), non_neg_integer()) :: table()
```

Registers `op_name` as a prefix operator at `prec`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
