Modules
The semantic half of Aether's front-end: turns an Aether.Reader.Grammar
CST into a fully resolved Aether.Grammar, with every body already
Grammar.IR.
The fully compiled output of Aether.Parser: grammar-wide settings plus
every token and rule body, each already resolved to Grammar.IR.
Turns .aether source text into a flat list of Aether.Tokens.
Parses .aether source into a fully resolved Aether.Grammar, in two
stages: Aether.Reader (pure syntax -- a token/rule reference split,
pragmas, the expression grammar -- into a concrete syntax tree with no
desugaring) followed by Aether.Eval (that CST into Grammar.IR,
handling everything that needs whole-grammar context: predefined-token
override/use tracking, case-insensitivity, character-class/regex
desugaring, inline-literal promotion, @skip splicing, and final
@root/@skip validation).
The pure-syntax half of Aether's front-end: turns .aether source (via
Aether.Lexer) into a concrete syntax tree that's a faithful record of
what was written -- every pragma, token, and rule definition, in file
order -- with no desugaring and no cross-definition semantics applied.
The CST Aether.Reader.read/2 produces: grammar-wide pragmas plus
every token/rule definition in file order, each body an
Aether.Reader.cst() tree. Aether.Eval.build/1 is the only
consumer.
A single lexical token produced by Aether.Lexer from .aether source
text.
The analysis pass between a front-end (Aether.Parser, or one of the
ABNF/BNF/EBNF/PEG importers) and either backend (Grammar.VM or native
codegen)
The interpreted graph-structured-stack backend: runs the same
Grammar.LRTable SLR(1) table Grammar.LR does, but accepts
conflicts instead of requiring their absence -- every action in a
conflict cell is taken, forking the parse across a real
Grammar.GLR.GSS (node-sharing, not independent per-branch stacks:
two derivations that reach the same state at the same position
merge, which is what keeps this from blowing up combinatorially on a
grammar with only a handful of local conflicts). The GSS-driving loop
itself lives in Grammar.GLR.Runtime, shared unchanged with
Grammar.Native.GLR -- this module's own job is just building the
table (recompiled on every call, the same "VM interpreted" convention
Grammar.VM and Grammar.LR already follow) and wrapping its plain
action/goto maps into the closures Runtime.run/6 expects.
The normalized grammar AST every Ichor front-end (Aether, ABNF, BNF,
EBNF, PEG) compiles down to, and that every later stage -- the analysis
pass, Grammar.VM, native codegen -- consumes instead of talking to any
particular front-end's own syntax.
Positive lookahead, consumes nothing: &expr.
Matches any single character: ..
Named capture for AST construction: name:expr.
Character class, including unicode ranges: [a-z0-9_].
Ordered PEG choice: a | b | c -- first match wins, always.
@native("Module", "function", dep1, dep2, ...): a rule body that
delegates to hand-written Elixir code (an Ichor.CustomRule
implementation) instead of ordinary Grammar.IR combinators -- the
escape hatch for constructs no static PEG grammar can express (a
Prolog-style mutable operator-precedence table, C's typedef-vs-
expression ambiguity, and similar "what matches here depends on
something read earlier" problems).
@native("Module", "function", dep1, ...) at token position: a
token whose entire body is hand-written Elixir code (an
Ichor.CustomLexeme implementation) instead of ordinary character-level
combinators -- the escape hatch for lexical constructs no fixed
maximal-munch tokenizer can express (a heredoc's dynamic terminator, a
string literal with embedded interpolated expressions).
Indentation-sensitive block: @indent(expr) / @samecol(expr).
Exact string match.
Source-location metadata carried by every Grammar.IR node, so an error
raised at any later stage (analysis, either backend) can still point
back at the exact line/column in the original grammar file rather than
just naming an IR node in the abstract.
Negative lookahead, consumes nothing: !expr.
Zero or one: expr?.
One or more: expr+.
Bounded repetition: expr{n}, expr{n,}, expr{n,m}.
Reference to another rule (or token), by name.
Ordered sequence of sub-expressions: a b c.
Zero or more: expr*.
The deterministic bottom-up backend: builds an SLR(1) table via
Grammar.LRTable and requires it be conflict-free -- a real conflict
is a compile-time error here (compile/1), never something forked
through (that's what Grammar.GLR is for). No graph-structured-stack
bookkeeping at all: an ordinary shift-reduce loop over a single stack.
Canonical LR(0) item-set construction (closure/goto over
Grammar.LRTable.Desugar's flat production list), plus SLR(1)
action/goto table construction on top of it.
Builds an SLR(1) action/goto table (Grammar.LRTable, from
ichor_runtime) from an @engine lr/@engine glr grammar -- shared
by both Grammar.LR (which additionally requires the result be
conflict-free) and Grammar.GLR (which accepts conflicts and forks
over them at runtime).
Flattens grammar.rules' PEG-shaped IR into the flat CFG production
list Grammar.LRTable.Production describes, for Grammar.LRTable's
LR(0)/SLR(1) construction to consume.
Standard textbook nullable/FIRST/FOLLOW fixpoint algorithms, over
Grammar.LRTable.Desugar's flat CFG production list rather than the
original PEG IR -- recomputed fresh (not reusing Grammar.Analysis's
own compute_nullable/1) since the augmented nonterminal set here
includes every helper nonterminal a Star/Plus/Opt/Rep/group
desugared into, which Grammar.Analysis never sees.
The compile-time codegen backend: turns a validated %Aether.Grammar{}
into quoted Elixir function definitions -- the same two-stage Lexer ->
Parser split Grammar.VM compiles to bytecode for, but here as direct
function calls a use Ichor, grammar:, actions: caller splices
straight into its own module (via Ichor's __using__ macro),
skipping bytecode interpretation entirely.
Compiles every token in a grammar (character-level: Literal,
CharClass, Any, RuleRef to another token, and the combinators
Seq/Choice/Star/Plus/Opt/Rep/AndPred/NotPred -- the
same shapes Grammar.VM.CharCompiler compiles) into quoted Elixir
function definitions instead of bytecode.
The compile-time codegen backend for @engine glr grammars: builds
the SLR(1) table via Grammar.LRTable once, at Elixir-compile-time
(conflicts are expected here, never rejected -- that's
Grammar.Native.LR's job), and compiles the action/goto lookup into
per-state generated function clauses instead of the interpreted
Grammar.GLR's Map.gets.
The compile-time codegen backend for @engine lr grammars: builds the
SLR(1) table via Grammar.LRTable once, at Elixir-compile-time (inside
use Ichor's macro expansion), and requires it be conflict-free --
same requirement Grammar.LR.compile/1 enforces at runtime, just
checked once here instead of on every call.
Compiles every rule in a grammar into quoted Elixir function
definitions run against the lexer's token stream (never raw
characters -- Aether's own two-stage split), mirroring
Grammar.VM.RuleCompiler's own semantics exactly (bare-reference
implicit self-capture, Indent/@samecol, capture-shape rules) but
producing direct function calls instead of bytecode.
Generates the Tokenizer -> Lexer half of a compiled grammar's tokenize/2
(char-level maximal munch via Grammar.Native.CharCompiler, then
@keywords/@refine reclassification via the shared, backend-agnostic
Grammar.Lexer) -- extracted once out of Grammar.Native.generate/2 so
Grammar.Native.LR/Grammar.Native.GLR can reuse it unchanged: every
engine consumes the exact same token stream, only the parser half
differs (direct PEG combinator calls, vs. compiled LR/GLR state
dispatch).
Token introspection for a compiled Aether.Grammar -- the logic behind
mix ichor.tokens. Depends only on the Aether front-end's output
(token_order/tokens/anon_tokens), not the analysis pass or either
backend, so it works on any grammar that parses, even one the analysis
pass would otherwise reject.
The interpreted runtime backend: compiles an Aether.Grammar to
bytecode and runs it against real input.
Compiles every token in a grammar (character-level: Literal,
CharClass, Any, and RuleRef to another token -- the only node
shapes an ordinary token body can contain) into one linked
Grammar.VM.Program, run by Grammar.VM.Tokenizer.
Runs a Grammar.VM.CharCompiler-produced program against raw input
text -- the machine Grammar.VM.Tokenizer uses to test one token at a
given starting position.
The combinator half of Grammar.VM's bytecode compilation, shared
between the character-level compiler (token bodies, run by the lexer)
and the token-stream-level compiler (rule bodies, run by the parser).
Seq/Choice/Star/Plus/Opt/Rep/AndPred/NotPred compile
identically either way -- only the leaf nodes differ
(Literal/CharClass/Any/RuleRef for tokens; RuleRef/Indent/
Capture for rules -- tokens can never contain a Capture), so each
caller supplies its own leaf_fun for those.
Turns [{name, ops}] -- one label-relative op list per token or rule,
as produced by Grammar.VM.Compiler -- into a single linked
Grammar.VM.Program.
Linked PEG bytecode: every token (or every rule) in a grammar, compiled
into one shared instruction tuple so :call can jump between them by
index, plus a name -> entry-index map for looking up where to start.
Compiles every rule in a grammar into one linked Grammar.VM.Program,
run by Grammar.VM.TokenInterpreter against the lexer's token stream
(never against raw characters -- Aether's two-stage Lexer -> Parser
split). A RuleRef compiles to {:token, name} (consume one stream
token of that type) when it names a token, or {:call, name} (jump
into that rule's own compiled code) when it names another rule -- the
two are otherwise indistinguishable in the IR, so the grammar's own
token/rule namespaces are what disambiguate.
Runs a Grammar.VM.RuleCompiler-produced program against a lexed token
stream -- the machine Grammar.VM uses for the parser stage.
Tokenizes a full input string against a Grammar.VM.CharCompiler
program, using maximal munch: at each position, every declared token is
tried, the longest match wins, ties are broken by declaration order.
Trivia/skip tokens are emitted like any other -- filtering them out is
the parser's job, via the Star(skip_token) that Aether.Parser
already spliced into rule bodies, not the lexer's.
Ichor reads grammar definitions -- its own language Aether, plus ABNF,
BNF, EBNF, and PEG importers -- and turns each one into a working
Lexer + Parser + Executor for whatever language the grammar describes,
via either an interpreted VM backend (Grammar.VM) or compile-time
native codegen (__using__/1 below).
The ABNF (RFC 5234 + RFC 7405) front-end: priv/grammar/abnf.aether
parsed by Aether's own front-end, compiled by the native codegen
backend (use Ichor), dispatching to Ichor.ABNF.Actions
(Grammar.IR as its target category, same as Regex.Actions).
Turns a parsed ABNF rulelist (RFC 5234 + RFC 7405) into a real
%{rule_name_atom => Grammar.IR.expr()} map -- one Grammar.IR tree
per ABNF rule, mirroring how Regex.Actions turns a single
/pattern/ into one Grammar.IR tree. This is an importer's own
output, not a runnable Aether.Grammar -- ABNF has no lexer/rule
(token/parser) split the way Aether does, so deciding which of an
ABNF ruleset's productions become Aether tokens vs. rules is a
separate concern this module doesn't address.
The classical BNF front-end -- no single citable standard exists;
this follows the ALGOL 60 Report's own convention with quoted
terminals, matching how BNF is actually written today (see
Ichor.BNF.Actions's own moduledoc for the deliberate deviation that
is): priv/grammar/bnf.aether parsed by Aether's own front-end,
compiled by the native codegen backend (use Ichor), dispatching to
Ichor.BNF.Actions (Grammar.IR as its target category).
Turns a parsed classical BNF grammar_file into a real
%{nonterminal_name_atom => Grammar.IR.expr()} map -- one
Grammar.IR tree per rule, the same target category Ichor.ABNF.Actions
and Regex.Actions both use.
The ISO/IEC 14977 EBNF front-end: priv/grammar/ebnf-iso.aether
parsed by Aether's own front-end, compiled by the native codegen
backend (use Ichor), dispatching to Ichor.EBNF.ISO.Actions
(Grammar.IR as its target category).
Turns a parsed ISO/IEC 14977 EBNF syntax into a real
%{meta_identifier_atom => Grammar.IR.expr()} map -- one Grammar.IR
tree per rule, the same target category Ichor.ABNF.Actions and
Ichor.BNF.Actions both use.
The W3C-style EBNF front-end -- the notation the XML 1.0 spec's own
section 6 "Notation" uses, also shared by XQuery/XPath's grammars:
priv/grammar/ebnf-w3c.aether parsed by Aether's own front-end,
compiled by the native codegen backend (use Ichor), dispatching to
Ichor.EBNF.W3C.Actions (Grammar.IR as its target category).
Turns a parsed W3C-style EBNF grammar_file (the notation the XML 1.0
spec's own section 6 uses, also shared by XQuery/XPath's grammars)
into a real %{ident_atom => Grammar.IR.expr()} map -- one
Grammar.IR tree per rule, the same target category every other
importer this project builds uses.
Turns an ABNF/BNF/ISO-EBNF/PEG ruleset (as Ichor.ABNF/Ichor.BNF/
Ichor.EBNF.ISO/Ichor.PEG produce them -- %{rule_name_atom => Grammar.IR.expr()}, explicitly documented as "not a runnable
Aether.Grammar") into one that actually is: picks a root (the
source's own first-declared rule, unless overridden), assembles a
full %Aether.Grammar{}, and (for ABNF specifically) fills in RFC
5234 Appendix B's implicit "core rules" (ALPHA, DIGIT, CRLF,
...) for anything the source references but never defines itself --
extremely common in real ABNF, which generally assumes those are
always available.
The PEG front-end (Ford's paper, pest/PEG.js-style convention):
priv/grammar/peg.aether parsed by Aether's own front-end, compiled
by the native codegen backend (use Ichor), dispatching to
Ichor.PEG.Actions (Grammar.IR as its target category).
Turns a parsed PEG grammar_file (Ford's paper, pest/PEG.js-style
convention) into a real %{ident_atom => Grammar.IR.expr()} map --
one Grammar.IR tree per rule, the same target category every other
importer this project builds uses.
The behaviour a @refine("Module", "function", ...) token suffix
dispatches to -- reclassifying (or validating/decoding) a token the
Tokenizer already matched, before the Parser ever sees it. @keywords
is sugar for the common table-lookup case of this same mechanism (see
Grammar.Lexer); @refine is the escape hatch for anything needing
real logic: escape-sequence decoding, or disambiguating a token based
on what came immediately before it (JS's / being a regex-literal
start or a division operator, depending on the preceding token).
Small, IR-agnostic quote/unquote mechanics -- not a code
generator, and deliberately not a framework for building one (no
tree-walker, no visitor dispatch, no assumed node shape, no assumed
function-signature convention). Each function here solves exactly one
mechanical pain point that recurs when hand-writing a compiler-to-
quoted-Elixir backend, extracted from Grammar.Native's own codegen
(CharCompiler, RuleCompiler, Native.LR) after the same handful
of tricks turned up independently duplicated across (and within) those
modules -- the same duplication signal that justified
Ichor.Toolkit.Fixpoint.
The generic "repeatedly recompute a value from itself until it stops
changing" primitive -- extracted from a pattern that had already been
hand-written three separate times in Ichor's own compiler internals
before this existed: Grammar.Analysis's private fixpoint/3
(nullable-set and always-empty-set computation, both over a
MapSet.t(atom()) of rule/token names) and Grammar.LRTable.Sets'
private fixpoint/2/fixpoint_map/2 (FIRST/FOLLOW-set computation,
one over a MapSet, one over a Map). None of those three needed
anything MapSet- or Map-specific about the iteration itself -- only
about what step_fn does with the value in between -- which is
exactly what this generalizes: step_fn closes over whatever fixed
data it needs (a grammar's own rule list, a graph, ...), this module
never sees that data at all, only the value being iterated.
Small, generic graph primitives -- extracted from Grammar.Analysis's
own private reachable/2/do_reachable/3 (used to detect left
recursion: a rule is left-recursive exactly when it's reachable from
its own leading references). Nothing here is grammar-specific --
neighbors_fn is the caller's own adjacency, a plain function rather
than a hardcoded map, so this works over whatever graph an author's
own AST/IR/dependency structure represents, not just Ichor's own
rule-reference graphs.
The off-side-rule (indentation-sensitive layout) algorithm Python's
own tokenizer uses, and Haskell's/F#'s: given each logical line's
indentation width, in order, maintain a stack of open indentation
levels and emit :indent/:dedent markers as the width rises or
falls -- so a parser downstream never has to know about columns at
all, just consumes ordinary tokens.
A generic nested lexical scope / symbol table: a stack of name->value
bindings, innermost scope first, supporting the "define here, look up
through enclosing scopes" shape almost every real language's semantic
analysis needs (variable resolution, nested function/block scoping,
...). No existing internal precedent to extract this from (unlike
Ichor.Toolkit.Fixpoint/Graph) -- a from-scratch design, but a
standard, well-understood one.
Hindley-Milner-style let-polymorphism: generalize/4 and
instantiate/3, built on Ichor.Backtrack.Bindings.unify_occurs_check/4
(plain unify/4 deliberately has no occurs-check, matching ISO
Prolog's own default -- a type checker needs one, or unifying a type
variable with a type containing itself would silently build an
infinite type instead of failing).
Mix Tasks
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.
Reads a .aether grammar file and lists every token it defines --
user-declared, anonymous (auto-promoted from an inline rule literal),
and the five predefined tokens (always present whether overridden or
left at their default) -- in token_order, the same order the Lexer's
maximal-munch tie-break actually uses, alongside a rendered pattern for
each (Grammar.Tokens).