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.

The inline use Ichor form above is the fastest way to try Ichor out — for anything beyond that, prefer mix ichor.gen (below): it's the same codegen, run once ahead of time instead of on every compile.

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.

Three ways to run a grammar

A grammar's Ichor.Actions module is the same no matter how the grammar itself gets turned into a parser — but when that happens is a real choice, with a real consequence for what your app depends on at runtime:

PathWhen codegen runsichor needed at runtime?Best for
mix ichor.gen (recommended)Once, ahead of time, from the command lineNo — ichor_runtime onlyAnything shipping to production
use IchorEvery mix compileYesA grammar that's still actively changing
Raw pipeline (Aether.Parser + Grammar.Analysis + Grammar.VM)At your program's own runtimeYesA grammar not known until runtime (user-supplied, plugins, a REPL)

The reason mix ichor.gen is the recommended default is concrete, not stylistic: a module using use Ichor needs the real Ichor module — and so the whole ichor package — present at compile time, in whatever environment you compile in, including a mix release build (which normally compiles under MIX_ENV=prod). That means ichor can't be marked only: :dev, runtime: false in any app that still has so much as one use Ichor module in its tree; Mix won't even load the Ichor module under :prod to expand the macro, so compilation fails outright. mix ichor.gen-generated code has no macro dependency on ichor at all — just ordinary function calls into ichor_runtime — so it's the only one of the three that actually lets ichor be dev-only. See the tutorial for a full worked example of all three (§7-9), or the cheatsheet for a quick reference once you know which one you want.

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

Recommended: if every grammar you use is pregenerated ahead of time (mix ichor.gen, never use Ichor at your app's own compile time), add ichor_runtime as your real runtime dependency and keep ichor itself dev-only:

def deps do
[
{:ichor_runtime, "~> 0.1.0"},
{:ichor, "~> 0.2.0", only: :dev, runtime: false}
]
end

If you're still using use Ichor at your app's own compile time (a grammar that's still actively changing, say), ichor has to be a real dependency instead — it can't be only: :dev once anything in your app still expands that macro:

def deps do
[
{:ichor, "~> 0.2.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.