Write optional settings by name, reuse groups of options, and choose values with the units a command expects.
What it is#
Every builtin with more than a couple of knobs can take its optional arguments by name, in braces, after the positional ones. The braces are an ordinary record, so option bundles are values you can bind, share and override with with. Forward aliases, local bindings, record factories, projections and literal compile-time choices preserve each field’s scope. Skipping a middle argument, spelling a default to reach a later one, and remembering slot order all go away.
Surface#
wide = play pad mel |> galactic 0.4 { bigness = 0.8, mix = 0.35 } -- brightness/detune default
rich = saw (freq pitch) |> chorus 0.4 { rate = 2Hz }
slow = adsr { attack = 2s, release = 3s } -- decay 0ms, sustain 1
airy = play pad mel |> shimmer 0.28 { feedback = 0.26 } -- was: shimmer 0.28 1200 0.26
filt = saw (freq pitch) |> svf { mode = "lp", cutoff = 800Hz, q = 2 } -- required knobs may be named too
hall = { bigness = 0.9, mix = 0.4 } -- an options bundle
padT = play pad mel |> galactic 0.4 hall
bassT = play bass bassPat |> galactic 0.4 (hall with { mix = 0.15 })
The rules:
- The braces come last. Positional arguments still bind left to right; the record binds by name. A knob given both ways, an unknown name, or a name given twice is a check-time error that lists the options still open.
- A call that carries braces is a complete call: every required parameter must be present, positionally or by name.
- Skipped slots before the last named one take their schema default (the same value the effect uses when the argument is omitted); slots after it are left out exactly as a positional call would leave them. A skipped slot with no default is refused (
macro "x" 0.4 { blend = … }has no modulation graph), except inspectralMap/spectralPolar, whose four optional ports may be skipped to reachframeorfold(params::SKIPPABLE_ABSENT). - ADSR has four required positional stages, but a named record completes omitted stages with attack/decay/release 0ms and sustain 1. Thus
adsr {}is complete, whileshort = adsr 10mswaits for decay, sustain and release. Aliases, imports and returned functions use the same rules;short {}completes the remaining stages with their named defaults. - A field value is a full expression, so
{ cents = -1200 }and{ gain = -3dB }need no parentheses. - Mode strings are checked when you compile, not when the track lowers:
svf "lpp"→`svf`'s `mode` must be one of "lp", "bp", "hp", … — got "lpp". - Too many positional arguments is refused with the signature:
`delayTrack` takes at most 3 arguments after the track: `time feedback [damp]`.
Hover a keyword or built-in to see its call form, return value and named arguments. Each argument shows its accepted type, whether it is required, its default when one exists, its range or mode choices, and whether it accepts live modulation. Time arguments name their units; Number (0..1) describes a normalized level. ADSR distinguishes its required positional stages from the defaults available in a named options record. A trailing optional value with no default is labeled as optional.
The same compiler generator (compiler/src/docs/help.rs) joins the parameter schemas, builtin type registry and reference catalogue for native/browser hover cards and LSP hover. Polymorphic signatures and syntax descriptions live in the catalogue; schema names, defaults and policies are generated from the checker metadata. The editor's active-argument hint uses these same details. The compact catalogue mirror preserves the options instead of dropping them.
Cards support qualified spellings such as fx.galactic and track.gain. Long cards scroll within the viewport. Only a character under the pointer can start a hover: whitespace, the gutter, space after a line, and the area below the document do not snap to a nearby token. String and comment contents do not acquire builtin help. Clicking to place a caret still uses the normal nearest-position behavior.
Completion inside { … } offers unset options as name = and signature help follows the active field. The reference catalogue's generated options list is checked against the schema, and the UI mirror is checked against the catalogue.
Edit a call as a form#
Enable Parameters in the source editor footer, or Display → Editor & notebook → Parameter mode. The preference is saved. Place the caret on a function name or inside its call and press Tab. Accepting a function completion with Tab, Enter or a click enters the first missing required parameter directly. The source editor, import tabs and notebook cells share the form. Ordinary hover remains a passive help card.
Each row shows its name, type and current expression. The active row's help shows its meaning, default, accepted range or choices, and modulation policy where the compiler supplies them. A pipe's input is already connected. Expressions such as 600Hz + sweep stay expressions. The compact form stays within the editor's horizontal bounds, highlights the corresponding source argument, and shows Parameter N of M. Changing the active field keeps the card anchored and its short help area stable. Details expands the full function, range and modulation documentation; Unapplied edit marks a value not yet applied to source. Validation appears beside the field that needs attention. Applied fields are source edits; run the source to hear them. Secondary shortcuts live in tooltips and Details. Done and Discard current edit stay visible below a single scrolling content region; the header also stays fixed. Values use larger type and numeric/choice controls have larger pointer targets.
- Tab / Shift-Tab accept the draft and move forward/backward. Moving before the first field returns before the function name; moving beyond the final field returns after the call.
- Enter / Done accept and finish once required arguments are present. Command-Enter (Control-Enter on Windows/Linux) also runs the edited source.
- Escape first dismisses an open suggestion list. With no suggestions open, it discards the active draft and returns to source. Clicking outside parks the most recent changed draft; Resume draft in the editor footer or Tab on the same call restores it. Source changes prevent automatic resumption, but Recover draft keeps the value available to copy. It never writes through old source positions. Discard current edit explicitly drops only the active draft. Values already accepted remain ordinary undoable edits.
- Optional parameters (N) reveals omitted options without inserting defaults. Filling one adds a named field. Use default removes an explicit named override or safely removable trailing positional value.
- Ctrl-Space requests expression completion. Matching symbols, functions and units also appear while typing; current type information ranks candidates. Up/Down selects a suggestion; Tab/Enter inserts it into the draft, keeping the field active. Press Tab again to accept that field. The Tab keycap is followed by Accept suggestion, Apply & next, or Finish. Suggestions sit beside the card when the editor has room, or in a reserved area inside it on narrow panes, keeping fields and actions unobstructed. Suggestions show their category and the selected result’s signature and description. Already complete names and units do not open a distracting suggestion list.
- Alt-O toggles optional fields; Alt-D uses the default; Alt-H toggles detailed help. Alt-Down opens choices for enum fields or completion for expressions. Alt-Up/Down nudges numeric literals. Enum rows also have an adjacent dropdown; selecting a choice changes the draft.
- Drag the ↔ handle beside a numeric literal to adjust it in its authored units. Positive frequencies move logarithmically (120 pixels per octave); explicit discrete slots such as unison count and phaser stages move in whole steps. Numeric controls respect schema clamp ranges and normalized/bipolar units; authored text expressions remain unrestricted. Shift gives coarse travel; Command/Control gives fine travel. A drag remains unapplied until Tab/Enter/Done; Escape cancels it even while held. Expressions have no numeric handle and remain ordinary text.
- Ctrl-Alt-Right opens the first nested call in the active expression. The Edit nested buttons select individual sibling calls. Ctrl-Alt-Left and the breadcrumb buttons return to containing calls. Navigation accepts the active draft, reparses the changed call, and preserves ordinary Undo.
- Ctrl-Alt-Up/Down, or Previous effect / Next effect, moves through the same authored pipe chain. Navigation applies the current draft and reparses later positions; it does not jump into a nested argument's separate chain.
Every accepted field is a source edit with its own Undo step. Surrounding code, pipes, comments and named-field order are retained. Syntax errors, known incompatible literal units and invalid enum literals keep the form open with a message; the normal compiler checks complete expressions. A concurrent source change closes the form before stale offsets can write. Automatic formatting pauses while the form is open.
The first version supports schema-backed builtins, ordinary fixed-arity builtins and known local functions. Ambiguous calling shapes, pattern-last overloads, ambient mixer/bus sections, shared option bundles, multi-caret selections and structural mode continue through source editing. Pinned forms and specialized envelope/filter/pattern visuals are deferred; see Call Editor.
In the mixer / on a bus#
The same braces work in bus … = … chains and inserts = […] lists, and sections use ordinary expression checking and track-effect lowering (Mixing tracks and buses §1.2). Unit suffixes are read for the slot:
bus hall = galactic 0.4 { bigness = 0.9 } |> width 1.3
bus dub = tape 0.375c 0.4 -- three eighths of a CYCLE (was 0.375 s)
mixer { master = { inserts = [limiter { ceiling = -2dB, release = 60ms }] } }
250ms in a seconds slot is 0.25, (3c/8) and (200ms) fold inside parens, 2kHz is 2000 Hz, and a unit the slot cannot read is refused (`tape`'s `time` reads Seconds, not Hz) rather than dropped. Bare numbers use seconds for delay times. Direct ambient limiter sections retain the legacy millisecond release scale; an authored track-transform lambda uses ordinary track semantics (seconds). Write explicit 60ms when sharing a limiter call. duck attack/release use seconds (write 5ms 250ms; a bare 5 80 is a five-second attack, and the compiler warns). Cents/semitone slots convert compatible pitch units at the shared parameter lowering boundary, preserving live affine values.
Bare numbers in time and frequency ports#
A bare number adopts its port's unit, so the number is judged against the range the port can use: its declared clamp (the clamp column of the schema — duck's attack reads 0.5–200 ms; the voice delay/comb/feedbackAt lines declare their two-second ring), or, for a port with no clamp, a narrow per-unit default — more than 10 s, less than 1 ms, or under 20 Hz for an audio-band frequency (a cutoff, freq, tune, formant, masterFreq). Rate ports and the long-by-nature over and offset have no default. A number outside is warning[BareNumberUnit], which names the reading and the suffix that lands the same digits in range:
warning[BareNumberUnit]: `duck`'s `attack` reads 5 as 5 seconds — did you mean `5ms`?
The patch envelope fields (attack, decay, release — seconds — and glide, cutoff) are judged the same way. Under @strict (a binding's "on" or the module's "module") every bare number in a seconds, milliseconds or Hz port is an error, plausible or not; zero is exempt everywhere. The warning is for the entry file: an imported module's numbers are that module's.
Levels read dB#
gain — the track fader (gainTrack), a bus or master insert, and the voice product — and every mixer level (strip, group, send, master) declare a LEVEL: a linear gain or dB, converted 10^(dB/20) by the port, the one logarithmic conversion in the language. A level that folds converts once, in dB; a dB literal inside a live level is its trim (fade * -6dB is 0.501 · fade); a live dB signal (macro -6dB) is refused, because neither level reader has a per-sample dB law. The patch gain field stays linear and refuses dB with its own sentence ("gain expects Linear … Use a linear multiplier such as 0.5").
Pattern transforms#
The fifteen pattern-last operations in Read and write pattern pipes share the same named-argument binder. Explicit pipes identify the subject before the fixed arguments bind, then normalize to the existing lowering order:
main = play "saw" ("c3 e3" |> every { f = transpose { n = 12 }, n = 4 })
A named transform section supplies every fixed argument and leaves its subject omitted. Bare positional calls retain their historical order. Naming the numeric slot (p |> transpose { n = amounts }) disambiguates two numeric patterns without changing the meaning of an older pipe. Sample chop / slice modifiers also accept { n = 4 }; their receiver and lowering order remain first, and so do the descriptor-owned sample modifiers below (sample "x" |> trim { start = 0.2, end = 0.8 }). Calling-shape metadata selects the schema, so these operations are not added to the table of single-shape effects.
The twenty-seven descriptor-owned transforms (§ "Pattern-transform descriptors" below) take named options in either family — p |> ply { n = 2 }, p |> sometimesBy { prob = 0.3, f = rev }, p |> mask { boolPat = gate } — and report one arity message: p |> ply 2 3 is `ply` takes at most 1 argument after the pattern: `n`. A number in a transform port (p |> every 2 0.5) or in a structure port (p |> mask 3) is a check-time error that names the port, directly or through a helper.
Scale, gate and inverse#
These functions accept named values: m |> scale { range = [500Hz, 4000Hz] }, gate { threshold = 500ms, m = duration }, and inverse { m = depth }. Scale requires exactly two compatible numeric bounds. Its list structure is prepared; live bounds and the input are read per sample. Gate compares compatible quantities; inverse requires dimensionless input. All three preserve audio-rate inputs. Positional scale 0 and gate 500ms leave one argument to supply later.
Events, clocks and blends#
These functions also bind from their descriptors: phase { start = 0.25 }, interpolate { mode = "linear" }, syncTo { scope = "note" }, filterTrig { trigger = hits, gate = enabled }, and crossfade { a = 500ms, b = 1s, mix = 0.5 }. Receiver modifiers still take the receiver before these options. Modes are case-insensitive fixed strings; a string-producing function is checked again when the graph is realized.
Phase accepts a prepared dimensionless/cycle seed on oscillators and periodic LFOs; a live offset requires an LFO. Gate/reset inputs retain their Boolean, dimensionless-control or Trigger constraints through partial and returned calls. syncTo chooses its clock ownership explicitly. Numeric signal rate does not say when configuration is read: modes are prepared, while live phase and gates are read per sample.
Not (yet) covered#
- Other builtins with multiple calling shapes, such as
warp, keep their positional behavior.latchCyclehas one fixed shape:cycles m, with both arguments required;latchCycle { cycles = 2, m = depth }also works. withbinds looser than application:galactic 0.4 hall with { … }applies thewithto the result; write(hall with { … }).
Compile-time and live values#
After checking a declaration, the compiler uses each builtin parameter's Live::Const classification to reject demonstrably live arguments. The diagnostic names the builtin, parameter and Const policy at the argument, with a literal/compile-time-expression remedy. This includes macros and signal-producing builtins through arithmetic, forward aliases, lexical bindings, record and tuple projections, list projections (head, tail, nth — head [knob] in a Const slot is the knob), functions and captured closures.
Live::Structural carries no such obligation, whatever its Slot. "Not a value knob at all" describes the SHAPE a port reads, not closedness, and a structural port routinely carries a live value inside it — a structural number on a pattern transform takes a signal (p |> degradeBy (lfoSine @ 1c)), and a structural sample name accepts the sampler source itself (modalOf (sample "bowl")). Those ports' obligations belong to their own families, which have better sentences for them.
A port's declaration describes the port, not the value in it. A Compilation port still installs live inputs: process's lambda folds to kernel bytecode, yet each free macro in its body becomes a MacroRef input of that Kernel node; Slot::Lambda reads overloads::lambda::Arrow per port, not per family. And a literal-only knob goes live for free: Param is untagged with Const(f64) first, so turning an Option<f64> into an Option<Param> round-trips 0.6 as the same bare number (fold_param plus a ParamBind on the plan); offline readers take .value(), the constant or the live form's initial.
A port that declares choices is checked the same way: a mode string is refused at CHECKING, naming the legal set, whether it was written in the seat or reached it through a binding, an import, a function's result, a record field or a list element. One sentence serves every spelling (params::choice_message), and a name with two positional conventions (arp "up" p and p |> arp "up") is bound by the transform descriptor's declared subject position rather than by argument index.
Both laws refuse what the bounded proof shows unsatisfiable, never what it merely cannot settle: an unproven value defers to the lowerer, whose evaluator is strictly stronger (it substitutes top-level bindings and reduces closures).
A port that declares Live::Const or Live::Structural requires a compile-time value, never a compile-time spelling. The lowering arms that read one match a literal shape — a number, a mode string, a list, a lambda, a pitch — and lower::expr::closed_value puts the port's own value in front of them whichever way it was written. Three seams call it, and between them they cover every arm:
lower::expr::Lowerer::lower_app, for every seatparams::closed_port_seatsnames, which is every builtin application the lowerer dispatches;- the pattern pipeline's
resolve_pattern, for every argument a pattern generator or transform takes; lower::expr::substitute_closed_ports, ONE walk over the whole program before anything reads it, for the readers built from the AUTHORED declaration — a top-leveltempo = accel …,tuning = edo n,markers […],meter = meterMap […], awavetableOfbinding, thebounceOf/bouncesOf/sampleOf/comppre-scans, anautomationcurve or aregion's configuration record, and every insert in a track pipeline, none of which is alower_appdispatch.
So n = 2 then p |> fast n is fast 2, an imported "hann" is the window it names, a bound [0, 1] is scale's resolved bounds, d = 19 then tuning = edo d installs 19-EDO, and a marker list held in a record field folds like the literal it names. Values come back untouched by design: an expression that READS A MACRO (a knob must stay live — freezing one at its declared initial is the error static_scalar exists to avoid) and a LAMBDA (process, process2, shape and feedback own resolvers of their own, and pre-reducing a kernel body would rewrite a kernel that already compiles; those arms retry through the value reader only once their own reading has refused). A pattern (lower::expr::carries_identity — a pattern literal or value, or a constraint hole) carries source offsets and a generative salt the caller did not write, so it is substituted only as the WHOLE value of a seat whose arm reads it (patternOfEvents ctxr.ctxv), never nested inside a list or record — arrange (map (\c -> [1, c]) cells) reaches the arm as the computation it was and the arm resolves each pattern by reference — and the declaration seam never substitutes one at all. A list of pattern NAMES is a list of references, and a list of Event records is data; both may move. Nor is any other IDENTITY-BEARING seat (params::identity_bearing_seat: a Slot::Pattern port, an instrument value, a track's content, a trigger stream), because those arms resolve a reference or read source positions rather than fold a value.
The declaration seam leaves two further kinds of seat alone, and both say so on the descriptor rather than in a list of names. An argument the call captures UNEVALUATED (core::calls::captured_unevaluated — clock's exact rational is ArgumentMode::Structure, a region's content is Content, a pattern transform's section is Transform) has no value to substitute. A NAME port whose declared spellings do not include overloads::operand::Spelling::Elaborated — a plugin path, a MIDI device, a modulate or automate target, an imported song — states which syntactic forms name the thing it names, and widening that set is a descriptor change, not a lowering one. The same is true of a function port whose overloads::lambda::Arrow does not list Spelling::AnyCallable. exit_matrix::a_closed_port_accepts_its_own_value_through_every_abstraction derives all three from the descriptors, so the ports that refuse are the ports that declare it.
The declaration seam is speculative: a seat that moves keeps its gas; one that does not gives back its gas, log notes and call frames (eval::speculate / eval::discard), and the binding walk meets any gas-cap error itself. Its environment holds every declaration first, so a seat may read one written below it (tuning = edo n above n = 19). A COMPUTED declaration (x = if c then 120 else 120, n = m + 7) is settled through closed_value only when a seat reaches it, always at no charge (the walk elaborates it anyway); a cycle is visited once and left alone. An error inside a value the seam wrote points at the definition's token and names the use ("while elaborating ml at line 3").
A settled unit value folds to the number its literal folds to. The evaluator keeps 200ms in the unit the author wrote (CtValue::UnitNum(200, Milliseconds)); a slot stores the graph value, and semantics::graph_value (milliseconds become seconds, every other unit keeps its numeral) is the one conversion, applied by const_scalar to a literal and by lower::expr::ct_value_as_number to a value reached through a name or the evaluator — fold_const_scalar, lower_var's static binding, a song's scalar slots and morph's t. So d = 200ms then delayTrack (d + m * 10ms) … is a 0.2 s affine offset, exactly like delayTrack (200ms + m * 10ms) … (language_foundations::a_bound_duration_folds_like_its_inline_spelling_at_every_seat).
Two operands DO declare Elaborated, and each names one elaborator in compiler/src/lower/handles.rs — the walk that PROVES a spelling rather than matching it. operand::BANK_SAMPLE (every receiver that plays, slices or analyses a bounceOf / sampleOf / comp handle) resolves through handles::bank_key; operand::TRACK_OR_BUS — duck's source, tap's and tapLive's, follower's, onsets' and pitchOf's track, convLive's and specMask's source, all eight — resolves through handles::source_name. That one walk is what makes the operand mean the same thing at all three seams a call can reach the lowerer by: lower_track_expr for the insert, lower_app for the two cross-synthesisers, and lower/expr/events.rs::fold_source_name for the five readers. Its spellings are a string literal ("input" is the live audio-input hub), a Ty::Track reference — which resolves to its own link name, the bus key, and is never followed to the play … that built it — and any value that resolves to one of those: a binding, a record field, a function's result, a list element. A top-level binding is followed by its TYPE, not its shape: a String binding is evaluated, so which = "ki" ++ "ck", which = if … then "kick" else …, which = pick 0 and which = head ["kick"] all name kick at all eight ports; a Track binding names itself; a bus or group binding names itself; a plain value binding (x = 0.5) is refused in the operand's words.
Widening the SPELLINGS is not widening the names that exist. A track name written as a STRING is namespace-qualified where the author WROTE it (resolve::qualify_macros, derived from these same declarations through builtins::track_name_ports), so a string that reaches the port through a binding inside an imported library carries no track identity; and a name that names nothing is refused at the ARGUMENT the author wrote, at every site of the family, never rendered as silence: handles::refuse_dangling_sources asks the built module through SourceRef::role (the resolver volta_ir::validation::validate_for refuses with, which stays behind it as the backstop) and blames the recorded seat — "duck's source references missing audio source snare", at the operand in the CLI and the editor.
Static if/case choices follow the elaborator's literal arithmetic rules; discarded live fields, unused arguments and dead branches do not make the selected result live. const 1 remains valid despite its signal-shaped type.
Patch compile-time fields (including voices and glide) use the same policy. Sibling references and inherited fields are rechecked against a template's merged overrides. Ordinary records and helper fields remain open. Mixer sections use the same builtin checks.
The proof is bounded and never runs DSP, random generators or logging. That half is measured rather than asserted: compiler/src/probe.rs counts every random draw (lower::salt::next_salt, plus the pattern search's generator) and every user closure body the static evaluator reduces, and compiler/tests/exit_matrix.rs::checking_draws_no_random_and_evaluates_no_callback requires both counters to read zero after check::check_program for thousands of programs synthesized from the descriptors. Nothing in the compiler branches on the counters. Unknown higher-order values, unresolved structural choices, holes and exhausted proof budgets defer to the ordinary elaborator/lowerer; they are not assumed live. Supported live and affine parameter slots remain available.
Naming a value does not change what it means#
A port's declared type is the law for its argument whatever spelling reached the seat — written inline, bound at the top level, imported, returned from a function, read out of a record field or taken from a list. Three rules carry it, and compiler/tests/exit_matrix.rs proves all three over every descriptor-declared port at once:
- The declared unit reaches every spelling. The lowerer reads a numeric constant's unit off the argument's type, and inference only rewrites a literal argument to the declared port type — so a named number used to arrive carrying its binding's own type.
params::with_adopted_port_unitsmakes every bare numeric argument (Int,Float,Const(())) adopt its port's declared unit, in the lowerer, before any family dispatches. The handful of ports whose bare number means a unit the declared arrow cannot spell —fm/sync's modulator frequency,pulse/as3340's duty cycle — state that in one place,controls::Descriptor::lifted_numeric_port, read by both the checker and the lowerer. - A constant is constant however it is spelled. A port that bakes a constant into a node field and builds a live control node otherwise asks
lower::expr::static_scalar, not "is this a literal": a literal folds, anything reading a macro stays live (that is what a knob is for), and anything else that folds at compile time folds.adsr's stages,drift/freqShift/ringMod's amounts andunison/supersaw/jp8000's voice knobs all lower the same graph either way. - A graph value may wear a scalar type.
freq's result is declaredConst(Hz)even thoughpitchis a per-note signal, so a binding likef = freq pitchis a graph value with a scalar type. AConst-typed binding that does not fold to a compile-time value is recorded as a free-floating modulator and inlined at its use, exactly like aSig-typed one.
A fourth rule closed the last of these on 2026-09-22: a reader built from an expression's syntactic head sees through a name too. segment reads its cycle rate off the head (segment 16 (lfoSine (4c))) and additive recognises a spectrum generator the same way, so both ask for the expression the name stands for — segment only after its own head test has failed, and additive because lower.rs's binding walk now records a spectrum application as a compile-time value. patternOf's element fold reads each element's closed value for the same reason.
A fifth rule closed the last graph difference on 2026-09-22: an operand is not an instrument. Naming an audio expression used to publish it as an instrument as well, so m = sine 220Hz … fm m 2 carried one extra single-out synth that no track played. A top-level binding that some other expression only CONSUMES, and that nothing addresses as an instrument (play/playOn/sound, a shred trigger, a bus voice route), is no longer published — compiler/src/lower/publish.rs holds the rule, the reference census it reads and the owner's decision. A binding referenced NOWHERE is still published: "unused" was never the test, and a sound you are sketching must stay in the rack.
Two exceptions remain, pinned in the matrix with their reasons, and neither is a synth: naming a play … operand publishes an extra track, which is audible (k = play p !"c2" feeding duck is in the mix; the inline sidechain source is not), and naming a partialsOf … operand publishes an extra load-time analysis. The closed ports that still refuse their own argument — clock's structural ratio, feedback's body (a type-inference question now, not a spelling one), the declaration-level readers and the prepared track-effect family — are named one by one in exit_matrix::CLOSED_PORTS_THAT_REFUSE_THEIR_OWN_VALUE.
Engine-recognized patch fields and first-sound envelope controls use params::patch, reusing Slot, Live and DefaultValue. Their known units are checked in literals and again after merging/lowering; omission defaults are separate from per-event lane fallbacks. Arbitrary helper fields remain legal. See Your first sound for the surface and advisory limits.
Where it lives#
compiler/src/params.rs— the data model,normalize_call, sharednormalize_with_schemabinder and rendering.compiler/src/params/pattern_calls.rs— calling order and fixed parameter schemas for pattern pipes, named transform sections and sample overloads; a migrated transform's entry is projected fromcompiler/src/builtins/pattern_transforms.rs.compiler/src/builtins/controls.rsandcompiler/src/builtins/overloads.rs— authoritative descriptors for migrated control and math families; schemas, signatures, constraints and executable handler selection share these owners.compiler/src/builtins/grains.rs— the grain surface: the granular source, the reader switch and every cloud modifier. A modifier adds no node — it folds a port into theSamplenode it receives — so the receiver is a declared port and each control's reader is the grain spawn, not the sample.compiler/src/builtins/envelopes.rs— ADSR arities, duration/level units and note-on/retrigger execution policy; named defaults are separate from the schema's required positional prefix.compiler/src/builtins/track_modifiers.rs— the track-chain stages that set a scheduling field instead of installing an insert.compiler/src/params/table.rs— the legacy hand-transcribed table, EMPTY since 2026-09-21 and kept only as the shape a future unmigrated builtin would take.compiler/src/check.rs::infer_app— binds the record, normalizes the call.compiler/tests/exit_matrix.rs— the generated P2 exit matrix: every spellable port × six abstraction contexts (inline, a top-level binding, an import, a user function's result, a record field, a list element), asserting that aLive::Signal/Live::Controlport accepts its value however it is written, that aLive::Const/Live::Structuralport's refusal is the same one in every context, and that a wrong-class value is refused everywhere at CHECKING with a caller-owned span.EXIT_MATRIX_REPORT=1prints the per-port table; ports it cannot spell are named with a reason instead of skipped.compiler/src/params/liveness.rs— the closed elaboration boundary: the bounded declaration walk, evidence of a live argument in aConstslot, and thechoiceslaw over a proven string. It runs at CHECKING fromcheck.rs(every declaration) andcheck/mixer.rs(effect sections), and again post-substitution fromcore::calls::refresh_application, the seamparams::check_port_unitsalready hangs on.tests/parameter_liveness.rspins it.compiler/src/params/patch.rs— patch-field/voice-control schema, unit checks and shared IDE policy;tests/patch_parameters.rspins the boundary.compiler/src/builtins.rs::builtins()— pads each arrow to the schema's arity, so signature help and the checker see every optional slot.compiler/src/check/mixer.rs— typed ambient records and track sections, feeding the normal call checker and liveness policy.compiler/src/lower/mixer.rs— applies sections to a synthetic receiver;lower/track.rsconstructs effects for tracks, buses and master inserts.params::check_port_unitsandlower/expr.rs::fold_pitch_param— the port-unit gate and pitch-unit conversion. The gate runs for EVERY schema whose call shape can be aligned with its ports (not mixer effects alone, which is all it covered before 2026-09-21), it runs LAST incore::calls::refresh_applicationso a family with a richer sentence for its own port still speaks first, and the law it applies issemantics::units::port_fit— see Language reference § 2 "The unit policy, in one table". A port may declare a narrower domain than that law; none may declare a wider one. Every port-unit refusal — the gate's and each family's — is spelled byparams::port_unit_message, so the wording no longer depends on which surface the call was written on. It, the arity, option, choice and liveness sentences name a receiver-dispatched twin as the author reaches it (plausibility::spoken:delayTrackisdelay), while a schema LOOKUP still takes the registry name.compiler/src/params/plausibility.rs— the bare-number judgement: the scan over the entry's typed program, the per-port range (clamp, then the per-unit default), theBareNumberUnitwarninglower.rscollects and the@strictrefusalenforce_strictraises;tests/bare_numbers.rspins it and walks the corpus (BARE_NUMBER_CENSUS=1prints every site).compiler/src/lower/mixer.rs::linear_level— the one level reader: mixer levels and the track/bus/mastergain;amplitude::Descriptor::graph_operandtypes the voicegain's dB constant, whichlower/expr.rsfolds.compiler/tests/named_options.rs— the mechanism, andschema_defaults_match_the_lowerer, which compiles every schema builtin bare / fully-defaulted / last-option-named and demands the same graph.
FM operators, frequency/phase/ring modulation, sync, soften, AS3340, coloured noise, crackle, oscillator stacks (unison, supersaw, jp8000) and the tap/follower/onset/pitch reader families also use those descriptors. Named-source and inline-audio detector overloads have separate input execution policies. Optional source arguments default once the required ports are supplied; a prefix missing a required port remains callable through aliases and imports. Required non-pattern prefixes use the same typed binder for positional and named arguments: tracked = keytrack (linear 0.5) accepts {refPitch = ~C3}, and band = lowShelf 500Hz accepts {gain = -3dB}. Optional ports still default once the required arguments are complete.
The voice forms of drive, vca, bitcrush, fold, clip and tube also use the shared descriptors. Their amplitude and shaping inputs reject physical units, and partial drive/vca calls retain named options. Live inputs retain each processor's read policy (see The effects reference).
hiss, sub and voice pan also share those descriptors. Mixing controls are dimensionless. Sub's whole octave count (1–4) is prepared; its level, hiss amount and voice pan amount remain live per-sample inputs. Track pan preserves its constant, pattern and LFO receiver forms through helpers.
The voice-color family (slew, slewNoise, bloom, sweeten, hypersoft, tremolo, gatelope) also uses shared descriptors: all knobs are dimensionless per-sample controls, and partial calls preserve named options and defaults. Their processor-specific smoothing and stepped responses remain unchanged.
The 78 track-effect descriptors cover ambience, dynamics, color, spatial, delay, spectral and resource/aggregate effects, the granularize cloud's modifiers, the insertPoint boundary and the master-only varispeed. A modifier that folds into an earlier insert, and an effect whose only surface is a master insert, are descriptor-owned like any other. Numeric controls reject unrelated physical units through aliases, containers, factories and imports. Frequency, dB and time ports keep their units. Shimmer accepts prepared cents/semitones, and kPlate a prepared mode string. ClipOnly/cornerClip ceilings accept prepared dB values; live ceiling controls stay linear.
Prepared control models use the same binding rules: linear {slope = 0.5}, uniform {bounds = [0.2, 0.8]}, keytrack {curve = response, refPitch = ~C3} and randomPerNote {dist = distribution}. Their callables and model values retain prepared dimensionless/Pitch obligations through imports and containers.
The six EQ band constructors share frequency/gain/Q obligations through ordinary function values. Band structure prepares while persistent controls stay live; physical-unit macros retain Hz or dB, while bare dimensionless macros retain normalized mapping. Named options and default Q values use the same descriptor as positional calls.
Partial calls retain named options and defaults: f = reverb track followed by f 0.4 {wet = 0.2}. A call whose required ports are complete does not expose optional ports for later currying. Saturation's optional third argument may be a prepared shape string or a numeric wet mix; defaults follow that layout. Delay transition strings may be prepared function results, while live selection is rejected. Fixed beats/bars use the compilation meter; live delay times require seconds or milliseconds. Sample durations need an explicit clock.
Resource names for convolve, spectralMorph, neural/neuralMod and granularize are prepared strings, including values returned by functions. Harmonizer degrees are prepared dimensionless values; sympathetic strings are prepared Pitch values. EQ lists contain EqBand values. Neural bias lists contain dimensionless persistent controls, so [knob, 0.2] remains live. List shape must prepare even when its payloads are live; a live condition cannot choose a resource or change the list's shape. Empty neural bias and an omitted granular source retain their existing meanings.
Overload descriptors: what they own#
457 builtin names are bound by an overload descriptor in compiler/src/builtins/ rather than a row in params/table.rs: math (overloads.rs), graph controls, sources, filters, readers, the drum box and the voice-level physical models and processors — comb, phaser, flanger, freqShift, discontinuity, zFilter, glitchShift, bbd, ensembleChorus, duckBy, delay, chorus — (controls.rs), track inserts (track_effects.rs), track modifiers (track_modifiers.rs), EQ bands (eq_bands.rs), prepared models and their consumers (prepared_controls.rs), amplitude (amplitude.rs), adsr (envelopes.rs), mkCurve (curves.rs), segment (sampling.rs), the sample sources, playback modifiers and load-time bakes (samplers.rs), the grain surface (grains.rs), the additive, spectral and partials sources (spectral.rs), the waveguide, pulsar and phase-distortion sources (voice_sources.rs), the modal-table family (modal.rs), the compile-time list algebra (list_algebra.rs), MIDI output and hosted plugins (midi.rs, plugins.rs), the latent and neural names (latent.rs), the song, timeline and automation forms (timeline_forms.rs), the track sources (track_sources.rs), the five names whose one port has several readings (readings.rs) and the pattern transforms, constructors and merges (pattern_transforms.rs, pattern_constructors.rs). The rest of the language still uses the legacy registry and table.
A descriptor is the single owner of:
- Binding — the
BuiltinSchema: port names, receiver position, required prefix, defaults, clamps, mode choices.params::schemareturns this same value, so the binder, signature help and the catalogue cannot disagree. A schema may also decline the named-options reading of a trailing record (BuiltinSchema::named_options, set bypositional_only()), for a call whose ports hold record VALUES:play ep seed, whereseedis aChordrecord, is a chord and not an options bundle. Declining is only sound when every port is required — there is then nothing naming could reach that writing the argument does not — and a gate holds every such schema to that. The finer form is per port: a port that takes a record VALUE isSlot::Record, or the binder eats the record as options. A trailing record landing on aRecordport is that port's operand (automation { points = … }); beyond it, or in the RECEIVER position (delay { time = … }before the pipe), it is options.BuiltinSchema::record_operand_atholds the law; the twocheck.rs::infer_apppre-passes that split a trailing record before the binder runs ask it too, andArgumentShape::may_fillanswersfalsefor every known shape on such a port (params.rs::record_operand_tests). The other binder declaration isReceiver::Pattern(two positional conventions):normalize_with_schemaskips positional mode-checking, a pipe-and-prefix name (whenSig) names the piped spelling's ports, and onlypattern_transformsdescriptors bind both. In a schema,default.is_none()is not a required-argument test: a named option may carry a default while positional arguments stay required (adsr's first four stages), andDefaultValue::Absentis optional with no default at all (bbd's mode) and acts as a wall — nothing after it may carry a synthesizable default. - Registration —
builtins::descriptor_registry()projects every descriptor's name, signature and handler kind;builtins()adds nothing by hand for a migrated name. - Implementations — the handler identities a consumer selects (
Consumer,GraphImplementation, amplitude/samplingImplementation). - Execution — cadence and instance scope for each port of each implementation, declared in Rust as one of the named profiles in
overloads::exec(GRAPH_SAMPLE,TRACK_SLICE_END,FOLDED, …). 841 ports are declared. It is never derived from a signal's rate or from the port'sLiveflag:lfoSine'srateisLive::Signalon the surface, prepared once by the fixed-rate implementation and read per sample by the live one.speed'srateandchopAt'sindexare the same shape and differ the same way: the engine reads one every sample and latches the other at the note. Nor is it derived from where the engine's loop reads a buffer: the drum voices,meshand thestrike/pick/scrapebursts are read every sample by theireval_*arm but latch inconfigureon the first one, so their declared cadence isNoteOn. The granular cloud is the other shape: it reads all ten controls every sample, but onlywet/dry/freezeact there and the rest latch at grain spawn. Read the model'stick, not theeval_*arm, then measure: alinram a b ton aNoteOnknob renders as constanta. - The bare form —
ControlFn::bare_kindsays which functions are also a nullary value: a control signal (sine,saw,square,perlin,lorenz,henon,orbit) or, for the knobless 808 one-shotsrim808andcowbell808, the whole audio voice at its hardware defaults.builtins::signal_sourceprojects it. - Scene-address units — a scene endpoint aimed at an insert slot (
bus.limiter.ceiling = -3dB) is read in the port's ownSlot::Unitwhentrack_effects::scene_unitlists the port as physically addressed. - Derived defaults —
reverb/plateomitwetthrough a namedWetLawthat lowering evaluates; theDerived("…")text is help only, and a test evaluates it against the law. - The arity diagnostic —
BuiltinSchema::too_many_arguments/too_few_argumentsname the accepted form (`reverb` takes at most 2 arguments after the track: `amount [wet]`) for direct calls and for calls that reach the descriptor through a value. Lowering arms index their arguments without re-checking the count. A call left short of its required ports is an ordinary partial application, so the patch field it lands in reports it — and names the builtin, its accepted form and the first missing port's own sentence, which is where a spelling trap is written down (freqShift's(0 - 150Hz)down-shift).
What a descriptor does not own: the DSP (handwritten lowering arms and the engine), the catalogue prose in compiler/assets/docs-data.json, and the reader-facing sentences of compiler/assets/parameter-audit.json. That audit also records an execution per destination, as evidence: the compiler never reads it for a migrated port, and tests/descriptor_execution.rs requires the Rust declaration and the audit row to exist and agree for every descriptor × implementation × port, and every call/<name>/overload/… row to belong to a real port. scripts/parameter-inventory.py additionally requires each audit profile's path::symbol evidence anchor to be defined in the cited file.
A broken descriptor assumption reached from user source (an EQ band port without a unit, a clock rate on a non-clock) is a spanned internal compiler error, not a panic.
A builtin that means one thing in a voice and another on a track is TWO descriptors joined by receiver dispatch, never one descriptor with two receivers: bbd/bbdTrack, ensembleChorus/ensembleChorusTrack and duckBy/duckByTrack follow gatelope/gatelopeTrack (check.rs::track_twin_selected). The voice half is a control descriptor, the bus half a track-effect descriptor, and each declares its own execution.
A track chain stage that sets a FIELD on the track instead of pushing an insert is a track modifier, and belongs to track_modifiers.rs rather than track_effects.rs — swing, nudge and phraseOrigin write Track.swing, Track.nudge and Track.phrase_origin. The two families have mirror-image gates: an insert descriptor must lower a track with exactly one insert, a modifier descriptor must lower one with none. TrackRole::Modifier is wider than the module — delayTrack, tune, deess, harmonize and formantShift carry that role for headroom accounting but do push an insert.
params/table.rs is empty. One track insert has no schema at all: bypassed, a soft keyword registered as a bare Ty::Hole whose arguments are the un-applied head and arguments of ANOTHER effect — its arity, port names and units are that effect's, so a schema for it would be a function of the wrapped builtin's schema and its signature is not an arrow. tests/descriptor_touchpoints.rs::UNMIGRATED_INSERTS records the reason; every other insert, modifier, source, transform and constructor with a call port is descriptor-owned (2026-09-21).
Still hand-kept beside descriptors, each marked P2.3 in the code: the bare value names (tri, isaw, rand, silence, noise, pink, …), which have no ports to declare; ir.rs's *_DEFAULT_* constants (shared with the engine) and DRUM_ONE_SHOT_KNOB, the value a REQUIRED drum knob takes in the knobless one-shot form, which no schema can state; and the two physically-addressed scene slots whose unit is the family's own statement rather than a port's (track_effects.rs::SCENE_PHYSICAL_PORTS, SceneSlot::Engine: reverb/plate.decay names an engine slot with no call port, and tune.speed is a port the call spells as a bare number of milliseconds). Every other omitted optional in a lowering arm reads params::defaults(<builtin>)(<port>) (schema_defaults_match_the_lowerer pins it; a receiver twin's spoken name, defaults("delay"), reaches delayTrack's ports where delay's own schema does not answer), and lower_sim reads its arity off the simulation descriptors.
A name with several readings#
A name with several readings declares them and the rule that picks one; the call site only supplies evidence. One implementation row per reading (the amplitude.rs shape): panTrack installs an insert in two readings and writes a track field in two, so it fails both descriptor_touchpoints gates and lives in builtins/readings.rs; the lowering arm passes the proven shape, the descriptor says which reading and where it writes. Two calling SHAPES use overloads::reading (warp/speed, eq/wavetableOf/ voiceLines): the descriptor says TrackReceiver, ListHead(which) or Arity; the fallback is the primary reading; a ListHead rule is Rule::decisive, or an over-applied call loses its arity sentence to "fits neither". Readings differing only in their reader (concat) are two Execution rows under one schema, and selection counts with params::selection_positional_len (no trailing-record split). overloads::operand is for names resolved at lowering time; a port resolved earlier wants a family-local Operand enum (ModalTable, InstrumentValue, OnsetPattern).
Pattern-transform descriptors#
compiler/src/builtins/pattern_transforms.rs owns fast slow rev ply every sometimesBy degradeBy transpose struct mask off jux superimpose within inside outside firstOf lastOf degrade undegradeBy sometimes often rarely someCyclesBy shuffle scramble humanize palindrome iter iterBack linger bite chunk zoom stut press pressBy hurry rolled squeeze brak inv chop slice arp inScale sampleAdd voiceLead fitScale transposeDiatonic invertVoicing dropVoicing imitate ctrl (design: Pattern Transform Descriptors). sew, stitch and wedge stay outside it on purpose: they are prefix-only merges of two sibling patterns, with no subject to pipe, and belong to the constructor family below. Besides binding and registration, a row declares:
- Subject position in the lowerer's argument order (
FirstorLast). A transform with no fixed port (rev,palindrome,press,rolled,brak,inv,degrade) has one order to write and is declaredLast. ALasttransform with at least one fixed port also has the legacy pattern-last convention, andparams::pattern_calls::getreturns the descriptor's own entry for it —pattern_calls::CALLSlists only the pattern-last calls not yet migrated. - Port kinds — a number (and how a numeric pattern in it is read: per argument window for
fast/slow/every/off/iter/iterBack/linger/chunk, per subject event forply/transpose, per event or from a control-signal shape fordegradeBy/sometimesBy, and squeezed forbite's index, where each index event plays its whole slice inside its own window;within,inside,outside,firstOf,lastOf,undegradeBy,someCyclesBy,shuffle,scramble,zoom,stut,pressBy,hurryandbite's slice count take fixed numbers only), a transform (Pattern a -> Pattern a), or a boolean structure pattern in which a rest,f,falseand a numeric0are false (mask,struct,squeeze). Six further kinds take something that is neither: a mode name from a closed set the port lists (arp "up|down|updown|thumb"), an open name (slice n "amen" ix), a scale tag (inScale (tonality "c dorian"),fitScale,transposeDiatonic), a bare signal source (sampleAdd isaw [lo, hi]), a[lo, hi]range, and a list of whole numbers (dropVoicing [2, 4]). A bare string is read as mini-notation in a number, transform or structure port and as a NAME in the other six; a wrong mode name is a check-time error listing the set. - Payload — a transform is
Pattern a -> Pattern a: the result type is the subject's.PitchedOnly(transpose,arpand the pitch-theory family) rewrites pitches and passes numeric, sample and rest events through unchanged.Valued(sampleAdd) rewrites every sounding value in place, a pitch if the event has one and otherwise its number.Controlled(ctrl) moves each value onto the named control, leaving the rhythm.Structure(inv) reads its subject as a boolean rhythm and carries none of its values — the result holds1in every window the subject left empty.Replaced(slice) carries NOTHING: an index pattern becomes a sample pattern, so neither the payload nor the unit travels. - Reading —
chopandsliceare also sample modifiers (sample "x" |> chop 8), a reading the lowerer selects from the receiver's shape. Their declared argument and result types therefore stay open, and their port obligations are the lowering arms', not the checker's. - Subject planning —
fast,slowandinScaleresolve their own subject: a top-levelfastrides the pattern pair's rate multiplier, andinScalemust set its key before the subject literal is parsed. - Random identity —
None; the shared unsalted event lattice (degradeBy,undegradeBy,sometimesBy,sometimes,often,rarely: redrawn by a regionseed, unaffected by the binding's name — which is whysometimesBy x ftransforms exactly the eventsdegradeBy xdrops); a binding salt (humanize,shuffle,scramble,someCyclesBy: the owning binding's name plus an occurrence index, also redrawn by a regionseed); ordegrade's fixed lattice, which nothing redraws.degradeanddegradeBy 0.5drop different events. - Duration law — what the transform does to a sounding event's duration, the fact a lookbehind (a query's ring bound) is computed from:
Preserved(rev,transpose,iter,iterBack),DividedByFirstPort(fast,ply,hurry),MultipliedByFirstPort(slow),NeverLonger(linger),SubjectOrPort(jux,superimpose,off,every,firstOf,lastOf,within,chunk— the subject's own durations or the port transform's), orUndeclared. Every declaring row is proved against the compiled events (temporal_laws::every_declared_duration_law_holds_on_the_materialized_path). - Structure roles —
struct,maskandsqueezeare declaredLast, so the prefix takes Tidal's order (struct bools p) and the pipe normalizes to it; a call whose subject is boolean-shaped (t/f/x/true/false/1/0steps) while the structure slot holds values (the subject-firststruct p boolsorder, or a pipe from the booleans) is refused at check time with the pipe spelling. - Sections — supplying every fixed port leaves a callable
Pattern a -> Pattern a, for every one of them:thin = degradeBy 0.3,gate = mask !"t ~". - Execution —
ContentPlanscope: ports are read while the compiler plans the content into events, again for each seeded placement, and never while audio runs. Cadences areContentPlan,ArgumentWindowandEventOnset.fast/slow/revalso have asignal-foldimplementation for a control-signal subject (subject prepared in the enclosing graph, factor folded).
A function in a transform port receives the whole subject, exactly as it would outside the port, and the port reads cycle k of the result at the absolute cycle k: every 1 f is f for every transform — every 1 (slow 2) keeps both halves, an inner every 2 rev sees the real cycle index, and palindrome, iter n, brak and chunk n f spread across cycles as they do outside (Pattern Semantics S1).
Pattern-constructor and merge descriptors#
compiler/src/builtins/pattern_constructors.rs owns sew stitch wedge seq par stack arrange wchoose run pure patternOf chord rwalk bouncePat. These build a pattern out of numbers, values, mode strings or several sibling patterns; none has a subject, so none has a pipe form or a section. A row declares:
- Operand kinds — a fixed number; a mode string, which is a name and never mini-notation (
chord "c4" "maj7"); a payload value or list of values, where a string is a sample name (pure "bd"); a pitch literal; a value pattern; a boolean structure pattern in which a rest is false; a list of patterns; or a list of[weight, pattern]pairs. A bare string is read as a pattern only in a pattern or structure operand. - Payload —
runyields whole numbers from zero;chord,rwalkandbouncePatcompute pitches and type asPattern Pitch;pureandpatternOfcarry their values' payload; the merges and list constructors (sew stitch wedge seq par stack arrange wchoose) require every sounding value pattern to share one payload and carry it — a pinned unit on any sibling is adopted by bare ones (sew b !"2 4"Hz !"3"is aPattern Hz), and a silent sibling constrains nothing. - Period — one cycle (
seq run pure patternOf chord bouncePat); the common period of every pattern operand, a boolean's included (sew stitch wedge par stack); the sum of the section lengths or rounded weights (arrange,wchoose); or a named operand (rwalk'scycles). - Random identity — only
rwalkdraws: a binding salt, so renaming its binding or seeding its placement redraws the walk and moving it in the source does not.wchoosedraws nothing: each pattern plays forround(weight)consecutive cycles in the order written. - Execution — every operand is
ContentPlan: read while the content is planned, again for each seeded placement, never while audio runs.
A prefix of the required operands is an ordinary function of the rest (weave = sew !"t ~", then weave ride shaker); a complete call is a pattern.
Sampler descriptors — sources and playback modifiers#
compiler/src/builtins/samplers.rs owns sample sampleAt pitched speed loop reverse trim marks chopAt sliceAt snap stretch pitchShift warpLock bounceOf sampleOf bouncesOf take fades. Every one of them resolves to the SAME buffer-playback story, which is why they are one family: a source names a bank entry and builds the playback node, a modifier writes one of that node's own fields rather than wrapping it — so sample "x" |> pitched ~C3 |> speed 0.5 |> trim 0.1 0.9 |> loop is still one node in the IR — and a bake renders a buffer while the module compiles and binds its bank handle, so sample <binding> plays it and every modifier above works on it. A modifier's receiver is its first positional slot, so its pipe form and its prefix form are the same call and there is no second convention to learn.
A row declares the receiver as a port of its own, the port kinds and clamps, the registry arrow port for port (widening one would change accepted calls and the units the lowerer reads out of each argument), and one Execution per port, taken from what the engine does with the field:
| Reading | Ports | Why |
|---|---|---|
| Folded when the voice graph is built | name, names, root, start, end, boundaries, n, ratio, cents | the value becomes a field of the Sample / Stretch node; the engine never sees the expression |
| Latched once per note | chopAt/sliceAt's index, sampleAt's index | a hard switch with no crossfade; the sounding voice keeps what it latched |
| Read every active sample | speed's rate, and every modifier's receiver | so a brake/spinUp rate envelope is audible |
| Folded while the module compiles | every port of bounceOf sampleOf bouncesOf take fades | the render happens at load; nothing survives as a knob |
The last row is the reason execution is declared and never projected from the liveness flag: speed's rate and chopAt's index are both Live::Signal on the surface and are read on completely different clocks.
In a bake's duration port a BARE NUMBER IS SECONDS (bounceOf 4c is four cycles, bounceOf 4 is four seconds), which the port's own sentence now says.
warp is deliberately NOT here. One name carries two readings with different arities and different receivers — the unary sample modifier (sample "x" |> warp) and the two-argument per-event track lane (|> warp !"1 0", Control patterns for individual events) — and one schema cannot state both without inventing a calling convention for the track lane inside a sampler row. chop and slice have the mirror-image split and live in pattern_transforms.rs under Reading::AlsoSample.
Adding a descriptor-owned builtin#
compiler/src/builtins.rs— aBuiltinKindvariant, and for a track insert itstrack_rolearm.- The family module under
compiler/src/builtins/— one entry: schema, receiver, and anExecutionper implementation and port (receiver first for controls and amplitude; parameters only for track inserts, whose receiver isTRACK_EXECUTION). A control also gets aGraphImplementationvariant, itsimplementations()arm and its family predicates. A port kind is not always aSlot: where the accepted set is a named ELABORATOR or a structural reader, the entry declares anOperandbeside the slot and cites that one reader —modal.rs'sOperand::ModalTable(lower/modal.rs::fold_modes) andtrack_sources.rs'sOperand::InstrumentValue(lower/instruments.rs::elaborate, the one path by which a literal, named, local, imported, returned or container-held patch reachesplay). The registryTystays open in that case, and the reader keeps the proof. - The handler — a
lower_controlarm (lower/expr/controls.rs; the match is exhaustive) or alower_track_expr_innerarm (lower/track.rs; it is not exhaustive, sodescriptor_touchpointslowers every insert). Type obligations for a control live insemantics/controls.rs. compiler/assets/parameter-audit.json— acall/<name>/overload/<impl>/<port>row per port whose profile execution matches the Rust declaration; thenpython3 scripts/parameter-inventory.py. One port does not always have one execution: when a port's cadence depends on a SIBLING port being live (string,resonate), or a name has a second reading with its own receiver and arity (warp,speed), each case is its own implementation row with its own executions. A NEW audit profile must also be classified inir::ParamCadence::from_profileand incompiler/tests/timeline.rs::NOT_A_DESTINATION, or it silently defaults to a per-sample automation cadence. The registry PADS a legacy-table builtin's arrow with open slots up toschema.max_args(), and only for names still in that table: a descriptor must spell one port per slot, optional trailing ones included, ornamed_options' arity gate fails while golden IR stays green.compiler/assets/docs-data.json— the catalogue entry; refresh options and mirrors with the command below.UPDATE_SNAPSHOTS=1 cargo test -p volta-compiler --test builtins_manifest --test builtin_modulesfordocs/reference/builtins.jsonandtests/snapshots/builtin_modules.txt.- Tests —
control_descriptors::inputs(exhaustive) for a control; a contract case for anything else. A scene-addressable unit port goes onSCENE_PHYSICAL_PORTSor on the pinned plain list intrack_effect_contracts.
A new family module additionally joins builtins::descriptor_registry, params::schema, capability::argument/argument_profile, compiler/src/docs/help.rs, compiler/examples/parameter_inventory.rs and the walk in tests/descriptor_execution.rs. Engine work, when the builtin needs a new node, is Contribute a language feature.
tests/descriptor_touchpoints.rs fails when steps 1–5 disagree: a descriptor not registered or overridden by a hand-written registration, without a schema, presentable contract or catalogue entry (or with different port names there), an insert descriptor with no handler arm or track role, or an insert handler with no descriptor.
For a remaining table-backed effect, update the lowering arm and its row in table.rs; the test fails until the two agree. Then UPDATE_SNAPSHOTS=1 cargo test -p volta-compiler --test docs_catalogue catalogue_matches_the_parameter_schema && python3 compiler/scripts/sync-docs-options.py && node runtime/scripts/gen-docs-data-ts.mjs && node runtime/scripts/gen-docs-data-json.mjs refreshes the catalogue and its mirrors.