Runs the exact same parse -> analyze -> native-codegen pipeline as
use Ichor, grammar: ..., actions: ... (Ichor.generate/3), but once,
from the command line, writing the result to disk as an ordinary
module instead of splicing it into a macro expansion. This is the
recommended way to use Ichor for anything shipping to production --
see below for why.
$ mix ichor.gen calculator.aether \
--module Calculator \
--actions Calculator.Actions \
--out lib/calculator.ex
The point is moving the expensive part -- parsing the grammar, running
Grammar.Analysis, building an LR/GLR table if @engine calls for
one, and generating Elixir functions from it -- out of the consuming
app's own mix compile (where use Ichor would otherwise redo all of
that on every compile) and into a one-time step whose output gets
checked in like any other source file.
The generated module still calls a handful of small support modules by
name (Ichor.Actions, Ichor.Error, Grammar.Native.Runtime.Parser,
Grammar.Native.Runtime.Tokenizer, Grammar.VM.Token, and -- for an
@engine lr/glr grammar -- the LR/GLR shift-reduce and GSS runtime)
for capture dispatch, error formatting, and token matching, exactly as
use Ichor-generated code does -- but as ordinary function calls, not
a macro expansion, so nothing about the generated module needs Ichor
(or ichor the package at all) present once it's been written. That
handful of modules is exactly what ichor_runtime is: a consuming app
can depend on ichor_runtime as an ordinary runtime dependency and
mark ichor itself only: :dev, runtime: false -- the Aether
front-end, the format importers, Grammar.Analysis, the LR/GLR table
builder, and the codegen backends themselves (the bulk of the
library) genuinely never ship, including in a mix release build.
Contrast with use Ichor: because that macro needs the real Ichor
module loaded to expand, ichor has to be a real dependency in
whatever environment compiles it, and Mix simply won't load a
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 and ichor was marked dev-only. This
task exists precisely to avoid that tradeoff.
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 for being
unused even when a particular grammar's shape leaves some unreferenced
(e.g. a predefined token like HEX no rule actually uses); written
out as ordinary source, the same functions would trip
--warnings-as-errors on an unrelated grammar-shape detail.
Regenerate by rerunning the same command whenever the grammar changes -- there's no automatic staleness check between the checked-in file and its source grammar.
Grammar styles: Aether, ABNF, BNF, ISO EBNF, PEG
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/IEC 14977) |
.peg | PEG (Ford/pest/PEG.js convention) |
Override the detected style with an @style line anywhere in the
source (@style abnf, @style bnf, @style ebnf, @style peg, or
@style aether) -- it's stripped before the source reaches whichever
parser actually runs, so it doesn't need to be valid syntax in the
target format at all.
None of ABNF, BNF, ISO EBNF, or PEG have Aether's own @root
pragma, named-capture syntax (name:expr), or @skip auto-splicing
convenience. For a non-Aether style:
- The root is the source's own first-declared rule, unless
overridden with
--root NAME. - 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 actually written. - An
Ichor.Actionsmodule written against the generated parser only ever sees the default fallback shape (Ichor.Node/single- capture passthrough) -- never a named capture, since the source format has no 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. SeeIchor.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).
Options
--module(required) -- the generated module's name, e.g.MyApp.Calculator.--actions(required) -- theIchor.Actionsmodule the generatedrun/1,2dispatches to, e.g.MyApp.Calculator.Actions. Not required to exist yet -- it's baked in as a literal module reference, resolved when the generated file's own callers run, not when it's generated.--out(required) -- path to write the generated module to.--root(optional, non-Aether styles only) -- the entry rule's name, overriding the source's own first-declared rule.