← Back to index · Previous: Flows
Idƴl has no if/else, no while. Control flow
is expression-based: ternary selection replaces conditionals, generator
expressions produce flows declaratively, and each loops
iterate imperatively over a counted range.
The ternary operator selects between options based on a condition.
Syntax:
condition ? option0; option1
condition is 0 (false) → returns
option0condition is non-zero (true) → returns
option1abs_value(x) = (x >= 0) ? (0 - x); x
safe_divide(a, b) = (b != 0) ? 0; (a / b)Options are separated by ; (semicolons), not commas —
this avoids ambiguity with function argument separators.
When only one option is given, the implicit second option is
_ (rest):
// (m) ? expr is equivalent to m ? _; expr
// → evaluates expr only when m is truthy; returns _ otherwise
(m) ? osc_send(handle, "/gate", 1)This is the idiomatic way to make an expression conditional on a trigger.
More than two options — selected by integer index:
// condition selects by index: 0 → first, 1 → second, …
note(degree) = (degree % 5) ? 261; 293; 329; 349; 391
state_out(state, idle, attack, sustain) =
state ? idle; attack; sustainWrapping the condition in parentheses is recommended for clarity but not required:
x > 0 ? 0; 1 // works
(x > 0) ? 0; 1 // clearer — recommendedException — lambda block update statements: inside a
|> lambda block, avoid parenthesising a ternary
condition when the preceding statement ends with a bare literal or
identifier. The parser may treat the ( as the start of a
function call on the preceding expression. Write the condition without
outer parentheses instead:
// ✗ Ambiguous in a lambda block
x = x + 1
(x >= 3) ? _ ; stop
// ✓ Clear
x = x + 1
x >= 3 ? _ ; stopThis restriction only applies inside lambda block update bodies, not in pure-function definitions or top-level expressions.
stop as a ternary
optionInside a temporal lambda block, stop can be used as one
of the ternary options to self-terminate an instance:
x >= limit ? _ ; stop // stop when x reaches limit; otherwise pass throughSee Chapter
4 for the full stop semantics.
| Operator | Description |
|---|---|
+ |
Addition (also string concatenation) |
- |
Subtraction |
* |
Multiplication |
/ |
Division |
% |
Modulo |
pow(x, e) |
Power (use the builtin — see Chapter 3) |
sum = 3 + 4
product = 2 * 5
power = pow(2, 10) // 1024
modulo = 17 % 5 // 2| Operator | Description |
|---|---|
- |
Negation |
neg = -42All comparisons return 1 (true) or 0
(false). There is no boolean type.
| Operator | Description |
|---|---|
== |
Equal |
!= |
Not equal |
< |
Less than |
> |
Greater than |
<= |
Less or equal |
>= |
Greater or equal |
is_loud = amplitude > 0.5
in_range = (x >= 0) * (x <= 1) // AND via multiplicationSince there are no boolean operators (&&,
||), use multiplication for AND and addition/max for
OR:
both = (a > 0) * (b > 0) // 1 only if both true
either = max((a > 0), (b > 0)) // 1 if either trueOperate on integer-converted values:
| Operator | Description |
|---|---|
& |
Bitwise AND |
\| |
Bitwise OR |
^ |
Bitwise XOR |
<< |
Left shift |
>> |
Right shift |
~ |
Bitwise NOT |
a = int(12) // 1100 in binary
b = int(10) // 1010 in binary
result = a & b // 1000 = 8Use bit(value, index) to extract a single bit:
pattern = int(7) // 0111 in binary
b0 = bit(pattern, 0) // 1
b3 = bit(pattern, 3) // 0From highest to lowest:
()-, ~*, /, %+, -<<, >><, >, <=,
>===, !=&^ (XOR)|? … ; (ternary condition, then
options)Use parentheses when in doubt:
// Clear
result = (x + y) * z
// Ambiguous without knowing precedence
result = x + y * z // = x + (y * z)Instead of imperative loops, use generators to create flows:
// Range
numbers = [i in 0..10 : i]
// With transform
table = [i in 0..256 : sin(i / 256 * 2 * pi)]
// Chromatic scale
chromatic(root) = [s in 0..11 : root * pow(2.0, s / 12.0)]Generators are declarative, composable, and produce flows directly.
each — counted loopThe each statement runs its body for a range of integer
indices. Unlike generator expressions (which produce flows),
each is an imperative loop — its body can contain side
effects, flow accesses, and nested temporal blocks.
Syntax:
each var in count [, dt=time_expr] : {
statement
...
}
var takes values 0, 1, …, count − 1.
import("stdlib")
flow notes = [60, 62, 64, 67, 69]
process: {
each n in len(notes): {
print(notes[n]) // 60 62 64 67 69
}
}len(flow) returns the physical length —
all slots including repeated copies. each n in len(flow)
naturally visits every physical position:
import("stdlib")
flow seq = {
degree: [1, 2, 3 |3|, 13, 8] // physical len = 7
}
process: {
each n in len(seq): {
print(seq[n].degree) // 1 2 3 3 3 13 8
}
}dt)The optional dt parameter spaces each iteration by a
duration, turning the loop into a scheduled sequence. This is typically
used inside an on block where a temporal context is already
running:
import("stdlib")
flow arp = { degree: [0, 3, 5, 7] }
process: {
clk = clock(120bpm)
on metro(clk(4b)): {
each n in len(arp), dt=clk(0.5b): {
print(arp[n].degree) // steps through one slot every half-beat
}
}
}Without dt, all iterations execute synchronously on the
same tick.
@ — deferred execution
blockThe @(time_expr): { } block schedules a block of
statements to run once, after a specified delay. It is
the “fire and forget at time T” primitive.
process: {
print("start")
@(500ms): {
print("this runs 500ms later")
}
@(2s): {
print("this runs 2 seconds later")
}
}@(time_expression): {
statement
statement
...
}
The single-statement form (braces optional, colon required) is also valid:
@(1s): print("one second later")The time expression can be any value that resolves to a duration — a literal, a variable, or a computed expression:
process: {
delay = 800ms
@(delay): {
print("fired after delay")
}
@(delay * 2): {
print("fired after double the delay")
}
}The handler block sees the process block’s live scope. Variables modified in the handler are visible to subsequent code — including other at-blocks that haven’t fired yet:
process: {
x = 10
@(300ms): {
x = 99
print("x is now:", x) // prints 99
}
@(600ms): {
print("x is still:", x) // also prints 99
}
}process: {
osc = osc_out("127.0.0.1", 9000)
osc_send(osc, "/gate", 1)
@(500ms): {
osc_send(osc, "/gate", 0) // release after 500ms
}
}