Skip to content

feat: destructuring, generators, labeled statements, Symbol, nonBlocking mode, and delayed synchronous results#52

Merged
nyariv merged 20 commits into
mainfrom
feat/syntax-support-3
Apr 10, 2026
Merged

feat: destructuring, generators, labeled statements, Symbol, nonBlocking mode, and delayed synchronous results#52
nyariv merged 20 commits into
mainfrom
feat/syntax-support-3

Conversation

@nyariv
Copy link
Copy Markdown
Owner

@nyariv nyariv commented Apr 5, 2026

✨ 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:

  • Array destructuring: const [a, b, c] = arr
  • Object destructuring: const { x, y } = obj
  • Nested patterns: const { a: { b } } = obj
  • Rest elements: const [head, ...tail] = arr and const { a, ...rest } = obj
  • Default values: const { x = 1 } = {} and function fn(a = 0) {}
  • Renaming: const { a: myA } = { a: 1 }
  • Computed keys: const { [key]: val } = obj
  • Destructuring in for-of / for-in loop variables
  • Destructuring in function parameters, including with defaults: function fn({ a = 1, b = 2 } = {}) {}

How it works: Destructuring patterns are expanded at parse time into a series of temporary internal variable 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(), and for-await-of.

What's supported:

  • function* gen() { yield 1; }
  • yield* delegation to any iterable or iterator
  • yield as an expression: const x = yield computeValue()
  • async function* with yield await promise
  • for await (const item of asyncIterable) {}
  • Iterator protocol: .next(value), .return(value), .throw(err)
  • SandboxGeneratorFunction and SandboxAsyncGeneratorFunction constructors exposed on the eval context

How 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 recursive function* that mirrors the executor's statement/loop/try/if dispatch with yield* recursion. This gives true lazy evaluation and correct next(value) injection without pausing the host runtime. A syncYieldPauseSentinel symbol is used for internal control flow signaling, and asIterableIterator / asAsyncIterableIterator helpers normalize both sync and async iterables for yield* delegation.

New test files: generators.data.ts, generators.spec.ts


🏷️ Labeled Statements (b6be7ba)

Full support for labeled break and continue targeting outer loops or blocks.

What's supported:

  • outer: for (...) { inner: for (...) { break outer; } }
  • continue label to skip to the next iteration of a specific loop
  • break label on non-loop labeled blocks

How it works: ExecReturn now carries a structured ControlFlowSignal ({ type: 'break' | 'continue', label?: string }) instead of bare breakLoop / continueLoop booleans. A ControlFlowTarget system 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, ControlFlowAction

New 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 Symbol implementation 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 symbol
  • Symbol.for(key) — per-sandbox global registry (isolated from host Symbol.for)
  • Symbol.keyFor(sym) — reverse lookup within the sandbox registry
  • Well-known symbols via symbolWhitelist: Symbol.iterator, Symbol.asyncIterator, Symbol.toPrimitive, Symbol.toStringTag, and more
  • Throws TypeError if called with new (matches spec)

How it works: getSandboxSymbolCtor lazily constructs a SandboxSymbol function per IExecContext, backed by a SandboxSymbolContext that holds a Map<string, symbol> registry and reverse map. Whitelisted well-known symbols are copied from the host Symbol via Object.defineProperty. A default SAFE_SYMBOLS static getter on SandboxExec exposes the standard well-known symbols.

New option: symbolWhitelist?: ISymbolWhitelist on IOptionParams

New test file: symbol.spec.ts


🚫 nonBlocking Mode Enforcement (f8d52df)

When nonBlocking: true is set in options, calling Sandbox.compile() now immediately throws a SandboxCapabilityError directing the caller to use compileAsync() 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 typed HaltContext union ('error' | 'manual' | 'yield') instead of an untyped optional object, making halt reasons explicit and type-safe
  • subscribeHalt callback signature updated to (context: HaltContext) => void
  • sanitizeProp short-circuits early for primitive values, avoiding unnecessary object lookups
  • checkHaltExpectedTicks refactored: duplicated op-dispatch logic extracted into a shared performOp helper, eliminating ~40 lines of copy-pasted code
  • Extensive new tests in ticksQuotaHalt.spec.ts covering 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 use async/await.

