Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Relux Syntax Reference

General

  • Line-oriented, newline-terminated statements (no ;)
  • Comments: // to end of line
  • All values are strings
  • Every expression produces a string value
  • Blocks use { }

Naming Conventions

Naming conventions are enforced at the syntactic level (parse error on violation):

  • Effect names must start with an uppercase letter (CamelCase): StartDb, Effect1, MyService
  • Function names, variable names, shell names, and parameters must start with a lowercase letter or underscore (snake_case): start_server, _helper, my_shell
  • Import aliases must preserve the casing kind of the original name: foo as bar (both lowercase), StartDb as Db (both uppercase)
  • Overlay keys accept either casing (environment variables are conventionally UPPER_SNAKE_CASE)

Imports

import <path> { <name>, <name> as <alias>, }
import <path>
  • <path> resolves from project root (e.g. lib/module1)
  • Selective: import lib/m { foo, bar as b, StartDb as Db } — trailing commas allowed
  • Wildcard: import lib/m — imports all names

Functions

fn <name>(<param>, <param>) {
    <body>
}
  • Return value: last expression in body
  • Execute in the caller’s shell context
  • Shell operators (>, =>, <?, <=, etc.) are valid inside body

Pure Functions

pure fn <name>(<param>, <param>) {
    <body>
}
  • Return value: last expression in body
  • Cannot contain shell operators (>, =>, <?, <=, !?, !=, timeouts)
  • Cannot call impure built-in functions or regular fn functions
  • Only let, variable reassignment, and expressions (including pure BIF calls) are allowed
  • Can be called from condition markers, overlay expressions, and regular shell blocks

Effects

effect <EffectName> {
    expect <VAR>, <VAR>, <VAR>
    start <EffectName>
    start <EffectName> as <alias>
    start <EffectName> as <alias> { KEY = expr, KEY }
    let <name> = <expr>
    expose <shell_name>
    expose <alias>.<shell_name>
    expose <alias>.<shell_name> as <public_name>
    shell <name> { <body> }
    shell <alias>.<shell_name> { <body> }
    cleanup { <body> }
}
  • expect declares required environment variables (comma-separated)
  • start declares dependencies (one per line)
  • start Effect runs the dependency for side effects only — its shells are not accessible
  • start Effect as alias runs the dependency and makes its exposed shells available via dot-access
  • start Effect as alias { KEY = expr } provides an overlay; shorthand KEY is equivalent to KEY = KEY
  • expose declares which shells are part of the effect’s public interface
  • expose alias.shell as name re-exports a dependency’s shell under a new name
  • shell alias.shell_name { ... } — qualified shell block for operating on a dependency’s exposed shell
  • Internal shells not listed in expose are terminated after setup
  • cleanup block: only >, =>, let, variable reassignment allowed (no match operators)

Tests

