Core Language Specification

Core Language Specification

rill is an embeddable, sandboxed scripting language designed for AI agents.

Experimental. Active development. Breaking changes will occur before stabilization.

Overview

rill is an imperative scripting language that is dynamically typed and type-safe. Types are checked at runtime, but type errors are always caught—there are no implicit conversions. Type annotations are optional, but variables lock their type on first assignment. The language is value-based: all values are immutable, all comparisons are by value. Empty values are valid (empty strings, lists, dicts), but null and undefined do not exist. Control flow is singular: no exceptions, no try/catch. Data flows through pipes (->), not assignment.

Design Principles

For design principles, see Design Principles.


Quick Reference Tables

Expression Delimiters

DelimiterSemanticsProduces
{ body }Deferred (closure creation)ScriptCallable
( expr )Eager (immediate evaluation)Result value

Operators

CategoryOperators
Arithmetic+, -, *, /, %
Comparison==, !=, <, >, <=, >=
Comparison.eq, .ne, .lt, .gt, .le, .ge methods
Logical! (unary), &&, ||
Pipe->
Capture=>
Spread... (list/tuple spread), ordered[...] spread
Extractiondestruct<> (destructure), slice<> (slice)
Conversion:>type (convert type)
Type:type (assert), :?type (check), :T1|T2 (union assert/check)
Member.field, [index]
Default?? value
Existence.?field, .?$var, .?($expr), .?field&type, .?field&T1|T2 (union), .?field&list(T) (parameterized)

See Operators for detailed documentation.

Control Flow

SyntaxDescription
cond ? then ! elseConditional (if-else, supports multi-line)
$val -> ? then ! elsePiped conditional ($ as condition, supports multi-line)
(cond) @ bodyWhile loop
@ body ? condDo-while
break / $val -> breakExit loop
return / $val -> returnExit block or script
passReturns current $ unchanged (use in conditionals, dicts)
assert cond / assert cond "msg"Validate condition, halt on failure
error "msg" / $val -> errorHalt execution with error message

See Control Flow for detailed documentation. Script-level exit functions must be host-provided.

Collection Operators

SyntaxDescription
-> each { body }Sequential iteration, all results
-> each(init) { body }Sequential with accumulator ($@)
-> map { body }Parallel iteration, all results
-> filter { cond }Parallel filter, matching elements
-> fold(init) { body }Sequential reduction, final result

See Collections for detailed documentation. All four operators (each, map, filter, fold) work on streams, consuming chunks as they arrive and returning collected results when the stream closes. See Collections for stream operator behavior.

Types

TypeSyntaxExampleProduces
String"text", """text""""hello", """line 1\nline 2"""String value
Number123, 0.542, 0.9Number value
Booltrue, falsetrueBoolean value
List[a, b] or list[a, b] (canonical)list["file.ts", 42], list[...$a, 3]List value
Dict[k: v] or dict[k: v] (canonical)dict[output: "text"], dict[$key: 1]Dict value
Orderedordered[k: v]ordered[a: 1, b: "hello"]Ordered value
Tupletuple[...]tuple[1, 2]Tuple value
Datetimedatetime(...) or now()datetime("2024-01-15T10:30:00Z"), now()Datetime value
Durationduration(...)duration(...dict[days: 1, hours: 2])Duration value
Vectorhost-providedapp::embed("text")Vector value
Closure||{ }|x|($x * 2)ScriptCallable
Stream:stream(T):Rhost-providedStream value
Block{ body }{ $ + 1 }ScriptCallable

Type names (valid in :type assertions, :?type checks, and parameter annotations): string, number, bool, closure, list, dict, ordered, tuple, vector, datetime, duration, stream, any, type

Parameterized forms (list(T), dict(k: T, ...), tuple(T, ...), stream(T):R) are also valid in all annotation positions and deep-validate element types at runtime. See Types for stream type documentation and Closures for stream closure syntax.

