LoopIT

RALGOL language reference

neuroConn GmbH

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 for std::add, std::subtract, std::multiply, std::divide.
  • Literals are integers (true/false stand 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. Inside script {} the surrounding module is the one named on the interface {} block (conventionally ral), so self.field, ral.self.field, and the shorthand ral.field all 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.self addressing and interface modes (oneof) are still being finalised. Plain self.field is stable; reaching a mode’s fields through self is 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;
  • Equalityif (expr == N) : ... fi;
  • Switch-like — integer cases with is, optional else (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.
LoopIT documentation · neuroConn