This tutorial introduces Aether's own features one at a time, in the order you're likely to need them, ending with a complete, working grammar. It assumes you already know the basics of running a grammar through Ichor — if not, read the Ichor tutorial first. This page is about Aether the language; see the reference for exhaustive detail on any single feature.
Every snippet below can be parsed and matched the same way:
{:ok, grammar} = Aether.Parser.parse(source)
{:ok, grammar} = Grammar.Analysis.run(grammar)
Grammar.VM.parse(grammar, input)1. The smallest grammar
Two pragmas are always required: @grammar names the grammar,
@root says which rule matching starts from.
@grammar "greeting"
@root greeting
greeting := "hello"Grammar.VM.parse(grammar, "hello") #=> {:ok, 1}
Grammar.VM.parse(grammar, "goodbye") #=> {:error, %Ichor.Error{}}greeting is a rule (lowercase name) whose body is a single string
literal. Aether automatically promotes that literal into a
compiler-generated token behind the scenes — you never declared a token
yourself, and that's fine for a grammar this simple.
2. Tokens: ALL_CAPS names
A character class ([a-z]), a /pattern/ literal, or a bare . can
only ever appear inside a token body, never a rule body directly — a
rule may only reference named tokens and rules. So once a pattern
needs character-level logic (a range, a repetition over characters)
rather than just a fixed string, give it a name as a real token:
@grammar "greeting"
@root greeting
HELLO := "hello"
WORD := [a-zA-Z]+
greeting := HELLO WORDGrammar.VM.parse(grammar, "hello world") #=> {:ok, 3}Notice this already needs no special handling for the space between
HELLO and WORD — that's the next feature. Also notice HELLO is
declared as a real token here, not left as a bare "hello" literal the
way step 1 did: both HELLO and WORD can match "hello" itself, with
the same length, so this is a maximal-munch tie — and ties are
broken by declaration order. HELLO needs to come first in the file so
it wins that tie; see the reference for the
full rule.
3. Whitespace happens automatically
By default, every rule's sequence elements (after the first) get
whitespace-tolerance spliced in for free. You don't write it, and you
don't see it in the grammar text — it's just there. To turn it off
(needed once whitespace becomes meaningful, e.g. for HTTP or Markdown),
add @noskip:
@grammar "tight"
@root pair
@noskip
pair := "a" "b"Grammar.VM.parse(grammar, "ab") #=> {:ok, 2}
Grammar.VM.parse(grammar, "a b") #=> {:error, ...} -- no auto-skip here4. Choice, repetition, and grouping
Standard PEG operators, first-match-wins for |:
@grammar "list"
@root list
WORD := [a-z]+
list := WORD ("," WORD)*Grammar.VM.parse(grammar, "a,b,c") #=> {:ok, 5}? (optional), {n}/{n,}/{n,m} (bounded repetition), and &/!
(lookahead predicates, matching nothing) round out the operator set —
see the reference for the complete list.
5. Naming what you capture
So far, matching only tells you whether input is valid. To get
structure back out for an Ichor.Actions module to work with, name the
parts you care about with name:expr:
@grammar "pair"
@root pair
WORD := [a-z]+
pair := key:WORD "=" value:WORDAn Ichor.Actions module's handle_rule(:pair, %{key: k, value: v}, ctx)
now receives exactly those two captures. A bare reference to a named
token or rule (like WORD used without a name: prefix elsewhere)
is captured under its own name automatically — you only need name:
when you want a different name, or when the same thing is referenced
more than once in the same rule and needs to be told apart.
6. Case sensitivity
Individual literals can force a case either way with a suffix
("lit"i always case-insensitive, "lit"cs always case-sensitive), or
the whole grammar can default every bare literal to case-insensitive:
@grammar "kw"
@root stmt
@case_insensitive
SELECT := "select"
stmt := SELECTGrammar.VM.parse(grammar, "SELECT") #=> {:ok, 1}
Grammar.VM.parse(grammar, "Select") #=> {:ok, 1}7. A regex shorthand for token bodies
Token bodies (never rule bodies) can use a /pattern/ literal as
shorthand for character-level matching, when writing the equivalent out
by hand would be tedious:
NUMBER := /-?\d+(\.\d+)?/This isn't a real regex engine — it desugars entirely into ordinary Aether primitives at parse time, and deliberately rejects anything with no meaningful PEG equivalent (anchors, backreferences, named groups). See the reference for exactly what's supported.
8. Layout-sensitive grammars
@indent(expr) and @samecol(expr) (rule bodies only) give
YAML-style "this line must be indented under that one" structure
without treating whitespace as ordinary skippable trivia:
@grammar "nested"
@root doc
@skip INLINE_WS
DASH := "-"
NEWLINE := "\n"
INLINE_WS := [ \t]*
WORD := [a-z]+
doc := @indent(list)
list := item (NEWLINE @samecol item)*
item := DASH WORDGrammar.VM.parse(grammar, "-a\n-b\n-c") #=> {:ok, 8}Every item in the same list must line up in exactly the same
column — @samecol checks against the column @indent established
when the list began.
9. Putting it together
A tiny key-value config format, combining most of the above:
@grammar "config"
@root config
SPACE := [ \t\n]+
KEY := [a-zA-Z_][a-zA-Z0-9_]*
STRING := "\"" (!"\"" .)* "\""
NUMBER := /-?\d+(\.\d+)?/
config := entry*
entry := key:KEY "=" value:(STRING | NUMBER) ";"No @skip pragma is declared here — leaving it out uses the default
skip mode with the built-in SPACE token. SPACE is redeclared above
with its own definition (equivalent to the default in this case, but it
didn't have to be) because it's redefined before anything actually
uses it; had this grammar written @skip SPACE explicitly, that
pragma itself would immediately count as a use, and the later
SPACE := ... line would be rejected as "already used" — see the
reference for why.
{:ok, grammar} = Aether.Parser.parse(source)
{:ok, grammar} = Grammar.Analysis.run(grammar)
Grammar.VM.parse(grammar, ~S(name = "ichor"; version = 1;))
#=> {:ok, 10}Pair this with an Ichor.Actions module implementing handle_rule(:config, %{entry: entries}, ctx) and handle_rule(:entry, %{key: k, value: v}, ctx)
to fold the whole thing into a real %{String.t() => term()} map — see the
Ichor tutorial for exactly
that pattern, or Aether examples for more complete,
runnable grammars.