Union types (T1|T2, T1|T2|T3) are valid in all annotation positions. A union matches if the value satisfies any member. Members can be parameterized: list(string)|dict. See Type System for union type documentation.

List and dict syntax: Both [1, 2] and list[1, 2] produce a list; both [a: 1] and dict[a: 1] produce a dict. The keyword forms (list[...], dict[...]) are canonical — they appear in formatValue output and the LLM reference. Use either form in source; the runtime treats them identically.

See Types for detailed documentation.

Functions

SyntaxDescription
|p: type|{ } => $fnDefine and capture function; type can be parameterized (e.g., |x: list(string)|)
|p: type| { }:rtypeDefine closure with enforced return type; rtype can be parameterized (e.g., :list(string))
|p = default|{ }Parameter with default
|^(min: 0) p|{ }Parameter with annotation
|type|{ } => $fnAnonymous typed closure; $ holds piped value of declared type
$fn(arg)Call function directly
arg -> $fn()Call with pipe value as single arg
arg -> $fn(...)Spread pipe value into call arguments
$fn(...$expr)Spread a variable into call arguments
$fn(a, ...$rest)Mix fixed args with a spread
arg -> $fnPipe-style invoke

Spreading is opt-in via ... or ...$expr. Passing a tuple or ordered value without ... passes it as a single argument. The implicit auto-spread behavior has been removed. At most one spread is permitted per call.

See Closures for detailed documentation.

Special Variables

VariableContainsSource
$Piped value (current scope)Grammar
$ARGSCLI positional args (list)Runtime
$ENV.NAMEEnvironment variableRuntime
$nameNamed variableRuntime

See Variables for detailed documentation.

$ Binding by Context

Context$ contains
Inline block -> { }Piped value
Each loop -> each { }Current item
While-loop (cond) @ { }Accumulated value
Do-while @ { } ? condAccumulated value
Conditional cond ? { }Tested value
Piped conditional -> ? { }Piped value
Stored closure |x|{ }N/A — use params
Dict closure ||{ $.x }Dict self
Anonymous typed closure |type|{ }Piped value (typed)
No-args closure ||{ } outside dictNot bound — $ access raises RILL-R005

Property Access

SyntaxDescription
$data.fieldLiteral field access
$data[0], $data[-1]Index access
$data.$keyVariable as key
$data.($i + 1)Computed key
$data.(a || b)Alternative keys
$data.field ?? defaultDefault if missing
$data.?fieldExistence check (literal)
$data.?$keyExistence check (variable)
$data.?($expr)Existence check (computed)
$data.?field&typeExistence + type check
$data.^keyAnnotation reflection

Type Constructors

Type constructors are primary expressions that produce structural type values. They describe the internal structure of a collection type.

ConstructorSyntaxExample
List typelist(T)list(number), list(list(string))
Dict type (uniform)dict(T)dict(number)
Tuple type (uniform)tuple(T)tuple(number)
Ordered type (uniform)ordered(T)ordered(string)
Dict typedict(k: T [= literal], ...)dict(a: number, b: string = "x")
Tuple typetuple(T, T2 [= literal], ...)tuple(number, string = "x")
Ordered typeordered(k: T [= literal], ...)ordered(a: number, b: string = "x")
Dict field with annotationdict(^(...) k: T, ...)dict(^("label") name: string)
Ordered field with annotationordered(^(...) k: T, ...)ordered(^("x") x: number, ^("y") y: number)
Tuple field with annotationtuple(^(...) T, ...)tuple(^("x") number, ^("y") number)
Closure sig|p: T| :R|x: number| :string
Closure sig with param default|p: T = literal| :R|x: string = "gpt-4"| :string
Annotation default|p: dict(k: T = literal)||a: dict(b: number = 5)|

