Conventions and Idioms
This document collects conventions and best practices. It is a living document that will grow as the language matures.
Naming
Case Style: snake_case
Use snake_case for all identifiers in rill:
# variables
"hello" => $user_name
[1, 2, 3] => $item_list
true => $is_valid
# closures
|x|($x * 2) => $double_value
|s|($s -> .trim) => $cleanup_text
# dict keys
[first_name: "Alice", last_name: "Smith", is_active: true] => $userVariables
Use descriptive snake_case names with $ prefix:
"hello" => $greeting # good: descriptive
"hello" => $g # avoid: too terseFor loop variables, short names are acceptable when scope is small:
[1, 2, 3] -> each |x| ($x * 2) # fine: small scopeClosures
Name closures for their action:
|x|($x * 2) => $double # verb describing transformation
|s|($s -> .trim) => $cleanup # verb describing action
||{ $.count * $.price } => $total # noun for computed value
trueCapture and Flow
Prefer inline capture when continuing the chain
Capture mid-chain with => to store and continue:
# good: capture and continue
prompt("Read file") => $raw -> log -> .contains("ERROR") ? {
error("Failed: {$raw}")
}
# less clear: separate statements
prompt("Read file") => $raw
$raw -> log
$raw -> .contains("ERROR") ? { error("Failed: {$raw}") }Use explicit capture before branching
Capture values before conditionals when you need them in multiple branches:
# good: $result available in both branches
checkStatus() => $result
$result -> .contains("OK") ? {
"Success: {$result}"
} ! {
"Failed: {$result}"
}Collection Operators
Choose the right operator
| Use case | Operator | Why |
|---|---|---|
| Transform each element | map | Parallel, all results |
| Transform with side effects | each | Sequential order |
| Keep matching elements | filter | Parallel filter |
| Reduce to single value | fold | Final result only |
| Running totals | each(init) | All intermediate results |
| Find first match | each + break | Early termination |
Prefer method shorthand in collection operators
# good: concise
["hello", "world"] -> map .upper
# equivalent but verbose
["hello", "world"] -> map { $.upper() }
["hello", "world"] -> map |x| $x.upper()Method chains work too:
[" HELLO ", " WORLD "] -> map .trim.lowerUse grouped form for negation
# correct: grouped negation
["", "a", "b"] -> filter (!.empty)
# wrong: .empty returns truthy elements
["", "a", "b"] -> filter .empty # returns list[""]Use fold for reduction, each(init) for running totals
# sum: use fold (returns final value)
[1, 2, 3] -> fold(0) { $@ + $ } # 6
# running sum: use each (returns all intermediates)
[1, 2, 3] -> each(0) { $@ + $ } # list[1, 3, 6]Break returns partial results in each
[1, 2, 3, 4, 5] -> each {
($ == 3) ? break
$ * 2
}
# Result: [2, 4] (elements processed BEFORE break)Loops
Use $ as accumulator in while/do-while
# good: $ accumulates naturally
0 -> ($ < 5) @ { $ + 1 }
# avoid: named variables don't persist across iterations
0 -> ($ < 5) @ {
$ => $x # $x exists only in this iteration
$x + 1
}Prefer do-while for retry patterns
Do-while runs body at least once, eliminating duplicate first-attempt code:
# good: body runs at least once
@ {
attemptOperation()
} ? (.contains("RETRY"))
# less clean: separate first attempt
attemptOperation() => $result
$result -> .contains("RETRY") @ {
attemptOperation()
}Use each for collection iteration, not while
# good: each is designed for collections
$items -> each { process($) }
# avoid: manual iteration with while
$items -> .first() -> (!$.done) @ {
process($.value)
$.next()
}Conditionals
Condition must be boolean
The condition in cond ? then ! else must evaluate to boolean:
# correct: .contains() returns boolean
"hello" -> .contains("ell") ? "found" ! "not found"
# correct: comparison returns boolean
5 -> ($ > 3) ? "big" ! "small"Use ?? for defaults, not conditionals
# good: concise default
$dict.field ?? "default"
# avoid: verbose conditional
$dict.?field ? $dict.field ! "default"Chain conditionals for multi-way branching
($status == "ok") ? {
"Success"
} ! ($status == "pending") ? {
"Waiting"
} ! {
"Unknown: {$status}"
}Multi-line conditionals for readability
Use 2-space indent for ? and ! continuations:
# good: multi-line conditional
some_condition
? "yes"
! "no"
# good: piped conditional split
value -> is_valid
? "ok"
! "error"
# good: chained else-if
$val -> .eq("A") ? "a"
! .eq("B") ? "b"
! "c"
# avoid: inconsistent indent
condition
? "yes"
! "no"Closures
Use braces for complex bodies
# simple: parentheses ok
|x|($x * 2) => $double
# complex: braces required
|n| {
($n < 1) ? 1 ! ($n * $factorial($n - 1))
} => $factorial
trueCapture loop variable explicitly for deferred closures
# good: explicit capture per iteration
[1, 2, 3] -> each {
$ => $item
|| { $item }
} => $closures
# result: closures return [1, 2, 3] when called
trueDict closures for computed properties
Zero-arg closures auto-invoke when accessed:
[
items: [1, 2, 3],
count: ||{ $.items -> .len }
] => $data
$data.count # 3 (auto-invokes)Parameterized closures work as methods:
[
name: "test",
greet: |x|{ "{$.name}: {$x}" }
] => $obj
$obj.greet("hello") # "test: hello"Type Safety
Annotate closure parameters for clarity
|name: string, count: number| {
"{$name}: {$count}"
} => $format
$format("test", 1)Capture with type assertion for documentation
"processing" => $status:stringUse type assertions sparingly
Type assertions (:type) are for validation, not conversion:
# good: validate external input
fetch_data($url):dict => $data
# unnecessary: type is already known
5:number => $nString Handling
Use triple-quotes for multiline content
"""
Analyze this content:
{$content}
Provide a summary.
"""Use .empty for emptiness checks
# idiomatic: use .empty property
"" -> .empty ? "empty" ! "not empty"Direct string comparison works but .empty is preferred:
# works, but verbose
$str == "" ? "empty"
# idiomatic: clearer intent
$str -> .empty ? "empty"Chain string methods naturally
" HELLO world " -> .trim.lower.split(" ")Error Handling
Validate early with conditionals
$input -> .empty ? { error("Input required") }
# continue with validated input
process($input)Use explicit signals for workflow control
prompt("...") => $result
$result -> .contains(":::ERROR:::") ? {
error("Operation failed: {$result}")
}
$result -> .contains(":::DONE:::") ? {
"Complete" -> return
}Anti-Patterns
Avoid reassigning variables
Variables lock to their first type. Reassigning suggests misuse:
# avoid: confusing reassignment
"initial" => $x
"updated" => $x # works but unclear
# prefer: new variable or functional style
"initial" -> transform() => $resultAvoid bare $ in stored closures
# confusing: what is $?
|| { $ + 1 } => $fn # $ is undefined when called
# clear: explicit parameter
|x| { $x + 1 } => $fn
$fn(5)Avoid break in parallel operators
Break is not supported in map or filter (they run in parallel):
# wrong: break in map
[1, 2, 3] -> map { ($ > 2) ? break }
# correct: use each if you need break
[1, 2, 3] -> each { ($ > 2) ? break }
# or filter first
[1, 2, 3] -> filter { $ <= 2 } -> map { $ }Avoid complex logic in conditions
# hard to read
(($x > 5) && (($y < 10) || ($z == 0))) ? { ... }
# clearer: extract to named check
($x > 5) => $big_enough
(($y < 10) || ($z == 0)) => $valid_range
($big_enough && $valid_range) ? { ... }Formatting
Spacing Rules
Operators: space on both sides
# good
5 + 3
$x -> .upper
"hello" => $greeting
($a == $b) ? "yes" ! "no"
# avoid
5+3
$x->.upper
"hello"=>$greetingParentheses: no inner spaces
# good
($x + 1)
($ > 3) ? "big"
[1, 2, 3] -> each |x| ($x * 2)
# avoid
( $x + 1 )
( $ > 3 ) ? "big"Braces: space after { and before }
# good
{ $x + 1 }
[1, 2, 3] -> each { $ * 2 }
|x| { $x -> .trim }
# avoid
{$x + 1}
[1, 2, 3] -> each {$ * 2}Multiline braces: opening brace on same line, closing on own line
# good
[1, 2, 3] -> each {
$ => $item
$item * 2
}
# avoid
[1, 2, 3] -> each
{
$ * 2
}Brackets: no inner spaces for indexing
# good
$[0]
$dict.items[1]
# avoid
$[ 0 ]List/dict literals: space after colons and commas
# good
[1, 2, 3]
[name: "alice", age: 30]
# avoid
[1,2,3]
[name:"alice",age:30]Closure parameters: no space before pipe, space after
# good
|x| ($x * 2)
|a, b| { $a + $b }
|| { $.count }
# avoid
| x | ($x * 2)
|a,b|{ $a + $b }Method calls: no space before dot or parentheses
# good
$str.upper()
$list.join(", ")
"hello" -> .trim.lower
# avoid
$str .upper()
$list.join (", ")Pipes: space on both sides of -> and =>
# good
"hello" -> .upper -> .len
"value" => $x -> log
# avoid
"hello"->.upper->.len
"value"=>$x->logImplicit $ shorthand: prefer sugared forms
# methods: $.foo() -> .foo
# good
"hello" -> .upper -> .len
[1, 2, 3] -> map { $ -> :>string }
# avoid
"hello" -> $.upper() -> $.len
# (no shorthand form for :>type operators — closure form is canonical)
# global functions: foo($) -> foo
# good
"hello" -> log -> .upper
$val.^type
# avoid
"hello" -> log($) -> .upper
$val.^type()
# closures: $fn($) -> $fn
# good
|x| ($x * 2) => $double
5 -> $double
# avoid
5 -> $double($)No throwaway captures: don’t capture just to continue
# avoid: unnecessary intermediate variables
"hello" => $x
$x -> .upper => $y
$y -> .len
# good: use line continuation instead
"hello"
-> .upper
-> .len
# good: capture only when reused later
"hello" => $input
$input -> .upper => $upper
"{$input} became {$upper}" # both variables referencedChain continuations: indent continued lines by 2 spaces
# good: align continuation with pipe
$data
-> .filter { $.active }
-> map { $.name }
-> .join(", ")
# good: long method chains
" hello world "
-> .trim
-> .upper
-> .split(" ")
-> .join("-")
# good: capture mid-chain
prompt("analyze {$file}")
=> $result
-> log
-> .contains("ERROR") ? { error($result) }
# good: conditional continuation
value -> is_valid
? "ok"
! "error"
# good: split else-if chain
$val -> .eq("A") ? 1
! .eq("B") ? 2
! 3
# avoid: no indent on continuation
$data
-> .filter { $.active }
-> map { $.name }One statement per line for complex code
# good: clear structure
$input -> validate() => $valid
$valid -> process() => $result
$result -> format()
# acceptable for simple chains
$input -> .trim -> .lower -> .split(" ")Indent block contents
"first" => $a
"second" => $b
"{$a} {$b}"Align related captures
prompt("Get name") => $name
prompt("Get age") => $age
prompt("Get role") => $roleChecker Modes
rill supports two checker modes that control how the type checker treats dynamic use forms, untyped host references, and direct extension calls.
Strict Mode
Strict mode is recommended for LLM-generated code. The checker rejects constructs that cannot be statically verified:
# Rejected in strict mode: variable use-id form
use<$module_name>
# Rejected in strict mode: computed use-id form
use<($get_module())>
# Rejected in strict mode: untyped host reference
app::fetch($url) # host function without declared return type
# Rejected in strict mode: direct extension call
ext::fn() # extension function called without module bindingThese forms are rejected because they cannot be resolved at check time. LLM-generated scripts benefit from this restriction because it surfaces ambiguities before execution.
Permissive Mode
Permissive mode is the default for human-authored code. The checker emits warnings rather than errors for the constructs that strict mode rejects:
# Allowed in permissive mode (warning issued): variable use-id
use<$module_name>
# Allowed in permissive mode (warning issued): untyped host reference
app::fetch($url)Use permissive mode when iterating on scripts interactively, where runtime behavior is the primary feedback mechanism.
Mode Selection
Set the checker mode via RuntimeOptions.checkerMode:
import { createRuntimeContext } from "@rcrsr/rill";
// Strict mode for LLM-generated scripts
const ctx = createRuntimeContext({ checkerMode: "strict" });
// Permissive mode (default) for human-authored scripts
const ctx = createRuntimeContext({ checkerMode: "permissive" });The default value is 'permissive'. Omitting checkerMode or passing undefined is equivalent to 'permissive' — both allow warnings without errors. Pass 'strict' when running scripts from untrusted or machine-generated sources.
This document will be extended as conventions emerge from real-world usage.
See Also
- Design Principles — Core philosophy
- Reference — Language specification
- Guide — Getting started tutorial