Idƴl Documentation

6 — Control flow & operators

← 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.


Ternary selection

The ternary operator selects between options based on a condition.

Syntax: condition ? option0; option1

abs_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.

Single-option shorthand

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.

Multi-way selection

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; sustain

Style note

Wrapping the condition in parentheses is recommended for clarity but not required:

x > 0 ? 0; 1      // works
(x > 0) ? 0; 1    // clearer — recommended

Exception — 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 ? _ ; stop

This restriction only applies inside lambda block update bodies, not in pure-function definitions or top-level expressions.

stop as a ternary option

Inside 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 through

See Chapter 4 for the full stop semantics.


Arithmetic operators

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

Unary

Operator Description
- Negation
neg = -42

Comparison operators

All 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 multiplication

Since 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 true

Bitwise operators

Operate 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 = 8

Use bit(value, index) to extract a single bit:

pattern = int(7)                    // 0111 in binary
b0 = bit(pattern, 0)               // 1
b3 = bit(pattern, 3)               // 0

Operator precedence

From highest to lowest:

  1. Parentheses ()
  2. Unary -, ~
  3. *, /, %
  4. +, -
  5. <<, >>
  6. <, >, <=, >=
  7. ==, !=
  8. &
  9. ^ (XOR)
  10. |
  11. ?; (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)

Generator expressions (iteration)

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 loop

The 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
    }
}

Iterating over physical slots (repeat bars)

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
    }
}

Timed steps (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 block

The @(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")
    }
}

Syntax

@(time_expression): {
    statement
    statement
    ...
}

The single-statement form (braces optional, colon required) is also valid:

@(1s): print("one second later")

Time expression

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")
    }
}

Scope

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
    }
}

Use cases

process: {
    osc = osc_out("127.0.0.1", 9000)
    osc_send(osc, "/gate", 1)

    @(500ms): {
        osc_send(osc, "/gate", 0)    // release after 500ms
    }
}

Next: Emit & catch →