Restricted TypeScript: what you can't write in a DSP kernel, and why
Jul 2, 2026
If you are writing your first Codex Music kernel, you are going to hit a compiler error that tells you a perfectly normal piece of TypeScript is not allowed. This post is so that when you do, you know whether it is a bug worth filing or the system working as designed.
The short version: a DSP kernel is not a program that runs fast. It is a program that runs in a bounded amount of time, every single block, forever. Most of the restrictions follow from that one requirement.
The audio thread has a deadline
At 48 kHz with a 128-sample block, your process() function is called every 2.67 milliseconds and must return before the next call. Not on average. Every time. Miss it once and the output buffer is not ready, and what the listener hears is a click.
Anything that can occasionally take much longer than usual is therefore disqualified, even if it is fast on average. That is the filter that most of the rules pass through.
No allocation
You cannot allocate on the audio thread. No object literals, no array creation, no string concatenation, no closures that capture, no new of anything.
Allocation is unbounded because it may trigger garbage collection, and garbage collection is a pause of unpredictable length at an unpredictable time. Your synth sounds perfect for nine minutes and clicks in the tenth, which is the worst possible bug: nondeterministic, unreproducible, and heard by your user before you.
State lives in preallocated buffers created during initialization, when there is no deadline. In practice this means helpers take values as arguments and write into buffers rather than returning fresh objects, which reads a little more verbosely and runs in constant time.
No exceptions
throw and try are not available in kernel code. Exception machinery has costs and control flow that differ across the two compile targets, and a kernel that can bail out mid-block has no defined output for that block anyway — what would you emit, silence?
The kernel's contract is total: for any input, it produces output. Invalid parameter values get clamped at the boundary, not rejected. A divisor that could be zero gets guarded with a comparison. Validation happens in the UI and the schema layer, above the audio thread, where failing is a reasonable thing to do.
No closures on the audio thread
Closures capture an environment, and creating one allocates. They also make it much harder for the compiler to prove what a piece of code touches, which matters when the same source has to become native code with predictable register and memory behavior.
Function references passed around dynamically are out for the same reason. Kernel dispatch is static: the compiler knows every call site.
No unbounded loops, no dynamic length
Loops must have bounds the compiler can establish. while (!done) where done depends on the signal is rejected. Arrays are fixed-length, sized at initialization. Recursion with data-dependent depth is out.
This one catches people writing anything iterative — a solver, an adaptive filter, a search. The workaround is the same one real-time DSP has always used: pick a fixed iteration count that is good enough, and take the same amount of time every block whether you converged or not.
The subtler one: no platform math you have not pinned
You can call the math you need, but some functions are provided by the kernel's own library rather than the platform's. This is not arbitrary. sin, exp, and friends are allowed to differ in the last bit between implementations, and two builds whose last bits differ do not produce identical PCM. Since sample-identical output across web and native is the property this whole engine exists to guarantee, the transcendental functions are pinned in the kernel rather than inherited from whatever libm happens to be linked.
Denormal handling is pinned for the same reason. It used to differ between targets, which cost us both a parity failure and about eight percent of CPU on long reverb tails.
What you still get
All of the above sounds restrictive, and it is, but the thing left over is more comfortable than C++. You have real types, generics in the parts of the code that run at build time, a language server that knows what you meant, and error messages that name the rule you broke and the line you broke it on. The parameter schema derives the type of your parameter object, so a mistyped parameter name is a compile error rather than a knob that does nothing.
The rules are also stable. They are not a moving target we tighten each release — they are the fixed consequence of running in a place where being late is the same as being wrong.