Skip to content

vm, compiler: give each class evaluation its own private names - #722

Merged
nooga merged 1 commit into
mainfrom
fix/560-private-names
Oct 17, 2026
Merged

nooga merged 1 commit into
mainfrom
fix/560-private-names

Conversation

@nooga

@nooga nooga commented Oct 17, 2026

Copy link
Copy Markdown
Owner

Part of #560 (class private-name cluster).

Problems

  • Private names compiled to "K:name" keys per class declaration, so every evaluation of the same class shared them. Instances made by one evaluation passed another evaluation's brand checks: c1.access(c2) returned instead of throwing a TypeError.
  • Field initializers ran before private methods and accessors were installed, so a = this.#m() threw.
  • Private fields could not be added to a Proxy returned from a base constructor.

Fix

  • Per-evaluation private names. Each class evaluation pushes a PrivateEnv node for K (new OpPushPrivateEnv / OpPopPrivateEnv).
    • Closures created inside the class body capture the node chain (ClosureObject.PrivateEnv).
    • The class-evaluation code itself runs in the enclosing frame and sees the node through a stack tagged with frame depth. That stack is trimmed at frame push, like the with-object stack.
    • Private keys resolve through the innermost node for their K. The first evaluation keeps the compiled key, so a class evaluated once costs nothing extra. Later evaluations get a suffixed key, cached per node (pkg/vm/private_env.go).
  • Initialization order. The constructor's injected initializers now add private methods and accessors before any field initializer, as InitializeInstanceElements requires. Accessor emission order is now sorted, so it is deterministic.
  • Proxies. ProxyObject gets its own private-element store, which the private ops use without touching the handler.

Testing

  • New smoke test tests/scripts/class_private_names_per_evaluation.ts. TestScripts is green.
  • test262 (language + built-ins, vs main): +20, no regressions.

Five private-related class tests still fail for separate reasons:

  • decorator-member-expr-private-identifier
  • privatefieldput-primitive-receiver
  • privatefieldset-evaluation-order-1
  • privatefieldset-typeerror-10
  • super-access-inside-a-private-method

🤖 Generated with Claude Code

Private names compiled to keys "K:name" per class declaration K, so every
evaluation of one class shared them: instances of a second evaluation
passed the first one's brand checks.

Each class evaluation now runs with a PrivateEnv node for K, pushed by a
new OpPushPrivateEnv and popped by OpPopPrivateEnv. Closures created
inside the class body capture the node chain, and the class evaluation
code sees it through a frame-depth-tagged stack. A private key resolves
through the innermost node for its K. The first evaluation keeps the
compiled key; later ones get a key suffixed with their evaluation number.

Also:
- instances get their private methods and accessors before any field
  initializer runs, as InitializeInstanceElements requires
- a Proxy returned from a base constructor stores the derived class's
  private elements on itself, without consulting its traps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nooga
nooga merged commit f1e5306 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