Skip to content

fix(runtime): bind this to receiver in js_get_iterator for yield*-over-a-Context-Tag (#321) - #2032

Merged
proggeramlug merged 1 commit into
mainfrom
fix-321-yieldstar-context-tag-this
May 27, 2026
Merged

fix(runtime): bind this to receiver in js_get_iterator for yield*-over-a-Context-Tag (#321)#2032
proggeramlug merged 1 commit into
mainfrom
fix-321-yieldstar-context-tag-this

Conversation

@proggeramlug

Copy link
Copy Markdown
Contributor

Problem

Effect.gen over a Context.Tag yielded empty. The last effect survey blocker:

const program = Effect.gen(function* () {
  const g = yield* Greeter;        // Greeter is a Context.Tag class
  return g.greet("Ada");
});
const result = Effect.runSync(Effect.provide(program, GreeterLive));
console.log("ctx/layer:", result);  // Node: "ctx/layer: Hello, Ada"  Perry: (empty)

Effect.map over a Tag and Layer DI already worked; basic Effect.gen + yield* Effect.succeed(x) already worked. The break was specifically yield* <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 to TagProto = { ...EffectPrototype, ... }, and EffectPrototype[Symbol.iterator] is:

[Symbol.iterator]() { return new SingleShotGen(new YieldWrap(this)); }

This is an object-literal method inherited via the prototype chain whose body reads this. Perry codegen bakes this for object-literal methods to the defining literal (the CAPTURES_THIS_FLAG reserved slot — correct for [Symbol.toPrimitive] reading own fields). When the yield* desugar calls js_get_iterator(Greeter), the inherited [Symbol.iterator] was invoked via js_closure_call0 with no receiver, so this stayed the prototype object instead of the Tag.

Consequence: new YieldWrap(this) wrapped the prototype, not the Tag. yieldWrapGet(state.value) in effect's fiber OP_ITERATOR loop returned a non-Effect (_op was undefined), so the fiber never resolved it to the service and never resumed the generator — empty output.

Isolated with a manual generator drive (no fiber):

  • Node: yieldWrapGet(it.next().value)._op === "Tag"
  • Perry (before): ..._op === undefined

And reproduced with no effect at all (object-literal prototype method reading this, called with a receiver -> Perry bound this to the prototype, Node to the receiver).

Fix

In js_get_iterator, rebind the resolved [Symbol.iterator] closure's this to the original iterable (via the existing clone_closure_rebind_this) before calling it — matching the spec that iterable[Symbol.iterator]() runs with this === iterable. It is a no-op for iterator methods that do not capture this (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; minimal Effect.gen + Context.make (no Layer) -> min: Hi Ada
  • 12 other effect survey items (succeed/map/flatMap/pipe/fail_catch/all/sync/either/option/data/nref/real4_ref) byte-identical to Node
  • test-files/test_gap_yieldstar_inherited_iterator_this.ts (standalone, no effect) byte-identical to Node
  • Iteration regression sweep (for-of array/gen/Map/Set/string, yield*-over-generator, spread) byte-identical to Node
  • cargo test --release -p perry-runtime --lib -- --test-threads=1 -> 690 passed, 0 failed
  • cargo fmt --all -- --check clean

…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).
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