Compiles a grammar file to a plain .ex file, once, ahead of time. This
is the recommended way to use Ichor for anything shipping to
production — see the tutorial
for the full reasoning, or the cheatsheet
for a quick-reference version of everything on this page.
mix ichor.gen PATH_TO_GRAMMAR --module MODULE --actions ACTIONS_MODULE --out PATH [--root NAME]
It runs the exact same parse → analyze → codegen pipeline as
use Ichor, grammar: ..., actions: ..., but as a one-time command
instead of a macro re-run on every mix compile, writing the result to
disk as an ordinary module you check in like any other source file.
Options
| Option | Required | Meaning |
|---|---|---|
--module | Yes | The generated module's name, e.g. MyApp.Calculator. |
--actions | Yes | The Ichor.Actions module the generated run/1,2 dispatches to, e.g. MyApp.Calculator.Actions. Baked in as a literal reference — it doesn't need to exist yet, only when the generated file's own callers run. |
--out | Yes | Path to write the generated module to. |
--root | No | The entry rule's name. Only meaningful for non-Aether grammar styles (see below) — Aether always uses its own @root pragma. |
--lossless | No | Also generate parse_lossless/1,2 and run_lossless/1,2: the lossless tree of an input (Ichor.Syntax), every byte of it, for a tool that edits a document and writes it back. parse/1,2 and run/1,2 are exactly what they are without it; the module is larger. See Tutorial §10. |
Example: a basic grammar
mix ichor.gen calculator.aether \
--module Calculator \
--actions Calculator.Actions \
--out lib/calculator.ex
Open lib/calculator.ex afterward and it reads like ordinary,
hand-written Elixir — tokenize/1, parse/1, run/1,2, plus whatever
private helper functions the codegen backend needed, all emitted as
def (not defp, unlike use Ichor's macro-expanded output — see
"Why every helper is public" below):
# Generated by:
#
# mix ichor.gen calculator.aether --module Calculator --actions Calculator.Actions --out lib/calculator.ex
#
# Do not edit by hand -- rerun the command above instead.
defmodule Calculator do
@moduledoc false
# tokenize/1, parse/1, run/1, run/2, ...
end
Calculator.run("2 + 3 * 4")
#=> {:ok, 14}Rerun the same command whenever calculator.aether changes — there's
no automatic staleness check between the checked-in file and its source
grammar, so it's worth wiring into a mix.exs alias of your own (a
gen.grammar alias calling "ichor.gen ...", say) rather than typed by
hand each time.
Example: keeping ichor out of production
The whole point of generating ahead of time is that the output only
ever calls a small, fixed set of support modules by name — no macro,
no compile-time dependency on ichor itself. That fixed set is exactly
what the independently-published
ichor_runtime package is, so
a project that only ever consumes pregenerated parsers can mark ichor
dev-only:
def deps do
[
{:ichor_runtime, "~> 0.6.1"},
{:ichor, "~> 0.7", only: :dev, runtime: false}
]
endContrast with use Ichor: because that macro needs the real Ichor
module loaded to expand, ichor has to be a genuine dependency in
whatever environment compiles it — Mix won't load an only: :dev
dependency under MIX_ENV=prod, so a mix release build (which
compiles under :prod by default) fails outright if any module still
says use Ichor while ichor is dev-only. mix ichor.gen-generated
code has no such constraint.
Example: importing a non-Aether grammar
PATH_TO_GRAMMAR doesn't have to be .aether source — ABNF, BNF, ISO
EBNF, and PEG grammars work too, via Ichor.GrammarImport. The style is
picked automatically from the file extension:
| Extension | Style |
|---|---|
.aether (or anything unrecognized) | Aether (default) |
.abnf | ABNF (RFC 5234 + RFC 7405) |
.bnf | Classical BNF |
.ebnf, .iso.ebnf | ISO EBNF (ISO/IEC 14977) |
.w3c.ebnf | W3C EBNF (the XML 1.0 notation) |
.peg | PEG (Ford/pest/PEG.js convention) |
mix ichor.gen http_request_line.abnf \
--module HttpRequestLine \
--actions HttpRequestLine.Actions \
--out lib/http_request_line.ex \
--root request_line
--root overrides the source's own first-declared rule as the entry
point — here request_line, since ABNF has no @root pragma of its
own to say so. Two other differences from Aether source apply to every
non-Aether style: the grammar is always @noskip (whitespace tolerance
has to already be written explicitly into the source, matching how real
ABNF/BNF/EBNF/PEG grammars are written), and an Ichor.Actions module
written against the generated parser only ever sees the default
fallback capture shape (Ichor.Node/single-capture passthrough) —
never a named capture, since none of these formats have syntax for one.
For ABNF specifically, RFC 5234 Appendix B's own "core rules" (ALPHA,
DIGIT, CRLF, ...) are filled in automatically for whichever of them
the source references but never defines itself — extremely common in
real ABNF, which generally assumes those are always available. See
Ichor.GrammarImport's own moduledoc for the one sharp edge this can't
fully paper over (two genuinely-overlapping core rules both referenced
by the same source).
Example: overriding the detected style with @style
An @style line anywhere in the source wins over the extension-based
guess — useful for a .txt grammar, or one whose extension doesn't
match its actual format:
@style abnf
request-line = method SP request-target SP http-version CRLFmix ichor.gen request_line.txt \
--module RequestLine \
--actions RequestLine.Actions \
--out lib/request_line.ex
@style accepts abnf, bnf, ebnf, peg, or aether, and is
stripped before the source reaches whichever parser actually runs, so
it doesn't need to be valid syntax in the target format at all.
Why every helper is public
Every private helper function the codegen backend generates
(tokenizer/rule sub-functions, LR/GLR dispatch clauses, ...) is emitted
as def, not defp. Through use Ichor, these come from macro
expansion, which Elixir's compiler doesn't flag as unused even when a
particular grammar's shape leaves some unreferenced (a predefined token
like HEX compiles a lexer function whether or not any rule in this
grammar uses it). Written out as ordinary source instead, the same
defps would trip --warnings-as-errors on an unrelated grammar-shape
detail — def sidesteps that.
See also
- Tutorial §9 — the full worked example, alongside
use Ichorand the rawGrammar.VMpipeline for comparison. - Cheatsheet — quick reference once you already know which path you want.
mix ichor.tokens— inspect a grammar's tokens without generating anything.