Operators

Overview

CategoryOperators
Pipe->
Capture=>
Arithmetic+, -, *, /, %
Comparison==, !=, <, >, <=, >=
Comparison Methods.eq, .ne, .lt, .gt, .le, .ge
Logical! (unary), &&, `
Chainchain($fn), chain([...])
Orderedordered[k: v] (named ordered container)
Extractiondestruct<...> (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 -> ($ * $ + $)               # 30

Method 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 $count

Capture and Continue

=> captures AND continues the chain:

"hello" => $a -> .upper => $b -> .len
# $a is "hello", $b is "HELLO", result is 5

See Variables for detailed scoping rules.


Arithmetic Operators

OperatorDescription
+Addition
-Subtraction
*Multiplication
/Division
%Modulo (remainder)
5 + 3                          # 8
10 - 4                         # 6
3 * 4                          # 12
15 / 3                         # 5
17 % 5                         # 2

Precedence

Standard mathematical precedence (high to low):

  1. Unary: -, !
  2. Multiplicative: *, /, %
  3. Additive: +, -
2 + 3 * 4                      # 14 (multiplication first)
(2 + 3) * 4                    # 20 (parentheses override)
-5 + 3                         # -2

Type Constraint

All operands must be numbers. No implicit conversion:

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

Error Handling

10 / 0                         # ERROR: Division by zero
10 % 0                         # ERROR: Modulo by zero

Comparison Operators

OperatorDescription
==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                         # true

Value Comparison

All comparisons are by value, not reference:

[1, 2, 3] == list[1, 2, 3]         # true
[a: 1] == dict[a: 1]               # true
"hello" == "hello"                     # true

Comparison Methods

Methods provide readable alternatives in conditionals:

MethodEquivalent
.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

OperatorDescription
&&Logical AND (short-circuit)
||Logical OR (short-circuit)
!Logical NOT
(true && false)                # false
(true || false)                # true
!true                          # false
!false                         # true

Short-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)              # true

Grouping 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 = true

No 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])    # 22

Chain a single closure:

|x|($x * 2) => $dbl
5 -> chain($dbl)                           # 10

ordered[...] 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 = 3

With 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 = 4

Dict destructuring (explicit key mapping):

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

Nested destructuring:

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

Errors:

[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 found

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

Member Access Operators

Field Access .field

Access dict fields:

[name: "alice", age: 30] => $person
$person.name                     # "alice"
$person.age                      # 30

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

See 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                   # 0

With 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 undefined

With 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 undefined

Method calls evaluate fully before the default operator applies.


Existence Operators

Field Existence .?field

Returns boolean:

[name: "alice"] => $user
$user.?name                      # true
$user.?age                       # false

Existence with Type .?field&type

Check existence AND type:

[name: "alice", age: 30] => $user
$user.?name&string               # true
$user.?age&number                # true
$user.?age&string                # false

The &type position accepts parameterized types and union types.

Parameterized type:

[items: [1, 2, 3]] => $data
$data.?items&list(number)
# Result: true

Dict structural type:

[cfg: [key: "x"]] => $data
$data.?cfg&dict(key: string)
# Result: true

Union type:

[score: 42] => $data
$data.?score&string|number
# Result: true

The & 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 string

Type Check :?type

Returns boolean:

42:?number                       # true
"hello":?number                  # false
"hello" -> :?string              # true

Coarse checks return boolean directly:

[1, 2, 3] -> :?list              # true
[a: 1] -> :?dict                 # true

Structural 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                      # number

The 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
listno-operrorvaliderrorerrorvalid¹error
dicterrorno-operrorvaliderrorvalid¹error
tuplevaliderrorno-operrorerrorvalid¹error
orderederrorvaliderrorno-operrorvalid¹error
stringerrorerrorerrorerrorvalid²no-opvalid³
numbererrorerrorerrorerrorno-opvalid¹valid⁵
boolerrorerrorerrorerrorvalid⁴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

FormDescription
$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: 7

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

  1. Member access: .field, [index]
  2. Type operators: :type, :?type
  3. Unary: -, !
  4. Multiplicative: *, /, %
  5. Additive: +, -
  6. Comparison: ==, !=, <, >, <=, >=
  7. Logical AND: &&
  8. Logical OR: ||
  9. Default: ??
  10. Pipe: ->
  11. 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 }
# 6

The 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