Skip to content

Variables & Scope

Why names resolve the way they do, and what that means while music is playing, is in Names & Live Editing.

FormMeaning
let x = value;Declare x in the current scope (or redefine it)
let x;Declare x with the value NUL
x = value;Assign to an existing x; any type
x += value;Compound assignment — see Operators
let (a, b) = tuple;Destructure a tuple
private let x = value;A module-level binding not exported — see Modules

A name can’t be a keyword: let if = 1; is a parse error. See Keywords.

let x = 10;
x = "now a string"; // a variable takes any type
let count = 1;
count += 1;
assert count == 2;
let later;
assert later == NUL;

A destructuring let unpacks a tuple into names. _ discards an element, and patterns nest. The shape must match: a non-tuple, or a tuple of another length, is a runtime error.

let (x, y) = (3, 4);
let (first, _) = (10, 20);
let (a, (b, c)) = (1, (2, 3));
assert x + y + first + b == 19;

A let inside { … } — a block, a loop body, a function body, a match arm — is local to it and may shadow an outer name:

let x = "outer";
{
let x = "inner";
assert x == "inner";
}
assert x == "outer";
print(x);

Within one piece of source — one file, or one selection you execute — a name has one definition.

  • A fn, class or dsp definition may not repeat a name. Arity does not distinguish functions; use default values for optional parameters.
  • The same holds inside a definition: one definition per method or field name in a class, and per helper fn in a dsp block. A field and a method may share a name.
  • A top-level let claims its name too: let tempo = 120; beside fn tempo() is an error in either order.
  • A name a use binds is claimed the same way, by a second use too: use "std/effects" { Delay }; beside dsp effect Delay is an error. Rename the import with as.
  • A let may repeat a let. A let inside a function or block is its own.

A repeated definition is reported where it is written, and nothing in that source runs:

fn greet() { print("hi"); }
fn greet(name) { print(f"hi {name}"); } // error: fn 'greet' repeats the fn at 1:1

A later run may bind the name again, with any kind of binding: a fn replaces a let or an import, a use replaces a fn. The binding executed last wins, from the next line or call on.

fn, class and dsp definitions are written at the top level of a file. Inside a block or a function they are an error; bind a closure with let instead.

A definition exists from the line that defines it: a call written above its fn fails as undefined unless an earlier run defined it.

The session has one namespace: every top-level let, fn, class and dsp definition, and every name a use binds, executed last wins. Below it is the standard library.

Where a name is usedResolves against
At the top levelThe session, then the standard library
Inside a fn body or a class method, a name that is not a parameter or localThe session, at call time, then the standard library — never the caller’s locals
Inside a closure (fn(…) { … } value), a name it readsThe value, captured when the closure is created
Inside a closure, a name it callsThe session, when the closure runs, as in a fn body
Inside a pattern callback (map, map_with_timing, a class’s query)As in a closure: a name it reads is captured when the pattern is made, a name it calls is looked up each time the pattern computes a cycle
Inside a moduleThe module’s own names, then the standard library — see Modules
let tempo = 120;
fn beat_ms() {
return 60000 / tempo; // the session's tempo, looked up at each call
}
fn play(tempo) { // a parameter, local to play
return beat_ms(); // still reads the session's tempo
}
assert play(999) == 500;
tempo = 150;
assert beat_ms() == 400; // session lookups are live
let base = 100;
let get_base = fn() { return base; };
base = 999;
assert get_base() == 100; // the closure captured 100
let twice = fn(x) { return double(x); };
fn double(x) { return x * 2; }
assert twice(4) == 8; // a called name is looked up when the closure runs
print("resolved");

A closure captures only the variables it reads. A fn may refer to a name defined later in the file, as long as that name exists when the function is called.

Assignment changes the binding a name has, whichever kind it is: after fn f() {}, f = 5; makes f the number 5. A name only the standard library defines can’t be assigned; bind your own with let or fn.

Any standard-library name can be bound in the session, and from then on your code gets yours. The standard library’s own functions keep resolving within the standard library, so your binding cannot break them:

fn clamp(x, lo, hi) { return x; } // your clamp, for your code
assert clamp(5, 0, 1) == 5;
let print = "not a function";
put("put still prints\n"); // put calls the standard library's print