When using :> to convert a value, the runtime applies two default behaviors for collection-typed fields:

  • Nested synthesis — A missing field with no explicit default is synthesized as an empty collection when all its children have defaults. Missing children are filled from the nested type.
  • Explicit default hydration — An explicit collection default is hydrated through the nested type. Child defaults fill any fields the explicit default omits.

If any required child field has no default, :> raises RILL-R044.

^type returns a structural type value — not a coarse string:

[1, 2, 3] => $list
$list.^type.name
# Result: "list"
[a: 1, b: "hello"] => $d
$d.^type.name
# Result: "dict"

Type constructors appear as values and can be compared:

[1, 2, 3] => $list
$list.^type == list(number)
# Result: true

.^type.name returns the coarse type name string. .^type returns a type value; .name then accesses the dot-notation property on that type value (not annotation interception):

[1, 2, 3] => $list
$list.^type.name
# Result: "list"

Type constructors are also valid in annotation positions:

|x: list(number)| { $x -> each { $ * 2 } } => $fn
$fn(list[1, 2, 3])
# Result: list[2, 4, 6]
list[1, 2, 3] -> :list(number)
# Result: list[1, 2, 3]

Type constructor defaults work in annotation position. The runtime fills missing fields from the annotation defaults when a value is passed:

|a: dict(b: number = 5)| { $a.b } => $fn
$fn(dict[])
# Result: 5

See Type System for detailed structural type documentation.

Dispatch

Pipe a value to a collection (dict or list) to retrieve the corresponding element.

Dict Dispatch: Match keys and return associated values.

SyntaxDescription
$val -> [k1: v1, k2: v2]Returns value for matching key
$val -> [k1: v1, k2: v2] ?? defaultReturns matched value or default
$val -> [["k1", "k2"]: shared]Multi-key dispatch (same value)
$value -> [apple: "fruit", carrot: "vegetable"]  # Returns "fruit" if $value is "apple"
$value -> [apple: "fruit"] ?? "not found"        # Returns "not found" if no match
$method -> [["GET", "HEAD"]: "safe", list["POST", "PUT"]: "unsafe"]  # Multi-key dispatch

Type-Aware Dispatch: Keys match by both value and type.

InputDictResult
1[1: "one", 2: "two"]"one"
"1"[1: "one", "1": "string"]"string"
true[true: "yes", false: "no"]"yes"
"true"[true: "bool", "true": "string"]"string"
1[[1, "one"]: "match"]"match"
"one"[[1, "one"]: "match"]"match"

Dict keys can be identifiers, numbers, or booleans. Multi-key syntax [k1, k2]: value maps multiple keys to the same value.

Hierarchical Dispatch: Navigate nested structures using a path list.

SyntaxDescription
[path] -> $targetNavigate nested dict/list using path of keys/indexes
[] -> $targetEmpty path returns target unchanged
[k1, k2, ...] -> $dictSequential navigation through nested dicts
[0, 1, ...] -> $listSequential navigation through nested lists
[k, 0, k2] -> $mixedMixed dict keys and list indexes

Path element types: string keys for dict lookup, number indexes for list access (negative indexes supported). Type mismatch throws RUNTIME_TYPE_ERROR.

Terminal closures receive $ bound to the final path key.

["name", "first"] -> [name: dict[first: "Alice"]]              # "Alice" (dict path)
[0, 1] -> list[list[1, 2, 3], list[4, 5, 6]]                      # 2 (list path)
["users", 0, "name"] -> [users: list[dict[name: "Alice"]]]    # "Alice" (mixed path)
["req", "draft"] -> [req: dict[draft: { "key={$}" }]]         # "key=draft" (terminal closure)

List Dispatch: Index into a list using a number.

SyntaxDescription
$idx -> [a, b, c]Returns element at index (0-based)
$idx -> [a, b, c] ?? defaultReturns element or default if out of bounds
0 -> ["first", "second", "third"]     # "first"
1 -> ["first", "second", "third"]     # "second"
-1 -> ["first", "second", "third"]    # "third" (last element)
5 -> ["a", "b"] ?? "not found"        # "not found" (out of bounds)

