Skip to content

Lexical structure

Three comment forms, all trivia (preserved losslessly for tooling, insignificant to the grammar):

// a line comment, to end of line
/* a block comment; the one form that may span lines */
/// a documentation comment, to end of line

A documentation comment is exactly three slashes followed by a non-slash (//// and longer runs are ordinary line comments, matching the Roslyn rule). A run of consecutive /// lines immediately preceding a declaration — a type, function, method, field, property, constant, enum or union case, or an interface requirement — attaches to that declaration: the markers are stripped, the lines join in order, and the text becomes the declared symbol’s documentation, surfaced by tooling (hover) and carried like metadata XML docs. Blank lines between the run and the declaration do not detach it (all trivia between two declarations belongs to the following one). Doc text may be plain prose or XML-doc elements (<summary>, <param>, <returns>, <remarks>); plain prose renders as a paragraph.

identifier = ( letter | "_" ) { letter | digit | "_" } .
letter = "A" … "Z" | "a" … "z" .
digit = "0" … "9" .

A letter or underscore, followed by letters, digits, or underscores. Case-sensitive.

Initial case carries no meaning. A declaration, member, local, or parameter may be spelled in any case, and the compiler reads the same program either way. The conventions below are recommendations.

Case used to be part of the grammar: an upper-case initial was how Foo { ... } was told from a value’s member-then-block, so the convention had to be enforced as a hard error to stop a lower-case type from silently never constructing. The grammar decides by CONTEXT instead — a brace that opens a body (a control-flow condition, a match subject, a catch guard, a for … in collection) is never an initializer — so the requirement is gone and ES2160 / ES2161 are retired.

  • Types are PascalCase, by convention. struct, class, union, ref union, enum, interface, static, and delegate func names. Interfaces additionally follow the .NET I-prefix convention in E# source (ISized, IEnumerable<T>).
  • Free functions are camelCase, by convention. A method (a function with a receiver block) and a static member are conventionally PascalCase, to match the .NET member they sit beside.
  • Private fields may be _underscore-prefixed (_cache, _count), the familiar C# spelling — legal, and a reasonable style for a genuine private raw field, especially one coexisting with a same-named public surface. Note, however, that C#‘s _field prevalence does not translate: it exists there largely to hand-write backing fields behind properties, and in E# a member let/var declaration is a property with compiler-managed storage. Reaching for _field plus manual accessors where a let/var member (or a bare name: T field) would do is an antipattern in this language — prefer the property forms, and keep the underscore for the raw-field cases that remain.

Always reserved — never usable as identifiers:

GroupKeywords
Control flowif else return while for in match default defer select break continue
Type reflectiontypeof
Declarationsnamespace struct class func const let var enum union interface using ref pub priv readonly with
Concurrencyspawn chan await
Errors / boundarytry catch throw out params
Operator wordsand or not
Constantstrue false nil

Contextual keywords — matched by text in a specific position only; valid identifiers everywhere else (so let static = 1 is legal):

KeywordReserved position
static abstract open virtual task base returnsdeclaration position
protectedbefore init or func in a class body — a family constructor or method
deriveimmediately after @ in declaration position — the member-generation prefix
selfthe receiver; also after : between an init parameter list and body — constructor delegation
delegatebefore func at member scope — mints a nominal delegate type
eventbefore name : in a class / interface body
raisebefore EventName( in statement position
newexpression position, immediately before a type name followed by {, (, or <
initmember position in a class body — a constructor; namespace position before { — once-only host initialization
requiredfield position in a struct / class body — the field must be set by a composite literal
yieldstatement start inside an IAsyncEnumerable<T> function
extbefore the receiver name in a receiver block inside a static facet — marks an extension
assemblybefore : at the start of an attribute at unit scope — the attribute target

typeof is reserved rather than contextual, and that is forced rather than chosen: typeof(T) is spelled exactly like a call of a one-argument function named typeof, so a contextual reading would be ambiguous at every occurrence rather than at an unusual one.

new is the sharpest case: it is recognized only before Type{ / Type( / Type< in expression position (new Point { x: 1 }, new Vec2(3, 4)), and is an ordinary identifier everywhere else (let new = 1 compiles). There is no async keyword — await alone makes a function asynchronous (see Concurrency).

pub and priv are the visibility modifiers, written after any leading compiler directives or CLR attributes and before the declaration keyword. There are three levels, and the absence of a modifier is the meaningful middle one:

ModifierCLR accessibilityApplies to
pubpublicexposed to a referencing assembly
(bare)assembly (internal) — the defaultvisible across the current assembly only
privprivatevisible to the declaring type only

A bare declaration is internal, not public — a top-level struct, func, field, or property with no prefix is reachable from every type in the same assembly and invisible to external consumers, mirroring C#‘s default. Only pub crosses the assembly boundary. priv is the narrowest, for a member the declaring type alone should reach.

A nested type inverts the default to private (the C# nested-type default): a nested type with no prefix is NestedPrivate, reachable only from its enclosing type, while pub makes it NestedPublic and a bare-internal nested type is NestedAssembly. The accessibility a construct lowers to per position — field, property accessor, type, nested type, and constructor — is tabulated in CLR mapping → field & property visibility; the field and property surface specifically is in Declarations → field & property visibility.

CategoryOperators
Arithmetic+ - * / %
Bitwise and shifts& | ^ ~ << >> >>>
Comparison== != < > <= >=
Logical&& || ! (or and or not)
Compound assignment+= -= *= /= %= &= |= ^= <<= >>= >>>=
Error propagation? (postfix)
Ternary? :
Null-coalescing??
Null-conditional?.
Range..
Index-from-end^
Heap constructionnew (contextual keyword)
Address-of&
By-ref pass (call site)* / &
Return type->
Expression body= (after the signature)
Arrow lambda=>
Assignment=
Compiler directive@

The postfix ? (try-unwrap) is disambiguated from the ternary ? by lookahead: if the next token can start an expression, it is a ternary.

@ is also the sigil of a delimited region — a block whose contents are not E# tokens at all, but raw text for the template that claims the sigil. E# defines no sigil of its own; @doc below stands for one a referenced pack claims:

@doc { <p>{source.title}</p> }

The set of claimed sigils is a project input, not a keyword table: it comes from the provider packs the project references, and is known before lexing begins. This is the one place where what a file tokenizes to depends on the project, and it is why derive is reserved after @ while every other sigil is not.

A { opens a region body only immediately after @ and a claimed sigil, with an optional ( … ) target group between. The body runs to its matching }, counting braces and nothing else: the lexer does not know what a string or a comment is inside a grammar it has never seen, so brace depth is the only rule it can state. An unbalanced region is ES2940.

Where nothing claims the sigil, no region exists — @nope { … } lexes as @, an identifier, and an ordinary block, and fails as ordinary E#. The whole body is one token, so nothing inside it is subject to any rule on this page.

A newline ends a statement. It does not end one when the token across it can only be the middle of an expression, so an expression may break on either side of a binary operator with no line-ending marker and no significant whitespace:

const Banner = "one\n"
+ "two\n"
+ "three\n"
let ready = first.open()
&& second.open()
&& third.open()
let scaled = width *
factor

The two directions carry different obligations.

After an operator the continuation is unconditional. An operand is already owed, so the newline cannot be a terminator — this holds for every binary operator, for ??, for both halves of a ternary, and for =. A blank line does not end it either: the one-newline limit belongs to the other direction, and there is nothing a dangling operator could mean on its own.

= owes a value exactly as + owes an operand, so a long value takes its own line. This holds wherever an = appears — a let or var initializer, a const, an assignment or compound assignment, a store through a path or an indexer, and a field default:

let render: Func<Request, Response> =
(req) => Response(req.path, 200)
table[Key(section, row)] =
Entry(label, weight, revision)

Before an operator the newline is consumed only when the continuation is unambiguous, under three conditions:

  • One newline. A blank line ends the expression. It is the strongest separator a reader has, and letting an operand attach to something a paragraph above turns a typo into an error far from its cause.
  • A dual-role operator shall be spaced. +, -, *, and & are also prefix operators, so a line-leading one is ambiguous with a pointer store (*p = v), an address-of, or a negation. Spacing decides: spaced on both sides is binary, glued to its operand is prefix. A glued line-leading operator is therefore a located error rather than a silent reinterpretation of the line above.
  • Not a match arm body. Inside => Expr a leading .case or -1 is the NEXT ARM’S PATTERN. The same restriction applies to the leading-. fluent chain below and for the same reason.

A leading . continues a fluent chain under the identical rule; see Methods §Fluent calls.

Turtle()
.apply(a)
.apply(b)

is and as participate as ordinary binary operators, so a type test may break before its keyword.

int_lit = dec_lit | hex_lit | bin_lit .
dec_lit = digit { digit | "_" } .
hex_lit = ( "0x" | "0X" ) hex_digit { hex_digit | "_" } .
bin_lit = ( "0b" | "0B" ) ( "0" | "1" ) { "0" | "1" | "_" } .
hex_digit = digit | "a" … "f" | "A" … "F" .
float_lit = digit { digit | "_" } "." digit { digit | "_" } [ exponent ]
| digit { digit | "_" } exponent .
exponent = ( "e" | "E" ) [ "+" | "-" ] digit { digit } .
string_lit = `"` { string_char | interpolation } `"` .
byte_string_lit = "b" `"` { string_char | escape } `"` .
char_lit = "'" ( char_char | escape ) "'" .
bool_lit = "true" | "false" .
nil_lit = "nil" .
TypeExamples
Integer42 · 0 · 1_000_000 (underscore separators)
Hex / binary0xEDB88320 · 0xFF · 0b1010 (a hex/binary integer)
Float3.14 · 0.5 · 1.0e10
String"hello" · "hello {name}" (interpolated)
Byte stringb"MFL1" · b"\x00\x01" (a byte[] of the content’s UTF-8 bytes)
Char'a' · '\n'
Booleantrue · false
Nullnil

A leading - is the unary negation operator, not part of the literal. Integer and fractional literals use decimal source digits; _ may separate digit groups anywhere within the digits (1_000_000, 3.141_592). A hexadecimal (0x…) or binary (0b…) integer is prefixed, never suffixed, and binds uint/int/long from context exactly as a decimal literal does (0xFFFFFFFF takes a uint-typed target with no u); an out-of-range value is ES2236. A byte-string b"…" is a byte[] constant of the content’s UTF-8 bytes (a \xNN escape contributes a raw byte), the E# analogue of C#‘s "…"u8const-foldable and returnable. A numeric token has no suffix. Its exact source digits are retained until contextual numeric binding; in particular, a literal expected as decimal does not first become double.

Inside a string or char literal, \ introduces an escape:

EscapeMeaningEscapeMeaning
\nnewline\bbackspace
\rcarriage return\fform feed
\ttab\vvertical tab
\0null\aalert
\\backslash\uXXXXUTF-16 code unit (4 hex digits)
\" \'quote\xH…code unit (1–4 hex digits)

An unrecognized escape passes the following character through literally.

interpolation = "{" Expr "}" .

No prefix required. { expr } inside a string inserts the value of any expression — a variable, a member chain ({x.field}), an operator expression ({a + b}), a call ({f(x)}), a ternary ({x > 0 ? "+" : "-"}), an index ({xs[0]}). Each hole is handed to the expression parser and type-checked like any other expression; value types are boxed into the underlying string.Concat call. Braces nest, so object/collection literals inside a hole balance.

The hole rule: a { opens a hole only when the next character can start an expression — a letter, _, (, or !. A leading digit is excluded, so {0} and {0:d} stay literal and BCL format strings pass through to string.Format unchanged.

let msg = "user {u.name} has {u.count} items" // holes
let fmt = "progress: {0:p1}" // literal — passes to string.Format