Compile {{this.#field}} via runtime accessor injection#21376
Open
NullVoxPopuli-ai-agent wants to merge 1 commit into
Open
Conversation
Glimmer's path walker hits `instance['#foo']` for private-field tail
segments, which never reaches a real private slot — JS private fields are
only accessible through the lexically-scoped `obj.#foo` syntax. Earlier
attempts at AST-rewriting the template into a synthetic helper invocation
duplicated work the path walker already does and pushed the JavaScript
private-field semantics into a separate compile-time channel; this
replaces that with a single seam at the property walker.
Plumbing:
- New WeakMap registry in `@ember/-internals/metal/lib/private_field_reader`.
A class registers a single `(instance, fieldName) => unknown` reader; the
reader closes over the class's lexical scope, so `__inst.#name` inside it
is bound at parse time to the class's private names. Lookup walks the
prototype chain, which is correct for subclass instances since a parent
class's reader can read its own private fields on subclass instances.
- `_getProp` short-circuits when the key starts with `#`: it grabs the
registered reader for the receiver's class and delegates. This is the
only seam that needed teaching; nothing else in the runtime changes.
- Lightweight `collect-private-fields` AST plugin (no rewrite). It walks
PathExpressions and writes each `#`-prefixed tail segment into
`meta.privateFields`. The host (`template()`) wires that bag up before
precompile and consumes the names afterwards.
- After precompile, if any private fields were referenced, `template()`
asks the user's `eval` to compile a single switch-based reader function.
Because `eval` is invoked from inside the class's `static {}` block, the
`#name` syntax in the reader's body parses against the class's private
slots; the resulting closure is registered against the component class.
- Glimmer-side AST formalization: `@glimmer/syntax` exports
`isPrivateFieldSegment` / `privateFieldName` so consumers don't have to
string-match. The `tail: string[]` shape is preserved (every existing
AST consumer keeps working).
The explicit `scope` form still does not support private fields — its
scope arrow is evaluated outside the class body, so there's no way to
bind `#name` in it. The implicit (`eval`) form covers gjs/`<template>`,
which is what RFC 0921 / content-tag emit.
Coverage:
- Three integration tests in `runtime-template-compiler-implicit-test.ts`:
single field, multiple fields piped through a helper, and a base-class
reader read on a subclass instance.
- Two unit tests in `template_test.ts`: registry registration and the
no-eval error path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1605ad8 to
b0ce1e0
Compare
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.
Summary
Closes the gap on #21375 — templates that read a class's private field
(
{{this.#foo}}) used to render empty because Glimmer's path walker(
_getProp) triedinstance['#foo'], which never reaches a realprivate slot in JS.
This change teaches the property walker about private fields directly,
so the existing path-resolution machinery handles
{{this.#foo}}end-to-end:Per-class reader registry in
@ember/-internals/metal. A classregisters a single
(instance, fieldName) => unknownreader; thereader is constructed inside the class body so its
__inst.#namesyntax is bound at parse time to the class's private slots, and its
closure preserves that access for any future instance handed to it.
Lookup walks the receiver's prototype chain — correct for subclasses
since a parent class's reader can read its own private fields on
subclass instances.
_getPropshort-circuit: when the key starts with#, it grabsthe reader for the receiver's class and delegates. This is the only
seam in the runtime that needed teaching.
Lightweight
collect-private-fieldsAST plugin (no rewrite). Itwalks
PathExpressions and writes each#-prefixed tail segmentinto
meta.privateFields. The host (template()) wires that bag upbefore precompile and consumes the names afterwards.
template()wires up the reader via the user'sevalafterprecompile. Because
evalsits inside the class'sstatic {}block,the
__inst.#namesyntax in the generated reader binds to theclass's private slots; the resulting closure is registered against
the component class.
AST formalization in
@glimmer/syntax: newisPrivateFieldSegment/
privateFieldNamehelpers so consumers don't have to string-match.The
tail: string[]shape is preserved so existing AST consumerskeep working.
The explicit
scopeform does not support{{this.#field}}— itsscope arrow is evaluated outside the class body, so there's no way to
bind
#namein it. The implicit (eval) form covers gjs /<template>,which is what RFC 0921 /
content-tagemit.Test plan
private field access > it works)passes in classic, embroiderWebpack, and embroiderVite scenarios.
runtime-template-compiler-implicit-test.ts: single field,multiple fields piped through a helper, and a base-class reader
read on a subclass instance.
template_test.ts: registry registration andthe no-eval error path.
pnpm testem cirun — 9313 pass / 17 skip / 0 fail.pnpm type-checkclean. (pnpm lint:eslinthas the same 24pre-existing errors in vendored glimmer-vm code as
main/thisPR's base; none of my files have lint errors.)
🤖 Generated with Claude Code