Lexical structure
Comments
Section titled “Comments”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 lineA 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.
Identifiers
Section titled “Identifiers”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, anddelegate funcnames. Interfaces additionally follow the .NETI-prefix convention in E# source (ISized,IEnumerable<T>). - Free functions are camelCase, by convention. A method (a function with a receiver block)
and a
staticmember 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_fieldprevalence does not translate: it exists there largely to hand-write backing fields behind properties, and in E# a memberlet/vardeclaration is a property with compiler-managed storage. Reaching for_fieldplus manual accessors where alet/varmember (or a barename: Tfield) would do is an antipattern in this language — prefer the property forms, and keep the underscore for the raw-field cases that remain.
Keywords
Section titled “Keywords”Always reserved — never usable as identifiers:
| Group | Keywords |
|---|---|
| Control flow | if else return while for in match default defer select break continue |
| Type reflection | typeof |
| Declarations | namespace struct class func const let var enum union interface using ref pub priv readonly with |
| Concurrency | spawn chan await |
| Errors / boundary | try catch throw out params |
| Operator words | and or not |
| Constants | true false nil |
Contextual keywords — matched by text in a specific position only; valid identifiers everywhere
else (so let static = 1 is legal):
| Keyword | Reserved position |
|---|---|
static abstract open virtual task base returns | declaration position |
protected | before init or func in a class body — a family constructor or method |
derive | immediately after @ in declaration position — the member-generation prefix |
self | the receiver; also after : between an init parameter list and body — constructor delegation |
delegate | before func at member scope — mints a nominal delegate type |
event | before name : in a class / interface body |
raise | before EventName( in statement position |
new | expression position, immediately before a type name followed by {, (, or < |
init | member position in a class body — a constructor; namespace position before { — once-only host initialization |
required | field position in a struct / class body — the field must be set by a composite literal |
yield | statement start inside an IAsyncEnumerable<T> function |
ext | before the receiver name in a receiver block inside a static facet — marks an extension |
assembly | before : 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).
Visibility
Section titled “Visibility”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:
| Modifier | CLR accessibility | Applies to |
|---|---|---|
pub | public | exposed to a referencing assembly |
| (bare) | assembly (internal) — the default | visible across the current assembly only |
priv | private | visible 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.
Operators
Section titled “Operators”| Category | Operators |
|---|---|
| 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 construction | new (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.
Delimited regions
Section titled “Delimited regions”@ 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.
Line continuation
Section titled “Line continuation”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 * factorThe 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
=> Expra leading.caseor-1is 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.
Literals
Section titled “Literals”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" .| Type | Examples |
|---|---|
| Integer | 42 · 0 · 1_000_000 (underscore separators) |
| Hex / binary | 0xEDB88320 · 0xFF · 0b1010 (a hex/binary integer) |
| Float | 3.14 · 0.5 · 1.0e10 |
| String | "hello" · "hello {name}" (interpolated) |
| Byte string | b"MFL1" · b"\x00\x01" (a byte[] of the content’s UTF-8 bytes) |
| Char | 'a' · '\n' |
| Boolean | true · false |
| Null | nil |
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 "…"u8 — const-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.
Escape sequences
Section titled “Escape sequences”Inside a string or char literal, \ introduces an escape:
| Escape | Meaning | Escape | Meaning |
|---|---|---|---|
\n | newline | \b | backspace |
\r | carriage return | \f | form feed |
\t | tab | \v | vertical tab |
\0 | null | \a | alert |
\\ | backslash | \uXXXX | UTF-16 code unit (4 hex digits) |
\" \' | quote | \xH… | code unit (1–4 hex digits) |
An unrecognized escape passes the following character through literally.
String interpolation
Section titled “String interpolation”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" // holeslet fmt = "progress: {0:p1}" // literal — passes to string.Format