This is a tutorial-style guide to the Reactive language, covering values, control flow, reactivity, and common patterns.
- Integers: 32-bit signed integers
- Characters: Unicode scalar values ('A', 'b', '\n')
- Strings: Mutable arrays of characters ("HELLO")
- Arrays: Fixed-size, zero-initialized arrays of values (integers, characters, structs, or arrays)
- Lazy values: Expressions stored as ASTs and evaluated on access
- Structs: Heap-allocated records with named fields
- Functions: Callable units that may return integers, characters, arrays, or structs
Arrays (including strings) evaluate to their length when used as integers.
- Arithmetic:
+ - * / - Modulo
% - Comparison:
> < >= <= == != - Logic:
&& || ! - No boolean type:
0is false, non-zero is true - Casts:
(int) expr,(char) expr - Ternary
x ? y : z;
- Program starts in the
mainfunction. if { } else if { } else { }conditional executionreturn x;returns a value from a functionloop { }infinite loopbreakexits the nearest loopcontinueskip to the next iteration of loop
Each loop iteration creates a fresh immutable := scope, while mutable and reactive locations persist.
The language has three assignment forms, each with a distinct meaning.
= creates or mutates a mutable location.
func main(){
x = 10;
println x; # 10 #
}Mutable variables created with = are local to the current function invocation, unless they refer to an existing global or heap location.
func foo(x, y){
z = x + y;
return z;
}
func main(){
x = 1;
y = 2;
z = foo(x, y);
println z; # 3 #
}Inside structs, = creates per-instance mutable fields.
struct A {
x = 0;
}
func main(){
a = struct A;
b = struct A;
a.x = 5;
println b.x; # 0 #
}Struct fields are not shared between instances.
When used inside arrays, = assigns the location of the index in the array to a value.
func main(){
arr = [2]
arr[0] = 1;
println arr[0]; # 1 #
}::= defines a relationship between locations. It stores an expression and its dependencies, not a value.
func main(){
x = 1;
y ::= x + 1;
println y; # 2 #
}The expression is evaluated when read. If any dependency changes, the result updates automatically.
func main(){
x = 1;
y ::= x + 1;
println y; # 2 #
x = y;
print y; # 3 #
}Reactive assignments:
- capture dependencies, not snapshots
- are lazy evaluated
- attach to the location, not the name
They are commonly used to build progression variables in loops:
func main(){
x = 0;
dx ::= x + 1;
loop {
println x;
if x >= 4 { break; }
x = dx;
}
}Reactive assignments work uniformly for variables, struct fields, and array elements.
struct Counter {
x = 1;
step = 1;
next;
}
func main(){
c = struct Counter;
c.next ::= c.x + c.step;
println c.next; # 2 #
c.x = c.next;
println c.next; # 3 #
}Reactive assignments may use ternary expressions on the right-hand side.
func main(){
arr =[2]
arr[1] ::= arr[0] + 2;
x ::= arr[1] > 1 ? 10 : 20;
println arr[0]; # 0 #
print x; # 10 #
}:= is value capture, not assignment. It does not create a location or participate in the reactive graph.
That name:
- is immutable
- is not reactive
- disappears when the scope ends
- cannot be reassigned
- cannot be observed reactively
If the := is binding an array or struct, the contents are mutable.
Reactive bindings ::= store relationships, not values. This means:
arr[i] ::= arr[i - 1] + 1;does not mean: use the current value of i. It means: use whatever i refers to when this expression is evaluated.
So if i keeps changing, the dependency graph becomes self-referential or incorrect.
func main(){
arr = [3];
i = 0;
loop {
arr[i] ::= i * 10;
i = i + 1;
if i >= 3 { break; }
}
println arr[0];
println arr[1];
println arr[2];
}Becomes:
arr[0] = 30
arr[1] = 30
arr[2] = 30
and not:
arr[0] = 0
arr[1] = 10
arr[2] = 20
To fix this, capture i with :=:
func main(){
arr = [3];
i = 0;
loop {
j := i;
arr[j] ::= j * 10;
i = i + 1;
if i >= 3 { break; }
}
}Character literals use single quotes:
func main(){
c = 'A';
println c; # A #
}Characters coerce to integers in numeric contexts. Casting is explicit:
func main(){
n := (int)'A';
c := (char)(n + 1);
println n; # 65 #
println c; # B #
}Strings use double quotes and are compiled as arrays of characters:
func main(){
s := "HELLO";
println s; # HELLO #
println s[1]; # E #
println (int) s; # 5 #
}Strings are:
- indexable
- mutable
- usable anywhere arrays are allowed
func main(){
s = "HELLO";
s[0] = 'X';
println s; # XELLO #
}func main(){
text := "HELLO";
i = 0;
di ::= i + 1;
c ::= text[i];
println c; # H #
i = di;
println c; # E #
}func main(){
struct Label {
text;
}
l = struct Label;
l.text = "OK";
l.text[1] = '!';
println l.text; # O! #
}Returned strings are shared by reference:
func make() {
return "HI";
}
func main(){
a = make();
b = a;
b[0] = 'X';
println a; # XI #
}- print / println automatically detect strings and characters
- strings print as text, not arrays
- characters print as characters, not numbers
func main(){
println 'A'; # A #
println "ABC"; # ABC #
println (char)("A"[0]+1); # B #
}Structs define heap-allocated records with named fields.
=mutable field:=immutable bind::=reactive field
Reactive fields may depend on other fields in the same struct.
struct Counter {
x = 0;
step := 1;
next ::= x + step;
foo;
}
func main(){
c = struct Counter;
println c.next; # 1 #
c.x = 10;
println c.next; # 11 #
}Fields in a struct must be declared in the struct definition.
struct Empty {}
func main(){
e := struct Empty;
e.foo = 1; # Error #
}Reactive fields defined with ::= do not capture free variables from the surrounding environment. Instead, reactive fields inside structs are evaluated entirely in the context of the struct instance.
x := 10;
struct Example {
y;
x;
sum ::= x + y;
}
func main(){
e = struct Example;
e.y = 1;
e.x = 1;
println e.sum; # 2 #
}If you want to use a global immutable within a struct reactive assignment:
x := 10;
struct Example {
y;
xx := x;
sum ::= xx + y;
}Arrays are fixed-size, heap-allocated containers of values.
func main(){
arr = [2];
arr[1] = 10;
println arr[1]; # 10 #
}When used as integers, arrays evaluate to their length.
Array elements are accessed with brackets:
func main(){
arr = [2];
arr[1] = 10;
x = arr[1];
print x; # 10 #
}Array elements support both mutable (=) and reactive (::=) assignment.
Bounds are checked at runtime.
func main(){
matrix = [2];
matrix[0] = [2];
matrix[1] = [2];
matrix[1][1] = 5;
println matrix[1][1]; # 5 #
}func main(){
base = 0;
arr = [2]
arr[0] ::= base;
arr[1] ::= arr[0] + 1;
base = arr[1];
println arr[1]; # 2 #
}struct Cell {
y = 0;
yy ::= y * 2;
}
struct Container {
m := [2];
}
func main(){
c = struct Container;
c.m[0] = [2];
c.m[1] = [2];
c.m[0][0] = struct Cell;
c.m[0][1] = struct Cell;
c.m[0][0].y = 5;
println c.m[0][0].y; # 5 #
println c.m[0][0].yy; # 10 #
}Functions encapsulate reusable logic and may return integers, characters, arrays, or structs.
func add(a, b) {
return a + b;
}
println add(2, 3); # 5 #Calling a function:
- Creates a new immutable scope for parameters
- Binds arguments to parameter names immutably
- Executes the function body
- Returns a value (or
0if no return executes)
func f(x) {
x = 10; # error: x is immutable #
}Returns are eager. Reactive relationships do not escape the function unless explicitly attached to a location outside.
func f(x) {
y ::= x + 1;
return y;
}
func main(){
a = 10;
b = f(a);
a = 20;
println b; # 11 #
}Arrays and structs are heap-allocated and returned by reference.
struct Counter {
x = 0;
step := 1;
next ::= x + step;
}
func make() {
s := struct Counter;
return s;
}
func main(){
c1 = make();
c2 = c1;
c1.x = 10;
println c2.x; # 10 #
}Returning an immutable binding yields a mutable value to the caller.
func f() {
x := 5;
return x;
}
func main(){
y = f();
y = 10; # allowed #
}Reactive bindings may reference expressions that evaluate to heap-allocated values, including structs and arrays returned from functions.
struct Pair{
x = 0;
y = 0;
xy ::= x + y;
}
func newpair(x,y){
pair = struct Pair;
pair.x = x;
pair.y = y;
return pair;
}
func main(){
result ::= newpair(1, 2);
println result.xy; # 3 #
}Reactivity is expression-based, not identity-based:
struct Counter {
x = 1;
step = 1;
}
func buildcounter(start) {
c := struct Counter;
c.x = start;
return c;
}
func main(){
counter ::= buildcounter(10);
counter.x = 20;
println counter.x; # PRINTS 10, NOT 20 #
}Use := to capture the object instead:
func main(){
counter := buildcounter(10);
counter.x = 20;
println counter.x; # PRINTS 20 #
}The language supports file-based imports using dot-separated paths.
import std.maths;Imports load and execute another source file exactly once. Imports are not namespaced. Import order matters.
Imports are resolved relative to the program root by translating dots into folders:
game/entities/player.rx
The standard library is implemented as ordinary source files under project/std/.
Importing std.file registers native filesystem functions:
file_read(path)-> stringfile_write(path, contents)-> number of chars writtenfile_exists(path)-> 1 if exists, 0 otherwisefile_remove(path)-> 1 on success
assert and error stop execution and print a stack trace.
assert expr;fails ifexprevaluates to 0error "message";always fails (string literal only)
func div(a, b) {
assert b != 0;
return a / b;
}
func main(){
div(10, 0);
}