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.

TypeSyntaxExample
String"text""hello"
Number123, 0.542, 0.9
Booltrue, falsetrue
List[a, b]["file.ts", 42]
Dict[k: v][output: "text", code: 0]
Tuple*[...]*[1, 2], *[x: 1, y: 2]
Vectorhost-providedvector(voyage-3, 1024d)
Closure||{ }|x|($x * 2)

Key principles:

  • Type-safe: No implicit coercion—"5" + 1 errors, not "51" or 6
  • 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 chain

Multiline 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
-7

Arithmetic operators: +, -, *, /, % (modulo)

Type constraint: All arithmetic operands must be numbers. No implicit conversion:

5 + 3                      # 8
# Error: Arithmetic requires number, got string
"5" + 1

Booleans

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 comparison

Type-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 number

Use explicit boolean checks when needed:

"" -> .empty -> (!$) ? "has content" ! "empty"    # Negate boolean result
[1,2,3] -> .empty -> (!$) ? "has items" ! "none"  # Check non-empty

Lists

Ordered sequences of values:

[1, 2, 3] => $nums
$nums[0]                   # 1
$nums[-1]                  # 3 (last element)
$nums -> .len              # 3

List 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 string

Access 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 bounds

Use ?? 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 list

Access 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

MethodDescription
.keysAll keys as list
.valuesAll values as list
.entriesList 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 tuple

Using 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

PropertyTypeDescription
.dimensionsnumberNumber of dimensions in the vector
.modelstringSource model name
app::embed("hello world") => $vec
$vec -> .dimensions
# Result: 3

$vec -> .model
# Result: "mock-embed"

Methods

MethodReturnsDescription
.similarity(other)numberCosine similarity, range [-1, 1]
.dot(other)numberDot product
.distance(other)numberEuclidean distance, >= 0
.norm()numberL2 magnitude
.normalize()vectorUnit 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.0

Comparison

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: true

Vectors 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: false

Behavioral 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, fold on 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 keys

Check Type (:?type)

Returns boolean, no error:

# Postfix form
42:?number                    # true
"hello":?number               # false

# Pipe target form
"hello" -> :?string           # true

Type checks work in conditionals:

$val -> :?list ? process() ! skip()   # branch on type

Supported 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   # 4

Use 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")  # 5

Type-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 number

Inline Capture with Type

"hello" => $x:string -> .len  # type annotation in mid-chain

Type annotations validate on assignment and prevent accidental type changes:

|x|$x => $fn                  # locked as closure
"text" => $fn                 # ERROR: cannot assign string to closure

Global Type Functions

FunctionDescription
typeReturns type name as string
jsonConvert 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 } -> json throws “Cannot serialize closure to JSON”
  • Closures in dicts → skipped: [a: 1, fn: ||{ 0 }] -> json returns '{"a":1}'
  • Closures in lists → skipped: [1, ||{ 0 }, 2] -> json returns '[1,2]'

See Also