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
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_name>
expose <alias>.<shell_name>
expose <alias>.<shell_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 available via dot-accessstart Effect as alias { KEY = expr }provides an overlay; shorthandKEYis equivalent toKEY = KEYexposedeclares which shells are part of the effect’s public interfaceexpose alias.shell as namere-exports a dependency’s shell 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
- 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 - Qualified form (
shell alias.shell_name) operates on a dependency’s exposed shell - 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 (base env 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
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
| 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