30 KiB
El Language Specification
Version 1.0.0 — April 29, 2026
Overview
El is a statically-typed, compiled programming language designed as the execution substrate for the Neuron agent runtime. El is self-hosting: the El compiler is written in El, compiled to ELVM bytecode, and executed by the El Virtual Machine. A Rust genesis compiler bootstraps the first iteration; all subsequent compilation is performed by the self-hosted compiler.
El has four defining properties:
-
Self-hosting compiler. The compiler (
lexer.el,parser.el,codegen.el) is written in El, produces ELVM bytecode, and runs on the ELVM. The genesis Rust compiler (el-compilercrate) is used only for bootstrapping. -
First-class application identity. The
appblock is a language-level construct, not a library call. It declares service name, version, typed configuration schema, secrets, and feature flags — all resolved by the runtime before the program body executes. -
Graph-native builtins. Knowledge graph operations (
graph_compile,graph_traverse,graph_write_node,graph_write_edge) are language primitives dispatched by the VM, not library imports. -
Sealed artifact target. The
prodcompilation target produces AES-256-GCM encrypted, BLAKE3-authenticated bytecode containers — quantum-resistant sealed artifacts that are indistinguishable from random bytes without the deployment key.
1. Lexical Structure
1.1 Source Encoding
El source files are UTF-8 encoded. The canonical extension is .el.
1.2 Comments
// Single-line comment — extends to end of line
Block comments are not supported. Use // on each line.
1.3 Whitespace
Spaces, tabs, newlines (\n), and carriage returns (\r) are whitespace. Whitespace is not significant except as a token separator.
1.4 Identifiers
identifier = (alpha | '_') (alnum | '_')*
Identifiers are case-sensitive. Identifiers beginning with __ (double underscore) are reserved for compiler-generated names.
1.5 Keywords
The following words are reserved and cannot be used as identifiers:
let fn type enum match return if else for in while
import from as with sealed activate where test seed
assert protocol impl retry times fallback reason parallel
trace requires deploy to via target true false app
version config secrets flags
1.6 Token Types
| Token | Pattern |
|---|---|
Int |
[0-9]+ |
Float |
[0-9]+ '.' [0-9]+ |
Str |
`'"' (char |
Bool |
true or false |
Ident |
identifier (not a keyword) |
Let Fn Type etc. |
keyword tokens |
EqEq |
== |
NotEq |
!= |
LtEq |
<= |
GtEq |
>= |
Arrow |
-> |
FatArrow |
=> |
ColonColon |
:: |
And |
&& |
Or |
|| |
PipeOp |
|> |
1.7 String Escape Sequences
| Sequence | Character |
|---|---|
\n |
Newline (U+000A) |
\t |
Tab (U+0009) |
\r |
Carriage return (U+000D) |
\" |
Double quote |
\\ |
Backslash |
\0 |
Null byte |
2. Type System
2.1 Primitive Types
| Type | Description | Example literals |
|---|---|---|
Int |
64-bit signed integer | 42, -7, 1_000 |
Float |
64-bit IEEE 754 double | 3.14, 0.5 |
String |
UTF-8 string | "hello" |
Bool |
Boolean | true, false |
Uuid |
RFC 4122 UUID | (runtime-produced only) |
Void |
Unit type; no value | — |
Any |
Dynamically-typed value | (for generic containers) |
2.2 Composite Types
| Type form | Description |
|---|---|
[T] |
Array (ordered sequence) of T |
T? |
Optional T — may be absent |
Map<K, V> |
Key-value map with string keys |
List |
Untyped list (dynamic, used in builtins) |
Named |
User-defined struct or enum |
2.3 Type Inference
The compiler infers types for let bindings without annotation:
let x = 42 // inferred: Int
let s = "hello" // inferred: String
let b = true // inferred: Bool
Function parameter types and return types must always be annotated. Function signatures are specification.
2.4 Type Coercions
Intis implicitly coercible toFloat.Floatis not coercible toInt(usefloat_to_intbuiltin).Tis assignable toT?(non-optional is a subtype of optional).String + Stringperforms concatenation via the+operator overload.
2.5 Optional Type and Null Coalescing
T? denotes an optional value. The null-coalescing operator ?? returns the left operand if it is not nil, otherwise the right:
let name: String? = maybe_get_name()
let display: String = name ?? "unknown"
The ? postfix operator on a function call propagates nil outward (early return of nil if the subexpression is nil):
let val = some_optional_fn()?
2.6 Type Casting
The as keyword performs explicit type casts at runtime:
let n: Int = 42
let f: Float = n as Float
let s: String = f as String
Casting to String invokes the value's string representation. Casting numeric types performs the standard numeric conversion. Invalid casts produce Nil.
3. Variables and Bindings
3.1 Let Bindings
let name: Type = expression
let name = expression // type inferred
All bindings are block-scoped. Bindings are immutable by default. Shadowing is permitted: a new let in the same scope with the same name creates a new binding that shadows the previous one. This is the mechanism for mutation in El — the VM's StoreLocal instruction overwrites the slot for the name.
let count: Int = 0
let count = count + 1 // shadows previous binding; effective mutation
3.2 Scope
Bindings are valid from the point of declaration to the end of the enclosing block. Function bodies, if branches, for bodies, and while bodies each introduce a new scope.
4. Functions
4.1 Function Definition
fn name(param1: Type1, param2: Type2) -> ReturnType {
// body
return expression
}
Functions are first-class values. A function definition emits a jump over the function body and registers the entry IP in the VM's function table via a Push(Int(entry)) StoreLocal("__fn_name") stanza.
4.2 Function Type
The type of a function is expressed as:
fn(Type1, Type2) -> ReturnType
Functions can be passed as values and stored in variables:
let f: fn(Int) -> Int = double
4.3 Return Statement
return expression
An implicit return nil is appended by the compiler if no explicit return is present at the end of a function body.
4.4 Calling Convention
Arguments are pushed left-to-right onto the stack. The function body pops parameters in reverse order (right-to-left) using StoreLocal. Return values are left on the stack top when Return executes.
5. Control Flow
5.1 If/Else
if condition {
// then branch
} else {
// else branch
}
Both branches must produce the same type when used as an expression. The else branch is optional; its absence produces Void. If/else chains use else if:
if x > 0 {
"positive"
} else if x < 0 {
"negative"
} else {
"zero"
}
5.2 While Loops
while condition {
// body
}
Condition is evaluated before each iteration. The loop exits when the condition is false.
5.3 For Loops
for item in collection {
// body
}
collection must be a List or [T]. The compiler desugars for into: compute list, store length, initialize counter at 0, loop while counter < length, load element at counter, execute body, increment counter, jump back.
5.4 Match Expressions
match expression {
Pattern1 => result_expr1
Pattern2 => result_expr2
_ => default_expr
}
Pattern forms:
| Pattern | Meaning |
|---|---|
_ |
Wildcard — always matches |
name |
Binding — captures subject into name |
42 |
Integer literal |
"str" |
String literal |
true / false |
Boolean literal |
EnumName::Variant |
Unit enum variant (future) |
EnumName::Variant(binding) |
Payload-bearing variant (future) |
All arms must produce the same type. A match with no matching arm evaluates to nil.
6. Data Structures
6.1 Struct Types
type TypeName {
field1: Type1
field2: Type2
}
Struct types are registered in the type environment at compile time. Field access is value.field_name, checked at compile time. The VM represents struct instances as Value::Struct { type_name, fields }.
6.2 Enum Types
enum EnumName {
Variant1
Variant2
VariantWithPayload(PayloadType)
}
Variants without parentheses carry no payload. Variants with parentheses carry exactly one value of the given type. Enum variants are referenced as EnumName::Variant.
6.3 Array Literals
let numbers: [Int] = [1, 2, 3]
let empty: [String] = []
6.4 Map Literals
let m: Map<String, Any> = { "key1": value1, "key2": value2 }
Map literals use string keys and Any values. The VM represents maps as Value::Map(Vec<(String, Value)>), preserving insertion order.
6.5 Field Access and Index Access
let field = struct_value.field_name
let elem = array[0]
let val = map["key"]
Index expressions on arrays require an Int index. Bounds violations return nil. String indexing s[n] returns the nth character as a String.
7. Operators
7.1 Arithmetic Operators
| Operator | Types | Result |
|---|---|---|
+ |
Int, Float, String | Same as operands (String: concatenation) |
- |
Int, Float | Same |
* |
Int, Float | Same |
/ |
Int, Float | Same (integer division for Int) |
% |
Int, Float | Modulo |
7.2 Bitwise Operators
| Operator | Types | Result |
|---|---|---|
& |
Int | Bitwise AND |
| |
Int | Bitwise OR |
^ |
Int | Bitwise XOR |
~ |
Int | Bitwise NOT (unary) |
<< |
Int | Left shift |
>> |
Int | Right shift |
7.3 Comparison Operators
| Operator | Result |
|---|---|
== |
Bool |
!= |
Bool |
< > <= >= |
Bool |
7.4 Logical Operators
| Operator | Result |
|---|---|
&& |
Bool |
|| |
Bool |
! |
Bool (unary) |
7.5 Special Operators
| Operator | Meaning |
|---|---|
?? |
Null coalescing: left if not nil, else right |
? (postfix) |
Optional propagation: return nil if subexpression is nil |
as |
Type cast |
|> |
Pipe: x |> f desugars to f(x) |
7.6 Operator Precedence (high to low)
!~(unary), postfix?*/%+-<<>>&^|<><=>===!=&&||??|>
8. Module System
8.1 Import
import "filename.el"
Imports all top-level bindings from the named file into the current scope. The path is relative to the importing file's directory. Import cycles are not permitted.
8.2 From-Import
from module_name import { Name1, Name2 }
Imports named symbols from a module. The parser records the module name and symbol list; the linker resolves them at build time.
9. The App Block
The app block is a first-class language construct that declares service identity. It must appear at top level, before the program body.
app "service-name" {
version "1.0.0"
config {
KEY: Type = default_value
prod {
KEY = "production-override"
}
}
secrets {
SECRET_NAME: Type
}
flags {
feature_name: Bool = false
}
}
9.1 Config Block
Config entries declare typed configuration keys with optional defaults. Environment variables with the same name override defaults. The prod { } sub-block provides environment-specific overrides applied when NEURON_ENV=prod.
Config values are accessed via the config(key) builtin:
let api_url: String = config("NEURON_API_URL")
9.2 Secrets Block
Secrets entries declare required secret values. The runtime loads them from environment variables or from ~/.neuron/secrets.json. Secrets are redacted from all trace output.
Secrets are accessed via the secret(key) builtin:
let token: String = secret("NEURON_TOKEN")
9.3 Flags Block
Feature flags declare boolean runtime switches. Flags are accessed via flag(name):
let enabled: Bool = flag("bidirectional_ctx")
9.4 Runtime Resolution
When the El runtime encounters an app block, it:
- Parses the app block from the source before compilation begins (the
parse_app_blockfunction inbin/el/src/main.rs). - Resolves the active environment from
NEURON_ENV(default:"dev"). - Applies environment-specific config overrides.
- Loads secrets from environment variables, falling back to the secrets file.
- Populates thread-local state:
APP_SERVICE,APP_VERSION,APP_CONFIG,APP_SECRETS,APP_FLAGS,APP_INSTANCE. - Registers secrets in the
SECRET_VALUESset for redaction.
The program body executes after resolution completes. config(), secret(), and flag() builtins read from the thread-local state.
10. The Sealed Block
sealed {
let api_key: String = env("API_KEY")
// sensitive operations
}
The sealed {} construct marks a code region as containing sensitive material. In debug builds, it emits SealedBegin and SealedEnd bytecode markers that signal the debugger not to expose values from this region. In prod builds, the entire artifact is AES-256-GCM encrypted, making the sealed {} annotation redundant but preserved for documentation and tooling.
11. The Activate Construct
activate TypeName where "semantic query string"
activate is a first-class language construct that performs a spreading activation query over the connected Engram knowledge graph and returns a typed array of results.
At runtime, the ELVM dispatches an Activate { type_name, query } instruction to the Engram HTTP API at ENGRAM_URL (default http://localhost:8742). The API performs semantic search over graph nodes and returns matching nodes.
The result type is always [TypeName]:
let users: [User] = activate User where "recent premium subscribers"
When no Engram instance is connected, activate returns an empty array.
12. Graph Builtins
The following builtins provide direct access to the Engram knowledge graph. They are VM primitives dispatched by name without imports.
| Builtin | Signature | Description |
|---|---|---|
graph_compile |
(query: String, depth: Int) -> String |
Compile graph context for LLM injection |
graph_traverse |
(node_id: String, depth: Int) -> List |
BFS from node, return activated nodes |
graph_write_node |
(label: String, content: String, tier: String, tags: [String]) -> String |
Create a node, return UUID |
graph_write_edge |
(from_id: String, to_id: String, relation: String, weight: Float) -> Bool |
Create an edge |
graph_search |
(query: String, limit: Int) -> List |
Full-text search over nodes |
graph_get_node |
(node_id: String) -> Map<String, Any>? |
Fetch a single node by ID |
graph_activate |
(seeds: [String], depth: Int, limit: Int) -> List |
Spreading activation from seed set |
13. HTTP Builtins
| Builtin | Signature | Description |
|---|---|---|
http_get |
(url: String) -> String |
HTTP GET, returns response body |
http_post |
(url: String, body: String) -> String |
HTTP POST with JSON body |
http_put |
(url: String, body: String) -> String |
HTTP PUT with JSON body |
http_delete |
(url: String) -> String |
HTTP DELETE |
http_serve |
(handler_fn: String) -> Void |
Start HTTP server on configured port |
http_serve_on |
(port: Int, handler_fn: String) -> Void |
Start HTTP server on specified port |
HTTP builtins use blocking I/O. The runtime dispatches them synchronously. The handler function receives a request map and must return a response map.
14. Standard Library Builtins
14.1 String Operations
| Builtin | Description |
|---|---|
str_len(s) |
Length in characters |
str_slice(s, start, end) |
Substring |
str_starts_with(s, prefix) |
Boolean |
str_ends_with(s, suffix) |
Boolean |
str_contains(s, substr) |
Boolean |
str_replace(s, from, to) |
Replace first occurrence |
str_split(s, delim) |
Split into List |
str_join(list, delim) |
Join List into String |
str_trim(s) |
Strip leading/trailing whitespace |
str_upper(s) |
Uppercase |
str_lower(s) |
Lowercase |
str_pad_left(s, width, pad) |
Left-pad with pad character |
str_pad_right(s, width, pad) |
Right-pad with pad character |
str_format(template, data) |
{key} interpolation from map |
str_char_at(s, idx) |
Character at index |
str_char_code(s, idx) |
Unicode code point at index |
str_from_char_code(code) |
Character from code point |
14.2 Integer Operations
| Builtin | Description |
|---|---|
int_to_str(n) |
Integer to string |
str_to_int(s) |
Parse integer |
int_to_float(n) |
Widen to float |
abs(n) |
Absolute value |
min(a, b) |
Minimum |
max(a, b) |
Maximum |
14.3 Float Operations
| Builtin | Description |
|---|---|
float_to_str(f) |
Float to string |
float_to_int(f) |
Truncate to integer |
str_to_float(s) |
Parse float |
format_float(f, decimals) |
Format to N decimal places |
math_sin(f) |
Sine |
math_cos(f) |
Cosine |
math_sqrt(f) |
Square root |
math_pi() |
π constant |
decimal_round(f, places) |
Round to N decimal places |
14.4 List Operations
| Builtin | Description |
|---|---|
list_len(l) |
Length |
list_get(l, idx) |
Element at index |
list_append(l, v) |
Append, return new list |
list_push(l, v) |
Alias for list_append |
list_new() |
Empty list |
list_join(l, delim) |
Join to string |
list_range(start, end) |
Integer range |
list_map(l, fn_name) |
Map over list |
list_filter(l, fn_name) |
Filter list |
list_reduce(l, init, fn_name) |
Reduce list |
list_peek_last(l) |
Last element without removing |
14.5 JSON Operations
| Builtin | Description |
|---|---|
json_parse(s) |
Parse JSON string to value |
json_stringify(v) |
Serialize value to JSON |
json_get_string(json, key) |
Extract string field |
json_get_int(json, key) |
Extract integer field |
json_get_float(json, key) |
Extract float field |
json_get_raw(json, key) |
Extract raw JSON sub-object |
14.6 I/O Operations
| Builtin | Description |
|---|---|
println(s) |
Print line to stdout |
print(s) |
Print without newline |
env(key) |
Read environment variable |
getpid() |
Current process ID |
args() |
Command-line arguments as List |
exit(code) |
Exit process |
14.7 File System Operations
| Builtin | Description |
|---|---|
fs_read(path) |
Read file contents as String |
fs_write(path, content) |
Write string to file, return Bool |
fs_mkdir(path) |
Create directory |
fs_list(path) |
List directory entries as List |
fs_exists(path) |
Boolean |
14.8 Time Operations
| Builtin | Description |
|---|---|
time_now_utc() |
Current time as Unix seconds |
time_format(ts, format) |
Format timestamp ("ISO", "RFC") |
time_to_parts(ts) |
Decompose into year/month/day/etc. |
time_from_parts(secs, nanos, tz) |
Construct timestamp |
time_add(ts, amount, unit) |
Add duration ("day", "hour", etc.) |
time_diff(ts1, ts2, unit) |
Difference in units |
14.9 State Operations (Global Mutable State)
The VM maintains a global key-value string store accessible within a process lifetime:
| Builtin | Description |
|---|---|
state_set(key, value) |
Store string value |
state_get(key) |
Retrieve string value |
state_delete(key) |
Delete key |
14.10 Color/Terminal Formatting
| Builtin | Description |
|---|---|
color_bold(s) |
Bold ANSI formatting |
color_dim(s) |
Dim ANSI formatting |
color_red(s) |
Red text |
color_green(s) |
Green text |
color_yellow(s) |
Yellow text |
color_cyan(s) |
Cyan text |
14.11 Native List Primitives (Self-Hosting Compiler)
These primitives are used by the self-hosting compiler internals and are not available in user programs:
| Builtin | Description |
|---|---|
native_list_empty() |
Create empty list |
native_list_append(l, v) |
Append to list |
native_list_get(l, idx) |
Element at index |
native_list_len(l) |
List length |
native_string_chars(s) |
Split string into character list |
native_string_contains(s, substr) |
Substring test |
native_str_to_int(s) |
Parse integer |
native_int_to_str(n) |
Format integer |
15. Compilation Model
15.1 Pipeline
source.el
→ [Lexer] → token list
→ [Parser] → AST (list of statement maps)
→ [Codegen] → JSON bytecode string
→ [Linker] → resolved imports
→ [Wrapper] → ELVM binary container (.elc)
→ [Sealer] → encrypted sealed artifact (.sealed) [prod only]
15.2 Self-Hosting Architecture
The El compiler is written in El:
el-compiler/src/lexer.el— tokenizerel-compiler/src/parser.el— recursive descent parserel-compiler/src/codegen.el— bytecode emitter
These files are compiled by the Rust genesis compiler (engrams/el-compiler/) to produce the self-hosting compiler binary. Once bootstrapped, the self-hosting compiler compiles itself and all subsequent El programs.
The genesis Rust compiler implements the same pipeline in Rust (el-parser, el-compiler crates) as a structural mirror of the El source. Both produce identical bytecode for valid El programs.
15.3 Compilation Targets
| Target | Artifact | Behavior |
|---|---|---|
debug |
.elc + .map.json |
Full source maps, no dead-code elimination, type errors are warnings |
release |
.elc |
No source maps, minor dead-code pruning, type errors are warnings |
prod |
.sealed |
AES-256-GCM encrypted, type errors are fatal, no debug info |
15.4 Incremental Builds
The build system tracks a BLAKE3 hash of every source file in .el/build-cache.json. Only changed files and their dependents are recompiled.
16. Sealed Artifact Format
16.1 Purpose
The prod target produces sealed artifacts — bytecode containers encrypted with AES-256-GCM and authenticated with BLAKE3. Without the deployment key, the artifact is indistinguishable from random bytes. Decompilers and static analysis tools receive AES-GCM ciphertext.
16.2 Wire Format
Offset Size Field
────── ────── ──────────────────────────────────────────────────
0 8 Magic: b"ENGRAM01"
8 2 Format version: u16 big-endian (currently 1)
10 * JSON body: SealedArtifact
SealedArtifact JSON:
{
"algorithm_id": "aes256gcm-v1",
"signature": "<base64 BLAKE3 keyed MAC>",
"encapsulated_key": "<base64 K XOR BLAKE3(deployment_material)>",
"nonce": "<base64 12-byte AES-GCM nonce>",
"ciphertext": "<base64 AES-256-GCM ciphertext + 128-bit auth tag>",
"deployment_fingerprint": "<base64 BLAKE3(binding_key) or null>"
}
16.3 Sealing Process
- Generate a cryptographically random 256-bit symmetric key K.
- Encrypt:
ciphertext = AES-256-GCM(K, nonce=random_96bit, plaintext=bytecode). - Derive binding hash:
H = BLAKE3(deployment_material). - Encapsulate:
encapsulated_key = K XOR H. - Compute MAC:
signature = BLAKE3-keyed(K, algorithm_id ‖ nonce ‖ ciphertext). - Serialize:
ENGRAM01 ‖ version_u16be ‖ JSON(artifact).
16.4 Deployment Binding Modes
| Mode | Description | Security |
|---|---|---|
EnvironmentKey(var) |
Key from environment variable | High |
MachineFingerprint |
Key from hostname + OS + architecture | Medium |
None |
Zero vector (development only) | None |
16.5 Security Properties
AES-256 provides 128-bit post-quantum security under Grover's algorithm. The algorithm_id field supports forward migration to ML-KEM (CRYSTALS-Kyber) without format changes.
17. ELVM Integration
El compiles to ELVM bytecode. See the ELVM specification (elvm.md) for the complete instruction set and execution model. Key integration points:
- The El codegen emits function registrations as
Push(Int(entry_ip)) StoreLocal("__fn_name")stanzas that the VM's scan pass collects into the function table. - User-defined functions are called via
Call { name, arity }which the VM resolves first against the builtin dispatch table, then against the function table. - The
appblock is parsed from source by theelbinary before compilation; it does not appear in the bytecode directly. - The
activateconstruct compiles to anActivate { type_name, query }instruction. - The
reasonconstruct compiles to aReason { query }instruction. - The
parallelblock compiles to aParallel { entries }instruction. - The
deployconstruct compiles to aDeployFn { fn_name, route, target }instruction.
18. Package System
18.1 Project Manifest — manifest.el
The project manifest is an El file — everything is El. The file is named manifest.el
and lives at the project root. It uses El block syntax: space-separated declarations,
no equals signs, strings in "...", integers as bare numbers, arrays as [...].
// manifest.el
package "my-service" {
version "0.1.0"
description "What this does"
authors ["Will Anderson <will@neurontechnologies.ai>"]
edition "2026"
}
dependencies {
engram-http "1.2"
some-local { path "../some-local" }
}
build {
target "prod"
entry "src/main.el"
output "dist/"
seal_key "env:ENGRAM_SEAL_KEY"
}
cross {
targets ["x86_64-linux", "aarch64-linux", "aarch64-macos", "wasm32"]
}
Rules:
- String values use
"..."— no equals sign - Integer values are bare numbers — no equals sign
- Arrays use
[...] - Block sections use
{ }— no[section]headers - Only include sections that are relevant to the project
- The
appsection is for native desktop apps (el-ui); it sets window dimensions
18.2 CLI Reference
el new <name> scaffold new project
el add <pkg>[@ver] add dependency
el remove <pkg> remove dependency
el update update all deps
el build [--target prod] build project
el build --cross build for all cross targets
el run build debug and run
el test run tests
el check type-check only
el fmt format source
el clean clean artifacts
el publish publish to registry
el search <query> search registry
el seal <artifact> seal existing artifact
el unseal <artifact> decrypt sealed artifact
el build-file <file.el> compile single file
19. Grammar (EBNF)
program = (app_block | stmt)* EOF
app_block = "app" STRING "{" app_entry* "}"
app_entry = "version" STRING
| "config" "{" config_entry* "}"
| "secrets" "{" secret_entry* "}"
| "flags" "{" flag_entry* "}"
config_entry = IDENT ":" type_expr ("=" expr)?
| IDENT "{" (IDENT "=" expr)* "}" // env overlay
secret_entry = IDENT ":" type_expr
flag_entry = IDENT ":" "Bool" ("=" expr)?
stmt = let_stmt
| return_stmt
| fn_def
| type_def
| enum_def
| import_stmt
| while_stmt
| for_stmt
| expr_stmt
let_stmt = "let" IDENT (":" type_expr)? "=" expr ";"?
return_stmt = "return" expr? ";"?
fn_def = "fn" IDENT "(" param_list ")" "->" type_expr "{" stmt* "}"
type_def = "type" IDENT "{" (IDENT ":" type_expr ","?)* "}"
enum_def = "enum" IDENT "{" variant* "}"
variant = IDENT ("(" type_expr ")")? ","?
import_stmt = "import" STRING ";"?
| "from" IDENT "import" "{" (IDENT ","?)* "}" ";"?
while_stmt = "while" expr "{" stmt* "}"
for_stmt = "for" IDENT "in" expr "{" stmt* "}"
expr_stmt = expr ";"?
param_list = (param ("," param)*)?
param = IDENT ":" type_expr
type_expr = IDENT
| "[" type_expr "]"
| type_expr "?"
| "Map" "<" type_expr "," type_expr ">"
| "fn" "(" (type_expr ("," type_expr)*)? ")" "->" type_expr
expr = null_coalesce_expr
null_coalesce_expr = or_expr ("??" or_expr)*
or_expr = and_expr ("||" and_expr)*
and_expr = eq_expr ("&&" eq_expr)*
eq_expr = cmp_expr (("==" | "!=") cmp_expr)*
cmp_expr = bitwise_expr (("<" | ">" | "<=" | ">=") bitwise_expr)*
bitwise_expr = add_expr (("&" | "|" | "^" | "<<" | ">>") add_expr)*
add_expr = mul_expr (("+" | "-") mul_expr)*
mul_expr = unary_expr (("*" | "/" | "%") unary_expr)*
unary_expr = "!" unary_expr | "~" unary_expr | postfix_expr
postfix_expr = primary ("." IDENT | "(" arg_list ")" | "[" expr "]" | "?" | "as" type_expr)*
primary = INT | FLOAT | STRING | BOOL
| "(" expr ")"
| "[" arg_list "]"
| "{" (STRING ":" expr ","?)* "}"
| "if" expr "{" stmt* "}" ("else" "{" stmt* "}")?
| "match" expr "{" match_arm* "}"
| "while" expr "{" stmt* "}"
| "for" IDENT "in" expr "{" stmt* "}"
| "activate" IDENT "where" STRING
| "sealed" "{" stmt* "}"
| "parallel" "{" (IDENT ":" expr ","?)* "}"
| "reason" STRING
| IDENT ("::" IDENT)*
arg_list = (expr ("," expr)*)?
match_arm = pattern "=>" expr ","?
pattern = "_" | IDENT | INT | STRING | BOOL