Variable Dispatch: Use stored collections.

[apple: "fruit", carrot: "vegetable"] => $lookup
"apple" -> $lookup                    # "fruit"
["a", "b", "c"] => $items
1 -> $items                           # "b"

Error Messages:

ScenarioError
Dict dispatch key not foundDict dispatch: key '{key}' not found
List dispatch index not foundList dispatch: index '{index}' not found
List dispatch with non-numberList dispatch requires number index, got {type}
Dispatch to non-collectionCannot dispatch to {type}

Core Methods

MethodInputOutputDescription
.lenAnyNumberLength
.trimStringStringRemove whitespace
.headString/ListAnyFirst element
.tailString/ListAnyLast element
.at(idx)String/ListAnyElement at index
.split(sep)StringListSplit by separator
.join(sep)ListStringJoin with separator
.linesStringListSplit on newlines
.lowerStringStringLowercase
.upperStringStringUppercase
.replace(pat, repl)StringStringReplace first match
.replace_all(pat, repl)StringStringReplace all matches
.contains(text)StringBoolContains substring
.starts_with(pre)StringBoolStarts with prefix
.ends_with(suf)StringBoolEnds with suffix
.match(regex)StringDictFirst match info
.is_match(regex)StringBoolRegex matches
.emptyAnyBoolIs empty
.has(value)ListBoolCheck if list contains value (deep equality)
.has_any([values])ListBoolCheck if list contains any value from candidates
.has_all([values])ListBoolCheck if list contains all values from candidates
.keysDictListAll keys
.valuesDictListAll values
.entriesDictListKey-value pairs
.paramsClosureDictParameter metadata (type, __annotations)

See Strings for detailed string method documentation.

Global Functions

FunctionParamsReturnDescription
identityvalue: anyanyReturns input unchanged
logmessage: anyanyPrint to console, pass through
jsonvalue: anystringConvert to JSON string
enumerateitems: list|dict|stringlistAdd index to elements
rangestart: number, stop: number, step: number = 1iteratorGenerate number sequence
repeatvalue: any, count: numberiteratorRepeat value n times
chainvalue: any, transform: anyanyApply closure(s) sequentially
datetimeinput|year, month, day, ...|unixdatetimeConstruct datetime from ISO string, named components, or unix ms
durationyears?, months?, days?, hours?, minutes?, seconds?, ms?durationConstruct duration from named unit parameters
now(none)datetimeCurrent UTC instant

See Iterators for range and repeat documentation.


Implied $

When constructs appear without explicit input, $ is used implicitly:

WrittenEquivalent to
? { }$ -> ? { }
.method()$ -> .method()
$fn()Passes $ as a single argument (no auto-spread)

Extraction Operators

Destructure destruct<>

Extract elements from lists or dicts into variables:

[1, 2, 3] -> destruct<$a, $b, $c>
# $a = 1, $b = 2, $c = 3

[name: "test", count: 42] -> destruct<name: $n, count: $c>
# $n = "test", $c = 42

Slice slice<>

Extract portions using Python-style start:stop:step:

[0, 1, 2, 3, 4] -> slice<0:3>        # list[0, 1, 2]
[0, 1, 2, 3, 4] -> slice<-2:>        # list[3, 4]
[0, 1, 2, 3, 4] -> slice<::-1>       # list[4, 3, 2, 1, 0]
"hello" -> slice<1:4>                    # "ell"

Capture variables accept type annotations: destruct<$a:list(string)>, destruct<$a:string|number>. The runtime validates each extracted element against the declared type. See Operators for detailed extraction operator documentation.


Annotation Reflection

Access annotation values using .^key syntax. Annotations attach to callables (script closures and host-provided functions). The key type is special-cased and works on any value — it returns the structural type.

.^key Dispatch Table

