Epsil Syntax
Notation
In the grammar below, the following notation is used:
- An arrow (→) marks grammar productions and can be read as "can consist of"
- Syntactic categories are written in lowercase italic (newline) on both sides of a production rule.
- Placeholders for recursive syntactic categories are indicated by ···.
- Literal words and punctuation are indicated in bold (+) or as a Unicode codepoint (U+00A0) or as a Unicode codepoint range (U+2000-U+200A).
- Alternatives are indicated by a vertical bar (|)
- Optional elements are indicated in square brackets
- Elements that can repeat 1 or more times are indicated by a trailing plus sign
- Elements that can repeat 0 or more times are indicated by a trailing star sign
- Elements that can repeat 0 or more times, separated by a another element are indicated with a trailing hash sign, followed by the separator. If no separator is provided, the comma (,) is implied.
Grammar overview
The productions below describe the source forms accepted by the current
parser. The Unicode identifier rules are described under
Symbols, and the type following a : or return
arrow is parsed using the
Compute Engine type language. Detailed
match patterns are documented under
Control Flow.
quoted-text-item → U+0000-U+0009 U+000B-U+000C U+000E-U+0021 U+0023-U+2027 U+202A-U+D7FF | U+E000-U+10FFFF
linebreak → (U+000A [U+000D]) | U+000D | U+2028 | U+2029
unicode-char → quoted-text-item | linebreak | U+0022
pattern-syntax → U+0021-U+002F | U+003A-U+0040 | U+005b-U+005E | U+0060 | U+007b-U+007e | U+00A1-U+00A7 | U+00A9 | U+00AB-U+00AC | U+00AE | U+00B0-U+00B1 | U+00B6 | U+00BB | U+00BF | U+00D7 | U+00F7 | U+2010-U+203E | U+2041-U+2053 | U+2190-U+2775 | U+2794-U+27EF | U+3001-U+3003 | U+3008-U+3020 | U+3030 | U+FD3E | U+FD3F | U+FE45 | U+FE46
inline-space → U+0009 | U+0020
pattern-whitespace → inline-space | U+000A | U+000B | U+000C | U+000D | U+0085 | U+200E | U+200F | U+2028 | U+2029
whitespace → pattern-whitespace | U+0000 | U+00A0 | U+1680 | U+180E | U+2000-U+200A | U+202f | U+205f | U+3000
line-comment → // (unicode-char)* linebreak)
block-comment → /* (((unicode-char)* linebreak)) | block-comment)
*/
digit → U+0030-U+0039 | U+FF10-U+FF19
hex-digit → digit | U+0041-U+0046 | U+0061-U+0066 | U+FF21-FF26 | U+FF41-U+FF46
binary-digit → U+0030 | U+0031 | U+FF10 | U+FF11
numerical-constant → NaN | Infinity | +Infinity |
-Infinity | oo | +oo | -oo
(oo is an input alias for Infinity; the serializer always emits the
canonical Infinity spelling.)
base-10-exponent → (e | E) [sign](digit)+
base-2-exponent → (p | P) [sign](digit)+
exponent → base-10-exponent | base-2-exponent
binary-number → 0b (binary-digit)+ [. (binary-digit)+
][exponent]
hexadecimal-number → 0x (hex-digit)+ [. (hex-digit)+
][base-2-exponent]
decimal-number → (digit)+ [. (digit)+ ][exponent]
The digit runs of a number literal may contain _ grouping separators
(1_000, 0xFF_FF); an underscore is ignored and never begins or ends a
run. A hexadecimal-number takes only a base-2-exponent because e and
E are hexadecimal digits, so they cannot double as an exponent marker.
sign → + | -
signed-number → numerical-constant | ([sign] (binary-number | hexadecimal-number | decimal-number))
symbol → verbatim-symbol | inline-symbol
verbatim-symbol → ` symbol-start (symbol-continue)*
`
The content of a verbatim-symbol is taken literally: no escape sequences
are applied, and it must still be a valid symbol name. The form exists to
write symbols whose name is a reserved word, e.g. `while`.
inline-symbol → symbol-start (symbol-continue)*
symbol-start and symbol-continue follow the Unicode profile described under Symbols. Reserved words are not accepted as inline-symbol; use the verbatim form.
escape-expression → \( expression )
single-line-string → " (escape-sequence | escape-expression |
quoted-text-item)* "
multiline-string → """ multiline-string-line """
extended-string → (#)+ " (unicode-char)* " (#)+
The number of trailing # must match the number of leading # that
opened the literal (#"…"#, ##"…"##, …). No escape sequences are applied
inside an extended string, so it can hold " and \ literally.
string → single-line-string | multiline-string | extended-string
String escapes, interpolation, multiline indentation and continuation are specified in Literals.
parenthesized → ( expression )
list → [ [(expression)#,] ]
set → { [(expression)#,] }
dictionary → { [(key-value-pair)#,] } | {->}
key-value-pair → expression -> expression
block → { [(statement)#statement-separator] }
do-block → do block
latex-island → $ (unicode-char | \$)* $
pragma → #line | #column | #url | #filename |
#date | #time | pragma-call
pragma-call → (#env | #navigator | #warning |
#error) ( [(expression)#,] )
if-expression → if expression block
[else (block | if-expression)]
match-expression → match expression { match-case+ }
primary → signed-number | symbol | string | pragma | latex-island | parenthesized | list | set | dictionary | do-block | if-expression | match-expression
call-clause → ( [(argument)#,] )
argument → [...] expression
index-clause → [ (expression)#, ]
field-clause → . symbol
— the . must abut the base; not after a number
literal
postfix-expression → primary (call-clause | index-clause |
field-clause | !)*
expression → primary | prefix-expression | infix-expression | postfix-expression
prefix-expression → (- | !) expression
infix-expression → expression operator expression
literal-parameter → signed-number | string | true | false
— a string literal parameter cannot contain interpolation
parameter → symbol [: type] | literal-parameter
parameters → ( [(parameter)#,] )
effect-label → console | entropy | environment |
fs_read | fs_write | network | random |
scope | time
effect-specifier → pure | any | (effect-label)+
— labels are space-separated; duplicates are rejected;
pure and any cannot be combined with another word
declaration → (let | const) symbol
[: type] [= expression] |
(let | const) tuple-pattern = expression |
symbol : type [= expression]
tuple-pattern → ( (symbol | tuple-pattern)#, )
— at least two elements; _ skips a position
math-function-signature → -> type |
effect-specifier -> type
type-parameter → symbol [: type]
— the bound must be a ground type (it may not mention
another type parameter)
type-parameter-clause → < (type-parameter)#, >
— at least one parameter (<> is rejected); duplicate
names are rejected; the names scope over the definition's HEAD only (its
parameters, effect specifier, and return type), not over its body
function-definition → symbol parameters
[math-function-signature] = expression |
function symbol [type-parameter-clause] parameters
[effect-specifier] [-> type] block
— the <…> clause is claimed only by the
function form: f<T>(x) = x is genuinely ambiguous with a relational
expression, so the math form does not take it
type-declaration → type alias symbol
[type-parameter-clause] = type |
type symbol = type
— only the alias form takes a clause; the
<…> slot of the bare (nominal) form is reserved and rejected. The clause
names scope over the definition only, and each must be used in it
while-statement → while expression block
for-statement → for symbol in expression block
statement → declaration | type-declaration | function-definition | while-statement | for-statement | expression
statement-separator → ; | linebreak
shebang → #! (unicode-char)* (linebreak | _eof)
epsil → ([shebang] (statement)#statement-separator [eof])
The Pratt (precedence-climbing) grammar for _infix-expression_,
_prefix-expression_, and _postfix-expression_ — the operator set, its
precedence, and its associativity — is documented as a table in
Operators rather than spelled out production by
production; the whitespace rule described there (an infix operator has
whitespace on both sides or neither; a prefix operator has no whitespace after
it, and a postfix operator none before it) is part of this grammar, not a
separate lexical concern.
Statements and sequencing
A program is a sequence of statements separated by a linebreak or a ;. Two
expressions on the same line with no separator between them is not a
silent sequence — it is a diagnostic:
1 2
Error: unexpected-symbol "2"
A multi-statement program is a sequence, evaluated in order, whose value is the
value of its last statement. ; is interchangeable with a linebreak as a
separator, so these two programs are identical:
a
2
a; 2
Primary expressions
A primary is the leaf of the expression grammar — the thing an operator or a call/index applies to. The primary forms are:
- a number:
2,3.14,0x1F,0b101 - a symbol:
x,Add - a verbatim symbol:
`while` - a string:
"hello" - a pragma:
#env("HOME") - a parenthesized expression:
(2 + 3) - a list:
[1, 2, 3] - a set:
{1, 2, 3} - a dictionary:
{one -> 1, two -> 2} - a
do { … }block expression:do { let t = 3; t + 1 } - a
$…$LaTeX island:$\frac{1}{2}$— see LaTeX Islands - a function call:
f(x, y) - an index expression:
xs[i] - a field access:
p.x
Calls, indexing and field access
A call is a symbol (or another primary) immediately followed — with no whitespace — by a parenthesized, comma-separated argument list:
f(x, y)
f()
An argument may be prefixed with ... to spread a tuple's elements into the
call's arguments (valid only in call argument lists — see
Spread):
f(...p)
f(1, ...p)
The callee does not have to be a bare symbol. A parenthesized expression, or the result of another call, can be called too:
(getF())(x)
(a + b)(2+1)
Indexing is a primary immediately followed — with no whitespace — by a
bracketed index expression. Indexing is 1-based (xs[1] is the first
element):
xs[i]
f(x)[0]
Field access is a primary immediately followed — with no whitespace — by a
. and a symbol. Chains associate left, and a field value can be called like
any other computed callee:
p.x
a.b.c
p.x(2)
A number literal never takes a field: the lexer folds a trailing dot into
the number, so 2.x is the multiplication 2. * x, and 1..5 stays a
range. See Types for what
p.x means on values of declared types, records and dictionaries.
In all three cases the (, [ or . must directly abut the
callee/indexed expression: whitespace before it means the form is a
separate primary (or, for ., a diagnosed stray token), not a
call/index/field — the same whitespace-sensitivity that governs operators.
Collections, tuples, and dictionaries
- List:
[a, b];[]is the empty list. - Set:
{a, b};{}is the empty set. - Tuple:
(a, b). A single parenthesized element,(a), is just the parenthesized expressiona, not a one-element tuple;()is a diagnostic (expression-expected) — there is no empty tuple — except immediately before a mapsto arrow, where() |-> expris a zero-parameter lambda. - Dictionary:
{k -> v}; an unquoted key becomes a string key. The empty dictionary is spelled{->}, not{}(which is the empty set).
{ … } is disambiguated by looking at the first element once it has been
parsed: if it is followed by a top-level ->, the whole { … } is a
dictionary and every subsequent element must also be a key -> value pair;
otherwise { … } is a set.
A { in expression position is therefore always a collection literal (set
or dictionary); to open a statement block in expression position, prefix it
with do. do { … } is a block expression — a statement sequence whose value
is its last statement — while a bare { … } stays a set/dictionary. See
Blocks.
{ one -> 1, two -> 2 }
Trailing commas are allowed in every collection form (lists, sets, tuples, dictionaries, and call/index argument lists) — friendly to notebook editing and diffs:
[1, 2, 3,] // same as [1, 2, 3]
A bare, top-level comma-separated sequence with no enclosing delimiter (for
example 1, 2, 3 on its own) is not a sequence literal — it is a
diagnostic. A sequence is written only as an explicit call, Sequence(1, 2, 3).
Round-trips
Reading a program and writing it back out reproduces its meaning, but not
necessarily its spelling. A few forms have one canonical rendering: numbers get
a single spelling (with _ digit grouping), a division is always written with
an explicit /, 2x keeps its juxtaposed form where that re-reads
unambiguously (but 2(x+1) and (x+y)(3+4) keep an explicit *, since a
juxtaposed group would read as a call), and is is written as in — the two
spell the same membership test.
Comments are not preserved by a round-trip — see Comments. For the exact list of normalizations, see Round-trip and serialization normalizations.
Relationship to the loose math parser
Epsil is a programming-language syntax. The Compute Engine also ships a
loose math parser that reads LaTeX/ASCII-math notation. The two share a few
surface forms but are not the same language: in Epsil a juxtaposed name is
a single identifier (sin is one symbol, not s·i·n), f(x, y) is a function
call rather than a product, and ** is exponentiation. Do not assume a snippet
means the same thing to both. See
Relationship to the loose math parser
for a form-by-form comparison.