Type System
Overview
rill is dynamically typed and type-safe. Types are checked at runtime, but type errors are always caught—there are no implicit conversions or coercions.
| Type | Syntax | Example |
|---|---|---|
| String | "text" | "hello" |
| Number | 123, 0.5 | 42, 0.9 |
| Bool | true, false | true |
| List | [a, b] | ["file.ts", 42] |
| Dict | [k: v] | [output: "text", code: 0] |
| Tuple | *[...] | *[1, 2], *[x: 1, y: 2] |
| Vector | host-provided | vector(voyage-3, 1024d) |
| Closure | ||{ } | |x|($x * 2) |
Key principles:
- Type-safe: No implicit coercion—
"5" + 1errors, not"51"or6 - Type-locked variables: A variable that holds a string always holds a string
- Value-based: All copies are deep, all comparisons by value
- No null/undefined: Empty values are valid (
"",[],[:]), but “no value” cannot exist - No truthiness: Conditions require actual booleans, not “truthy” values
Strings
Double-quoted text with variable interpolation using {$var}:
"hello world"
"Process {$filename} for review"
"Result: {$response}"Escape sequences: \n, \t, \\, \", {{ (literal {), }} (literal })
Interpolation
Any valid expression works inside {...}:
"alice" => $name
3 => $a
5 => $b
true => $ok
"Hello, {$name}!" # Variable
"sum: {$a + $b}" # Arithmetic
"valid: {$a > 0}" # Comparison
"status: {$ok ? \"yes\" ! \"no\"}" # Conditional
"upper: {$name -> .upper}" # Method chainMultiline Strings
Multiline strings use triple-quote syntax:
"World" => $name
"""
Hello, {$name}!
Line two
"""Triple-quote strings support interpolation like regular strings.
See Strings for string methods.
Numbers
Used for arithmetic, exit codes, and loop limits:
42
0
3.14159
-7Arithmetic operators: +, -, *, /, % (modulo)
Type constraint: All arithmetic operands must be numbers. No implicit conversion:
5 + 3 # 8# Error: Arithmetic requires number, got string
"5" + 1Booleans
Literal true and false. Conditional expressions (?), loop conditions (@), and filter predicates require boolean values. Non-boolean values cause runtime errors.
true ? "yes" ! "no" # "yes"
false ? "yes" ! "no" # "no"No truthiness: rill has no automatic boolean coercion. Empty strings, zero, and empty lists are not “falsy”—you must explicitly check:
"" -> .empty ? "empty" ! "has content" # Use .empty method
0 -> ($ == 0) ? "zero" ! "nonzero" # Use comparisonType-Safe Negation
The negation operator (!) requires a boolean operand. There is no truthiness coercion:
!true # false
!false # true
"hello" -> .empty -> (!$) # true (negates boolean from .empty)!"hello" # ERROR: Negation requires boolean, got string
!0 # ERROR: Negation requires boolean, got numberUse explicit boolean checks when needed:
"" -> .empty -> (!$) ? "has content" ! "empty" # Negate boolean result
[1,2,3] -> .empty -> (!$) ? "has items" ! "none" # Check non-emptyLists
Ordered sequences of values:
[1, 2, 3] => $nums
$nums[0] # 1
$nums[-1] # 3 (last element)
$nums -> .len # 3List Spread
Inline elements from another list using ... spread syntax:
[1, 2] => $a
[...$a, 3] # [1, 2, 3]
[...$a, ...$a] # [1, 2, 1, 2] (concatenation)
[...[], 1] # [1] (empty spread contributes nothing)Spread expressions evaluate before inlining:
[1, 2, 3] => $nums
[...($nums -> map {$ * 2})] # [2, 4, 6]Spreading a non-list throws an error:
"hello" => $str
[...$str] # Error: Spread in list literal requires list, got stringAccess methods:
[0],[1]— Index access (0-based)[-1],[-2]— Negative index (from end).head— First element (errors on empty).tail— Last element (errors on empty).at(n)— Element at index
Out-of-bounds access throws an error:
[] -> .at(0) # Error: List index out of bounds
["a"] -> .at(5) # Error: List index out of boundsUse ?? for safe access with default:
["a"] => $list
$list[0] ?? "default" # "a"See Collections for iteration operators.
Dicts
Key-value mappings with identifier, number, boolean, variable, or computed keys:
# Identifier keys
[name: "alice", age: 30] => $person
$person.name # "alice"
$person.age # 30# Number keys (including negative)
[1: "one", 2: "two", -1: "minus one"] => $numbers
1 -> $numbers # "one"
(-1) -> $numbers # "minus one"
# Boolean keys
[true: "yes", false: "no"] => $yesno
true -> $yesno # "yes"
# Variable keys (key value from variable, must be string)
"status" => $key
[$key: "active"] # [status: "active"]
# Computed keys (key from expression, must be string)
"user" => $prefix
[($prefix -> "{$}_name"): "alice"] # [user_name: "alice"]
# Multi-key syntax (same value for multiple keys)
[["a", "b"]: 1] # [a: 1, b: 1]
[[1, "1"]: "x"] # [1: "x", "1": "x"] (mixed types)
[a: 0, ["b", "c"]: 1] # [a: 0, b: 1, c: 1] (mixed entries)
[a: 0, ["a", "b"]: 1] # [a: 1, b: 1] (last-write-wins)
# Multi-key dispatch
[["GET", "HEAD"]: "safe", ["POST", "PUT"]: "unsafe"] => $methods
"GET" -> $methods # "safe"
"POST" -> $methods # "unsafe"Multi-key errors:
[[]: 1] # Error: Multi-key dict entry requires non-empty list
[[[1, 2], "a"]: 1] # Error: Dict key must be string, number, or boolean, got listAccess patterns:
.field— Literal field access (identifier keys only).$key— Variable as key.($i + 1)— Computed expression as key.(a || b)— Alternatives (try keys left-to-right).field ?? default— Default value if missing.?field— Existence check, literal key (returns bool).?$key— Existence check, variable key.?($expr)— Existence check, computed key.?field&type— Existence + type check (all forms support&type)
Note: Number and boolean keys require dispatch syntax (value -> dict) or bracket access. Dot notation (.1, .true) is not valid syntax.
Missing key access throws an error. Use ?? for safe access:
[:] => $d
$d.missing ?? "" # "" (safe default)Type-Aware Dispatch
Dict dispatch uses type-aware matching. Keys are matched by both value and type:
# Number vs string discrimination
[1: "number one", "1": "string one"] => $mixed
1 -> $mixed # "number one" (number key)
"1" -> $mixed # "string one" (string key)
# Boolean vs string discrimination
[true: "bool true", "true": "string true"] => $flags
true -> $flags # "bool true" (boolean key)
"true" -> $flags # "string true" (string key)This enables pattern matching where the same semantic value (e.g., 1 vs "1") triggers different behavior based on type.
Dict Methods
| Method | Description |
|---|---|
.keys | All keys as list |
.values | All values as list |
.entries | List of [key, value] pairs |
[name: "test", count: 42] -> .keys # ["count", "name"]
[name: "test", count: 42] -> .values # [42, "test"]
[a: 1, b: 2] -> .entries # [["a", 1], ["b", 2]]Reserved methods (keys, values, entries) cannot be used as dict keys.
Dict Closures
Closures in dicts have $ late-bound to the containing dict. See Closures for details.
[
name: "toolkit",
count: 3,
str: ||"{$.name}: {$.count} items"
] => $obj
$obj.str # "toolkit: 3 items" (auto-invoked)Tuples
Tuples package values for explicit argument unpacking at closure invocation. Created with the * spread operator:
# From list (positional)
*[1, 2, 3] => $t # tuple with positional values
# From dict (named)
*[x: 1, y: 2] => $t # tuple with named values
# Via pipe target
[1, 2, 3] -> * => $t # convert list to tupleUsing Tuples at Invocation
|a, b, c| { "{$a}-{$b}-{$c}" } => $fmt
# Positional unpacking
*[1, 2, 3] -> $fmt() # "1-2-3"
# Named unpacking (order doesn't matter)
*[c: 3, a: 1, b: 2] -> $fmt() # "1-2-3"Strict Validation
When invoking with tuples, missing required parameters error, and extra arguments error:
|x, y|($x + $y) => $fn
*[1] -> $fn() # Error: missing argument 'y'
*[1, 2, 3] -> $fn() # Error: extra positional argument
*[x: 1, z: 3] -> $fn() # Error: unknown argument 'z'Parameter Defaults with Tuples
|x, y = 10, z = 20|($x + $y + $z) => $fn
*[5] -> $fn() # 35 (5 + 10 + 20)
*[x: 5, z: 30] -> $fn() # 45 (5 + 10 + 30)Auto-Unpacking with Parallel Spread
When a closure is invoked with a single tuple argument, the tuple auto-unpacks:
# List of tuples with multi-arg closure
[*[1,2], *[3,4]] -> map |x,y|($x * $y) # [2, 12]
# Named tuples work too
[*[x:1, y:2], *[x:3, y:4]] -> map |x,y|($x + $y) # [3, 7]Vectors
Vectors represent dense numeric embeddings from language models or other ML systems. Host applications provide vectors through embedding APIs.
Display format: vector(model, Nd) where model is the source model name and N is the dimension count.
app::embed("hello world") => $vec
$vec
# Result: vector(mock-embed, 3d)Properties
| Property | Type | Description |
|---|---|---|
.dimensions | number | Number of dimensions in the vector |
.model | string | Source model name |
app::embed("hello world") => $vec
$vec -> .dimensions
# Result: 3
$vec -> .model
# Result: "mock-embed"Methods
| Method | Returns | Description |
|---|---|---|
.similarity(other) | number | Cosine similarity, range [-1, 1] |
.dot(other) | number | Dot product |
.distance(other) | number | Euclidean distance, >= 0 |
.norm() | number | L2 magnitude |
.normalize() | vector | Unit vector (preserves model) |
app::embed("hello") => $a
app::embed("hi") => $b
$a -> .similarity($b)
# Result: 1.0
$a -> .dot($b)
# Result: 0.14
$a -> .distance($b)
# Result: 0.0
$a -> .norm
# Result: 0.37
$a -> .normalize -> .norm
# Result: 1.0Comparison
Vectors support equality comparison (==, !=). Two vectors are equal when both model and all float elements match:
app::embed("test") => $v1
app::embed("test") => $v2
$v1 == $v2
# Result: trueVectors from different models are never equal, even with identical data:
# Different models
app::embed("test", "model-a") => $v1
app::embed("test", "model-b") => $v2
$v1 == $v2
# Result: falseBehavioral Notes
- Immutable: Vector data cannot be modified after creation
- Always truthy: Vectors evaluate to true in boolean contexts (non-empty by construction)
- No string coercion: Cannot be used in string interpolation or concatenation
- No collection operations: Cannot use
each,map,filter,foldon vectors
# Error: cannot coerce vector to string
"Result: {$vec}"
# Error: Collection operators require list, string, dict, or iterator, got vector
$vec -> each { $ * 2 }Type Assertions
Use type assertions to validate values at runtime.
Assert Type (:type)
Error if type doesn’t match, returns value unchanged:
# Postfix form (binds tighter than method calls)
42:number # passes, returns 42
(1 + 2):number # passes, returns 3
42:number.str # "42" - assertion then method
# Pipe target form
"hello" -> :string # passes, returns "hello"
"hello" -> :number # ERROR: expected number, got string
$val -> :dict -> .keys # assert dict, then get keysCheck Type (:?type)
Returns boolean, no error:
# Postfix form
42:?number # true
"hello":?number # false
# Pipe target form
"hello" -> :?string # trueType checks work in conditionals:
$val -> :?list ? process() ! skip() # branch on typeSupported types: string, number, bool, closure, list, dict, tuple, vector
In Pipe Chains
# Assert type and continue processing
[1, 2, 3] -> :list -> each { $ * 2 }
# Multiple assertions in chain
"test" -> :string -> .len -> :number # 4Use Cases
# Validate function input
|data| {
$data -> :list # assert input is list
$data -> each { $ * 2 }
} => $process_items
# Type-safe branching
|val| {
$val -> :?number ? ($val * 2) ! ($val -> .len)
} => $process
$process(5) # 10
$process("hello") # 5Type-Locked Variables
Variables lock type on first assignment. The type is inferred from the value or declared explicitly:
"hello" => $name # implicit: locked as string
"world" => $name # OK: same type
5 => $name # ERROR: cannot assign number to string
"hello" => $name:string # explicit: declare and lock as string
42 => $count:number # explicit: declare and lock as numberInline Capture with Type
"hello" => $x:string -> .len # type annotation in mid-chainType annotations validate on assignment and prevent accidental type changes:
|x|$x => $fn # locked as closure
"text" => $fn # ERROR: cannot assign string to closureGlobal Type Functions
| Function | Description |
|---|---|
type | Returns type name as string |
json | Convert to JSON string |
42 -> type # "number"
"hello" -> type # "string"
[1, 2] -> type # "list"
*[1, 2] -> type # "tuple"
[a: 1] -> type # "dict"
||{ $ } -> type # "closure"
[a: 1, b: 2] -> json # '{"a":1,"b":2}'app::embed("test") -> type # "vector"json closure handling:
- Direct closure → error:
|x|{ $x } -> jsonthrows “Cannot serialize closure to JSON” - Closures in dicts → skipped:
[a: 1, fn: ||{ 0 }] -> jsonreturns'{"a":1}' - Closures in lists → skipped:
[1, ||{ 0 }, 2] -> jsonreturns'[1,2]'
See Also
- Variables — Declaration, scope,
$binding - Closures — Closure semantics and patterns
- Collections — List iteration operators
- Strings — String methods reference
- Reference — Quick reference tables