From b0ce1e07b8b85527ac8b6fbe19c3976061defc71 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Fri, 8 May 2026 02:30:37 -0400 Subject: [PATCH] Resolve `{{this.#field}}` via per-class private-field reader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/`