Test262 group B: generator method ABI, empty-pattern throws (#2505)

This commit is contained in:
Chris Tate
2026-10-28 22:41:17 -05:00
committed by GitHub
8 changed files with 206 additions and 2 deletions
@@ -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
@@ -2617,7 +2617,7 @@ export function lowerDestructuringDecl(
init.type.kind === "dyn" &&
ts.isObjectBindingPattern(decl.name) &&
isJsSourceFile(ts.getSourceFileOfNode(decl)) &&
startsWithRuntimeKey(lowerer, decl.name) &&
(decl.name.elements.nodes.length === 0 || startsWithRuntimeKey(lowerer, decl.name)) &&
destrSpellingOf(decl.initializer) !== null;
if (
jsRuntimeKeyCheck ||
@@ -3181,6 +3181,39 @@ export function lowerBindingPattern(
// every statically non-nullish kind, record or not (the assignment
// path's rule); possibly-nullish sources would throw in JS and keep
// a fence.
//
// A CHECKED-DYNAMIC source still owes that step at runtime: an empty
// object pattern over null/undefined throws RequireObjectCoercible,
// and an empty array pattern throws GetIterator's "is not iterable".
// Nothing else about the pattern is observable, so the fence is the
// whole lowering.
if (srcType.kind === "dyn") {
const unitLoc = locOf(pattern);
const isObject = ts.isObjectBindingPattern(pattern);
for (const unit of ["null", "undefined"] as const) {
// V8 describes an argument-derived source by its type ("object
// null"); a bare non-empty pattern names its source instead, but
// those reads throw on their own before this check could fire.
const described = unit === "null" ? "object null" : "undefined";
const message = isObject
? `Cannot destructure '${described}' as it is ${unit}.`
: `${described} is not iterable (cannot read property Symbol(Symbol.iterator))`;
out.push({
kind: "if",
cond: { kind: "dynTest", test: unit, value: srcRef(), type: BOOL, loc: unitLoc },
then: [
{
kind: "exprStmt",
expr: nodeThrowExpr(1, "", message, VOID, unitLoc),
loc: unitLoc,
},
],
else_: null,
loc: unitLoc,
});
}
return;
}
if (
isUnitType(srcType) ||
(srcType.kind === "union" &&
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/8516-empty-pattern-requires-object-coercible.js"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/dyn-generator-method-value.js"
],
"diags": []
}
@@ -0,0 +1,96 @@
// An empty destructuring pattern still performs its one observable step:
// RequireObjectCoercible for `{}` and GetIterator for `[]`. A checked-dynamic
// source reaches that step at runtime, so null and undefined throw the same
// TypeError Node does while every other value — including primitives, which
// ToObject wraps — is accepted. The throws are printed through
// `instanceof` rather than the message, since V8 spells the source
// differently per position (an argument is "object null", a declaration
// names its expression).
function report(label, fn) {
try {
fn();
console.log(label, "no-throw");
} catch (error) {
console.log(label, error instanceof TypeError ? "TypeError" : "other");
}
}
function acceptsWhatever(value) {
return value;
}
report("object-null", () => {
const source = acceptsWhatever(null);
const {} = source;
});
report("object-undefined", () => {
const source = acceptsWhatever(undefined);
const {} = source;
});
function objectParameter({}) {
return "bound";
}
report("param-null", () => {
objectParameter(null);
});
report("param-undefined", () => {
objectParameter(undefined);
});
report("param-number", () => {
console.log("param-number returned", objectParameter(7));
});
report("param-string", () => {
console.log("param-string returned", objectParameter("text"));
});
function arrayParameter([]) {
return "bound";
}
report("array-param-null", () => {
arrayParameter(null);
});
report("array-param-undefined", () => {
arrayParameter(undefined);
});
function nestedObject({ inner: {} }) {
return "nested";
}
report("nested-null", () => {
nestedObject({ inner: null });
});
report("nested-undefined", () => {
nestedObject({ inner: undefined });
});
report("nested-object", () => {
console.log("nested-object returned", nestedObject({ inner: {} }));
});
// A loop head rebinds the pattern per iteration, exactly like a declaration.
report("for-of-object", () => {
for (const {} of acceptsWhatever([null])) {
console.log("never");
}
});
// Non-nullish primitives are accepted by both pattern kinds.
report("primitives", () => {
const numbers = [0, "", false, 1, "x"];
for (const value of numbers) {
const {} = value;
console.log("destructured", JSON.stringify(value), typeof value);
}
});
// The assignment form keeps its own (already exact) check.
report("assignment-null", () => {
let bound;
({} = bound);
});
report("assignment-number", () => {
let bound = 5;
({} = bound);
console.log("assignment-number finished");
});
@@ -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);
});
@@ -0,0 +1 @@
{ "plain": 2400, "san": 24000 }
@@ -0,0 +1 @@
{ "plain": 1349, "san": 2585 }