mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-08 08:44:37 +08:00
Dispatch a table exhaustively only from calls that name the function
A key parameter took no fallback arm when every call edge of its
function passed a string literal key of the table. A call through
`call` or `apply` passes the this value first, so `f.call("a", "c")`
looked like a call with the key "a", and the dispatch for "c" called
the table's last member where Node throws a TypeError. A builtin that
calls the function back chooses its own arguments too.
Each call now has to name the function itself: its callee is a binding,
or a constant object's member, whose values are only that function.
This commit is contained in:
@@ -1648,8 +1648,33 @@ export function planStaticObjectRewrites(
|
||||
if (touched) return false;
|
||||
const edges = analysis.calls.filter((c) => c.callee.node === fn);
|
||||
if (edges.length === 0) return false;
|
||||
// A call lines its arguments up with the parameters only when its callee
|
||||
// is the function itself: a binding, or a constant object's member, that
|
||||
// holds nothing else. `call` and `apply` pass the this value first, and
|
||||
// a builtin that calls the function back chooses its own arguments.
|
||||
const isFn = (a: Atom): boolean => a.kind === "function" && a.node === fn;
|
||||
const onlyFn = (values: ReadonlySet<Atom>): boolean =>
|
||||
values.size > 0 && [...values].every(isFn);
|
||||
const callsFn = (callee: ts.Expression): boolean => {
|
||||
while (ts.isParenthesizedExpression(callee)) callee = callee.expression;
|
||||
if (ts.isIdentifier(callee)) {
|
||||
const d = declOf(callee);
|
||||
return d !== undefined && onlyFn(analysis.declValues(d));
|
||||
}
|
||||
if (!ts.isPropertyAccessExpression(callee)) return false;
|
||||
const op = readOps.get(callee);
|
||||
if (op === undefined || op.name === null) return false;
|
||||
const recv = analysis.nodes[op.recv]!.atoms;
|
||||
if (recv.size === 0) return false;
|
||||
for (const a of recv) {
|
||||
if (!constantAtoms.has(a)) return false;
|
||||
const field = analysis.fields.get(a)?.get(op.name);
|
||||
if (field === undefined || !onlyFn(analysis.nodes[field]!.atoms)) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return edges.every((e) => {
|
||||
if (!ts.isCallExpression(e.site)) return false;
|
||||
if (!ts.isCallExpression(e.site) || !callsFn(e.site.expression)) return false;
|
||||
const args = e.site.arguments.nodes;
|
||||
if (args.slice(0, index + 1).some((a) => ts.isSpreadElement(a))) return false;
|
||||
const arg = args[index];
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
* - a table call whose key parameter the function rebinds (destructuring,
|
||||
* a loop target, another parameter's default or a `var` redeclaration)
|
||||
* keeps reading the table for other keys, though every caller passes one
|
||||
* of the table's keys.
|
||||
* of the table's keys;
|
||||
* - so does one whose function is called through `call` or `apply`, where
|
||||
* the first argument is the this value and not the key.
|
||||
* The checks read the rewritten package text SCRIPTC_DEBUG_SHAPE_OVERLAY
|
||||
* prints. */
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
@@ -59,6 +61,12 @@ t.redeclared = function (op, v) {
|
||||
var op = "c";
|
||||
return t.ops[op](v);
|
||||
};
|
||||
t.viaCall = function (op, v) {
|
||||
return t.ops[op](v);
|
||||
};
|
||||
t.viaApply = function (op, v) {
|
||||
return t.ops[op](v);
|
||||
};
|
||||
module.exports = { late, quiet, t };
|
||||
`;
|
||||
|
||||
@@ -74,6 +82,7 @@ const tryCall = (call) => {
|
||||
console.log(t.plain("a", 1), t.plain("b", 2));
|
||||
console.log(tryCall(() => t.pattern("a", 1)), tryCall(() => t.loop("b", 2)));
|
||||
console.log(tryCall(() => t.initializer("a", 1)), tryCall(() => t.redeclared("b", 2)));
|
||||
console.log(tryCall(() => t.viaCall.call("a", "c", 1)), tryCall(() => t.viaApply.apply("b", ["c", 2])));
|
||||
`;
|
||||
|
||||
let dir = "";
|
||||
@@ -146,3 +155,11 @@ test("a table call whose key parameter is rebound keeps reading the table", asyn
|
||||
for (const name of ["t_pattern", "t_loop", "t_initializer", "t_redeclared"])
|
||||
expect(body(text, name), name).toContain("t.ops[op](v)");
|
||||
});
|
||||
|
||||
test("a table call whose function is called through call or apply keeps reading the table", async () => {
|
||||
// The first argument of those calls, a key of the table, is the this
|
||||
// value; the key each call passes, "c", is not one.
|
||||
const text = await rewrittenPackage();
|
||||
for (const name of ["t_viaCall", "t_viaApply"])
|
||||
expect(body(text, name), name).toContain("t.ops[op](v)");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user