Skip to content

vm, compiler: scope with objects to the frame and block that pushed them - #719

Merged
nooga merged 1 commit into
mainfrom
fix/with-stack-frame-scope
Oct 17, 2026
Merged

nooga merged 1 commit into
mainfrom
fix/with-stack-frame-scope

Conversation

@nooga

@nooga nooga commented Oct 17, 2026

Copy link
Copy Markdown
Owner

The with-object stack was VM-global and was only popped when a with body exited normally. That caused several bugs:

  • objects leaked past return, throw, break, continue, catch and generator suspension
  • callees saw their callers' with objects (dynamic scoping)
  • with (primitive) skipped the push but still popped the enclosing with's object

Fix

  • Each entry records the frame depth that pushed it. A frame sees only its own entries plus those its closure captured lexically. Entries of exited frames are dropped before the next frame push (with_stack.go).
  • New OpTruncateWith N drops a frame's entries at loop break landings, before continue, and at catch/finally entry. It is emitted only in functions that contain a with.
  • Generators and async functions save their frame's entries on suspend and re-push them on resume.
  • Primitives are pushed as their ToObject wrappers, so push and pop stay balanced.

Known gap (marked FIXME): a continue routed through a finally leaves the with objects between the try and the loop pushed until the loop exits.

Testing

  • New smoke test tests/scripts/with_stack_frame_scope.ts. TestScripts is green.
  • test262 (language + built-ins, vs main): +2 in language (S13.2.2_A19_T6/T8), no regressions. The built-ins diff showed +4 URI tests; these are timing flakes.

🤖 Generated with Claude Code

The with-object stack was VM-global and only popped on normal exit from a
with body, so objects leaked past return, throw, break, continue, catch
and generator suspension, callees saw their callers' with objects
(dynamic scoping), and `with (primitive)` skipped the push but still
popped the enclosing with's object.

Each entry now records the frame depth that pushed it. A frame sees only
its own entries plus those its closure captured lexically; entries of
exited frames are dropped before the next frame push. A new
OpTruncateWith drops a frame's entries at loop break landings, before
continue, and at catch/finally entry. Generators and async functions
save their frame's entries on suspend and re-push them on resume.
Primitives are pushed as their ToObject wrappers so push and pop stay
balanced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nooga
nooga force-pushed the fix/with-stack-frame-scope branch from 65b4b3e to d4a092e Compare October 17, 2026 21:10
@nooga
nooga merged commit 9d1b0e1 into main Oct 17, 2026
8 checks 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