What's supported:

  • A host function wraps its body in delaySynchronousResult(async () => { ... }) and returns it synchronously
  • The sandbox executor detects the DelayedSynchronousResult wrapper and awaits the inner promise before continuing execution
  • Sandbox code calls the function as if it were synchronous — no await needed at the call site
  • Rejections (thrown errors inside the async callback) propagate correctly and can be caught with try/catch inside the sandbox
  • Works for both plain function calls and method calls on objects

How it works: After a call resolves to a DelayedSynchronousResult, the executor calls Promise.resolve(ret.result).then(done, done) instead of invoking done immediately. This suspends the execution tree at that call site until the promise settles, analogous to how nonBlocking mode inserts setTimeout(0) yield points. No changes to sandbox code or parser are required — the feature is entirely host-side.

New export: delaySynchronousResult(cb: () => unknown): DelayedSynchronousResult

New test file: test/delaySynchronousResult.spec.ts


🐛 Bug Fixes

Generator infrastructure (011b63b, c5d67ed)

  • True lazy generators: Replaced the initial eager-execution generator approach with executeGenBody, a native function* that lazily executes the body. This unblocks next(value) injection, correct return() / throw() protocol support, and proper yield as an expression value.
  • yield as expression: const x = yield 1 now works — the injected value from gen.next(value) is returned from the yield expression.
  • asIterableIterator / asAsyncIterableIterator: Normalizes yield* targets — handles objects with [Symbol.iterator], raw iterators, and async iterables uniformly, with bound protocol methods.
  • SandboxGeneratorFunction / SandboxAsyncGeneratorFunction exposed in the eval context, allowing sandbox code to construct generator functions dynamically.
  • Security tests: Added cases verifying sandboxed generators cannot escape the sandbox.
  • Syntax error tests: Added cases for syntax errors that were not previously covered.

Interval cleanup (c5d67ed)

  • sandboxedClearInterval now calls both clearInterval and clearTimeout on the stored handle. Previously, when the sandbox resumed from a halted state the interval was rescheduled via setTimeout, but only clearInterval was called on cleanup — leaving the timeout handle dangling.
  • sandboxedSetInterval resume path now updates handlObj.handle after rescheduling with setTimeout, so subsequent halt/resume cycles track the correct handle.

Computed property names (d5c8869)

  • Object literals now support computed property keys: { [expr]: value } and { [expr]() {} }.
  • KeyVal.key widened from string to PropertyKey to support symbol keys.
  • Removed "Computed property names" from the known-limitations list in TODO.md.

Parser & eval fixes (d5c8869, c5d67ed)

  • LispType.Block replaced with LispType.InternalBlock in the eval completion-value wrapper, preventing incorrect block scoping when computing the completion value of the last statement.
  • Function/inlineFunction regex updated to stop at ( rather than consuming the full parameter list, enabling destructured and default parameters to be processed by the expansion layer.
  • Trailing-comma support in function parameters fixed as a side-effect of the looser regex.
  • clearTimeout added alongside clearInterval in the halt subscription of sandboxedSetInterval.

🏗️ Infrastructure

Area Change
ExecReturn Carries ControlFlowSignal instead of breakLoop/continueLoop booleans; backward-compat getters preserved
Parser node types Added Labeled, InternalCode, Internal, LoopAction, Yield, YieldDelegate
HaltContext Typed union replacing untyped optional object for halt callbacks
IOptions Added symbolWhitelist, nonBlocking; prototypeWhitelist widened to Map<Function, Set<PropertyKey>>
IContext Added sandboxSymbols: SandboxSymbolContext
Test coverage 1100 → 1400+ tests across destructuring, defaults, generators, labeled statements, symbols, loops, error handling, security

@nyariv nyariv changed the title [codex] Add syntax support for destructuring, labels, and generators feat: Add syntax support for destructuring, labels, and generators Apr 5, 2026
@nyariv nyariv marked this pull request as ready for review April 5, 2026 22:46
@nyariv nyariv changed the title feat: Add syntax support for destructuring, labels, and generators feat: destructuring, generators, labeled statements, Symbol, and nonBlocking mode Apr 7, 2026
@nyariv nyariv changed the title feat: destructuring, generators, labeled statements, Symbol, and nonBlocking mode feat: destructuring, generators, labeled statements, Symbol, nonBlocking mode, and delayed synchronous results Apr 8, 2026
@nyariv
Copy link
Copy Markdown
Owner Author

nyariv commented Apr 10, 2026

Re-triggering CI after lockfile fix

nyariv and others added 4 commits April 10, 2026 15:49
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>
@nyariv nyariv merged commit 524be7a into main Apr 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant