Operators
Overview
| Category | Operators |
|---|---|
| Pipe | -> |
| Capture | => |
| Arithmetic | +, -, *, /, % |
| Comparison | ==, !=, <, >, <=, >= |
| Comparison Methods | .eq, .ne, .lt, .gt, .le, .ge |
| Logical | ! (unary), &&, ` |
| Chain | chain($fn), chain([...]) |
| Ordered | ordered[k: v] (named ordered container) |
| Extraction | destruct<...> (destructure), slice<...> (slice) |
| Convert | :>type (conversion) |
| Type | :type (assert), :?type (check) |
| Member | .field, [index] |
| Hierarchical Dispatch | [path] -> target |
| Default | ?? value |
| Existence | .?field, .?field&type |
Pipe Operator ->
The pipe operator passes the left-hand value to the right-hand side:
"hello" -> .upper # "HELLO"
42 -> ($ + 8) # 50
[1, 2, 3] -> each { $ * 2 } # list[2, 4, 6]Piped Value as $
The piped value is available as $:
"world" -> "hello {$}" # "hello world"
5 -> ($ * $ + $) # 30Method Syntax
Method calls are sugar for pipes:
"hello".upper # equivalent: "hello" -> .upper
"hello".contains("ell") # equivalent: "hello" -> .contains("ell")Implicit $
Bare .method() implies $ as receiver:
"hello" -> {
.upper -> log # $."upper" -> log
.len # $.len
}Capture Operator =>
Captures a value into a variable:
"hello" => $greeting # store in $greeting
42 => $count # store in $countCapture and Continue
=> captures AND continues the chain:
"hello" => $a -> .upper => $b -> .len
# $a is "hello", $b is "HELLO", result is 5See Variables for detailed scoping rules.
Arithmetic Operators
| Operator | Description |
|---|---|
+ | Addition |
- | Subtraction |
* | Multiplication |
/ | Division |
% | Modulo (remainder) |
5 + 3 # 8
10 - 4 # 6
3 * 4 # 12
15 / 3 # 5
17 % 5 # 2Precedence
Standard mathematical precedence (high to low):
- Unary:
-,! - Multiplicative:
*,/,% - Additive:
+,-
2 + 3 * 4 # 14 (multiplication first)
(2 + 3) * 4 # 20 (parentheses override)
-5 + 3 # -2Type Constraint
All operands must be numbers. No implicit conversion:
5 + 3 # OK: 8"5" + 1 # ERROR: Arithmetic requires number, got stringError Handling
10 / 0 # ERROR: Division by zero
10 % 0 # ERROR: Modulo by zeroComparison Operators
| Operator | Description |
|---|---|
== | Equal |
!= | Not equal |
< | Less than |
> | Greater than |
<= | Less or equal |
>= | Greater or equal |
5 == 5 # true
5 != 3 # true
3 < 5 # true
5 > 3 # true
5 <= 5 # true
5 >= 3 # trueValue Comparison
All comparisons are by value, not reference:
[1, 2, 3] == list[1, 2, 3] # true
[a: 1] == dict[a: 1] # true
"hello" == "hello" # trueComparison Methods
Methods provide readable alternatives in conditionals:
| Method | Equivalent |
|---|---|
.eq(val) | == val |
.ne(val) | != val |
.lt(val) | < val |
.gt(val) | > val |
.le(val) | <= val |
.ge(val) | >= val |
"A" => $v
$v -> .eq("A") ? "match" ! "no" # "match"
5 -> .gt(3) ? "big" ! "small" # "big"
10 -> .le(10) ? "ok" ! "over" # "ok"Logical Operators
| Operator | Description |
|---|---|
&& | Logical AND (short-circuit) |
|| | Logical OR (short-circuit) |
! | Logical NOT |
(true && false) # false
(true || false) # true
!true # false
!false # trueShort-Circuit Evaluation
(false && undefined_var) # false (right side not evaluated)
(true || undefined_var) # true (right side not evaluated)With Comparisons
(1 < 2 && 3 > 2) # true
(5 > 10 || 3 < 5) # trueGrouping Required
Compound expressions require grouping in simple-body contexts:
true -> ($ && true) ? "both" ! "not both" # "both"Negation in Pipes
In pipe targets, !expr binds tightly and returns a boolean:
"hello" -> !.empty # true (not empty)
"" -> !.empty # false (is empty)This works naturally with conditionals and captures:
"hello" -> !.empty ? "has content" ! "empty" # "has content"
"hello" -> !.empty => $not_empty # $not_empty = trueNo grouping needed — !.empty is parsed as a unit before ? or =>.
Chain and Ordered
chain() Built-in
chain pipes a value through a sequence of closures. Each closure receives the result of the previous one.
Chain a list of closures:
|x|($x + 1) => $inc
|x|($x * 2) => $double
|x|($x + 10) => $add10
# Chain: (5 + 1) = 6, (6 * 2) = 12, (12 + 10) = 22
5 -> chain([$inc, $double, $add10]) # 22Chain a single closure:
|x|($x * 2) => $dbl
5 -> chain($dbl) # 10ordered[...] Literal
ordered[...] produces a named, ordered container. It preserves insertion order and carries named keys. Use it to pass named arguments to closures:
|a, b, c|"{$a}-{$b}-{$c}" => $fmt
dict[c: 3, a: 1, b: 2] -> $fmt(...) # "1-2-3" (names matched, key order irrelevant)ordered values convert to plain objects via toNative() — the native field holds { key: value, ... }. Closures, iterators, vectors, and type values produce native: null.
See Types for full type documentation.
Extraction Operators
Destructure destruct<>
Extract elements from lists or dicts into variables. Returns the original value unchanged.
List destructuring (pattern count must match list length):
[1, 2, 3] -> destruct<$a, $b, $c>
# $a = 1, $b = 2, $c = 3With dict destructuring:
[code: 0, msg: "ok"] -> destruct<code: $code, msg: $msg>
# $code = 0, $msg = "ok"Skip elements with _:
[1, 2, 3, 4] -> destruct<$first, _, _, $last>
# $first = 1, $last = 4Dict destructuring (explicit key mapping):
[name: "test", count: 42] -> destruct<name: $n, count: $c>
# $n = "test", $c = 42Nested destructuring:
[list[1, 2], list[3, 4]] -> destruct<destruct<$a, $b>, destruct<$c, $d>>
# $a = 1, $b = 2, $c = 3, $d = 4Errors:
[1, 2] -> destruct<$a, $b, $c> # Error: pattern has 3 elements, list has 2
[name: "x"] -> destruct<name: $n, age: $a> # Error: key 'age' not foundType-Annotated Destructure
Capture variables in destruct<> accept type annotations using :type syntax. The runtime validates the extracted element against the declared type before assignment.
Parameterized type on a destructure capture:
[["a", "b"]] -> destruct<$a:list(string)>
$a[0]
# Result: "a"Dict structural type on a destructure capture:
[[name: "alice"]] -> destruct<$a:dict(name: string)>
$a.name
# Result: "alice"Union type on a destructure capture:
["hello"] -> destruct<$a:string|number>
$a
# Result: "hello"Type mismatch error:
# Error: Type mismatch: cannot assign list(number) to $a:list(string)
[[1, 2]] -> destruct<$a:list(string)>Slice slice<>
Extract a portion using Python-style start:stop:step. Works on lists and strings.
Basic slicing:
[0, 1, 2, 3, 4] -> slice<0:3> # list[0, 1, 2]
[0, 1, 2, 3, 4] -> slice<1:4> # list[1, 2, 3]Omitted bounds:
[0, 1, 2, 3, 4] -> slice<:3> # list[0, 1, 2] (first 3)
[0, 1, 2, 3, 4] -> slice<2:> # list[2, 3, 4] (from index 2)Negative indices:
[0, 1, 2, 3, 4] -> slice<-2:> # list[3, 4] (last 2)
[0, 1, 2, 3, 4] -> slice<:-1> # list[0, 1, 2, 3] (all but last)Step:
[0, 1, 2, 3, 4] -> slice<::2> # list[0, 2, 4] (every 2nd)
[0, 1, 2, 3, 4] -> slice<::-1> # list[4, 3, 2, 1, 0] (reversed)String slicing:
"hello" -> slice<1:4> # "ell"
"hello" -> slice<::-1> # "olleh"Edge cases:
[1, 2, 3] -> slice<0:100> # list[1, 2, 3] (clamped)
[1, 2, 3] -> slice<2:1> # [] (empty when start >= stop)[1, 2, 3] -> slice<::0> # Error: step cannot be zeroMember Access Operators
Field Access .field
Access dict fields:
[name: "alice", age: 30] => $person
$person.name # "alice"
$person.age # 30See Types for dict .keys and .entries documentation.
Index Access [n]
Access list elements (0-based, negative from end):
["a", "b", "c"] => $list
$list[0] # "a"
$list[-1] # "c"
$list[1] # "b"Variable Key .$key
Use a variable as key:
"name" => $key
[name: "alice"] => $data
$data.$key # "alice"Computed Key .($expr)
Use an expression as key:
0 => $i
["a", "b", "c"] => $list
$list.($i + 1) # "b"Alternative Keys .(a || b)
Try keys left-to-right:
[nickname: "Al"] => $user
$user.(name || nickname) # "Al"Hierarchical Dispatch
Navigate nested data structures using a list of keys/indexes as a path:
["name", "first"] -> [name: dict[first: "Alice", last: "Smith"]]
# Result: "Alice"Path Syntax
Pipe a list path to a target structure. Path elements are applied sequentially:
- Strings navigate dict fields
- Numbers index into lists
- Empty path returns target unchanged
Dict Path
["address", "city"] -> [address: dict[street: "Main", city: "Boston"]]
# Result: "Boston"List Path
[0, 1] -> list[list[1, 2, 3], list[4, 5, 6]]
# Result: 2 (first list, second element)Mixed Path
["users", 0, "name"] -> [users: list[dict[name: "Alice"], dict[name: "Bob"]]]
# Result: "Alice"Empty Path
[] -> [name: "test"]
# Result: [name: "test"] (unchanged)Error Handling
["missing"] -> [name: "test"] # Error: key 'missing' not found
[5] -> list[1, 2, 3] # Error: index 5 out of boundsSee Reference for full dispatch semantics including dict dispatch, list dispatch, and default values.
Default Operator ??
Provide a default value if field is missing or access fails:
[:] => $empty
$empty.name ?? "unknown" # "unknown"
[name: "alice"] => $user
$user.name ?? "unknown" # "alice"
$user.age ?? 0 # 0With Function Calls
The default operator works with any expression, including function and method calls:
get_data().status ?? "default" # "default" if status field missing
fetch_value() ?? "fallback" # "fallback" if fetch_value returns undefinedWith Method Calls
The ?? operator applies after method invocations in access chains:
$dict.transform() ?? "default" # default if method throws or result missing
$obj.compute().value ?? 0 # default if value field missing after method
$config.get_setting() ?? [:] # default if method returns undefinedMethod calls evaluate fully before the default operator applies.
Existence Operators
Field Existence .?field
Returns boolean:
[name: "alice"] => $user
$user.?name # true
$user.?age # falseExistence with Type .?field&type
Check existence AND type:
[name: "alice", age: 30] => $user
$user.?name&string # true
$user.?age&number # true
$user.?age&string # falseThe &type position accepts parameterized types and union types.
Parameterized type:
[items: [1, 2, 3]] => $data
$data.?items&list(number)
# Result: trueDict structural type:
[cfg: [key: "x"]] => $data
$data.?cfg&dict(key: string)
# Result: trueUnion type:
[score: 42] => $data
$data.?score&string|number
# Result: trueThe & operator binds to the entire union expression. $data.?score&string|number parses as $data.?score & (string|number), not ($data.?score&string) | number.
Type Operators
Type Assert :type
Error if type doesn’t match, returns value unchanged:
42:number # 42
"hello" -> :string # "hello"Structural type syntax is supported in assertions. The structural form specifies element or field types:
[1, 2, 3] -> :list(number) # passes: all elements are number
[a: 1, b: 2] -> :dict(a: number, b: number) # passes: fields match types
[1, "x"] -> :list(number) # ERROR: structural type mismatch"hello" -> :number # ERROR: expected number, got stringType Check :?type
Returns boolean:
42:?number # true
"hello":?number # false
"hello" -> :?string # trueCoarse checks return boolean directly:
[1, 2, 3] -> :?list # true
[a: 1] -> :?dict # trueStructural checks are also supported. These match element and field types:
[1, 2, 3] -> :?list(number) # true
[1, "x"] -> :?list(number) # false
[a: 1] -> :?dict(a: number) # true^type Operator
^type returns the structural RillTypeValue for a value. The type value carries both a coarse name and a full structural description:
[1, 2, 3] -> ^type # list(number)
[a: 1, b: "x"] -> ^type # dict(a: number, b: string)
42 -> ^type # numberThe type value formats as a structural string via :>string or string interpolation:
[1, 2, 3] -> ^type -> :>string # "list(number)"
"hello {[1,2,3] -> ^type}" # "hello list(number)"To get the type name only, chain .name on the type value:
[1, 2, 3] -> ^type -> .name # "list"
42 -> ^type -> .name # "number"See Types for detailed type system documentation.
Conversion Operator :>type
The :>type operator converts a value to the target type. Same-type conversions are no-ops. Incompatible conversions halt with RILL-R036.
| Source | :>list | :>dict | :>tuple | :>ordered(sig) | :>number | :>string | :>bool |
|---|---|---|---|---|---|---|---|
list | no-op | error | valid | error | error | valid¹ | error |
dict | error | no-op | error | valid | error | valid¹ | error |
tuple | valid | error | no-op | error | error | valid¹ | error |
ordered | error | valid | error | no-op | error | valid¹ | error |
string | error | error | error | error | valid² | no-op | valid³ |
number | error | error | error | error | no-op | valid¹ | valid⁵ |
bool | error | error | error | error | valid⁴ | valid¹ | no-op |
¹ Uses formatValue semantics for formatted output.
² Parseable strings only; halts with RILL-R038 on failure.
³ Accepts only "true" and "false"; halts with RILL-R036 otherwise.
⁴ true maps to 1, false maps to 0.
⁵ 0 maps to false, 1 maps to true; all other values halt with RILL-R036.
Structural conversion with signatures: :>dict(sig), :>ordered(sig), and :>tuple(sig) accept a structural type signature as the conversion target. The source value must match the target kind (dict-to-dict, tuple-to-tuple, or list-to-tuple). Fields present in the signature but absent from the source are hydrated with the signature’s default values. See Type System for structural type and default value documentation.
Spread Call Operator
The spread call operator expands a value into the positional or named arguments of a function call. Spreading is opt-in — passing a tuple or ordered value without ... passes it as a single argument.
Syntax Forms
| Form | Description |
|---|---|
$fn(...) | Spread piped value into call arguments |
$fn(...$expr) | Spread a specific expression into call arguments |
$fn(a, ...$rest) | Mix fixed args with a spread |
... (bare) is equivalent to ...$ — it spreads the current piped value.
At most one spread is permitted per call.
Piped Spread
Spread the piped value into a multi-param closure:
|a, b, c| { "{$a}-{$b}-{$c}" } => $fmt
tuple[1, 2, 3] -> $fmt(...)
# Result: "1-2-3"Variable Spread
Spread a stored value directly:
|a, b| { $a + $b } => $add
tuple[3, 4] => $args
$add(...$args)
# Result: 7Mixed Args
Combine fixed arguments with a spread:
|a, b, c| { "{$a}-{$b}-{$c}" } => $fmt
tuple[2, 3] => $rest
$fmt(1, ...$rest)
# Result: "1-2-3"No Spread (Pass-Through)
Without ..., a tuple passes as a single argument:
|t| { $t } => $passthrough
tuple[1, 2, 3] -> $passthrough()
# Result: tuple[1, 2, 3]Operator Precedence
From highest to lowest:
- Member access:
.field,[index] - Type operators:
:type,:?type - Unary:
-,! - Multiplicative:
*,/,% - Additive:
+,- - Comparison:
==,!=,<,>,<=,>= - Logical AND:
&& - Logical OR:
|| - Default:
?? - Pipe:
-> - Capture:
=>
Use parentheses to override precedence:
(2 + 3) * 4 # 20
5 -> ($ > 3) ? "big" ! "small" # "big"Operator-Level Annotations
Place ^(...) between the collection operator name and its body to attach operational metadata to that evaluation. Annotations apply per evaluation, not per definition.
Syntax:
collection -> each ^(limit: N) { body }
collection -> map ^(limit: N) { body }
collection -> filter ^(limit: N) { body }
collection -> fold ^(limit: N) |acc, x=0| { body }Examples:
[1, 2, 3] -> each ^(limit: 1000) { $ * 2 }
# [2, 4, 6][1, 2, 3] -> map ^(limit: 10) { $ + 1 }
# [2, 3, 4][1, 2, 3, 4] -> filter ^(limit: 50) { $ > 2 }
# [3, 4][1, 2, 3] -> fold ^(limit: 20) |acc, x=0| { $acc + $x }
# 6The limit key controls maximum iterations for sequential operators and maximum concurrency for parallel operators (map, filter). Invalid annotation keys produce a runtime error.
See Also
- Types — Type system and assertions
- Variables — Capture and scope
- Control Flow — Conditionals and loops
- Collections — Collection operators
- Reference — Quick reference tables