Variables and Scope
Overview
rill uses capture (=>) instead of assignment. Variables are type-locked after first assignment and follow strict scoping rules.
Key principles:
- Capture, not assign: Use
=>to capture values into variables - Type-locked: Variables lock type on first assignment
- No shadowing: Cannot redeclare a variable name from outer scope
- No leakage: Variables created inside blocks don’t exist outside
Variable Declaration
Variables are declared via capture (=>), not assignment:
"hello" => $greeting
42 => $count
[1, 2, 3] => $itemsCapture and Continue
The => operator captures the value AND continues the chain:
"hello"
=> $greeting # capture "hello" into $greeting
-> "{$} world" # $ is still "hello"
=> $message # capture "hello world" into $message
-> .upper # result: "HELLO WORLD"Terminal Capture
Capture at end of expression stores and ends the chain:
"hello" => $result # capture and end chain (result: "hello")The Pipe Variable $
$ holds the current piped value in the current scope:
"test value" -> {
.upper -> log # $ is "test value", logs "TEST VALUE"
}$ Binding by Context
| Context | $ contains |
|---|---|
Inline block -> { } | Piped value |
Each loop -> each { } | Current iteration item |
While-loop (cond) @ { } | Accumulated value |
Do-while @ { } ? cond | Accumulated value |
Conditional cond ? { } | Tested value |
Piped conditional -> ? { } | Piped value (also used as condition) |
Stored closure |x|{ } | N/A — use explicit params |
Dict closure ||{ $.x } | Dict self (this) — late-bound |
Implied $
When certain constructs appear without explicit input, $ is used implicitly:
| Written | Equivalent to | Context |
|---|---|---|
? { } | $ -> ? { } | Piped conditional ($ as condition) |
.method() | $ -> .method() | Method call without receiver |
$fn() | $fn($) | Closure call with no explicit args* |
*Closure calls receive $ only when: no explicit args, first param has no default, and $ is not a closure.
# Inside blocks, $ flows naturally
"test value" -> {
.upper -> log # $ is "test value"
}
# In each loops, $ is the current item
|x| { $x * 2 } => $double
[1, 2, 3] -> each { $double() } # $double receives 1, 2, 3When implied $ does NOT apply:
# Explicit args override implied $
|x| { $x } => $fn
$fn("explicit") # uses "explicit", not $
# Params with defaults use the default
|x: string = "default"| { $x } => $fn2
$fn2() # uses "default", not $Type-Locked Variables
Variables lock type after first assignment:
"hello" => $name # locked as string
"world" => $name # OK: same type5 => $name # ERROR: cannot assign number to stringExplicit Type Annotations
Declare type explicitly with :type:
"hello" => $name:string # declare and lock as string
42 => $count:number # declare and lock as numberSupported types: string, number, bool, closure, list, dict, tuple
Inline Capture with Type
"hello" => $x:string -> .len # type annotation in mid-chainType annotations validate on assignment and prevent accidental type changes:
|x|$x => $fn:closure # locked as closure"text" => $fn # ERROR: cannot assign string to closureScope Rules
Blocks, loops, conditionals, and grouped expressions create child scopes.
Three Rules
- Read from parent: Variables from outer scopes are accessible (read-only)
- No shadowing: Cannot assign to a variable name that exists in an outer scope
- No leakage: Variables created inside don’t exist outside
"context" => $ctx
"check" -> .contains("c") ? {
"process with {$ctx}" -> log # OK: read outer variable
"local" => $temp # OK: new local variable
}
# $temp not accessible here"context" => $ctx
"check" -> .contains("c") ? {
"new" => $ctx # ERROR: cannot shadow outer $ctx
}While Loops and $
While loops use $ as the accumulator since named variables in the body don’t persist across iterations:
# Use $ as accumulator (body result becomes next iteration's $)
0 -> ($ < 5) @ { $ + 1 } # Result: 5
# Variables inside loop body are local to each iteration
0 -> ($ < 3) @ {
($ * 10) => $temp # $temp exists only in this iteration
$ + 1
}
# $temp not accessible hereCommon Mistake: Attempting to modify outer-scope variables from inside loops. This pattern NEVER works:
0 => $count [1, 2, 3] -> each { $count + 1 => $count } # Creates LOCAL $count! $count # Still 0!Use
foldfor reductions, or pack multiple values into$as a dict. See Collections for accumulator patterns.
Reading Outer Variables
10 => $x
[1, 2, 3] -> each {
$x + $ # Reads outer $x = 10
}
# Result: [11, 12, 13]Special Variables
| Variable | Contains | Source |
|---|---|---|
$ | Piped value (current block scope) | Grammar |
$ARGS | CLI positional args (list) | Runtime |
$ENV.NAME | Environment variable | Runtime |
$name | Named variable | Runtime |
$ is a grammar-level construct. All other variables are runtime-provided with the same scoping rules as user-defined variables.
$ARGS
Access CLI positional arguments:
$ARGS[0] # first argument
$ARGS[1] # second argument
$ARGS -> each { log($) } # iterate all arguments$ENV
Access environment variables:
$ENV.HOME # /home/user
$ENV.PATH # /usr/bin:...
$ENV.DEPLOY_ENV ?? "dev" # with defaultRuntime-Provided Variables
Named variables like $file or $config are provided by the host runtime. rill treats them as any other variable in the outer scope:
---
args: file: string, retries: number = 3
---
# $file and $retries available because host parsed frontmatter
process($file, $retries)Inline Capture Pattern
Captures can appear mid-chain for debugging or later reference. Semantically, => $a -> stores the value and returns it unchanged (like log):
"analyze this" => $result -> .upper -> .len
# $result is "analyze this", final result is 12The value flows: "analyze this" → stored in $result → uppercased → length.
Debugging Pattern
"test" => $input -> log -> .upper => $output -> log
# logs "test", then logs "TEST"
# $input is "test", $output is "TEST"Common Patterns
Capture for Reuse
Capture when you need the value in multiple places:
"hello" => $greeting
"{$greeting} world" => $message
"{$greeting} there" => $altLet Data Flow
Prefer implied $ when the value flows directly to the next statement:
# Verbose — unnecessary capture
app::prompt("check status") => $status
$status -> .empty ? app::error("No status")
# Idiomatic — data flows naturally
app::prompt("check status")
.empty ? app::error("No status")Accumulation in Loops
Use $ for accumulation in while loops:
"" -> (.len < 5) @ { "{$}x" } # "xxxxx"See Also
- Types — Type system and type assertions
- Control Flow — Conditionals and loops
- Closures — Closure scope and late binding
- Reference — Quick reference tables