swc-loop-protect
v0.1.0
Published
An SWC plugin that protects against infinite loops and infinite recursion in dynamically evaluated JavaScript.
Readme
swc-loop-protect
An SWC plugin that protects against infinite loops and infinite recursion in dynamically evaluated JavaScript.
Inspired by @freecodecamp/loop-protect (a Babel plugin), rewritten from scratch in Rust as a native SWC/WASM plugin.
This does not solve the halting problem — it rewrites JavaScript at compile time, inserting time-based guards into loops and depth-based guards into functions.
Loop Protection
All loop types — while, for, do-while, for...in, and for...of — are wrapped with a Date.now() check that breaks out of the loop if it runs longer than a configurable timeout.
Input:
while (true) {
doSomething();
}
console.log('All finished');Output:
var _LP1 = Date.now();
while (true) {
if (Date.now() - _LP1 > 100) break;
doSomething();
}
console.log('All finished');The loop is cleanly exited and any code after it continues to execute.
Suspending loops are skipped
Loops containing await or yield are skipped — they suspend execution, so a wall-clock timeout would fire incorrectly. for await...of loops are also skipped. An await or yield inside a nested function does not cause the outer loop to be skipped.
Iteration-based checking
For code with large but finite loops, enable the iterations option. Date.now() is only checked once every N passes, reducing overhead:
var _LPC1 = 1;
var _LP1 = Date.now();
while (true) {
if (_LPC1++ % 100 === 0 && Date.now() - _LP1 > 100) break;
doSomething();
}Recursion Protection
When enabled, every function and arrow expression is wrapped with a global call-depth counter on globalThis._RD. This catches both direct and indirect recursion across files.
Input:
function foo() {
foo();
}Output:
function foo() {
globalThis._RD = (globalThis._RD || 0) + 1;
if (globalThis._RD > 1000) {
globalThis._RD = 0;
throw new Error("Maximum recursion depth exceeded");
}
try {
foo();
} finally {
globalThis._RD--;
}
}Key details:
- The counter lives on
globalThis._RD, so it is shared across all files/modules. try/finallyensures the counter is decremented even if the function throws.- The counter is reset to 0 before throwing, so the error can be caught without leaving the counter in a broken state.
- Expression-body arrows (
() => expr) are automatically converted to block bodies. - Disabled by default — set
maxRecursionDepthin the plugin config to enable.
Configuration
The plugin accepts a JSON config object:
| Option | Type | Default | Description |
| ------------------- | ---------------- | ------- | ------------------------------------------------------------- |
| timeout | number | 100 | Max loop runtime in milliseconds before break |
| iterations | number \| null | null | Only check Date.now() every N loop iterations |
| maxRecursionDepth | number \| null | null | Max call depth before throwing (enables recursion protection) |
Example SWC config (.swcrc)
{
"jsc": {
"experimental": {
"plugins": [
["swc-loop-protect", { "timeout": 100, "maxRecursionDepth": 1000 }]
]
}
}
}Building
# Native (for testing)
cargo test
# WASM plugin binary
cargo build-wasip1 --releaseThe WASM binary is output to target/wasm32-wasip1/release/swc_loop_protect.wasm.
License
MIT — based on the original loop-protect by Remy Sharp and contributors.