test "<name>" ~<duration> {
test "<name>" @<duration> {
test "<name>" {
    """
    <doc string>
    """
    let <name>
    start <EffectName>
    start <EffectName> as <alias>
    start <EffectName> as <alias> { KEY = expr, KEY }
    shell <name> { <body> }
    shell <alias>.<shell_name> { <body> }
    cleanup { <body> }
}

Condition Markers

# kind                                  // unconditional
# kind modifier expr                    // truthiness check
# kind modifier expr = expr             // equality comparison
# kind modifier expr ? regex            // regex match

Where:

  • kind: skip | run | flaky
  • modifier: if | unless
  • expr: quoted string with interpolation ("${VAR}", "literal", "${A}:${B}") or bare number (42)
  • regex: regex pattern with ${var} interpolation, to end of line

Examples:

# skip
# skip unless "${CI}"
# run if "${OS}" = "linux"
# run if "${COUNT}" = 0
# skip unless "${ARCH}" ? ^(x86_64|aarch64)$
# flaky if "${CI}" = "true"
# run if "${HOST}:${PORT}" = "localhost:8080"
# skip unless "${VER}" ? ^${MAJOR}\..*$
  • A bare marker (kind only, no modifier) is unconditional
  • One marker per line
  • Multiple markers stack with AND semantics (all must pass or test is skipped)
  • Placed immediately before test, effect, fn, or pure fn declarations (not inside the body)
  • When a function is skipped, all tests that call it are also skipped
  • Comments between markers and the declaration are allowed
MarkerModifierConditionMeaning
# skip(none)(unconditional)always skip
# skipiftruthyskip when condition is true
# skipunlessfalsyskip when condition is false
# run(none)(unconditional)no-op (always run)
# runiffalsyskip when condition is false
# rununlesstruthyskip when condition is true
# flaky(none)(unconditional)always mark as flaky
# flakyiftruthymark as flaky when condition is true
# flakyunlessfalsymark as flaky when condition is false

Truthiness

  • Empty string or unset variable = false
  • Any non-empty string = true
  • = returns the LHS value if LHS equals RHS, empty string otherwise
  • ? returns the regex match if matched, empty string otherwise

Shell Blocks

shell <name> {
    <statements>
}
shell <alias>.<shell_name> {
    <statements>
}
  • Unqualified form (shell name) creates or switches to a local shell
  • Qualified form (shell alias.shell_name) operates on a dependency’s exposed shell
  • Valid inside effect and test blocks

Variables

let <name>                  # declare, defaults to ""
let <name> = "<value>"      # declare with value
let <name> = <expression>   # declare from expression
<name> = <expression>       # reassign existing variable
  • Quoted values required for let assignments
  • Interpolation inside strings: "${name}", "${1}", "${2}", etc.
  • Bare variable reference: name, $1, $2
  • Escape $ with $$
  • Scoped to enclosing block; inner blocks can shadow outer variables
  • Environment variables are readable (base env available everywhere)

Operators

All operators are followed by a space, then payload to end of line.

Send

OperatorPayloadValue
> text to EOLsent string
=> text to EOLsent string
  • > sends with trailing newline
  • => sends without trailing newline (raw send)
  • Variable interpolation applies in payload

Match

OperatorPayloadValue
<? regex to EOLfull match ($0)
<= literal to EOLmatched text
  • <? matches regex against shell output; sets $1, $2, etc. for capture groups
  • <= matches literal with variable substitution
  • Both block until match or timeout

Buffer Reset

<?
<=
  • A match operator with no payload consumes all current output and resets the cursor
  • Useful to skip past output you don’t care about

Inline Timeout Override

Any match operator can be prefixed with ~<duration> (tolerance) or @<duration> (assertion) to set a one-shot timeout:

<~2s? regex pattern       # regex match with 2s tolerance timeout
<~500ms= literal text     # literal match with 500ms tolerance timeout
<@2s? regex pattern       # regex match with 2s assertion timeout
<@500ms= literal text     # literal match with 500ms assertion timeout
  • Duration uses compact humantime format (no spaces): 2s, 500ms, 1m30s
  • Applies only to that single operation — does not affect the scoped timeout
  • Works with both match operators: ?, =
  • Tolerance (~) timeouts are scaled by --timeout-multiplier; assertion (@) timeouts are never scaled

Fail Pattern

OperatorPayload
!? regex to EOL
!= literal to EOL
  • One active fail pattern at a time (single slot)
  • Setting a new one replaces the previous (regex or literal)
  • An empty !? or != (no payload) clears the active fail pattern

Timeout

~<duration>
@<duration>
  • Compact humantime format (no spaces): ~10s, @2s, ~500ms, ~2m30s
  • ~ sets a tolerance timeout — scaled by --timeout-multiplier
  • @ sets an assertion timeout — never scaled (asserts the system responds within a hard deadline)
  • Sets timeout for subsequent match operations in the current shell
  • Overrides previous timeout
  • Scoped to the current function call — reverts when the function returns

Expressions

Every expression produces a string value:

ExpressionValue
"<text>"string literal
namevariable value
$1, $2regex capture group
<fn>(<args>)function return value
> <text> / => <text>sent string
<? <regex>full match ($0)
<= <literal>matched text
<~dur? <regex>full match with timeout override
<~dur= <literal>matched text with timeout override
let x = <expr>assigned value

Last expression in a function body is the return value.

Effect Identity

(effect-name, evaluated overlay restricted to expect-declared vars) determines instance identity:

  • Same tuple = same instance (deduplicated)
  • Different tuple = different instance
  • Overlay expressions are evaluated at setup time; identity is based on evaluated values, not AST form

Cleanup Blocks

cleanup {
    <statements>
}
  • Runs in a fresh implicit shell
  • Any statement valid in a shell block is valid in a cleanup block
  • Always executes, regardless of pass/fail