feat: destructuring, generators, labeled statements, Symbol, nonBlocking mode, and delayed synchronous results#52
Merged
Merged
Conversation
Owner
Author
|
Re-triggering CI after lockfile fix |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@emnapi/core and @emnapi/runtime are required by @rolldown/binding-wasm32-wasi (an optional dep of rolldown) but were not resolved on macOS, causing npm ci to fail on Linux CI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
rolldown has cross-platform optional deps (@emnapi/core, @emnapi/runtime) that npm cannot fully resolve into the lockfile on macOS, causing npm ci to fail on Linux runners. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
✨ feat: destructuring, generators, labeled statements, Symbol, nonBlocking mode, and delayed synchronous results
Overview
This branch adds several major ECMAScript language features to SandboxJS, along with infrastructure improvements and bug fixes. Test coverage grew from ~1100 to ~1400+ tests; statement coverage improved to ~97% and branch coverage to ~92%.
🧩 New Features
🔀 Destructuring & Default Parameters (
802134e)Full support for variable and argument destructuring patterns, expanding the parser's function-parameter regex to accept arbitrary patterns instead of simple identifier lists.
What's supported:
const [a, b, c] = arrconst { x, y } = objconst { a: { b } } = objconst [head, ...tail] = arrandconst { a, ...rest } = objconst { x = 1 } = {}andfunction fn(a = 0) {}const { a: myA } = { a: 1 }const { [key]: val } = objfor-of/for-inloop variablesfunction fn({ a = 1, b = 2 } = {}) {}How it works: Destructuring patterns are expanded at parse time into a series of temporary
internalvariable assignments (a new internal-only keyword that desugars to scoped bindings). This avoids any executor changes and keeps the approach purely syntactic. A depth-aware comma splitter (splitByCommasDestructure) and top-level search helpers handle nested brackets correctly.New test files:
destructuring.data.ts,defaults.data.ts⚙️ Generator Functions (
1e90c6c,011b63b)Full support for synchronous and asynchronous generator functions, including
yield,yield*,next(value)injection,return(),throw(), andfor-await-of.What's supported:
function* gen() { yield 1; }yield*delegation to any iterable or iteratoryieldas an expression:const x = yield computeValue()async function*withyield await promisefor await (const item of asyncIterable) {}.next(value),.return(value),.throw(err)SandboxGeneratorFunctionandSandboxAsyncGeneratorFunctionconstructors exposed on the eval contextHow it works: The initial implementation (1e90c6c) eagerly ran the generator body and collected yielded values. The follow-up (011b63b) replaced this with a lazy native-generator approach (
executeGenBody) — a recursivefunction*that mirrors the executor's statement/loop/try/if dispatch withyield*recursion. This gives true lazy evaluation and correctnext(value)injection without pausing the host runtime. AsyncYieldPauseSentinelsymbol is used for internal control flow signaling, andasIterableIterator/asAsyncIterableIteratorhelpers normalize both sync and async iterables foryield*delegation.New test files:
generators.data.ts,generators.spec.ts🏷️ Labeled Statements (
b6be7ba)Full support for labeled
breakandcontinuetargeting outer loops or blocks.What's supported:
outer: for (...) { inner: for (...) { break outer; } }continue labelto skip to the next iteration of a specific loopbreak labelon non-loop labeled blocksHow it works:
ExecReturnnow carries a structuredControlFlowSignal({ type: 'break' | 'continue', label?: string }) instead of barebreakLoop/continueLoopbooleans. AControlFlowTargetsystem tracks which active statement each signal should resolve against, with separate flags for labeled vs. unlabeled break/continue and loop vs. switch semantics. The old boolean getters are kept as computed properties for compatibility.New types:
ControlFlowSignal,ControlFlowTarget,ControlFlowTargets,ControlFlowActionNew parser node types:
Labeled,LoopAction(replaces raw break/continue strings)New test cases in:
loops.data.ts,error-handling.data.ts🔣 SandboxSymbol (
4968190)A sandboxed
Symbolimplementation with its own per-context registry, keeping sandboxed code from leaking symbols into the host environment.What's supported:
Symbol(description)— creates a new unique symbolSymbol.for(key)— per-sandbox global registry (isolated from hostSymbol.for)Symbol.keyFor(sym)— reverse lookup within the sandbox registrysymbolWhitelist:Symbol.iterator,Symbol.asyncIterator,Symbol.toPrimitive,Symbol.toStringTag, and moreTypeErrorif called withnew(matches spec)How it works:
getSandboxSymbolCtorlazily constructs aSandboxSymbolfunction perIExecContext, backed by aSandboxSymbolContextthat holds aMap<string, symbol>registry and reverse map. Whitelisted well-known symbols are copied from the hostSymbolviaObject.defineProperty. A defaultSAFE_SYMBOLSstatic getter onSandboxExecexposes the standard well-known symbols.New option:
symbolWhitelist?: ISymbolWhitelistonIOptionParamsNew test file:
symbol.spec.ts🚫
nonBlockingMode Enforcement (f8d52df)When
nonBlocking: trueis set in options, callingSandbox.compile()now immediately throws aSandboxCapabilityErrordirecting the caller to usecompileAsync()instead, and will cause synchronous code to have intermittent setTimeout(0) breaks, making it non-blocking.Additional changes in this commit:
haltExecution()now accepts a typedHaltContextunion ('error' | 'manual' | 'yield') instead of an untyped optional object, making halt reasons explicit and type-safesubscribeHaltcallback signature updated to(context: HaltContext) => voidsanitizePropshort-circuits early for primitive values, avoiding unnecessary object lookupscheckHaltExpectedTicksrefactored: duplicated op-dispatch logic extracted into a sharedperformOphelper, eliminating ~40 lines of copy-pasted codeticksQuotaHalt.spec.tscovering quota, halt, resume, and nonBlocking interactions⏳
delaySynchronousResult— Async-backed synchronous host functions (a32183c)Allows host-provided functions to deliver their result asynchronously (e.g. after a
setTimeout) without requiring the sandbox code to useasync/await.What's supported:
delaySynchronousResult(async () => { ... })and returns it synchronouslyDelayedSynchronousResultwrapper and awaits the inner promise before continuing executionawaitneeded at the call sitetry/catchinside the sandboxHow it works: After a call resolves to a
DelayedSynchronousResult, the executor callsPromise.resolve(ret.result).then(done, done)instead of invokingdoneimmediately. This suspends the execution tree at that call site until the promise settles, analogous to hownonBlockingmode insertssetTimeout(0)yield points. No changes to sandbox code or parser are required — the feature is entirely host-side.New export:
delaySynchronousResult(cb: () => unknown): DelayedSynchronousResultNew test file:
test/delaySynchronousResult.spec.ts🐛 Bug Fixes
Generator infrastructure (
011b63b,c5d67ed)executeGenBody, a nativefunction*that lazily executes the body. This unblocksnext(value)injection, correctreturn()/throw()protocol support, and properyieldas an expression value.yieldas expression:const x = yield 1now works — the injected value fromgen.next(value)is returned from theyieldexpression.asIterableIterator/asAsyncIterableIterator: Normalizesyield*targets — handles objects with[Symbol.iterator], raw iterators, and async iterables uniformly, with bound protocol methods.SandboxGeneratorFunction/SandboxAsyncGeneratorFunctionexposed in the eval context, allowing sandbox code to construct generator functions dynamically.Interval cleanup (
c5d67ed)sandboxedClearIntervalnow calls bothclearIntervalandclearTimeouton the stored handle. Previously, when the sandbox resumed from a halted state the interval was rescheduled viasetTimeout, but onlyclearIntervalwas called on cleanup — leaving the timeout handle dangling.sandboxedSetIntervalresume path now updateshandlObj.handleafter rescheduling withsetTimeout, so subsequent halt/resume cycles track the correct handle.Computed property names (
d5c8869){ [expr]: value }and{ [expr]() {} }.KeyVal.keywidened fromstringtoPropertyKeyto support symbol keys.TODO.md.Parser & eval fixes (
d5c8869,c5d67ed)LispType.Blockreplaced withLispType.InternalBlockin the eval completion-value wrapper, preventing incorrect block scoping when computing the completion value of the last statement.(rather than consuming the full parameter list, enabling destructured and default parameters to be processed by the expansion layer.clearTimeoutadded alongsideclearIntervalin the halt subscription ofsandboxedSetInterval.🏗️ Infrastructure
ExecReturnControlFlowSignalinstead ofbreakLoop/continueLoopbooleans; backward-compat getters preservedLabeled,InternalCode,Internal,LoopAction,Yield,YieldDelegateHaltContextIOptionssymbolWhitelist,nonBlocking;prototypeWhitelistwidened toMap<Function, Set<PropertyKey>>IContextsandboxSymbols: SandboxSymbolContext