Ichor

Hex.pmDocumentation

Ichor compiles grammar definitions into working language implementations. Write a grammar once — in Aether (Ichor's own grammar language) or import one from ABNF, BNF, EBNF, or PEG — and Ichor turns it into a Lexer, a Parser, and (via your own Ichor.Actions module) an Executor, with no hand-written parsing code at all.

defmodule Calculator do
use Ichor, grammar_source: """
@grammar "calculator"
@root expr
@skip SPACE
NUMBER := DIGIT+ ("." DIGIT+)?
expr := term (op:("+" | "-") term)*
term := factor (op:("*" | "/") factor)*
factor := NUMBER | "(" expr ")"
""", actions: Calculator.Actions
end
Calculator.run("2 + 3 * 4")
#=> {:ok, 14}

Calculator.Actions is a plain module implementing the Ichor.Actions behaviour — it decides what each rule and token means; the grammar only decides what's valid syntax. See the tutorial for the full walkthrough of this example, including the Actions module.

Why

Most grammar tools stop at "does this text match" (a recognizer) or "here's a parse tree, good luck" (a parser generator). Ichor goes one step further: every grammar pairs with an Ichor.Actions module that says what to do with each piece of a match — evaluate it, build a config struct, execute a query, transpile it, whatever the grammar is for. The same grammar, parsed by either of two independent backends, always produces identical results through that same Actions module.

How it fits together

.aether source ABNF / BNF / EBNF / PEG source
│ │
▼ ▼
Aether.Reader + Aether.Eval Ichor.ABNF / .BNF / .EBNF.* / .PEG
│ │
└───────────────┬────────────────────────┘
Grammar.IR (+ Grammar.Analysis)
@engine peg | lr | glr
┌─────────────┴─────────────┐
▼ ▼
Grammar.VM Grammar.Native
(bytecode interpreter, (compile-time codegen,
PEG / LR / GLR engines) PEG / LR / GLR engines)
│ │
└─────────────┬─────────────┘
Ichor.Actions
(your module: evaluate, build, execute, transpile...)

Every Aether grammar compiles to a genuine multi-stage pipeline, never a single scannerless pass: tokens (ALL_CAPS names) are matched by maximal munch over raw characters — longest match wins, ties broken by declaration order — producing a token stream, optionally reclassified by @keywords/@refine (e.g. distinguishing a keyword from an ordinary identifier, or JavaScript's regex-literal-vs-division ambiguity); rules (snake-case names) then run over that stream as an ordered-choice (PEG-style, the default) parser, or — for a grammar tagged @engine lr/@engine glr — a deterministic shift-reduce or genuine Tomita-style GLR parser instead, for CFG shapes plain PEG can't express (left recursion, genuine ambiguity). This split is structural, not an implementation detail — it's what lets every backend/engine combination agree byte-for-byte on what a grammar means. A rule or token can also opt out of all of this entirely via @native(...), dispatching to your own Elixir callback for the rare case a grammar's meaning is mutated mid-file by something declared earlier (Prolog's op/3, heredocs, string interpolation).

Components

Installation

Add ichor to your list of dependencies in mix.exs:

def deps do
[
{:ichor, "~> 0.1.0"}
]
end

Where to go next

Development

mix deps.get
mix test
mix format --check-formatted
mix compile --warnings-as-errors
mix docs

See CONTRIBUTION.md for how to propose changes, and CHANGELOG.md for release history.

License

MIT — see LICENSE.