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: no references, all copies are deep, 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@ (sequential), * (tuple)
Extraction*<> (destructure), /<> (slice)
Type:type (assert), :?type (check)
Member.field, [index]
Default?? value
Existence.?field, .?$var, .?($expr), .?field&type

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.

Types

TypeSyntaxExampleProduces
String"text", """text""""hello", """line 1\nline 2"""String value
Number123, 0.542, 0.9Number value
Booltrue, falsetrueBoolean value
List[a, b], [...$list]["file.ts", 42], [...$a, 3]List value
Dict[k: v], [$k: v], [($e): v][output: "text"], [$key: 1]Dict value
Tuple*[...]*[1, 2], *[x: 1, y: 2]Tuple value
Closure||{ }|x|($x * 2)ScriptCallable
Block{ body }{ $ + 1 }ScriptCallable

See Types for detailed documentation.

Functions

SyntaxDescription
|p: type|{ } => $fnDefine and capture function
|p = default|{ }Parameter with default
|p ^(min: 0)|{ }Parameter with annotation
$fn(arg)Call function directly
arg -> $fn()Call function with pipe value
arg -> $fnPipe-style invoke

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

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

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", ["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: [first: "Alice"]]              # "Alice" (dict path)
[0, 1] -> [[1, 2, 3], [4, 5, 6]]                           # 2 (list path)
["users", 0, "name"] -> [users: [[name: "Alice"]]]         # "Alice" (mixed path)
["req", "draft"] -> [req: [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
.strAnyStringConvert to string
.numAnyNumberConvert to number
.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

FunctionDescription
typeReturns type name of value
logPrint to console, pass through
jsonConvert to JSON string
identityReturns input unchanged
range(start, end, step?)Generate number sequence
repeat(value, count)Repeat value n times
enumerate(collection)Add index to elements

See Iterators for range and repeat documentation.


Implied $

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

WrittenEquivalent to
? { }$ -> ? { }
.method()$ -> .method()
$fn()$fn($) (when no args, no default)

Extraction Operators

Destructure *<>

Extract elements from lists or dicts into variables:

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

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

Slice /<>

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

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

See Operators for detailed extraction operator documentation.


Annotation Reflection

Access annotation values on closures using .^key syntax.

Type Restriction: Annotations attach to closures only. Accessing .^key on primitives (string, number, boolean, list, dict) throws RUNTIME_TYPE_ERROR.

^(min: 0, max: 100) |x|($x) => $fn

$fn.^min     # 0
$fn.^max     # 100
$fn.^missing # Error: RUNTIME_UNDEFINED_ANNOTATION

Annotations are metadata attached to closures during creation. They enable runtime configuration and introspection.

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 on non-closure values throws RUNTIME_TYPE_ERROR:

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

Parameter Annotations

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

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

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

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

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
|x: number ^(min: 0, max: 100), 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
|value ^(min: 0, max: 100)|($value) => $bounded

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

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

See Closures for parameter annotation examples and patterns.


Tuples

Tuples package values for argument unpacking:

|a, b, c|"{$a}-{$b}-{$c}" => $fmt
*[1, 2, 3] -> $fmt()             # "1-2-3"
*[c: 3, a: 1, b: 2] -> $fmt()    # "1-2-3" (named)

See Types for detailed tuple documentation.


Runtime Limits

Iteration Limits

Loops have a default maximum of 10,000 iterations. Override with ^(limit: N):

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

Concurrency Limits

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

^(limit: 3) $items -> map { 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

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: