Focused examples of individual Aether features, each small enough to read in one go. For complete, larger grammars built from several of these features at once, see Ichor's own examples — LISP, YAML, SQL, HTTP, regex, and more.
Bounded repetition — a ZIP code
@grammar "zip"
@root zip
@noskip
zip := DIGIT{5} ("-" DIGIT{4})?Grammar.VM.parse(grammar, "12345") #=> {:ok, 5}
Grammar.VM.parse(grammar, "12345-6789") #=> {:ok, 10}{5} requires exactly five digits; the optional ("-" DIGIT{4})? group
handles the extended ZIP+4 form without a separate rule.
Lookahead — a keyword that isn't a prefix of a longer word
@grammar "kw"
@root kw
@noskip
END := "end"
WORD := [a-z]+
kw := END !WORDGrammar.VM.parse(grammar, "end") #=> {:ok, 1}
Grammar.VM.parse(grammar, "endless") #=> {:error, %Ichor.Error{}}!WORD (negative lookahead) requires that no WORD token starts right
where END left off, without consuming anything itself — the same
technique real keyword-vs-identifier disambiguation needs in any
language with reserved words. Notice END is declared as its own real
token, ahead of WORD in the file — both can match the literal text
"end" with the same length, and maximal munch breaks that tie by
declaration order.
A custom @skip token, for whitespace and comments
@grammar "cmt"
@root prog
@skip TRIVIA
SPACE := [ \t\n]+
COMMENT := "#" (!"\n" .)* "\n"?
TRIVIA := (SPACE | COMMENT)*
WORD := [a-z]+
prog := WORD+Grammar.VM.parse(grammar, "a # comment\nb") #=> {:ok, 3}@skip TRIVIA splices TRIVIA* between sequence elements instead of
the default SPACE* — since TRIVIA itself is defined as ordinary
whitespace or a comment, both are transparently skippable everywhere
the default @skip would only have skipped whitespace.
Repeated named captures arrive as a list
@grammar "csv"
@root row
WORD := [a-z]+
row := first:WORD ("," rest:WORD)*Parsing "a,b,c" gives an Ichor.Actions module
handle_rule(:row, %{first: first, rest: rest}, ctx) where rest is a
list of two captures (b and c) — always a list, even if the input
had been just "a" with no commas at all (rest would be [], never a
missing key).
~ — suppressing automatic whitespace for one term
@grammar "semver"
@root ver
WS := [ ]+
DOT := "."
NUMBER := DIGIT+
ver := NUMBER ~DOT NUMBER ~DOT NUMBERGrammar.VM.parse(grammar, "1.2.3") #=> {:ok, 5}
Grammar.VM.parse(grammar, "1 . 2.3") #=> {:error, %Ichor.Error{}}Every other sequence element still tolerates surrounding whitespace by
default (this grammar declares no @skip, so it uses the built-in
default); ~DOT suppresses that tolerance for just the two dots, so a
version number can't have stray spaces around its own separators even
though the rest of the grammar is whitespace-tolerant.
POSIX classes for identifiers
@grammar "ident"
@root ident
IDENT := [[:alpha:]_][[:alnum:]_]*
ident := IDENTGrammar.VM.parse(grammar, "_private2") #=> {:ok, 1}[:alpha:] and [:alnum:] reference the predefined ALPHA/ALNUM
tokens' own character sets, mixed freely with a literal _ in the same
brackets — the common "identifier" pattern (letter or underscore, then
any number of letters/digits/underscores) without spelling out
a-zA-Z0-9_ by hand.
Case-insensitive keywords
@grammar "bool"
@root bool
@case_insensitive
bool := "true" | "false"Grammar.VM.parse(grammar, "TRUE") #=> {:ok, 1}
Grammar.VM.parse(grammar, "FaLsE") #=> {:ok, 1}@case_insensitive applies to every bare quoted literal in the whole
grammar — useful for languages like SQL where keywords are
conventionally case-insensitive throughout.
A regex-literal token, for compact character-level patterns
@grammar "ip"
@root ip
@noskip
OCTET := /\d{1,3}/
DOT := "."
ip := OCTET DOT OCTET DOT OCTET DOT OCTETGrammar.VM.parse(grammar, "192.168.1.1") #=> {:ok, 7}/\d{1,3}/ desugars to the same Grammar.IR a hand-written
DIGIT{1,3} token body would produce — pure shorthand, valid only
inside a token body, never a real regex engine underneath. See the
reference for exactly what the shorthand
supports.