fix(runtime): bind this to receiver in js_get_iterator for yield*-over-a-Context-Tag (#321) - #2032
Merged
Merged
Conversation
…d*-over-a-Context-Tag (#321) `yield* Tag` (an effect `Context.Tag`, a yieldable Effect) resolved to an empty Effect: `Effect.gen(function*(){ const g = yield* Greeter; ... })` over a Context service produced no output where Node prints the resolved service value. Root cause: effect's `EffectPrototype[Symbol.iterator]` is an object-literal method shared via the prototype chain whose body is `new SingleShotGen(new YieldWrap(this))`. Perry codegen bakes `this` for object-literal methods to the defining literal (CAPTURES_THIS_FLAG), so when `js_get_iterator` invoked the inherited method via `js_closure_call0` (no receiver), `this` stayed the prototype object instead of the Tag. The YieldWrap then wrapped the prototype, `yieldWrapGet` returned a non-Effect (`_op` undefined), the fiber never resolved it nor resumed the generator — empty. Fix: in `js_get_iterator`, rebind the resolved `[Symbol.iterator]` closure's `this` to the original iterable via `clone_closure_rebind_this` before calling, matching the spec (`iterable[Symbol.iterator]()` runs with `this === iterable`). No-op for iterator methods that don't capture `this` (real generators, arrays, Map/Set/string iteration are untouched).
This was referenced May 27, 2026
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.
Problem
Effect.genover aContext.Tagyielded empty. The lasteffectsurvey blocker:Effect.mapover a Tag and Layer DI already worked; basicEffect.gen+yield* Effect.succeed(x)already worked. The break was specificallyyield* <ContextTag>— a Tag is a class-as-value that is itself a yieldable Effect.Root cause
A Context.Tag is yieldable because
Context.Tag(id)sets the class's prototype toTagProto = { ...EffectPrototype, ... }, andEffectPrototype[Symbol.iterator]is:This is an object-literal method inherited via the prototype chain whose body reads
this. Perry codegen bakesthisfor object-literal methods to the defining literal (theCAPTURES_THIS_FLAGreserved slot — correct for[Symbol.toPrimitive]reading own fields). When theyield*desugar callsjs_get_iterator(Greeter), the inherited[Symbol.iterator]was invoked viajs_closure_call0with no receiver, sothisstayed the prototype object instead of the Tag.Consequence:
new YieldWrap(this)wrapped the prototype, not the Tag.yieldWrapGet(state.value)in effect's fiberOP_ITERATORloop returned a non-Effect (_opwasundefined), so the fiber never resolved it to the service and never resumed the generator — empty output.Isolated with a manual generator drive (no fiber):
yieldWrapGet(it.next().value)._op === "Tag"..._op === undefinedAnd reproduced with no effect at all (object-literal prototype method reading
this, called with a receiver -> Perry boundthisto the prototype, Node to the receiver).Fix
In
js_get_iterator, rebind the resolved[Symbol.iterator]closure'sthisto the original iterable (via the existingclone_closure_rebind_this) before calling it — matching the spec thatiterable[Symbol.iterator]()runs withthis === iterable. It is a no-op for iterator methods that do not capturethis(real generators, arrays, Map/Set/string iteration unaffected).One runtime file changed (
crates/perry-runtime/src/symbol.rs, +12/-1) plus a standalone gap test.Verification
survey/real3_context_layer.ts->ctx/layer: Hello, Ada(was empty)survey/nlay.ts->provide: 42(Layer DI, no regression)survey/05_gen.ts->30(no generator regression)survey/ctx_works.ts->provide: 42; minimalEffect.gen+Context.make(no Layer) ->min: Hi Adatest-files/test_gap_yieldstar_inherited_iterator_this.ts(standalone, no effect) byte-identical to Nodecargo test --release -p perry-runtime --lib -- --test-threads=1-> 690 passed, 0 failedcargo fmt --all -- --checkclean