RALGOL language reference
2026-06-05
Introduction
This is the reference for the RALGOL onboard scripting language: its syntax and structure. It complements the scripting guide, which is the tutorial; this document is the lookup table. The callable functions are listed separately in the function library.
A note on scope: the device validates every script when you deploy it, checking that each function and field you use is actually available on that device. Treat the syntax here as the language definition, and let the device’s deploy-time check be the authority on what a specific device provides.
Syntax
Script structure
A script has an interface block declaring its own
fields and a script block that runs once per cycle. An
optional requires {} block lists external variables (it is
otherwise inferred).
1w interface {
1w emit unsigned ticks;
} ral;
script {
std::add(ral.ticks, 1) -> ral.ticks;
};Interface block
Sizes are given in words (1w = 32 bits) or bits
(12b). A field is at most 32 bits and may not cross a word
boundary; the whole interface is a whole number of words. An
execution-engine interface is named ral.
Field types:
| Type | Meaning |
|---|---|
unsigned / signed |
Integer, with an optional {unit = ...} and
{valid = ...}. |
bool |
One-bit true/false. |
enum { k = v, ... } |
Named integer states. |
oneof |
Tagged union — one region decoded differently per mode. |
reserved |
Padding; not exposed. |
A field may also be an array,
e.g. 4b unsigned channels[1..8].
Units and valid ranges:
{unit = 0.001 A}— the integer is interpreted in these physical units.{valid = (0, [1:2000])}— allowed values are 0, or 1 through 2000. The set is a parenthesised list of single values and[low:high](or[low:step:high]) ranges.
Field flags:
| Flag | Meaning |
|---|---|
emit |
Stream the field continuously over LSL. |
protected |
Readable by clients, not writable from outside the script. |
hidden |
Not shown in discovery (still usable if the name is known). |
persistent |
Value survives across script reloads (shareable between scripts). |
const |
Fixed configuration value, set once. |
Script block
Statements run top to bottom each cycle and end with
;.
- Assignment
expr -> target;stores the value on the left into the variable on the right. - Structured binding
[a, b] -> [x, y];assigns several values at once. - Function calls use
namespace::name(args)and nest:std::add(1, std::add(2, 3)). The operators+ - * /are shorthand forstd::add,std::subtract,std::multiply,std::divide. - Literals are integers
(
true/falsestand for 1/0). There are no floating-point literals; fractional quantities are handled by field units and the x1000 scaling convention (see the function library). - Comments are
//to end of line, or/* ... */.
Variables
A variable is module.index.field; the index counts a
module’s instances from zero (ads.0.voltage_chan_1,
cur.1.overdrive). Array elements and slices use
[i], [a..b], [..].
Two keywords stand in for a numeric index:
self— your own interface instance, and the way to address it. Insidescript {}the surrounding module is the one named on theinterface {}block (conventionallyral), soself.field,ral.self.field, and the shorthandral.fieldall name the same field.all— every instance of a module at once. Read it only through a function that reduces those instances to a single value (std::sum(cur.all.overdrive)); a value assigned the other way is broadcast to every instance (0 -> cur.all.stim).
Under development.
self/ral.selfaddressing and interface modes (oneof) are still being finalised. Plainself.fieldis stable; reaching a mode’s fields throughselfis not yet supported — address those with an explicit instance index for now.
1w interface {
1w emit unsigned overdriven_count;
} ral;
script {
// self.field (= ral.field) is this script's own interface instance
std::sum(cur.all.overdrive) -> self.overdriven_count;
// a value written to module.all.field reaches every instance of that module
if (self.overdriven_count) :
0 -> cur.all.stim;
fi;
};Reading an .all. field straight into a variable is
rejected — the instances hold different values, so there is nothing to
copy. Reduce them with a function instead:
1w interface {
1w emit unsigned n;
} ral;
script {
cur.all.overdrive -> self.n;
};Control flow
There are no loops (the script is the loop). Three if
forms, each closed by fi;:
- Truish — runs when the expression is non-zero:
if (expr) : ... fi; - Equality —
if (expr == N) : ... fi; - Switch-like — integer cases with
is, optionalelse(which must be last):
1w interface {
1w reserved;
} ral;
script {
if (dio.0.digout_1)
is 0: 1 -> dio.0.digout_1;
is 1: 0 -> dio.0.digout_1;
fi;
};Stateful instances
Primitives that persist across cycles are declared once in a
prolog {} block (which must be first in
script {}) and bound to an @-name; methods use
@name::method():
1w interface {
1w emit signed average_x1000;
} ral;
script {
prolog {
let ringbuffer(64) -> @window;
};
@window::append(ads.0.voltage_chan_1);
@window::mova() -> ral.average_x1000;
};Functions
The script functions — the std standard library, montage
helpers (mntg), and rolling-window analysis
(ringbuffer) — are documented in the function library, together with the
conventions they share (the integer ABI, the x1000 scaling convention,
and variadic flattening).
References
- The scripting guide (tutorial) and the device-specific function list are available from neuroConn.