Conventions and Idioms

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] => $user

Variables

Use descriptive snake_case names with $ prefix:

"hello" => $greeting           # good: descriptive
"hello" => $g                  # avoid: too terse

For loop variables, short names are acceptable when scope is small:

[1, 2, 3] -> each |x| ($x * 2)    # fine: small scope

Closures

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
true

Capture 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 caseOperatorWhy
Transform each elementmapParallel, all results
Transform with side effectseachSequential order
Keep matching elementsfilterParallel filter
Reduce to single valuefoldFinal result only
Running totalseach(init)All intermediate results
Find first matcheach + breakEarly 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.lower

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

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

Dict 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:string

Use 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 => $n

String 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() => $result

Avoid 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"=>$greeting

Parentheses: 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->log

Implicit $ 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 referenced

Chain 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") => $role

Checker 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 binding

These 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