The language

Variables

let and var, block scope, and why a var may be assigned inside a branch.

Anyone writing more than one line inside a frame.

Locals are declared with let or var. A bare type name is not a local declaration — float x; at statement level is rejected, because in a processor that spelling means state.

Playground
let x = in * 2.0f;      // immutable
var y = 0.0f;           // mutable
y = y + 1.0f;

Choose the simplest kind of memory#

Use let for a value that is computed once during the current pass. It gives the expression a name and prevents accidental reassignment. Use var when an algorithm updates a local result, such as accumulating a sum or finding the largest table entry. Neither one remembers its value for the next audio frame.

In the short example above, x names an amplified input. y begins at zero and becomes one. On the next frame the declaration runs again, so y becomes one again, not two. A counter that must keep counting belongs at processor scope.

A var may be assigned inside a branch#

Playground
float t[8];

var best = 0.0f;
for (wrap<8> i) { if (t[i] > best) { best = t[i]; } }

This used to be rejected. It works because a var is lowered into a memory slot rather than an SSA value, so nothing has to be joined at a branch merge; LLVM's mem2reg puts it back in a register afterwards. The restriction bought nothing and cost the most natural way to write a search.

Scope#

Locals are block-scoped, and may not shadow an endpoint or a state variable. Shadowing a name that means something else in the same processor is the kind of bug that reads correctly, so it is refused rather than allowed.

Locals are not state#

A local lives for one frame. It is not zeroed between frames so much as simply not the same variable — each frame's main body is a fresh pass.

If you want a value to survive to the next sample, it has to be processor state. That is the single most common thing to get wrong when arriving from a language where everything persists by default:

Playground
// Wrong: `phase` restarts every frame, so this is silence with a DC offset.
var phase = 0.0f;
phase = phase + 0.01f;
out <- sin (phase);

The fix is to move phase to processor scope, where it persists. The oscillator on the first-patch page does exactly that.