ValueKeyResult
Any valuetypeStructural type value via .^type
Type valuenameRuntime error: RILL-R008 (use .name dot notation instead)
Any callableany other keyCallable annotation value
Anything elseany keyRuntime error: RUNTIME_TYPE_ERROR
^(min: 0, max: 100) |x|($x) => $fn

$fn.^min     # 0
$fn.^max     # 100

Annotations are metadata attached at definition time. They enable runtime configuration and introspection.

Scope rule: Annotations apply only to the closure directly targeted by ^(...). A closure nested inside an annotated statement does not inherit the annotation.

# Direct annotation: works
^(version: 2) |x|($x) => $fn
$fn.^version    # 2
# Nested closure does NOT inherit outer annotation
^(version: 2)
"" -> {
  |x|($x) => $fn
}
$fn.^version    # Error: RUNTIME_UNDEFINED_ANNOTATION

Description Shorthand

A bare string in ^(...) expands to description: <string>:

^("Validates user input") |input|($input) => $validate
$validate.^description    # "Validates user input"

Mix the shorthand with explicit keys:

^("Fetch user profile", cache: true) |id|($id) => $get_user
$get_user.^description    # "Fetch user profile"
$get_user.^cache          # true

Common Use Cases

Function Metadata:

^(doc: "validates user input", version: 2) |input|($input) => $validate

$validate.^doc      # "validates user input"
$validate.^version  # 2

Configuration Annotations:

^(timeout: 30000, retry: 3) |url|($url) => $fetch

$fetch.^timeout  # 30000
$fetch.^retry    # 3

Complex Annotation Values:

^(config: [timeout: 30, endpoints: ["a", "b"]]) |x|($x) => $fn

$fn.^config.timeout      # 30
$fn.^config.endpoints[0] # "a"

Error Handling

Accessing undefined annotation keys throws RUNTIME_UNDEFINED_ANNOTATION:

|x|($x) => $fn
$fn.^missing   # Error: Annotation 'missing' not defined

Use default value operator for optional annotations:

|x|($x) => $fn
$fn.^timeout ?? 30  # 30 (uses default since annotation missing)

Accessing .^key with a non-type key on primitives, lists, or dicts throws RUNTIME_TYPE_ERROR. Use .^type first to get a type value, then access .name on the result:

"hello" => $str
$str.^key        # Error: Cannot access annotation on string

Reserved Annotation Keys

The parser rejects annotation keys that conflict with built-in dispatch semantics.

KeyStatusReason
typeReservedIntercepted at evaluation time for any value
inputActiveIntercepted on any callable; returns parameter shape as RillOrdered
outputActiveIntercepted on any callable; returns declared return type as RillTypeValue
descriptionUser-definedNot reserved; common metadata key
enumUser-definedNot reserved; common metadata key
defaultUser-definedNot reserved; common metadata key
^(type: "custom") name: string   # Error: annotation key "type" is reserved
^(input: "text") name: string    # Error: annotation key "input" is reserved
^(output: "text") name: string   # Error: annotation key "output" is reserved

The name key is not reserved — user annotations may use name on closures without restriction. On type values, .name is a dot-notation property (not annotation access). Accessing .^name on a type value raises RILL-R008. Use .^type.name to get the type name: .^type returns the type value, then .name accesses the dot-notation property.

Parameter Annotations

Parameters can have their own annotations using ^(key: value) syntax. These attach metadata to individual parameters.

Syntax: |^(annotation: value) paramName| body

Order: Parameter annotations appear before the parameter name, before the type annotation (if present) and default value (if present).

|^(min: 0, max: 100) x: number|($x) => $validate
|^(required: true) name: string = "guest"|($name) => $greet
|^(cache: true) count = 0|($count) => $process
true

Access via .params:

The .params property returns a dict keyed by parameter name. Each entry is a dict containing:

  • type — Type annotation (string) if present
  • __annotations — Dict of parameter-level annotations if present
