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 and effect aliases must start with an uppercase letter (
CamelCase):StartDb,Effect1,start Db as MyDb - Function names and shell names must be
snake_case:start_server,_helper,my_shell - Variable names and parameters are permissive (any alphanumeric + underscore, starting with letter or
_):port,DB_HOST,_private - 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
fnfunctions - 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 <shell_name>
expose shell <Alias>.<shell_name> as <public_name>
expose var <var_name>
expose var <Alias>.<var_name> as <public_name>
shell <name> { <body> }
shell <Alias>.<shell_name> { <body> }
cleanup { <body> }
}
expectdeclares required environment variables (comma-separated)startdeclares dependencies (one per line)start Effectruns the dependency for side effects only — its shells are not accessiblestart Effect as Aliasruns the dependency and makes its exposed shells/variables available via dot-access- Effect aliases must be CamelCase
start Effect as Alias { KEY = expr }provides an overlay; shorthandKEYis equivalent toKEY = KEYexpose shelldeclares which shells are part of the effect’s public interfaceexpose vardeclares which variables are part of the effect’s public interface; these arelet-bound values computed during setupexpose shell Alias.shell as namere-exports a dependency’s shell under a new nameexpose var Alias.var as namere-exports a dependency’s variable under a new nameshell Alias.shell_name { ... }— qualified shell block for operating on a dependency’s exposed shell- Internal shells not listed in
exposeare terminated after setup cleanupblock: 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|flakymodifier:if|unlessexpr: 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, orpure fndeclarations (not inside the body) - When a function is skipped, all tests that call it are also skipped
- A
# flakymarker on a function or effect propagates too: the test is marked flaky when a function or effect it reaches is flaky - Comments between markers and the declaration are allowed
| Marker | Modifier | Condition | Meaning |
|---|---|---|---|
# skip | (none) | (unconditional) | always skip |
# skip | if | truthy | skip when condition is true |
# skip | unless | falsy | skip when condition is false |
# run | (none) | (unconditional) | no-op (always run) |
# run | if | falsy | skip when condition is false |
# run | unless | truthy | skip when condition is true |
# flaky | (none) | (unconditional) | always mark as flaky |
# flaky | if | truthy | mark as flaky when condition is true |
# flaky | unless | falsy | mark 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; name must be snake_case - Qualified form (
shell Alias.shell_name) operates on a dependency’s exposed shell; qualifier is a CamelCase effect alias, name is snake_case - Valid inside
effectandtestblocks
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
letassignments - 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 (the layered base environment — host process plus any
.envfiles — is available everywhere)
Operators
All operators are followed by a space, then payload to end of line.
Send
| Operator | Payload | Value |
|---|---|---|
> | text to EOL | sent string |
=> | text to EOL | sent string |
>sends with trailing newline=>sends without trailing newline (raw send)- Variable interpolation applies in payload
Match
| Operator | Payload | Value |
|---|---|---|
<? | regex to EOL | full match ($0) |
<= | literal to EOL | matched text |
<?matches regex against shell output; sets$1,$2, etc. for capture groups<=matches literal with variable substitution- Both block until match or timeout
Multi-Pattern Match
<{
? <regex>
= <literal>
}
- Inner lines are
? <regex>(regex) or= <literal>(literal), one per line - The block waits for every pattern to match at least once, in any order
- The cursor advances once at block exit, to
max(end)across all per-pattern matches - Capture groups in inner regex patterns do not bind
- The empty form
<{ }is a parse error - Comments are permitted between inner lines
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
<~10s{ ? a ? b } # multi-pattern block with 10s tolerance timeout
<@500ms{ = a = b } # multi-pattern block 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 (
?,=) and the multi-pattern form<{ ... } - Tolerance (
~) timeouts are scaled by--timeout-multiplier; assertion (@) timeouts are never scaled
Fail Pattern
| Operator | Payload |
|---|---|
!? | 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:
| Expression | Value |
|---|---|
"<text>" | string literal |
name | variable value |
$1, $2 | regex 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