Aether's own syntax, written as an Aether grammar. Ichor doesn't read
.aether files with this grammar — Aether.Lexer and Aether.Reader
are hand-written — so this is a description, kept honest by a test
rather than by being the implementation: test/aether/grammar_doc_test.exs
compiles the block below and checks that it accepts exactly the files
Aether.Reader accepts, across every .aether file in this repository
and a set of deliberately broken ones.
See the reference for what each construct means; this page only says what is well-formed.
The grammar
@grammar "aether"
@root grammar_file
@skip TRIVIA
COMMENT := ";" (!"\n" .)*
TRIVIA := (SPACE | COMMENT)*
UPPER_NAME := [A-Z_] [A-Z0-9_]*
LOWER_NAME := [a-z] [a-z0-9_\-]*
NUMBER := DIGIT+
ESCAPE := "\\" ("x" HEX HEX | "u{" HEX+ "}" | [nrt] | [^a-zA-Z0-9])
STRING := "\"" (ESCAPE | !"\"" .)* "\"" (("cs" | "i") ![a-zA-Z0-9_] | !"cs" !"i")
POSIX := "[:" ("alpha" | "alnum" | "digit" | "space" | "hex") ":]"
CHAR_CLASS := "[" "^"? (POSIX | ESCAPE | !"]" .)* "]"
REGEX := "/" ("\\" . | "[" ("\\" . | !"]" .)* "]" | ![/\[] .)* "/"
grammar_file := TRIVIA? header definition* TRIVIA?
header := "@grammar" name:STRING "@root" root:LOWER_NAME pragma*
pragma := "@skip" UPPER_NAME | "@noskip" | "@case_insensitive" | "@engine" LOWER_NAME
definition := token_def | rule_def | keywords_def
token_def := UPPER_NAME ":=" choice refine?
rule_def := LOWER_NAME ":=" choice
keywords_def := "@keywords" UPPER_NAME "{" keyword ("," keyword)* "}"
keyword := STRING "->" UPPER_NAME
refine := "@refine" "(" STRING "," STRING ("," UPPER_NAME)* ")"
choice := sequence ("|" sequence)*
sequence := (!def_head item)+
def_head := (UPPER_NAME | LOWER_NAME) ":="
item := "~"? term
term := (LOWER_NAME ":")? postfix
postfix := ("&" | "!") primary | primary quantifier?
quantifier := "*" | "+" | "?" | "{" NUMBER ("," NUMBER?)? "}"
primary := STRING | CHAR_CLASS | REGEX | "." | UPPER_NAME | LOWER_NAME
| "(" choice ")" | layout | native
layout := ("@indent" | "@samecol") ("(" choice ")" | postfix)
native := "@native" "(" STRING "," STRING ("," LOWER_NAME)* ")" hint?
hint := "@hint" "(" hint_entry ("," hint_entry)* ")"
hint_entry := LOWER_NAME ":" (LOWER_NAME | "(" (LOWER_NAME ("," LOWER_NAME)*)? ")")Reading it
- Upper-case names are tokens, lower-case names are rules — the
same split the grammar describes.
UPPER_NAMEandLOWER_NAMEare exactlyAether.Lexer's two identifier shapes; anything else, likeFoo, matches neither and fails to lex. sequencestops in front ofNAME :=. That lookahead is the whole reason a definition needs no terminator: a bare name is otherwise a perfectly good next term.TRIVIA?at both ends ofgrammar_filebecause@skiponly splices between the parts of a rule, never before the first or after the last — a file may begin with a comment and end with a newline.- Pragmas are header-only.
pragma*sits insideheader, so a@skipwritten after the first definition is a syntax error, as it is forAether.Reader.
Beyond syntax
Aether.Reader checks more than a context-free grammar can say. It
also rejects:
- a character class,
.or/regex/in a rule body, and a rule name in a token body; - a
name:capture in a token body — a token matches as one piece of text; ~anywhere but directly before a bare name, inside a token, or under@noskip;- a pragma given twice,
@skiptogether with@noskip, and an@engineother thanpeg,lrorglr; - a name declared twice, and both
@keywordsand@refineon one token; - in
@hint, keys other thannullableandleading,leading:in a token, and aleading:name that isn't a dependency.
Undefined references, redeclared predefined tokens and left recursion
are checked later, by Aether.Eval and Grammar.Analysis, once the
whole grammar has been read.