|^(min: 0, max: 100) x: number, y: string|($x + $y) => $fn

$fn.params
# Returns:
# [
#   x: [type: "number", __annotations: [min: 0, max: 100]],
#   y: [type: "string"]
# ]

$fn.params.x.__annotations.min  # 0
$fn.params.y.?__annotations     # false (no annotations on y)

Use Cases:

# Validation metadata
|^(min: 0, max: 100) value|($value) => $bounded

# Caching hints
|^(cache: true) key|($key) => $fetch

# Format specifications
|^(format: "ISO8601") timestamp|($timestamp) => $formatDate
true

See Closures for parameter annotation examples and patterns.

Field Annotations in Type Constructors

dict, ordered, and tuple type constructors support ^() annotations on individual fields. The list() constructor does NOT support field annotations.

Syntax: ^(key: value) appears before the field name (named) or field type (positional).

dict(^("label") name: string)
ordered(^("label") name: string)
tuple(^("x") number, ^("y") number)
dict(^(description: "d", enum: "a,b") f: string)

Annotation placement rules:

ContainerField kindAnnotation position
dict()Named (k: T)Before field name
ordered()Named (k: T)Before field name
tuple()Positional (T)Before field type
list()N/ANot supported (RILL-P001)

Merge behavior: Multiple ^() blocks on one field merge into one annotation map.

Error contracts:

ConditionError
Invalid syntax inside ^()RILL-P014
^() with no following fieldRILL-P014
Missing , or ) after annotated fieldRILL-P014
Unclosed ^(RILL-P005
^() on list() fieldRILL-P001

See Closure Annotations for the shared ^() syntax and reflection patterns.


Return Type Assertions

The :type-target postfix after the closing } declares and enforces the closure’s return type. The runtime validates the return value on every call — a mismatch halts with RILL-R004.

Syntax: |params| { body }:returnType

|x: number| { "{$x}" }:string => $fn
$fn(42)    # "42"

Valid return type targets:

Type TargetDescription
stringString value
numberNumeric value
boolBoolean value
listList value
dictDict value
orderedOrdered container value
anyAny type (no assertion)
list(T), dict(k: T, ...), tuple(T, ...)Parameterized structural types (deep-validates)
|x: number| { list[1, $x, 3] }:list(number) => $fn
$fn(2)    # list[1, 2, 3]

Mismatched return type halts with RILL-R004:

|x: number| { $x * 2 }:string => $double
$double(5)    # RILL-R004: Type assertion failed: expected string, got number

Declared return type is accessible via $fn.^output:

|a: number, b: number| { $a + $b }:number => $add
$add(3, 4)    # 7

Ordered and Tuples

ordered[...] produces a named container that preserves insertion order. Use it for named argument unpacking:

|a, b, c|"{$a}-{$b}-{$c}" => $fmt
dict[c: 3, a: 1, b: 2] -> $fmt(...)    # "1-2-3" (named args by key, key order irrelevant)

Tuples are positional containers. Use tuple[...] with explicit spread ... for positional argument unpacking:

|a, b, c|"{$a}-{$b}-{$c}" => $fmt
tuple[1, 2, 3] -> $fmt(...)    # "1-2-3" (positional args, explicit spread)

See Types for detailed ordered and tuple documentation.


Operator-Level Annotations

Place ^(...) between the operator name and its body to attach metadata to that operation. Annotations apply per evaluation, not per definition.

Loops:

0 -> ($ < 50) @ ^(limit: 100) { $ + 1 }

Collection operators:

[1, 2, 3] -> each ^(limit: 1000) { $ * 2 }
[1, 2, 3] -> map ^(limit: 10) { $ + 1 }
[1, 2, 3] -> filter ^(limit: 50) { $ > 1 }
[1, 2, 3] -> fold ^(limit: 20) |acc, x=0| { $acc + $x }

Invalid annotation keys for operator context produce a runtime error.

