mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-01 07:33:12 +08:00
Throw on empty patterns over checked-dynamic sources
An empty binding pattern still performs its one observable step:
RequireObjectCoercible for `{}` and GetIterator for `[]`. Over a statically
non-nullish source that step is invisible, which is why the empty-pattern
lowering emitted nothing; a checked-dynamic source can be null or undefined
at runtime, where V8 throws.
Emit the runtime null/undefined test for dyn sources in the empty-pattern
branch, and let a JS-file empty object pattern take the source-spelling
message dynDestrCheck already produces for non-empty ones.
Test262 (static-adapted-v6, --workers 8), affected chapters before -> after:
language/statements/class/dstr pass 1518 -> 1526, fail 424 -> 416;
language/expressions/class/dstr pass 1518 -> 1526, fail 424 -> 416. No
pass -> fail.
Co-Authored-By: Claude 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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,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 @@
|
||||
{ "plain": 2400, "san": 24000 }
|
||||
Reference in New Issue
Block a user