diff --git a/packages/compiler/src/frontend/lowering/lower-classes.ts b/packages/compiler/src/frontend/lowering/lower-classes.ts index 61de74d83..014bbc71a 100644 --- a/packages/compiler/src/frontend/lowering/lower-classes.ts +++ b/packages/compiler/src/frontend/lowering/lower-classes.ts @@ -3451,7 +3451,15 @@ export function collectClassShapeInner( if (shape.bodyType) shape.bodyType = DYN; } ft.params = shapes.map((shape) => shape.type); - if (ft.ret.kind !== "void") ft.ret = DYN; + // A GENERATOR method's result is the suspended generator its spawn + // wrapper answers, so the generator kind IS the checked-crossable + // ABI: dynFrom boxes a generator by reference and dyn calls resume + // it through the dyn generator ops. Replacing it with plain DYN + // would leave the method no representation at all (the generator + // branch below needs the channels to build its metadata), which + // fenced every `c.m` read on a prototype-accessed class. Only the + // ARGUMENTS cross as checked values here. + if (ft.ret.kind !== "void" && member.asteriskToken === undefined) ft.ret = DYN; } // JS subclasses can add parameters to a method. Reserve checked // argument slots in the base before constructing its vtable; shorter diff --git a/tests/corpus/dyn-generator-method-value.js b/tests/corpus/dyn-generator-method-value.js new file mode 100644 index 000000000..f1565aab0 --- /dev/null +++ b/tests/corpus/dyn-generator-method-value.js @@ -0,0 +1,53 @@ +/* A generator method VALUE crossing the checked-value boundary. + * + * A class whose methods are reachable through its prototype object answers + * its methods as checked (dynamic) values: the value read off an instance is + * the same function object the prototype carries, a checked callback can hold + * it and call it, and the generator object it produces keeps the generator + * protocol across the box. Node is the oracle. */ +class Seq { + constructor(tag) { + this.tag = tag; + } + *items(n) { + for (let i = 0; i < n; i++) yield this.tag + ":" + i; + return n; + } + async *later(n) { + yield n + 1; + return n; + } +} + +function sameValue(actual, expected, label) { + if (actual !== expected) throw new Error("not the same: " + label); +} +function typeofOf(value) { + return typeof value; +} +function callWith(receiver, fn, ...args) { + return fn.apply(receiver, args); +} + +const seq = new Seq("a"); +sameValue(seq.items, Seq.prototype.items, "instance method value"); +sameValue(seq.items, seq.items, "repeated read"); +sameValue(seq.later, Seq.prototype.later, "async instance method value"); +console.log(typeofOf(seq.items), typeofOf(seq.later)); + +const gen = callWith(seq, seq.items, 2); +console.log(gen.next().value, gen.next().value); +const finished = gen.next(); +console.log(finished.value, finished.done); + +// The direct call answers its own generator object. (Identity and reflection +// on a generator object are separate, still-fenced operations.) +const direct = seq.items(1); +console.log(direct.next().value); + +// An async generator value crosses the same boundary. +callWith(seq, seq.later, 4) + .next() + .then(function (step) { + console.log("async", step.value, step.done); + }); diff --git a/tests/harness/corpus-costs/dyn-generator-method-value.js.json b/tests/harness/corpus-costs/dyn-generator-method-value.js.json new file mode 100644 index 000000000..cc16fb950 --- /dev/null +++ b/tests/harness/corpus-costs/dyn-generator-method-value.js.json @@ -0,0 +1 @@ +{ "plain": 1349, "san": 2585 }