[BUGFIX] plain method calling from templates (no explicit binding required -- follows JS semantics) - on still requires binding #21469
Conversation
| assert.equal(this.stashedFn(), 'arg1: foo, arg2: bar'); | ||
| } | ||
|
|
||
| '@test there is no `this` context within the callback'(assert) { |
There was a problem hiding this comment.
a change in behavior, but for the better -- I dare say: a bugfix
📊 Size reportTarball size — dist/dev 0.1%↑
dist/prod 0.1%↑
smoke-tests/v2-app-template/dist 0.1%↑
smoke-tests/v2-app-hello-world-template/dist 0.2%↑
🤖 This report was automatically generated by wyvox/pkg-size |
|
|
||
| // eslint-disable-next-line @typescript-eslint/no-unsafe-return -- @fixme | ||
| return (fn as AnyFn).call(context, ...args, ...invocationArgs); | ||
| return (fn as AnyFn).call(self, ...args, ...invocationArgs); |
There was a problem hiding this comment.
the actual fix is here
| * The reference this one was created from via a property read (`parent.path`), | ||
| * if any. See {@linkcode parentRefFor}. | ||
| */ | ||
| public parent: Nullable<Reference> = null; |
There was a problem hiding this comment.
more reference linking 🙈
|
from review times -- this PR needs to be re-worked to only solve |
f25e012 to
889278b
Compare
on still requires binding
4b573d6 to
9c29b51
Compare
| associateDestroyableChild(helperInstanceRef, helperRef); | ||
| } else if (isIndexable(definition)) { | ||
| let helper = resolveHelper(definition, ref); | ||
| let helper = resolveHelper(bindFunctionToParent(definition, ref), ref); |
There was a problem hiding this comment.
TODO: move to invokeHelper
9c29b51 to
31130d8
Compare
Why we considered touching the opcode/VM layer (and why we're backing it out)We explored gating The irreducible problem: at runtime, The only thing that differs is source syntax: Why it ballooned: the one-bit operand itself is ~5 small edits. The bulk of the complexity was making bare The decision: require parens for invocation-with-binding (
|
…fFor) `(this.obj.method)` invokes `method` with `this.obj` as `this` (JS member-call semantics), while `(@cb)` — a function passed as an argument — stays unbound, like `const f = obj.m; f()`. Whether a call has a receiver is a *syntactic* question (does the callee have a path tail?), independent of how the callee reference was derived. So the compiler computes the receiver (`receiverExpressionFor`: the callee minus its last path segment) and pushes it at the call site; `VM_DYNAMIC_HELPER_OP` reads it into `args.context`. `parentRefFor` is no longer consulted at the call site. Bare member-path appends (`{{item.greet}}`) have no syntactic receiver and are unbound — use `{{(item.greet)}}` to bind. Full suite: 9395 tests, 63898 assertions, green (the one red `(fn)` test stringifies an untouchable `this` and fails regardless of this change). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update: landed on a compiler-provided syntactic receiver (no opcode operand, no
|
| Template | binds this? |
|---|---|
{{(this.obj.method)}} |
✅ to this.obj |
{{(this.foo)}} |
✅ to this |
<Child @cb={{this.foo}} /> → {{(@cb)}} |
❌ (passed ref, like const f = obj.m; f()) |
{{(@cb.method)}} |
✅ to @cb |
{{(item.greet)}} (block param) |
✅ to item |
{{item.greet}} (bare append, no parens) |
❌ — use {{(item.greet)}} to bind |
() is now the marker for invoke-and-bind. Bare member-path appends flow through the shared cautious-append stdlib, which has no per-call-site receiver, so they invoke unbound. The this-binding-test.gjs iterated-item case was updated to {{(item.greet)}} accordingly.
Footprint
+56/−15 across 6 files, entirely in the compiler + the one opcode — no new opcode operand, no stdlib append variants, no reference-graph hacks. Full suite: 9395 tests / 63898 assertions, green. The single red test (a plain method passed through (fn) is not this-bound) fails independently of this work — it stringifies (fn)'s buildUntouchableThis (Symbol.toPrimitive), which throws in DEBUG; it needs a capture-and-compare rewrite rather than ${this}.
Matches the `receiver` local in `VM_DYNAMIC_HELPER_OP` and reads more clearly: `Arguments.receiver` / `args.receiver` is the object a member-call's function is invoked with as `this`. Pure rename — no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
supersedes
this PR enables/unblocks the change in recommendation RFC:
@actiondecorator rfcs#1045Notes
we can't tie this-fixes to invokeHelper, because a helper manager's return value is from
getValue, which only receives the args and definition from createHelper, so like.. 😬as in, the helper manager pattern is what is screwing us over here, because we (correctly?) abstracted references away from public API, but the reference is what is needed to invoke functions.
So, now the path forward I see is to... not use a helper a manager for the "default helper manager" case, and wire it all up using only private apis, no manager
I think the best I con do is bind the definition before createHelper (only for the default case)
Main test cases I care about:
Things we didn't fix (because these are problems with JavaScript)
on(addEventListener)REPL PR: NullVoxPopuli/limber#2170
Deployed: https://test-ember-source-nvp-fix-th.limber-glimdown.pages.dev/
Demo: https://test-ember-source-nvp-fix-th.limber-glimdown.pages.dev/edit?c=JYWwDg9gTgLgBAYQuCA7Apq%2BAzKy4DkAAgOYA2oI6UA9AMbKQZYEDcAUKJLHAN5wwoAQzoBrdABM4AXzi58xcpWo1BI0cFQk27dugAe3eBPTYhAVzLw6ZIQGc7cABLoyZCAHVoZKQZiYJRyQUZnhedjg4IjUxSTgGcyw4AF44AAYOCLgaGjgodABHc2B8qRgIOAAjdDghKDwAdzg7GGA3LM06fKok1IAKAEoUgD4%2BLMiYAAtgOwA6BKSAalSARg5I2V1IiQhzSrJJQbHIyPyYcyhUAWm5hfgAKjgAJnWZLbgAHn9wW39h8c%2BYGGAE1dnBJkIAG41GzAWJlSY1SrmGDlK68XhTGbzXZYaSyVpUOYfGhAgEfIFCVBSHZ7A4SABcfExN1mtP2knxJLJ5ORqLQAgAnmB0MkAER8tFi5kCsWwsTSrFzTrdTAwfHDBAUMQkyVof6REnfMC-dD-aTsIA&format=gjs