See Control Flow and Collections for detailed examples.

Runtime Limits

Iteration Limits

Loops have a default maximum of 10,000 iterations. Place ^(limit: N) at operator level, before the body:

0 -> ($ < 50) @ ^(limit: 100) { $ + 1 }

Concurrency Limits

The ^(limit: N) annotation also controls parallel concurrency in map:

$items -> map ^(limit: 3) { slow_process($) }

See Host Integration for timeout and cancellation configuration.


Host-Provided Functions

rill is a vanilla language. The host application registers domain-specific functions via RuntimeContext:

const ctx = createRuntimeContext({
  functions: {
    prompt: async (args) => await callLLM(args[0]),
    'io::read': async (args) => fs.readFile(String(args[0]), 'utf-8'),
  },
  variables: {
    config: { apiUrl: 'https://api.example.com' },
  },
});

Scripts call these as prompt("text") or io::read("file.txt").

See Host Integration for complete API documentation.


Script Frontmatter

Optional YAML frontmatter between --- markers. Frontmatter is opaque to rill—the host interprets it:

---
timeout: 00:10:00
args: file: string
---

process($file)

Script Return Values

Scripts return their last expression:

ReturnExit Code
true / non-empty string0
false / empty string1
[0, "message"]0 with message
[1, "message"]1 with message

Comments

Single-line comments start with #:

# This is a comment
"hello"  # inline comment

Newlines

Invariant: whitespace is insignificant inside a syntactic continuation.

Newlines are statement terminators. A newline ends a statement unless the parser is inside a syntactic continuation — any position where the preceding token cannot end a valid statement.

Continuation tokens

A newline after any of these continues the current statement:

ClassTokens
Binary arithmetic+ - * / %
Logical&& ||
Comparison== != < > <= >=
Pipe / capture-> =>
Conditional? !
Member access. .?
Type annotation:
Spread...
Annotation^
Open delimitersunclosed [ ( { | ||

A subset of continuation tokens also work as line-start continuations — placing them at the beginning of the next line continues the previous statement. This applies to ->, =>, ?, !, ., and .?:

"hello"
  => $greeting
  -> .upper
  -> .trim
(5 > 0)
  ? "yes"
  ! "no"

Arithmetic, logical, and comparison operators work as trailing continuations only — place the operator at the end of the line, not the beginning:

1 +
  2 +
  3

Statement-start tokens

These always begin a new statement:

TokenStarts
$Variable reference or closure call
identifierHost function call
@Loop
| ||Closure definition
literalsString, number, bool, [...], [...], tuple[...], ordered[...]

Disjoint sets

The two token classes are disjoint. No token is both a continuation token and a statement-start token. This makes the grammar unambiguous with one token of lookahead — no symbol table, no backtracking.


Grammar

The complete formal grammar is in grammar.ebnf.


Error Codes

Runtime and parse errors include structured error codes for programmatic handling.

CodeDescription
PARSE_UNEXPECTED_TOKENUnexpected token in source
PARSE_INVALID_SYNTAXInvalid syntax
PARSE_INVALID_TYPEInvalid type annotation
RUNTIME_UNDEFINED_VARIABLEVariable not defined
RUNTIME_UNDEFINED_FUNCTIONFunction not defined
RUNTIME_UNDEFINED_METHODMethod not defined (built-in only)
RUNTIME_UNDEFINED_ANNOTATIONAnnotation key not defined
RUNTIME_TYPE_ERRORType mismatch
RUNTIME_TIMEOUTOperation timed out
RUNTIME_ABORTEDExecution cancelled
RUNTIME_INVALID_PATTERNInvalid regex pattern
RUNTIME_AUTO_EXCEPTIONAuto-exception triggered
RUNTIME_ASSERTION_FAILEDAssertion failed (condition false)
RUNTIME_ERROR_RAISEDError statement executed

See Host Integration for error handling details.


See Also

For detailed documentation on specific topics: