RiX Syntax Guide
Part 1: Syntax → System Function
Operators
| Syntax | System Function | Example |
|---|---|---|
+ |
ADD |
3 + 4 |
- |
SUB |
10 - 3 |
* |
MUL |
5 * 6 |
/ |
DIV |
7 / 2 → 7/2 |
// |
INTDIV |
7 // 2 → 3 |
% |
MOD |
10 % 3 → 1 |
^ or ** |
POW |
2 ^ 10 → 1024 |
== |
EQ |
x == 5 (value equality) |
=== |
SAME_CELL |
x === y (identity — same cell) |
!= |
NEQ |
x != 0 |
< |
LT |
a < b |
> |
GT |
a > b |
<= |
LTE |
x <= 10 |
>= |
GTE |
x >= 0 |
&& |
AND |
a > 0 && b > 0 |
|| |
OR |
x == 0 || y == 0 |
?| |
HOLE_COALESCE |
a[2] ?| 9 — returns left if not a hole, else right |
! |
NOT |
!(x == 0) |
x ? :name |
SEMANTIC_HAS |
Checks .__type, ._type, and .__traits |
x ~: :type |
SEMANTIC_CONVERT_SOFT |
Convert or return _ |
x ~!: :type |
SEMANTIC_CONVERT_STRICT |
Convert or throw |
= |
ASSIGN |
y = x (alias/rebind) |
:= |
ASSIGN_COPY |
x := 5 (fresh copy) |
~= |
ASSIGN_UPDATE |
x ~= 9 (in-place) |
::= |
ASSIGN_DEEP_COPY |
y ::= x (deep copy) |
~~= |
ASSIGN_DEEP_UPDATE |
x ~~= val (deep in-place) |
[a, b] = rhs |
DESTRUCTURE_ASSIGN |
Destructure with outer = as the default per-entry binding mode |
{= a, b[:x] } := rhs |
DESTRUCTURE_ASSIGN |
Map destructuring with outer := as the default per-entry binding mode |
- (unary) |
NEG |
-x |
Implicit Multiplication & Callable Application
When two expressions appear adjacent with no binary operator between them, RiX interprets the adjacency as either implicit multiplication or implicit callable application.
Implicit Multiplication
Adjacent expressions that are not callable produce multiplication:
| Syntax | Equivalent | Notes |
|---|---|---|
3a |
3 * a |
Number × identifier |
3 a |
3 * a |
Space does not change semantics |
a b |
a * b |
Two lowercase variables |
3(7+x) |
3 * (7+x) |
Number × parenthesized expression |
(x+1)(x+2) |
(x+1) * (x+2) |
Two groupings |
3x^2 |
3 * (x^2) |
Exponentiation binds tighter |
ab is a single identifier, not a * b. Only separate tokens produce implicit multiplication.
Implicit Callable Application
An uppercase-leading callable identifier (like F, G) followed by an adjacent expression consumes the maximal multiplicative chunk as its argument:
| Syntax | Equivalent | Notes |
|---|---|---|
F 3 |
F(3) |
Simple application |
F x |
F(x) |
Identifier argument |
F 3x |
F(3*x) |
Chunk includes implicit mul |
F 3x^2 |
F(3*(x^2)) |
Chunk includes exponentiation |
F 3*x + 7 |
F(3*x) + 7 |
Chunk stops at + |
F 3/x + 7 |
F(3/x) + 7 |
Chunk includes / |
F (3x + 7) |
F(3*x + 7) |
Parens extend argument past chunk |
Nested application chains right-to-left through callables:
| Syntax | Equivalent | Notes |
|---|---|---|
F G 7 |
F(G(7)) |
Nested callable consumption |
3 F G 7 |
3 * F(G(7)) |
Number prefix is multiplication |
3 F G 7 H 9 |
3 * F(G(7 * H(9))) |
Full chain |
Precedence
From loosest to tightest: 1. Addition/subtraction (+, -) 2. Explicit multiplication (*, /, //, %) 3. Implicit multiplication (adjacency of non-callable expressions) 4. Implicit application (adjacency where left is callable) 5. Exponentiation (^)
Key rules
3 Fdoes not implicitly callF— it is3 * F, which errors ifFis a bare function value.Falone is just variable retrieval, not a call.F + 1does not auto-callF.- Explicit call syntax
F(args)is always available and unchanged. - Uppercase identifiers are callable; lowercase identifiers are not.
3 < 7remains an ordinary comparison — comparison operators are not affected.
Assignment & Definition
Variables name cells — mutable containers holding a value and meta properties. Different assignment operators produce different cell semantics.
| Syntax | System Function | Semantics |
|---|---|---|
x = expr |
ASSIGN |
Alias/rebind — if rhs is a variable, share its cell; otherwise create a fresh cell |
x := expr |
ASSIGN_COPY |
Fresh copy — shallow-copy value + all meta into a new cell |
x ~= expr |
ASSIGN_UPDATE |
In-place value replacement — preserve cell identity and ordinary meta |
x ::= expr |
ASSIGN_DEEP_COPY |
Deep copy — like := but recursively copies nested collections |
x ~~= expr |
ASSIGN_DEEP_UPDATE |
Deep in-place — like ~= but deep-copies the rhs value |
x += expr |
ASSIGN_UPDATE |
Combo ops desugar to x ~= x op expr (cell-preserving) |
x ++= expr |
ASSIGN_UPDATE |
Concatenation assignment — delegates to x ++ expr then updates in place |
x \/= expr |
ASSIGN_UPDATE |
Union assignment — delegates to x \/ expr then updates in place |
x /\= expr |
ASSIGN_UPDATE |
Intersection assignment — delegates to x /\ expr then updates in place |
x \= expr |
ASSIGN_UPDATE |
Difference assignment — delegates to x \ expr then updates in place |
x **= expr |
ASSIGN_UPDATE |
**-assignment — delegates to the existing x ** expr semantics, then updates in place |
x /^= expr |
ASSIGN_UPDATE |
Ceiling-division assignment — delegates to x /^ expr then updates in place |
x /~= expr |
ASSIGN_UPDATE |
Rounded-division assignment — delegates to x /~ expr then updates in place |
F(x) -> body |
FUNCDEF |
Sq(x) -> x ^ 2 |
(x) -> body |
LAMBDA |
(x) -> x + 1 |
F(x) ?- [prep] -> body |
FUNCDEF |
Soft prep phase before body |
(x) ?!- [prep] -> body |
LAMBDA |
Strict prep phase before body |
expr ?- pattern: [prep] |
PREP_TRIAL |
Return the candidate or _ on evaluation, binding, or prep failure |
expr ?!- pattern: [prep] |
PREP_TRIAL |
Return the candidate or throw on evaluation, binding, or prep failure |
F(x) => body |
MULTIFUNCDEF |
Append variant to multifunction |
F(x) ^=> body |
MULTIFUNCDEF |
Prepend variant to multifunction |
{> f, G, H[:Named] } |
MULTIFUNCTION |
Explicit multifunction literal; nested multifunctions flatten in order |
(x) /Name/ -> body |
LAMBDA |
Name a variant with variant.__name |
Key distinctions: - = shares a cell (aliases track mutations); := creates an independent copy. - ~= replaces the value inside an existing cell, so aliases see the change. = rebinds to a different cell. - Combo operators (+=, -=, *=, /=, //=, %=, ^=, ++=, \/=, /\=, \=, **=, /^=, /~=) use ~= semantics — they preserve cell identity.
Function Prep Phase
Prep runs after parameter binding and before the function body:
(x, y) ?- [
xr = x ~: :rational,
yr = y ~: :rational,
sum = xr + yr,
sum > 0
] -> sum
Rules:
- Prep entries execute left-to-right.
- Prep shares the function’s local scope.
- Prep-created bindings are visible to later prep entries and to the body.
- In
?-, a prep entry that returns_or throws makes the call return_. - In
?!-, the same failures throw.
Prep is meant for validation, conversion, destructuring, and local setup. RiX does not currently restrict mutation or IO in prep.
Prepared Trial Expressions
Expression-level prep evaluates a candidate once and gives it an arm-local binding or destructuring pattern:
F(3) ?- x: [x ? :Integer, x > 0]
F(3) ?!- {: x, y }: [x + y == 1]
On success the original candidate is returned. Soft ?- failure returns _; strict ?!- failure throws. Binding and prep locals do not escape. Gates may be chained, and the first gate determines whether candidate-evaluation errors are soft or strict:
F(3) ?- x: [x ? :Integer] ?!- x: [x > 0]
Temporary bindings are discarded after the trial; mutations and IO performed by prep code are not rolled back.
Inside {? ... }, soft trial failure advances to the next arm, while strict failure throws. Ordinary case expressions remain unconditional fallbacks:
{?
F(-3) ?- x: [x > 0];
F(4) ?- x: [x > 0];
5
}
Multifunctions
Multifunctions are callable arrays marked with ._type = :multifunction. RiX automatically applies that mark when an uppercase identifier is assigned an array. The explicit {> ... } literal creates the same runtime value directly, which is especially useful inline with pipes.
value |> {>
(x) ?- [x < 0] /Negative/ -> -x,
(x) /Other/ -> x
}
Literal entries may be functions or multifunctions. Nested multifunctions are recursively flattened in source order. Named selection returns the underlying function, so {> F[:Positive], G, H } composes selected variants, ordinary functions, and whole multifunctions using the same dispatch machinery.
F = [
(x) ?- [x > 0] /Positive/ -> x,
(x) /Fallback/ -> -x
]
Dispatch model:
- variants are considered in array order
- prep success selects the variant
- prep failure (
_or soft prep error) moves to the next variant - body errors propagate
- body return values are final, including
_ - if nothing matches, the result is
_
?!- keeps its strict meaning inside multifunctions: a thrown prep error or a prep result of _ propagates as a failure instead of trying the next variant.
Append and prepend forms mutate the multifunction definition:
F(x) => x + 1
F(x) ^=> x * 10
F(x) /Exact/ => x
If F is undefined, RiX creates a new multifunction. If F is a single function, RiX converts it into a multifunction before appending.
Named variants use /Name/ and can be dispatched directly:
F[:Exact](7)
$$[:Exact](7)
Inside a variant body:
$resolves the current variant$$resolves the parent multifunction
If execution reaches a variant with no prep and later variants still exist, RiX can emit a configurable warning because the later variants are unreachable from that point.
Left-Hand Destructuring
RiX supports left-hand destructuring for arrays, tuples, maps, and tensors. Destructuring:
- evaluates the rhs once
- extracts the actual stored entries from that source structure
- binds outward using ordinary RiX assignment semantics
The outer operator sets the default binding mode for all bound targets unless an entry overrides it:
[a, b] = rhs
[a, b] := rhs
{= a, ~=b[:x] } ~= rhs
Patterns:
[a, b, ...rest]
{: a, b, ...rest }
(a, b, ...rest)
{= a, b[:x], pair[:pt] = [u, v], [:meta] = {: p, q}, ...rest }
{:2x2: [a, b], [c, d]}
Failure rules:
- missing simple array/tuple positions bind a hole
- missing simple map keys bind a hole
- missing nested required structure is an error
- extra source contents are ignored unless captured by final
...rest
Map forms:
ameans keya, targetab[:a]means keya, targetba = patternmeans bind whole selected value toa, then destructure it[:a] = patternmeans destructure keyawithout binding the whole selected valueb[:a] = patternmeans bind the whole selected value tob, then destructure it
Per-entry overrides reuse assignment semantics directly:
[==a, :=b, ~=c, ~~=d]
{: ==a, b, :=c }
{= ==a, b[:x], ~=c[:y] }
Target-side semantic wrapping uses {^ /.../ target} inside patterns:
[{^ /::rational/ x}] = [2]
{= p[:pt] = {^ /:= #point ::Point :cartesian/ q} } = m
In destructuring headers, :trait is a required source-side check. The extracted source value must already satisfy that trait or evaluation fails.
Indexed destructuring extends the same system:
{.. a[1:3], b[2:4], c[3] } = [10, 20, 30, 40, 50]
{.. picked[-1:1] = [x, y, ...z] } = arr
{.. [2:4] = [u, v, ...w] } = arr
{.. row2[2, 1:3], block[1:2, 1:2] } = tensor
Rules:
- indexing applies to the source object being destructured, not to the target
- each indexed entry extracts independently from the same source object
- overlapping and repeated extraction is allowed
- per-entry binding overrides still apply after extraction
- nested
= patternforms destructure the extracted subobject, not the original source
Supported entry forms:
target[indexSpec]target[indexSpec] = pattern[indexSpec] = pattern
Examples:
{.. ==a[1:3], ~=b[2:4], :=c[3] } = arr
{= x[:a], y[:b] } = {= a = 1, b = 2 }
{.. row2[2, 1:3], col2[::, 2] } = tensor
Tuple bracket slices now produce tuples; sequence bracket slices produce sequences.
For sequence-like values, bracket slices use interval semantics: they are closed (inclusive on both ends), strict, and directed.
a[2:4] ## includes indices 2, 3, 4
a[4:2] ## traverses backward
a[-2:-1] ## counts from the end
This is intentionally different from receiver-first .Slice(start?, end?), whose end bound remains exclusive.
Headered Outfitting And Constructor Capture
RiX now uses a shared / ... / header zone for value outfitting and constructor defaults:
{^ 7}
{^ /#len ::Length :meters/ 7}
{= /:= #pt ::Point :cartesian/ x = 3, y = 4}
Semantic Inquiry And Conversion
RiX also has lightweight expression forms for semantic inquiry and explicit conversion:
x ? :rational
x ~: :rational
x ~!: :rational
Rules:
x ? :namesucceeds if any of these are true:x.__type == :name,x._type == :name, or:nameis contained inx.__traits- successful inquiry returns a non-null truthy value; failure returns
_ - no trait inheritance or group expansion is applied here
x ~: :typereuses the same semantic type conversion/canonicalization path as{^ /::type/ x}x ~: :typereturns the converted value on success, otherwise_x ~!: :typereturns the converted value on success and throws on failure- conversion targets are semantic types, not traits
- these operators return new outfitted values; they do not mutate the original binding unless assigned back
Soft conversion warnings are controlled by runtime config:
warnings: {
conversion: true
}When warnings.conversion === true, failed ~: emits a structured warning before returning _.
RiX now backs these forms with immutable type and trait registries. Types are protocol bundles: they can define conversion, normalization, validation, default traits, proto methods, export/import hooks, and operator variants. Traits can imply other traits and contribute proto methods. Implied traits are materialized into .__traits, so x ? :trait remains a direct membership check.
See types-and-traits-guide.md for the full registry, conversion, export/import, and system multifunction installation model.
{.. /::=/ a, ==b, ~=c}
{: /#pair/ a, b}
{:2x2: /#M ::Matrix :square/ 1, 2; 3, 4}
The header vocabulary is:
- capture mode:
==,:=,~=,::=,~~= - sticky name:
#name - sticky semantic type:
::TypeName - sticky traits:
:trait
Container construction follows the same cell/copy model as assignment. Every inserted entry uses one effective capture mode, chosen in this order:
- Per-entry override
- Constructor default
- Runtime default (
defaultConstructorCaptureMode, currentlydeep_copy)
The five constructor capture modes are assignment-analogues:
| Constructor mode | Meaning in constructors |
|---|---|
== |
Alias capture — keep the source value live like = on an existing cell source |
:= |
Fresh shallow copy |
~= |
Fresh “refreshing” shallow copy — same meta transfer rules as x ~= rhs when x is undefined |
::= |
Fresh deep copy |
~~= |
Fresh “refreshing” deep copy — same meta transfer rules as x ~~= rhs when x is undefined |
Maps can override capture per entry:
{::= a = x, b == y, c ~= z}
For non-map advanced brace constructors, an entry-level override uses prefix form:
{.. 1, == x, := y}
Advanced brace constructors set a constructor-wide default in the shared header:
{.. 1, 2, 3}
{.. /:=/ x, y}
{: /::=/ a, b, c}
{| /==/ item1, item2 |}
{:2x2: /::=/ a, b; c, d}
[...], {= ...}, {: ...}, {| ...}, and tensor literals without an explicit constructor header still participate in this model; they simply fall back to the runtime default capture mode.
Value outfitting uses the same header but applies to the resulting value itself after capture:
- evaluate the source expression
- apply capture mode (header or runtime default)
- assign sticky
.__nameif present - apply semantic type processing / canonicalization
- apply traits in textual order
- build semantic
.__proto - rebuild ephemeral
._typeand._proto
Sticky semantic metadata lives in .__name, .__type, .__traits, and .__proto. Ephemeral runtime metadata lives in ._type and ._proto.
Semantic .__proto is layered:
.__proto[:traits].__proto[:type]._proto
Method lookup therefore checks direct meta first, then semantic trait methods, then semantic type methods, then builtin/runtime methods. x._proto still exposes the builtin layer directly.
On ~= / ~~= updates, sticky semantic metadata is preserved unless explicitly replaced. If a value has a sticky semantic type, the new raw rhs is processed through that type again before installation. Traits are sticky augmentations in this version; they do not transform values. Automatic trait validation only runs when enabled globally or when the sticky :verify trait is present.
Examples:
xs := [1, 2]
ys = xs
xs ++= [3]
## ys is now [1, 2, 3]
s := {| 1, 2 |}
s \/= {| 2, 3 |}
## s is now {| 1, 2, 3 |}
t := {| 1, 2, 3 |}
t /\= {| 2, 3, 4 |}
## t is now {| 2, 3 |}
u := {| 1, 2, 3 |}
u \= {| 2 |}
## u is now {| 1, 3 |}
Brace Containers
| Syntax | System Function | Description |
|---|---|---|
{ a; b; c } |
BLOCK |
Sequential execution, returns last value. Optional top-of-block import header: { <...> ... } |
{; a; b; c } |
BLOCK |
Sequential execution (explicit block). Optional top-of-block import header: {; <...> ... } |
{? c1 ? v1; c2 ? v2; default } |
CASE |
Conditional branching (if/elseif/else) |
{@ init; cond; body; update } |
LOOP |
Loop with init, condition, body, update. A fifth after slot may be added: {@ init; cond; body; update; after }; it runs on normal completion and supplies the loop result. Loop headers also support {@name@ ... }, {@:100@ ... }, {@name:100@ ... }, {@::@ ... }, and {@name::@ ... }. Optional top-of-block import header: {@ <...> ... } |
{! expr } |
BREAK |
Break the nearest matching block/case/loop and use expr as that target’s final value |
{#x,y:z# p = x + y } |
SYSTEM_SPEC |
Symbolic system spec literal. Optional top-of-block import header: {#x,y:z# <...> ... } |
{= k1=v1, (expr)=v2 } |
MAP |
Map/object literal (k1 identifier sugar or parenthesized key expression) |
{.. a, b, c } |
ARRAY_CAPTURE |
Brace-form array literal with constructor capture controls |
{\| a, b, c } |
SET |
Set literal |
{: a, b, c } |
TUPLE |
Tuple literal |
{> f, G, H[:Named] } |
MULTIFUNCTION |
Ordered callable variants; nested multifunctions flatten in source order |
{+ a, b, c } |
ADD |
N-ary addition or concatenation |
{* a, b, c } |
MUL |
N-ary multiplication |
{&& a, b, c } |
AND |
N-ary logical AND (short-circuits on falsy) |
{\|\| a, b, c } |
OR |
N-ary logical OR (short-circuits on truthy) |
{\/ a, b, c } |
NARY_UNION |
N-ary set union / interval hull |
{/\ a, b, c } |
NARY_INTERSECT |
N-ary set intersection / interval overlap |
{++ a, b, c } |
NARY_CONCAT |
N-ary concatenation |
{<< a, b, c } |
MIN |
N-ary minimum (null args ignored) |
{>> a, b, c } |
MAX |
N-ary maximum (null args ignored) |
{/pattern/flags?mode} |
REGEX |
Regular expression literal |
System Context (. Dot Syntax)
The leading . refers to the system capability object — a frozen, sandboxable map of all built-in functions. System functions may only be called through this object.
| Syntax | IR Node | Description | Example |
|---|---|---|---|
. |
SYS_OBJ |
The system context as a RiX value (copy) | sys := . |
.Name |
SYS_GET |
Get a system capability reference | fn := .ADD |
.Name(args) |
SYS_CALL |
Call a system capability | .ADD(3, 4) → 7 |
.Eval(ast_or_str, b?, m?) |
SYS_CALL |
Evaluates a deferred AST or code string at runtime with optional bindings map and mode (:inherit or :fresh) |
.Eval("1 + 2") |
.FREEZE = 1 |
SYS_SET |
Freeze the system context | .FREEZE = 1 |
@_Name(args) |
SYS_CALL |
Alternative call syntax (identical to .Name(args)) |
@_ADD(3, 4) → 7 |
System context meta-methods (called via dot syntax): - .Withhold("NAME") — return a copy with a capability removed (for sandboxing loaded scripts) - .With("NAME", fn) — return a copy with an added or replaced capability
Deferred Syntax & Operator Aliases
| Syntax | Description | Example |
|---|---|---|
@{; ... } |
Deferred block (returns AST tree, does not evaluate) | f = @{; x + 1 } |
@{= ... } |
Deferred map | lazyMap = @{= a=1 } |
@{#x:p# p = x + 1 } |
Deferred symbolic system spec | lazySpec = @{#x:p# p = x + 1 } |
@@expr |
Evaluate a deferred AST node or code string at runtime directly in the caller’s scope (sugar for .Eval(expr)) |
f = @{; x = 2 }; @@f → sets x=2 locally |
@+, @*, @<, etc |
Retrieve operator’s system capability (alias for .ADD, .MUL, etc.) |
f = @+; f(10, 20) → 30 |
Built-in methods on deferred values
Deferred values (@{...}) have built-in methods for evaluation, introspection, and debugging:
| Method | Description |
|---|---|
d.Eval() |
Evaluate the deferred block in the current (caller’s) scope. Identical to .Eval(d). |
d.Eval(bindings) |
Evaluate with extra variable bindings injected into the scope. |
d.Eval(bindings, :fresh) |
Evaluate in a fresh isolated scope (no access to outer variables). |
d.Desugar() |
Return a string showing the full raw IR tree. BLOCK nodes and complex children are rendered with each argument on its own indented line. |
d.Desugar(n) |
Return the IR tree up to depth n; nodes beyond that depth render as FN(...). n = -1 (default) is unlimited. n = 0 gives just DEFER(...). |
d.Inspect() |
Run in a fresh isolated scope and return a report: inputs, a full execution trace, and the final output. |
d.Inspect(bindings) |
Same but inject bindings as the input variables. |
d.Inspect(bindings, depth) |
Control trace depth: depth = -1 (default) traces everything; depth = 0 disables the trace (inputs + output only); depth = n traces n levels into the call tree. |
Notes: - d.Eval() inherits the surrounding scope and can read/write outer variables (same as @@d). - d.Eval(_, :fresh) creates an isolated scope — outer variables are inaccessible and assignments do not escape. - d.Inspect() always runs in a fresh isolated scope regardless of bindings; outer variables are never visible. - d.Desugar() shows the underlying IR (e.g. ADD, RETRIEVE, LITERAL, BLOCK) — useful for understanding how expressions are lowered. - The trace in d.Inspect() evaluates BLOCK statements once each (no double-evaluation). For other node types, tracing sub-expressions involves a second evaluation of those sub-expressions, which is safe for pure arithmetic and variable reads but may produce duplicate entries for side-effectful operations (mutations, .Warn, etc.). Use depth = 0 to suppress the trace for such code.
Operator alias mapping:
| Operator Alias | System Capability |
|---|---|
@+ |
.ADD |
@- |
.SUB |
@* |
.MUL |
@/ |
.DIV |
@// |
.INTDIV |
@% |
.MOD |
@^ |
.POW |
@== |
.EQ |
@!= |
.NEQ |
@< |
.LT |
@> |
.GT |
@<= |
.LTE |
@>= |
.GTE |
@&& |
.AND |
@\|\| |
.OR |
@! |
.NOT |
Loop Headers And Break Blocks
Loop max syntax:
{@ ... }or{@name@ ... }uses the runtime default loop max.{@:100@ ... }or{@name:100@ ... }sets an explicit finite max.{@::@ ... }or{@name::@ ... }disables max checking.
The current runtime default is defaultLoopMax = 10000 unless the host changes it. The max check happens after the loop condition succeeds and before the next body execution, so a max of 100 permits exactly 100 iterations.
If a finite cap is exceeded, evaluation throws:
Loop exceeded max iteration count: 100
Loop forms:
{@ init; cond; body }uses the body as the whole iteration step and returns the last body value.{@ init; cond; body; update }runs update after each body execution and returns the last body value.{@ init; cond; body; update; after }runsafteronce whencondbecomes false and returns theaftervalue.
The after slot shares the loop scope, so it can read loop-local accumulators. BREAK exits the loop immediately and skips after; the break value is the loop result.
Blank loop slots are preserved as no-op holes. For example, {@ i = 0; i < 5; ; i += 1; i^2 } has a blank body slot, still has five loop slots, and returns 25.
Break block targeting:
{! expr }— nearest enclosing breakable construct of any supported kind{!; expr }— nearest enclosing block ({ ... }or{; ... }){!@ expr }— nearest enclosing loop{!? expr }— nearest enclosing case block{!name! expr }— nearest enclosing breakable construct namedname{!;name! expr },{!@name! expr },{!?name! expr }— typed named targeting
Breakable constructs are plain blocks, explicit blocks, case blocks, and loops. The break value becomes the target construct’s final result, and any remaining code inside that target is skipped.
Examples:
{;
a := 1
{! 5}
99
}
## => 5
{@:100@
i := 0;
i < 10;
{? i < 3 ? _; {!@ i} };
i += 1
}
## => 3
{;outer;
{;inner;
{!outer! 5}
};
9
}
## => 5
Scoping inside a break block is intentionally asymmetric:
- Plain reads can see the immediate surrounding scope without
@. - Writes stay local unless you use
@nameto write to that surrounding scope explicitly.
Code Blocks in Construct Positions (Scope Sharing)
When a scope-creating construct (loop, .Test, etc.) evaluates a sub-part that is itself a code block, that block shares the construct’s scope rather than creating an additional isolated scope. This means code blocks in these positions act as grouping (for multi-statement parts) without introducing a scope boundary.
## These are equivalent — the init block shares the loop's scope:
{@ x = 1; x < 4; x; x += 1 } ## => 3
{@ {; x = 1 }; x < 4; x; x += 1 } ## => 3
## All five loop parts can be code blocks:
{@ {; i = 0; j = 10 }; {; i < 3 }; {; i + j }; {; i += 1 }; {; i + j } } ## => 13
To get a genuinely isolated block inside a construct position, use nested braces. The outer block shares the construct’s scope, but the inner block creates its own:
{@ { { x = 1 } }; x < 4; x; x += 1 } ## Error: x is undefined in loop scope
This scope-sharing applies to any lazy construct that manages its own scope and evaluates sub-parts within it. Currently this includes loop sub-parts (init, condition, body, update) and .Test setup/test expressions.
Scoped Block Import Headers
Scoped execution blocks may begin with one optional import header immediately after the opening brace form:
{;
< a~x, b=y, z=, r >
...body...
}
Supported only for scoped execution blocks: - { ... } - {; ... } - {@ ... } - {# ... }
Not supported for: - {? ... } - {= ... } - {| ... } - {: ... }
Grammar:
importHeader := "<" importSpec ("," importSpec)* ">"
importSpec :=
IDENT
| IDENT "~"
| IDENT "~" IDENT
| IDENT "="
| IDENT "=" IDENT
Semantics: - name means name~name: create a new local name with a copy of outer name - name~ also means name~name - local~outer creates a new local local initialized from the current outer value of outer - name= means name=name: local name aliases the outer binding name - local=outer creates a local alias local to the same outer binding as outer
Resolution rules: - The left side is always the local name introduced in the block. - The right side is always resolved against the enclosing scope chain. - Sources do not resolve progressively within the same header. - Example: < a~x, b~a > makes b read the enclosing a, not the newly introduced local a.
Assignment behavior: - Copy imports remain ordinary locals after initialization. - Alias imports write through to the referenced outer binding. - @name still explicitly reads or writes the outer scope chain and is not changed by imports.
For {# ... }, imports affect symbolic name interpretation only. The spec literal stays symbolic: it does not copy concrete values during creation, and consumers such as Poly decide how to interpret those references later.
Errors: - Empty headers are invalid: <> - Duplicate local targets in one header are invalid: < x, x= > - Missing outer sources are errors - Malformed specs such as < a~~x >, < a==x >, or < a~x, > are errors - A header only has meaning in the top-of-block position for supported scoped blocks
Script Import Expressions
Script execution uses the angle-call form:
< "path" /capMods/ inputs ; outputs >
Examples:
<"math/square">
<"math/square" x>
<"worker" state=data>
<"net/fetch" /-All,+Core,+Net/ >
<"poly" x ; p=result, d=deriv>
The path is resolved relative to the current script directory, or the current execution base directory for a top-level run. If the path has no suffix, .rix is appended.
Behavior: - Every call creates a fresh execution state. There is no module singleton caching. - If the script has an explicit export declaration, the expression returns an export bundle. - Otherwise the expression returns the final expression value of the script body. - Caller-side ; outputs are only valid when the imported script declared explicit exports. - Output bindings happen before any surrounding assignment or enclosing expression consumes the returned value. - Active cycles are rejected. a.rix -> b.rix -> a.rix is an error.
Script Input Bindings
Inputs and outputs use the same binding family:
bindingSpec :=
IDENT
| IDENT "=" [source]
| IDENT "~" [source]
| IDENT ":" [source]
| IDENT "~~" [source]
| IDENT "::" [source]
Modes: - name or name~source means shallow copy without meta. - name:source means shallow copy with meta. - name~~source means deep copy without meta. - name::source means deep copy with meta. - name=source means alias/live cell sharing.
Caller-side source lookup rules: - Bare source reads only from the immediate scope where the import expression appears. - @source reads only from ancestor scopes, skipping that immediate scope. - This is different from ordinary RiX name lookup, which normally searches outward automatically.
Capability Modifiers
Capability modifiers live between slashes and are applied left-to-right:
<"child" /-All,+Core,+@MAP/ >
Rules: - All is a pseudo-group meaning every capability currently available to the caller. - Plain names such as Core or Arith are group names. - @Name names a single capability directly. - Nested scripts cannot gain capabilities their parent script does not currently have.
Default policy: - Imported scripts start from the configured default script capability base. - Imports is enabled by default. - Net and Files are not granted unless explicitly enabled and already available to the parent.
Script-Side Contracts and Exports
The first statement of an imported script may be an input contract:
< x, state=, cache:: >
This declares the local names the script expects. At runtime: - Missing declared inputs are errors. - Alias declarations (name=) require alias passing. - Copy-style declarations reject alias passing.
The last statement of an imported script may be an export declaration:
< result=r, deriv=d, state=cache, raw >
Semantics: - Left side is the exported/public name. - Right side is the script-local source name. - The operator controls whether the exported bundle entry is live or copied. - Live exported cells may outlive the script frame and keep working after the script returns.
Example:
< x >
r := x * x
< result=r >
Caller:
y = <"square" x ; z=result>
Set & Interval Algebra
| Syntax | System Function | Example | Description |
|---|---|---|---|
A \/ B |
UNION |
S1 \/ S2 |
Set union or interval hull |
A /\ B |
INTERSECT |
S1 /\ S2 |
Set intersection or interval overlap |
A \ B |
SET_DIFF |
S1 \ S2 |
Set/Map difference |
A <> B |
SET_SYMDIFF |
S1 <> S2 |
Symmetric difference |
x ? S |
MEMBER |
5 ? 1:10, "a" ? m |
Membership test (sets/intervals) or map key existence test |
x !? S |
NOT_MEMBER |
x !? S |
Non-membership / key absence test |
A ?& B |
INTERSECTS |
A ?& B |
Intersects predicate |
A ** B |
SET_PROD |
S1 ** S2 |
Cartesian product |
A ++ B |
CONCAT |
[1,2] ++ [3,4] |
Concatenation (ordered collections/strings) |
N-ary brace notes: - {\/ X} and {/\ X} return X. - {\/ } and {/\ } return the empty set {| |}. - {++ X} returns X, but {++ } is an error. - {<< X}/{>> X} return X; {<< }/{>> } are errors. - <> is binary only (no brace n-ary form). - <</>> in brace form are min/max, not bit shifts.
Pipe Operators
Note that in the text version below there is a leading escape slash in front of the pipes for markdown table compatibility. In actual use, do not use the escape slash.
Collection-traversing pipes support arrays, tuples (become arrays), strings, maps (for the traversal/fold operators only — see below), and tensors. All pipe operators return new collections; they never mutate the original.
| Syntax | System Function | Description |
|---|---|---|
x \|> F |
PIPE |
Pipe x as first arg to F (or concrete call if F is partial) |
x \|\|> F(_1) |
PIPE_EXPLICIT |
Alias for PIPE; used with placeholders for clarity |
coll \|>/ i:j |
PSLICE_STRICT |
Strict slice a collection based on interval; null if bounds are non-integers or invalid |
coll \|>// i:j |
PSLICE_CLAMP |
Clamped slice a collection based on interval; clamps exactly without failing |
coll \|>\| sep |
PSPLIT |
Split string or collection by delimiter string, regex or predicate (sequences/strings only) |
coll \|>#\| nOrFn |
PCHUNK |
Chunk string or collection by integer size or predicate boundary (sequences/strings only) |
coll \|>> fn |
PMAP |
Map fn over collection; for maps: preserves keys, transforms values |
coll \|>? pred |
PFILTER |
Filter collection by predicate |
coll \|>: fn |
PREDUCE |
Reduce (first element/value as init) |
coll \|:> init >: fn |
PREDUCE |
Reduce with explicit initial value |
coll \|>< |
PREVERSE |
Reverse collection (new copy; sequences/strings only) |
coll \|<> fn |
PSORT |
Sort with comparator (new copy; sequences/strings only) |
coll \|>&& pred |
PALL |
Every: last item/value if all pass, null on first failure or empty (short-circuits) |
coll \|>\|\| pred |
PANY |
Any/Some: first passing item/value, null if none pass or empty (short-circuits) |
Callback contract for collection-traversing pipes
For traversal pipes (|>>, |>?, |>&&, |>||, predicate forms of |>/| and |>#|), the callback receives:
(val, locator, src)
For reduce (|>: and |:> init >: fn), the callback receives:
(acc, val, locator, src)
For sort (|<>), the comparator receives only:
(a, b)
The locator is the native indexing/key form for the source collection kind: - sequences and strings: 1-based integer position - maps: the map key (as a canonical string, consistent with KEYOF and INDEX_GET) - tensors: 1-based index tuple
Callbacks that declare fewer parameters simply ignore the extra arguments — existing one-arg and two-arg callbacks continue to work without modification.
Map-specific behavior
Maps support the traversal/fold pipe operators |>>, |>?, |>&&, |>||, |>:, and |:> init >: fn. Maps are unordered — no iteration-order guarantee is made.
For map traversal, callbacks receive (value, key, sourceMap). The key is the canonical map key string.
map |>>— transforms values only, preserves original keys. For reshaping map structure, use reduce.map |>?— keeps entries whose predicate passes; returns a new map.map |>&&— returns last value if all pass;nullon first failure or empty.map |>||— returns first passing value;nullif none pass or empty.map |>:— implicit-init: first traversed value as accumulator; order is unspecified.map |:> init >: fn— explicit init; order is unspecified.
Tensor-specific behavior
- Tensor literal:
{:d1xd2x...: elems }creates a dense tensor in row-major order.{:d1xd2x...:}creates an empty mutable tensor filled with holes. - Tensor indexing:
A[i, j],A[::, 2],A[-1:1, ::]. Indices are 1-based; negative indices count from the end;0is invalid. - Tensor slices: bracket slices are strict, closed, and directed.
::is sugar for the full forward slice. - Transpose:
A^^swaps the two axes of a rank-2 tensor as a view. tensor |>>— callback receives(value, indexTuple, tensor)and returns a new tensor with the same shape.tensor |>?— callback receives(value, indexTuple, tensor)and returns a sequence of{: value, indexTuple }pairs for matches.tensor |>:/tensor |:> init >: fn— reducer receives(acc, value, indexTuple, tensor).
Maps do not support |>/| (split), |>#| (chunk), or |<> (sort) — these require ordered sequences.
Examples:
m = {= a=2, b=3 }
m |>> (v, k) -> v * 10 ## {= a=20, b=30 }
m |>? (v, k) -> v > 1 ## {= a=2, b=3 }
m |>? (v, k) -> k == "a" ## {= a=2 }
m |:> 0 >: (acc, v) -> acc+v ## 5
[10,20,30] |>> (v, k) -> k ## [1, 2, 3] (1-based locators)
[10,20,30] |:> 0 >: (acc, v, k) -> acc+k ## 6 (sum of locators 1+2+3)
Partial Functions & Placeholders
| Syntax | Description | Example |
|---|---|---|
_1, _2, … |
Argument placeholders (1-indexed) | Double = @*(_1, 2) |
__1, __2, … |
Alternative placeholder syntax | Double = @*(__1, 2) |
Partial application occurs when a function is called with one or more placeholders. This returns a [Partial: N] object (where N is the arity).
- Reordering:
@-(_2, _1)creates a function that subtracts its first argument from its second. - Duplication:
F(_1, _1)callsFwith the same argument in both slots. - Integration: Works seamlessly with pipes:
[1, 2, 3] |>> @*(_1, 10)→[10, 20, 30]. - Automatic Appending: Any arguments passed to a partial that aren’t consumed by placeholders are appended. For example, if
F(a, b, c)is called viaG = F(_1, _2), thenG(1, 2, 3)becomesF(1, 2, 3).
Arity-Capped Callable Views
fn[n] produces a callable wrapper that forwards only the first n arguments to fn, silently discarding any extras.
fn[n]
This is not partial application. It does not bind, reorder, or select arguments — it simply truncates the incoming argument list to the first n.
| Syntax | Semantics |
|---|---|
fn[0] |
Call fn() regardless of how many args are provided |
fn[1] |
Call fn(a1) — discard everything after first arg |
fn[2] |
Call fn(a1, a2) — discard everything after second arg |
fn[n][m] |
Equivalent to fn[min(n,m)] — caps compose |
Primary use case: Collection-pipe callbacks receive enriched argument lists (val, locator, src) or (acc, val, locator, src). Bare system functions like @+ are N-ary and would fail if they receive non-numeric locator/src values. Arity capping avoids this cleanly:
## Without cap, @+ in reduce receives (acc, val, locator, src) and fails on the sequence object
[1, 2, 3] |>: @+[2] ## 6 — only acc and val forwarded
[1, 2, 3] |:> 0 >: @+[2] ## 6
## Map and filter
double := (x) -> x * 2
[1, 2, 3] |>> double[1] ## [2, 4, 6] — locator dropped
isEven := (x) -> x % 2 == 0
[1, 2, 3, 4] |>? isEven[1] ## [2, 4]
## Map values in a map
{= a=2, b=3 } |>> double[1] ## {= a=4, b=6 }
## General call context
G := @+[2]
G(10, 20, 99, 99) ## 30 (only 10 and 20 forwarded)
Comparison with placeholders:
| Approach | Example | When to use |
|---|---|---|
| Arity cap | fn[n] |
Forward first n args |
| Placeholder | @+(_1, _2) |
Explicit selection / reordering |
Arity cap is simpler when you just want to drop trailing context args. Placeholders are more powerful when you need specific positions or reordering.
Rules: - n must be a non-negative integer. Non-integer or negative values error. - If fewer than n arguments are available, all are forwarded (no padding or defaults). - Works on any callable: lambdas, named functions, system references (@+, .LEN, etc.), partials, or nested arity-capped callables. - Ordinary collection indexing collection[i] is unaffected.
| Syntax | Current evaluator status | Example |
|---|---|---|
\|+n |
Arithmetic source; add n to the previous source value |
[2, \|+2, \|; 5] → [2,4,6,8,10] |
\|*n |
Geometric source; multiply the previous source value by n |
[1, \|*3, \|; 4] → [1,3,9,27] |
\|:f |
One-based index source; calls f(index, self) |
[\|: (i) -> i^2, \|; 5] |
\|>f |
History source when no source precedes it; otherwise transforms each candidate | [1,1, \|>F(_2,_1), \|; 7] |
\|?p |
Keep candidates for which p(value,index,self) succeeds |
[1 \|+1 \|? (x)->x%2==0 \|;5] |
\|;n, \|;p |
Eagerly materialize n accepted values, or through the triggering predicate value |
[2, \|+2, \|; 5] |
\|^n, \|^p |
Lazy count- or predicate-bounded sequence | [1, \|+1, \|^ 1000] |
Without |; or |^, a chain with a generation source is lazy and unbounded. Lazy sequences cache emitted values, support positive indexing and bounded slicing, and keep map/filter pipes lazy. Operations requiring the end of a sequence reject unbounded inputs. Generator safety exhaustion throws instead of silently truncating output.
Collection Syntax
| Syntax | System Function | Example |
|---|---|---|
[a, b, c] |
ARRAY |
[1, 2, 3] |
a:b |
INTERVAL |
1:10 (RationalInterval) |
a:b:c:d... |
INTERVAL |
2:3:5:7 (n-ary betweenness) |
a:(b:c):d |
INTERVAL |
2:(3:4):5 (nested betweenness) |
a:{|b:c|}:d |
INTERVAL |
2:{|3:4|}:5 (set unpacking) |
Colon-Strings
A colon in prefix position (where a value is expected) followed by an identifier or number produces a string literal. The colon must not follow a value — after a value, : remains the interval operator.
| Syntax | Result | Notes |
|---|---|---|
:hello |
"hello" |
Lowercase identifier |
:World |
"World" |
Case is preserved (not uppercased) |
:some_key |
"some_key" |
Underscores allowed |
:123 |
"123" |
Number becomes string |
a:b |
interval | : in infix position = interval |
a :b |
interval | Whitespace doesn’t change infix : |
Colon-strings work anywhere a value is expected: in arrays ([:a, :b]), as function arguments, after = or :=, after commas, etc. They are syntactic sugar for quoted strings and evaluate identically.
Special Number Literals
These formats all produce exact rational values parsed by LITERAL.
Repeating Decimals (#)
The # separates the non-repeating fractional part from the (infinitely) repeating part.
| Syntax | Value | Notes |
|---|---|---|
1.23#45 |
1.234̄5̄… | non-repeating 23, repeating 45 |
0.#3 |
1/3 | no non-repeating fractional digits |
1.#6 |
5/3 | integer + immediate repeating |
5#3 |
16/3 | integer part with repeating decimal |
Radix Shift (_^)
n_^k multiplies n by 10^k. Positive exponent shifts the decimal right; negative shifts left.
| Syntax | Value | Notes |
|---|---|---|
1_^2 |
100 | 1 × 10² |
3.14_^2 |
314 | 3.14 × 10² |
1_^-2 |
1/100 | 1 × 10⁻² |
1/3_^2 |
100/3 | 1/3 × 10² |
Continued Fractions (.~)
A continued fraction [a₀; a₁, a₂, …] is written as a₀.~a₁~a₂~….
Implicit-start — unsigned integer part, no leading ~:
| Syntax | Value |
|---|---|
3.~7~15~1 |
355/113 |
1.~2 |
3/2 |
Explicit-start — leading ~ marker, allows a signed integer part:
| Syntax | Value | Notes |
|---|---|---|
~1.~2 |
3/2 | same as 1.~2 |
~-1.~2 |
−1/2 | first coefficient is −1 |
~-2.~1~2~2 |
−9/7 |
Negating the CF value — unary minus on an explicit-start CF:
| Syntax | Value | Notes |
|---|---|---|
-~1.~2 |
−3/2 | negate the value of ~1.~2 (= 3/2) |
Forbidden — syntax error:
-1.~2 ## ❌ ambiguous: write ~-1.~2 (neg. coefficient) or -~1.~2 (negate value)
Number Base Literals
| Prefix | Base System | Example | System Function |
|---|---|---|---|
0x |
Hexadecimal (16) | 0xFF |
LITERAL |
0b |
Binary (2) | 0b1010 |
LITERAL |
0o |
Octal (8) | 0o755 |
LITERAL |
0t |
Ternary (3) | 0t121 |
LITERAL |
0z[N] |
Base N | 0z[32]abc |
LITERAL |
Other registered prefixes: 0q (Base 4), 0f (5), 0s (7), 0d (12), 0v (20), 0u (36), 0m (60), 0y (64).
Custom uppercase prefixes can be defined at runtime: - 0A = "0123456789ABCDEF" - 0B = {: 2, "01" }
Base conversion operators: - _>: value _> baseSpec returns a string in that base. - <_: string <_ baseSpec parses into an exact rational/integer.
Quoted prefixed literals are also valid: - 0A"4A.F"
Property & Meta Access
RiX separates two distinct access concepts: meta properties (external annotations on any value, accessed with .) and collection indices/keys (actual content of sequences and maps, accessed with [...]).
| Syntax | System Function | Description |
|---|---|---|
obj.name |
META_GET |
Get meta property (returns null if absent) |
obj.name = val |
META_SET |
Set meta property (null value = delete; respects immutable/frozen) |
obj.. |
META_ALL |
Get all meta properties as read-only map |
obj .= map |
META_MERGE |
Bulk merge map into meta properties (null values = delete) |
obj[expr] |
INDEX_GET |
Index into collection (1-based for sequences/strings; key for maps) |
obj[:name] |
INDEX_GET |
Map access by key literal (:name, :1, :"1") |
obj[i] = val |
INDEX_SET |
Set collection index (requires ._mutable value meta; arrays/maps/tensors default to mutable) |
obj.\| |
KEYS |
Get set of map keys |
obj\|. |
VALUES |
Get set of map values |
obj.Method(args) |
CALL_METHOD |
Receiver-first method call |
obj.Method!(args) |
CALL_METHOD |
Receiver-first mutating method call |
Note: obj..name is a parse error — use obj.name for meta access.
Method-call semantics: - Only obj.Method(...) and obj.Method!(...) trigger method resolution. - Plain property access like x = obj.Method stays a direct META_GET; it does not consult _proto. - CALL_METHOD(target, name, ...args) resolves methods in this order: 1. target.name 2. target.__name 3. target._name 4. target._proto[:name] 5. target._proto[:__name] 6. target._proto[:_name] - If a direct meta match is found and is not callable, evaluation errors immediately and does not fall back to _proto. - If no callable is found, evaluation raises Method not found. - The receiver is always passed as the first argument: fn(target, ...args). - Names ending in ! are the mutating variants. They require a mutable receiver and raise Cannot mutate immutable value when ._mutable is absent or the value is frozen/immutable. - _proto is a reserved meta property. It must be a map or null. Built-in prototypes are frozen and do not prototype-chain in v1. - The built-in method surface is documented in methods-guide.md.
Map Key Resolution (.KEYOF): - string -> same key - integer -> canonical integer string - otherwise -> use .key meta property (must be string/integer)
Expression map keys must be parenthesized in literals: - a = {= id=5, (expr)=9 } - {= 1=2 } is invalid; use {= (1)=2 }
Map literals reject duplicate keys after canonicalization: - {= a=1, ("a")=2 } -> error - {= (1)=1, ("1")=2 } -> error
Assertions
| Syntax | System Function | Description |
|---|---|---|
:=: |
SOLVE |
Solve/assert equality (assigns to variable to satisfy expr) |
:<: |
ASSERT_LT |
Assert less than |
:>: |
ASSERT_GT |
Assert greater than |
:<=: |
ASSERT_LTE |
Assert less or equal |
:>=: |
ASSERT_GTE |
Assert greater or equal |
Division Variants
| Syntax | System Function | Description |
|---|---|---|
/^ |
DIVUP |
Ceiling division |
/~ |
DIVROUND |
Rounded division |
/% |
DIVMOD |
Division with remainder |
Ternary Operator
| Syntax | System Function | Example |
|---|---|---|
cond ?? trueVal ?: falseVal |
TERNARY |
x > 0 ?? "pos" ?: "neg" |
Part 2: System Function Reference
Note: Functions in this reference that are marked with a leading
.(e.g.,.ADD,.RAND_NAME) are system capabilities accessible only via the dot syntax or@_prefix. Functions without a leading.are internal IR operations dispatched automatically by operator syntax — they are not directly callable by name.
Core
| Function | Description | Syntax Aliases |
|---|---|---|
LITERAL(str) |
Parse number literal string (with base prefixes) | — |
STRING(val) |
Create string value | "..." |
NULL() |
Null value | _ |
ASSIGN(name, val) |
Local assignment | x = 5, x := 5 |
RETRIEVE(name) |
Variable lookup | x |
Arithmetic
| Function | Description | Syntax Aliases |
|---|---|---|
ADD(a, b) |
Addition | a + b |
SUB(a, b) |
Subtraction | a - b |
MUL(a, b) |
Multiplication | a * b |
DIV(a, b) |
Rational division | a / b |
INTDIV(a, b) |
Integer (floor) division | a // b |
MOD(a, b) |
Modulo | a % b |
POW(a, b) |
Exponentiation | a ^ b, a ** b |
NEG(a) |
Negation | -a |
ABS(a) |
Absolute value | — |
SQRT(a) |
Square root | — |
Comparison
| Function | Description | Syntax Aliases |
|---|---|---|
EQ(a, b) |
Value equality (returns 1 or null) | a == b |
SAME_CELL(a, b) |
Identity — same cell (returns 1 or null) | a === b |
NEQ(a, b) |
Not equal | a != b |
LT(a, b) |
Less than | a < b |
GT(a, b) |
Greater than | a > b |
LTE(a, b) |
Less or equal | a <= b |
GTE(a, b) |
Greater or equal | a >= b |
MIN(args...) |
N-ary minimum (numbers or strings; null args ignored) | {<< a, b, c } |
MAX(args...) |
N-ary maximum (numbers or strings; null args ignored) | {>> a, b, c } |
Logic
| Function | Description | Syntax Aliases |
|---|---|---|
AND(a, b) |
Logical AND (internal) | a && b, .AND(a,b), @&& |
OR(a, b) |
Logical OR (internal) | a \|\| b, .OR(a,b), @\|\| |
NOT(a) |
Logical NOT (internal) | !a, .NOT(a), @! |
Assertions & Constraints
| Function | Description | Syntax Aliases |
|---|---|---|
SOLVE(name, expr) |
Solve/constrain variable | :=: |
ASSERT_LT(a, b) |
Assert a < b |
:<: |
ASSERT_GT(a, b) |
Assert a > b |
:>: |
ASSERT_LTE(a, b) |
Assert a <= b |
:<=: |
ASSERT_GTE(a, b) |
Assert a >= b |
:>=: |
Symbolic Specs
{# ... } builds a symbolic specification object instead of executing a runtime block.
Supported header forms:
{# ... }
{#x,y,z# ... }
{#:p,q# ... }
{#x,y,z:p,q# ... }
Meaning:
- names before
:are declared inputs - names after
:are declared outputs - both sides may be omitted
- header names must be bare identifiers
Current body rules:
- only
name = exprsymbolic assignments are supported - the left-hand side must be a bare identifier
=inside{# ... }is symbolic assignment, not ordinaryASSIGN- if outputs are declared, every assignment target must be one of them and each must appear exactly once
- if outputs are omitted, outputs are inferred from top-level assignment targets in encounter order
Result shape:
{=
kind = "systemSpec",
syntax = "#",
inputs = {: "x", "y" },
outputs = {: "p" },
statements = {:
{=
kind = "assign",
target = "p",
expr = ...
}
}
}
The expr field stores a structural symbolic tree. It is not evaluated eagerly and it is not stored as a raw source string.
Current minimal consumers:
Poly(spec)returns a callable for a restricted polynomial subsetDeriv(spec, "x")returns another compatible spec for that same subset
Current supported subset for Poly and Deriv:
- constants
- identifiers
+-*^with a nonnegative integer literal exponent
Constraint forms such as :=:, :<:, and :>: remain separate and are not part of {# ... } semantics yet.
Control Flow
| Function | Description | Syntax Aliases |
|---|---|---|
BLOCK(stmts...) |
Execute sequentially, return last | { a; b }, {; a; b } |
CASE(branches...) |
If/elseif/else branching | {? cond ? val; default } |
LOOP(init, cond, body, update, after) |
Loop with optional name/max metadata and optional completion slot | {@ init; cond; body; update }, {@ init; cond; body; update; after }, {@name:100@ ... }, {@::@ ... } |
BREAK(meta, value) |
Structured break that exits the nearest matching target | {! value }, {!@ value }, {!?name! value } |
SYSTEM_SPEC(meta) |
Create a symbolic system spec value | {#x,y:p# p = x + y } |
TERNARY(cond, t, f) |
Ternary conditional | cond ?? t ?: f |
IF(cond, t, f) |
If-then-else (stdlib) | — |
MULTI(a, b, c...) |
Evaluate all, return last (stdlib) | — |
Collections
| Function | Description | Syntax Aliases |
|---|---|---|
ARRAY(elems...) |
Create sequence | [a, b, c] |
SET(elems...) |
Create set | {\| a, b, c \|} |
TUPLE(elems...) |
Create tuple | {: a, b, c } |
MAP(pairs...) |
Create map/object | {= k=v, ... } |
TENSOR_LITERAL(shape, elems...) |
Create tensor with explicit shape | {:2x3: 1, 2, 3; 4, 5, 6 } |
ARRAY_CAPTURE(elems...) |
Create array with brace-form constructor capture controls | {.. 1, 2, 3 }, {.. /:=/ x, y } |
INTERVAL(args...) |
Create interval or check n-ary betweenness (unpacks nested intervals/sets) | a:b or a:b:c... |
UNION(a, b) |
Binary set union / interval hull | A \/ B |
INTERSECT(a, b) |
Binary set intersection / interval overlap | A /\ B |
CONCAT(a, b) |
Binary concatenation | A ++ B |
NARY_UNION(args...) |
N-ary set union / interval hull | {\/ A, B, C } |
NARY_INTERSECT(args...) |
N-ary set intersection / interval overlap | {/\ A, B, C } |
NARY_CONCAT(args...) |
N-ary concatenation | {++ A, B, C } |
LEN(coll) |
Length of collection/string | — |
FIRST(coll) |
First element | — |
LAST(coll) |
Last element | — |
GETEL(coll, i) |
Get element at 1-based index | — |
IRANGE(start, end) |
Integer range [start, end] |
— |
.RAND_NAME(len?, alphabet?) |
Random string generator | .RAND_NAME(), .RAND_NAME(8, "abc") |
.TGEN(shape, fn) |
Generate tensor by index tuple | .TGEN({: 2, 3 }, idx -> idx[1] * 10 + idx[2]) |
Functional / Pipes
| Function | Description | Syntax Aliases |
|---|---|---|
PIPE(val, fn) |
Pipe value into function | val \|> fn |
PIPE_EXPLICIT(val, fn) |
Pipe value into function explicitly | val \|\|> fn |
PSLICE_STRICT(coll, i:j) |
Strict slice collection | coll \|>/ i:j |
PSLICE_CLAMP(coll, i:j) |
Clamped slice collection | coll \|>// i:j |
PSPLIT(coll, sep) |
Split collection by delimiter | coll \|>/\| sep |
PCHUNK(coll, n) |
Chunk collection by size or predicate | coll \|>#\| nOrFn |
PMAP(coll, fn) |
Map function over collection (arrays/strings/maps/tensors); maps preserve keys; tensors preserve shape | coll \|>> fn, MAP(coll, fn) |
PFILTER(coll, pred) |
Filter by predicate (arrays/strings/maps/tensors) | coll \|>? pred, FILTER(coll, pred) |
PREDUCE(coll, fn, init) |
Reduce/fold (arrays/strings/maps/tensors) | coll \|>: fn, coll \|:> init >: fn, REDUCE(coll, fn, init) |
PREVERSE(coll) |
Reverse collection (new copy) | coll \|>< |
PSORT(coll, fn) |
Sort with comparator (new copy) | coll \|<> fn |
Functions
| Function | Description | Syntax Aliases |
|---|---|---|
FUNCDEF(name, params, body) |
Define named function | Name(params) -> body |
LAMBDA(params, body) |
Anonymous function | (params) -> body |
CALL(name, args...) |
Call a user-defined function | Name(args) |
Function-call lookup note: - Name(args) searches outward for a callable binding even across scoped block boundaries. - For functions exported from scripts, the outward callable search stops at that script module boundary. Module helpers can be called without @, but missing module helpers do not fall through to the importer. - Bare retrieval Name still follows normal lexical variable lookup rules. - This means {; F(2) } can call an outer F, while {; G = F } requires {; G = @F } if F is outside the block.
UPPER(str) | Convert to uppercase |SUBSTR(str, start, len) | Get substring |Property & Meta Access
| Function | Description | Syntax Aliases |
|---|---|---|
META_GET(obj, name) |
Get meta property (null if absent) | obj.name |
META_SET(obj, name, val) |
Set meta property (null = delete; blocked by .immutable; blocked by .frozen for non-frozen keys) |
obj.name = val |
META_ALL(obj) |
Get all meta properties as read-only map | obj.. |
META_MERGE(obj, map) |
Bulk merge map into meta (null values = delete) | obj .= map |
CALL_METHOD(obj, name, args...) |
Resolve and invoke a receiver-first method | obj.Method(args), obj.Method!(args) |
INDEX_GET(obj, key) |
Index into collection (1-based for sequences/strings; normalized keys for maps) | obj[expr], obj[:name], obj[:1] |
INDEX_SET(obj, key, val) |
Set index (requires ._mutable value flag) |
obj[i] = val |
BRACKET_GET(obj, specs...) |
Tensor-aware bracket indexing and slicing | obj[i, ::] |
BRACKET_SET(obj, specs..., val) |
Tensor-aware bracket assignment | obj[::, 1] = val |
.KEYOF(x) |
Resolve canonical map key string | .KEYOF(x) |
.KEYS(obj) |
Get keys of map as set | obj.\|, .KEYS(obj) |
.VALUES(obj) |
Get values of map as set | obj\|., .VALUES(obj) |
Meta Property Categories:
Meta properties are classified by key prefix, which controls how they behave during ~= / ~~= (in-place value replacement):
| Prefix | Category | Examples | Survives ~=? |
|---|---|---|---|
| (none) | Ordinary | .key, .lock, .frozen, .immutable |
Yes — preserved from lhs |
_ |
Ephemeral (runtime facts) | ._mutable, ._spec, ._deriv, ._type, ._proto |
Replaced/rebuilt from the concrete rhs |
__ |
Sticky (semantic interpretation) | .__units, .__format, .__name, .__type, .__traits, .__proto |
Preserved unless rhs supplies same key |
:=/::=copy all meta categories into the new cell.~=/~~=preserve ordinary, replace ephemeral, merge sticky (lhs wins unless rhs overrides).=(alias) shares the cell entirely — no meta copying occurs.
Cell-level protections (ordinary meta — govern whole-value replacement):
.lock: Blocks~=,~~=, and combo operators. Does not block=/:=(rebind/copy) or in-place index mutation of a mutable value..frozen: Blocks~=/~~=and ordinary meta edits (except unsetting.frozenitself). Does not block=/:=or in-place index mutation..immutable: Like.frozenbut permanent — cannot be unset. Blocks all meta edits and~=/~~=..keyidentity:.keymust be string/integer and is effectively write-once (idempotent same-value writes allowed; changing value is an error). Used byKEYOFfor map keys.
Value-level mutability (ephemeral meta — governs in-place structural mutation):
._mutable: When truthy, composite values (arrays, maps, tensors) allow index/bracket assignment (arr[i] = v). Arrays, maps, and tensors default to._mutable = 1. Remove withobj._mutable = _to make the value structurally immutable. Because._mutableis ephemeral, it is replaced wholesale when you do~=— so the lhs adopts the rhs’s._mutablestate.
Important: cell protections (.lock, .frozen, .immutable) and value mutability (._mutable) are independent. A locked cell may hold a mutable array; a ~= can replace a locked value (error) but a [i] = v succeeds if ._mutable is set.
x := [1, 2, 3]
x.lock = 1 ## cell locked against ~=
x ~= [9, 9, 9] ## ERROR: cell is locked
x[1] = 9 ## OK: ._mutable controls index assignment, not .lock
DeepMutable system function — recursively set or remove ._mutable throughout a nested value:
| Call | Effect |
|---|---|
.DeepMutable(value, 1) |
Set ._mutable on all nested arrays/maps/tensors |
.DeepMutable(value, _) |
Remove ._mutable from all nested arrays/maps/tensors |
The flag _ (null) means “remove”; any non-null value (including 0) means “add”. Only _ is false in RiX.
Regex
| Function | Description | Syntax Aliases |
|---|---|---|
REGEX(pat, fl, mode) |
Create regex matching function | {/pat/fl?mode} |
I/O
| Function | Description |
|---|---|
PRINT(args...) |
Print each argument via the host IO hook (context.env["__io__"]) |
Default PRINT formatting renders maps, arrays, sets, tuples, and export bundles over multiple lines. Hosts can replace either formatting or output by setting:
context.setEnv("__io__", {
format(value, helpers) { return helpers.prettyFormat(value); },
print(text, value, helpers) { console.log(text); }
});Variables
| Function | Description | Syntax |
|---|---|---|
ASSIGN(name, val) |
Alias/rebind — share rhs cell or create fresh | name = val |
ASSIGN_COPY(name, val) |
Fresh cell with shallow-copied value + all meta | name := val |
ASSIGN_UPDATE(name, val) |
In-place value replacement (cell-preserving) | name ~= val, name += val |
ASSIGN_DEEP_COPY(name, val) |
Deep-copied fresh cell | name ::= val |
ASSIGN_DEEP_UPDATE(name, val) |
In-place deep value replacement | name ~~= val |
GLOBAL(name, val) |
Set global variable | — |
RETRIEVE(name) |
Look up variable | name |
OUTER_ASSIGN(name, val) |
Rebind an existing outer scope variable | @name = val |
OUTER_UPDATE(name, val) |
In-place update of outer variable | @name += val, @name ~= val |
OUTER_RETRIEVE(name) |
Look up an outer scope variable | @name |
Scope note: - RETRIEVE(Name) remains lexical even for capitalized names. - Only direct call syntax Name(...) uses outward callable lookup. - Script module scopes are boundaries for that outward callable lookup. - Combo operators (+=, -=, *=, /=, //=, %=, ^=, ++=, \/=, /\=, \=, **=, /^=, /~=) desugar to ASSIGN_UPDATE / OUTER_UPDATE, preserving cell identity so aliases track changes.
Advanced Constructors and Extensions
| Function | Description |
|---|---|
DERIVATIVE(expr, var) |
Stub: returns a placeholder object |
INTEGRAL(expr, var) |
Stub: returns a placeholder object |
GENERATOR(args...) |
Internal array-generator marker consumed by ARRAY |
STEP(interval, step) |
Lazy exact stepped interval sequence (a:b :+ step) |
DIVIDE(interval, n) |
Exactly n lazy endpoint-inclusive points (a:b :: n) |
PARTITION(interval, n) |
n eager equal touching intervals (a:b :/: n) |
MEDIANTS(interval, levels) |
Nested exact mediant levels (a:b :~ levels) |
MEDIANT_PARTITION(interval, levels) |
Eager mediant partitions (a:b :~/ levels) |
RANDOM(interval, params) |
Rational samples; params are count, optional denominator, optional tolerance |
RANDOM_PARTITION(interval, params) |
Distinct rational interior partition points |
INFSEQ(start, step) |
Lazy unbounded arithmetic sequence (start::+step) |
MATRIX(rows...) |
Matrix literal |
TENSOR(data...) |
Legacy tensor constructor |
TENSOR_LITERAL(shape, elems...) |
Explicit-shape tensor literal |
TENSOR_TRANSPOSE(t) |
Rank-2 tensor transpose view |
UNIT(val, unit) |
Resolve unit through active Units map and multiply |
MATHUNIT(val, exact) |
Resolve exact through active Exact map and multiply |
.ConvertUnit(val, target) |
Select a compatible display unit |
.DefineUnit(name, definition) |
Construct a Unit value for a map overlay |
.DefineExactGenerator(name, polynomial) |
Construct an algebraic generator |
.Complex.Conjugate(value) |
Conjugate an exact complex expression |
.Complex.Re(value), .Complex.Im(value) |
Extract exact complex components |
.Complex.FromParts(real, imaginary) |
Construct real + imaginary*i |
.Complex.NormSquared(value) |
Return exact Re(value)^2 + Im(value)^2 |
.Complex.Cayley(value) |
Convert exact Cartesian complex form to Cayley polar form |
.Complex.Cayley(r, t) |
Construct exact Cayley magnitude/direction coordinates |
.Complex.Cartesian(value) |
Convert Cayley form to exact Cartesian form |
.Complex.Magnitude(value), .Complex.Direction(value) |
Inspect Cayley coordinates |
.Complex.Inverse(value) |
Return the exact reciprocal, preserving Cayley form |
Note: Combo assignments (+=, -=, *=, /=, //=, %=, ^=, ++=, \/=, /\=, \=, **=, /^=, /~=) automatically desugar into ASSIGN_UPDATE(x, OP(RETRIEVE(x), y)) or OUTER_UPDATE(x, OP(OUTER_RETRIEVE(x), y)), preserving the original cell.
Part 3: REPL Dot-Commands
Holes and Undefined
| Syntax | Meaning |
|---|---|
[1,,3] |
Array with a hole at position 2 |
[,1] |
Array with leading hole |
[1,] |
Array with trailing hole |
[,] |
Two holes |
F(,7) |
Call F with hole as first arg, 7 as second |
F(1,,3) |
Call F with hole as second arg |
a ?| b |
Hole-coalesce: returns a if non-hole, else b (expression-level) |
(x ?= 2, a) -> ... |
Param x defaults to 2 when caller omits it or passes a hole |
Key rules: - Holes are not assignable — they arise from syntax only. - Standard arithmetic/comparison operations error when given a hole. - ?| is lazy — the right side is not evaluated if left is not a hole. - At the REPL, a bare unbound identifier shows undefined (not an error). Expressions using an unbound identifier still error. - null (_) is a regular value; holes are distinct.
REPL-specific commands use all-lowercase dot notation. They are not part of the RiX language itself but provide tooling and reflection.
| Command | Description | Example |
|---|---|---|
.help |
Show help message | .help |
.exit |
Exit REPL | .exit |
.load[pkg] |
Load a package/file | .load[:stats] |
.vars |
List all variables in current context | .vars |
.fns |
List all registered system functions | .fns |
.reset |
Clear the current context | .reset |
.ast[expr] |
Show AST for an expression | .ast[1 + 2] |
.tokens[expr] |
Show tokens for an expression | .tokens[x = 5] |
Disambiguation:
.help,.vars, etc. are REPL shell commands (all lowercase). Any dot expression that starts with an uppercase letter (e.g.,.ADD(3,4),.RAND_NAME()) is a system capability call and is evaluated as RiX code, not a REPL command.
Ctrl-C Behavior: - If the current line is non-empty, Ctrl-C clears the line. - If the current line is empty, Ctrl-C exits the REPL.
Part 4: Diagnostics, Testing, and Debugging
All diagnostic system capabilities produce structured RiX map values with at minimum: kind, label, file, time, data.
Runtime errors include source locations when the evaluator has source text. This is automatic for parseAndEvaluate(...), script imports, and the CLI paths that use those APIs. Direct calls to evaluate(irNode, ...) only include line/column locations if the caller has attached source metadata or set the source environment for the context.
.Warn(label, dataMap ?= {=})
Emits a kind="warn" event. Returns the event object.
| Parameter | Type | Default | Description |
|---|---|---|---|
label |
string | required | Warning label |
dataMap |
map | {=} |
Structured payload |
.Info(label, level ?= 1, dataMap ?= {=})
Emits a kind="info" event with a severity level. Returns the event object.
| Parameter | Type | Default | Description |
|---|---|---|---|
label |
string | required | Info label |
level |
integer | 1 |
Severity/verbosity level |
dataMap |
map | {=} |
Structured payload |
.Error(label, dataMap ?= {=})
Emits a kind="error" event and aborts evaluation. Does not return normally.
| Parameter | Type | Default | Description |
|---|---|---|---|
label |
string | required | Error label |
dataMap |
map | {=} |
Structured payload |
.Stop(label, condition, dataMap ?= {=})
Conditional abort. If condition is null (_), returns null and emits nothing. If condition is non-null, emits a kind="stop" event and aborts.
| Parameter | Type | Default | Description |
|---|---|---|---|
label |
string | required | Stop label |
condition |
any | required | Abort if non-null |
dataMap |
map | {=} |
Structured payload |
.Test(label, setup, tests)
Runs a test group. Returns a rich result map. Two modes:
Sequential mode (third arg is array [...]): - Setup runs once; tests run in order in shared state - Null result = failure; stops remaining tests - Runtime error = error; stops remaining tests
.Test("group", {; x := 1 }, [
x == 1,
{; x ~= x + 1; x == 2 }
])
Isolated mode (third arg is map {= ... }): - Setup reruns fresh for each labeled test - All tests are attempted regardless of individual failures
.Test("group", {; x := 5 }, {=
add = x + 1 == 6,
mul = x * 2 == 10
})
Result shape (both modes):
| Field | Type | Description |
|---|---|---|
kind |
"test" |
Event kind |
label |
string | Test group label |
mode |
"sequential" or "isolated" |
Test mode |
file |
string | Source file path |
passed |
1 or _ |
Overall pass/fail |
results |
array or map | Per-test outcomes |
summary |
map | {= total, passed, failed, errored, skipped } |
Pass/fail semantics: null (_) = failure, any non-null = pass.
Duplicate labels: Two .Test(...) calls with the same label in the same file produce an error.
.TestError(label, setup, expr)
Tests that a single expression aborts with an error. Passes when expr aborts via .Error(...) or any runtime/interpreter error. The setup block runs first; if setup aborts for any reason, the entire test fails.
| Parameter | Type | Description |
|---|---|---|
label |
string | Test label (unique per file) |
setup |
block | Runs once before the tested expression |
expr |
expression | The single expression expected to abort with an error |
Pass criteria: - Setup completes normally, AND - expr aborts with .Error(...) → outcome "error", OR - expr aborts with a runtime error → outcome "runtimeError"
Fail criteria: - Setup aborts for any reason - expr returns normally (including returning _) - expr aborts with .Stop(...) instead of an error kind
.TestError("division by zero", {;
x := 10;
y := 0
}, x / y)
.TestError("explicit error", {;
x := 5
}, .Error("bad input", {= x = x }))
Result shape:
| Field | Type | Description |
|---|---|---|
kind |
"test" |
Event kind |
testKind |
"error" |
Abort test kind |
label |
string | Test label |
file |
string | Source file path |
passed |
1 or _ |
Overall pass/fail |
expected |
"error" |
Expected abort kind |
setup |
map | { passed, outcome, value?, abort?, error? } |
expr |
map | { passed, outcome, value?, abort?, error? } |
summary |
map | { expected, setupPassed, exprOutcome } |
Outcome values: "returned", "error", "stop", "runtimeError".
Duplicate labels: Two .TestError(...) (or .Test(...) / .TestStop(...)) calls with the same label in the same file produce an error.
.TestStop(label, setup, expr)
Tests that a single expression aborts via .Stop(...). Passes only when expr triggers a stop-kind abort. The setup block runs first; if setup aborts for any reason, the entire test fails.
| Parameter | Type | Description |
|---|---|---|
label |
string | Test label (unique per file) |
setup |
block | Runs once before the tested expression |
expr |
expression | The single expression expected to abort via .Stop(...) |
Pass criteria: - Setup completes normally, AND - expr aborts with .Stop(...) → outcome "stop"
Fail criteria: - Setup aborts for any reason - expr returns normally (including returning _) - expr aborts with .Error(...) or a runtime error instead of stop
.TestStop("negative guard", {;
x := -3
}, .Stop("negative", x < 0, {= x = x }))
The result shape is the same as .TestError(...) with testKind = "stop" and expected = "stop".
.Debug(label, expr)
AST-aware debug inspection. Captures the expression’s IR/source structure, evaluates it once, and returns the evaluated value (not the event). Also emits a kind="debug" event.
x := .Debug("check", a + b) ## x gets the value of a + b
Debug data payload includes exprSource, ast, and final.
.Trace(label, depth, trackedVars ?= [], thunkOrCallable)
Execution tracing wrapper. Establishes a trace context, invokes the callable, and returns the callable’s result. Emits a kind="trace" event.
| Parameter | Type | Default | Description |
|---|---|---|---|
label |
string | required | Trace label |
depth |
integer | required | Max call depth to trace |
trackedVars |
array of strings | [] |
Variable names to track |
thunkOrCallable |
callable | required | Code to trace |
.Trace("fib", 3, ["n", "acc"], () -> Fib(10))
Trace data payload includes depth, trackedVars, calls (array of enter/exit/write events), and final.
CLI: rix test
Test discovery and execution from the command line.
bun bin/rix.js test [filters...]
- No filters: recursively discovers all
*.test.rixfiles from the current directory - With filters: only runs files whose path contains one of the filter strings (case-insensitive)
- Each file runs in a fresh runtime context
- Exit code 0 if all test files pass; nonzero if any fail or error
- Output includes per-file pass/fail status, per-test-group summaries, and diagnostic event counts
Comments
## text## This is a comment/* text *//* multi-line */##TAG## ... ##TAG####NOTE## long comment ##NOTE##