mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
* fix(javascript): resolve module-level Electron options in functions * fix(javascript): limit module option ancestry walk * fix(javascript): preserve uncertain captured argument ordering * fix(javascript): retain hoisted activation uncertainty * fix(javascript): retain function ownership through loops * fix(javascript): account for classic global activations * fix(javascript): bind first activations to lexical identities * fix(javascript): retain uncertain global activation timing * fix(javascript): isolate receiver facts and share computed reads * fix(javascript): retain projected and opaque global activations * fix(javascript): refresh activation bounds as opaque effects grow * fix(ci): format website watch directories without changing configuration * fix(javascript): share source reads across opaque-effect updates * fix(javascript): retain constructed-code global activation bounds * fix(javascript): share runtime activation and receiver facts * fix(javascript): reuse pattern reads and function positions * fix(javascript): isolate copied paths and preserve runtime reads * style(javascript): format migrated reference imports * fix(javascript): normalize constructor activation and receiver effects * fix(javascript): retain aliased receiver timing and lexical global this * fix(javascript): preserve effect-free receiver identity across activations * fix(javascript): retain outer activation and prototype effects * fix(javascript): index implicit reads without deep path rescans * fix(javascript): retain aliased prototype and parameter read uncertainty * fix(javascript): index reflected reads through intrinsic invocation facts * fix(javascript): retain unresolved global activation reads * chore: refresh PR branch after review fix publication * fix(javascript): retain projected receiver reads and cache method facts * fix(javascript): retain inherited and implicit activation reads * fix(javascript): retain caller-owned this uncertainty * fix(javascript): retain reflected reads with unresolved spread arguments * fix(javascript): preserve enclosing activation for decorators * fix(javascript): track intrinsic literal prototype effects * fix(javascript): retain catch activation from implicit reads * perf(javascript): reuse effective keys for projected reads * fix(javascript): retain possible caller-owned ambient effects
695 lines
23 KiB
TypeScript
695 lines
23 KiB
TypeScript
import * as t from "@babel/types";
|
|
import { expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
analyzeJavaScriptSemantics,
|
|
indexJavaScriptBindingsSteps,
|
|
indexJavaScriptBindingsWithReceiversSteps,
|
|
} from "./javascriptSemanticAnalysis.js";
|
|
import {
|
|
resolveLocalCallables,
|
|
type LocalCallableResolution,
|
|
} from "./javascriptSemanticCallResolution.js";
|
|
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
|
|
import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js";
|
|
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
|
|
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
|
|
|
const indexed = (source: string) => {
|
|
const parsed = parseJavaScriptSource(source, "callback.js");
|
|
if (parsed === null) throw new Error("Expected valid callback fixture");
|
|
const steps = indexJavaScriptBindingsSteps(parsed.program);
|
|
let step = steps.next();
|
|
while (step.done !== true) step = steps.next();
|
|
const state = step.value;
|
|
const callableById = new Map(
|
|
state.callables.map((callable) => [callable.callableId, callable]),
|
|
);
|
|
const bindingCache = new WeakMap<t.Identifier, LocalCallableResolution>();
|
|
return {
|
|
state,
|
|
callableById,
|
|
bindingCache,
|
|
resolve: (node: t.Node) =>
|
|
resolveLocalCallables({
|
|
node,
|
|
state,
|
|
callableById,
|
|
bindingCache,
|
|
seenInitializers: new Set(),
|
|
}),
|
|
};
|
|
};
|
|
|
|
it("orders exported callable initializers within their enclosing declaration", () => {
|
|
const ir = analyzeJavaScriptSemantics(`
|
|
function original() {}
|
|
export const first = original, second = first;
|
|
second();
|
|
export const before = later, later = original;
|
|
before();
|
|
`);
|
|
const original = ir.callables.find(({ name }) => name === "original");
|
|
if (original === undefined) throw new Error("Missing original callable");
|
|
expect(ir.callSites[0]).toMatchObject({
|
|
resolution: "exact",
|
|
calleeCallableIds: [original.callableId],
|
|
});
|
|
expect(ir.callSites[1]).toBeDefined();
|
|
expect(ir.callSites[1]?.resolution).not.toBe("exact");
|
|
});
|
|
|
|
it.each([
|
|
"if(flag)({make}={make:replacement});",
|
|
"if(flag)[make]=[replacement];",
|
|
"({make}={make:replacement});",
|
|
"[make]=[replacement];",
|
|
])("does not prove the old callable after a projected write: %s", (write) => {
|
|
const analysis = analyzeJavaScriptSemantics(
|
|
`function original(){}function replacement(){}let make=original;make();${write}make();`,
|
|
);
|
|
const original = analysis.callables.find(({ name }) => name === "original");
|
|
expect(original).toBeDefined();
|
|
const [before, after] = analysis.callSites;
|
|
expect(before).toMatchObject({
|
|
resolution: "exact",
|
|
calleeCallableIds: [original?.callableId],
|
|
});
|
|
expect(after).toBeDefined();
|
|
expect(after?.resolution).not.toBe("exact");
|
|
expect(analysis.frontiers).toContainEqual(
|
|
expect.objectContaining({
|
|
kind: "dynamic-call",
|
|
location: after?.calleeLocation,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
"if(flag)({make}={make:replacement});",
|
|
"if(flag)[make]=[replacement];",
|
|
])(
|
|
"retains known callable candidates without claiming completeness: %s",
|
|
(write) => {
|
|
const context = indexed(
|
|
`function original(){}function replacement(){}let make=original;${write}make();`,
|
|
);
|
|
const last = context.state.program.body.at(-1);
|
|
if (!t.isExpressionStatement(last) || !t.isCallExpression(last.expression))
|
|
throw new Error("Missing projected call");
|
|
const original = context.state.callables.find(
|
|
({ name }) => name === "original",
|
|
);
|
|
expect(original).toBeDefined();
|
|
expect(context.resolve(last.expression.callee)).toMatchObject({
|
|
complete: false,
|
|
callableIds: [original?.callableId],
|
|
});
|
|
},
|
|
);
|
|
|
|
it("retains selected callable keys without admitting an unrelated return", () => {
|
|
const context = indexed(`
|
|
const table = { x() { return {}; }, y() { return {}; }, z() { return module; } };
|
|
const make = flag ? table.x : table.y;
|
|
make();
|
|
`);
|
|
const last = context.state.program.body.at(-1);
|
|
if (!t.isExpressionStatement(last) || !t.isCallExpression(last.expression))
|
|
throw new Error("Missing selected call");
|
|
const returns = createSemanticReturnedReferences(context.state)(
|
|
last.expression.callee,
|
|
).references;
|
|
expect(returns).toHaveLength(2);
|
|
expect(returns.every(({ node }) => t.isObjectExpression(node))).toBe(true);
|
|
});
|
|
|
|
it("retains correlations between selected callable path segments", () => {
|
|
const context = indexed(`
|
|
const table = { a: { b() { return {}; }, d() { return module; } },
|
|
c: { d() { return {}; }, b() { return module; } } };
|
|
const make = flag ? table.a.b : table.c.d;
|
|
make();
|
|
`);
|
|
const last = context.state.program.body.at(-1);
|
|
if (!t.isExpressionStatement(last) || !t.isCallExpression(last.expression))
|
|
throw new Error("Missing selected call");
|
|
const returns = createSemanticReturnedReferences(context.state)(
|
|
last.expression.callee,
|
|
).references;
|
|
expect(returns).toHaveLength(2);
|
|
expect(returns.every(({ node }) => t.isObjectExpression(node))).toBe(true);
|
|
});
|
|
|
|
it("bounds shared partial callback alias expansion by AST size", () => {
|
|
const measure = (depth: number) => {
|
|
const context =
|
|
indexed(`function fallback(){return {member:1};} function choose(cb,flag){
|
|
if(flag)cb=fallback;
|
|
const a0=cb;
|
|
${Array.from({ length: depth }, (_, i) => `const a${i + 1}=flag?a${i}:a${i};`).join("")}
|
|
return a${depth}();
|
|
}`);
|
|
const choose = context.state.program.body[1];
|
|
if (!t.isFunctionDeclaration(choose)) throw new Error("Missing choose");
|
|
const last = choose.body.body.at(-1);
|
|
if (!t.isReturnStatement(last) || !t.isCallExpression(last.argument))
|
|
throw new Error("Missing call");
|
|
const reads = vi.spyOn(context.bindingCache, "get");
|
|
const result = context.resolve(last.argument.callee);
|
|
expect(result).toMatchObject({ complete: false });
|
|
expect(result.callableIds).toHaveLength(1);
|
|
const callReads = reads.mock.calls.length;
|
|
const lookups = context.state.scopes.map((scope) =>
|
|
vi.spyOn(scope.bindings, "get"),
|
|
);
|
|
try {
|
|
expect(
|
|
createSemanticReturnedReferences(context.state)(last.argument.callee)
|
|
.references,
|
|
).toHaveLength(1);
|
|
return {
|
|
callReads,
|
|
referenceReads: lookups.reduce(
|
|
(total, lookup) => total + lookup.mock.calls.length,
|
|
0,
|
|
),
|
|
};
|
|
} finally {
|
|
for (const lookup of lookups) lookup.mockRestore();
|
|
}
|
|
};
|
|
const small = measure(8);
|
|
const large = measure(12);
|
|
expect(small.callReads).toBeGreaterThan(0);
|
|
expect(large.callReads).toBeLessThan(3 * small.callReads);
|
|
expect(small.referenceReads).toBeGreaterThan(0);
|
|
expect(large.referenceReads).toBeLessThan(3 * small.referenceReads);
|
|
});
|
|
|
|
it("reuses the immutable enclosing-function position across distinct captured reads", () => {
|
|
const { state } = indexed(
|
|
`const make=()=>1; ${"0;".repeat(64)} function use(){${"make();".repeat(64)}} use();`,
|
|
);
|
|
const binding = [...state.bindingsById.values()].find(
|
|
(b) => b.name === "make",
|
|
);
|
|
const fn = state.program.body.find(
|
|
(n) => t.isFunctionDeclaration(n) && n.id?.name === "use",
|
|
);
|
|
if (binding === undefined || fn === undefined)
|
|
throw Error("Missing fixture binding");
|
|
const lookup = vi.spyOn(state.program.body, "indexOf");
|
|
let reads = 0;
|
|
try {
|
|
traverseJavaScriptAst(fn, {
|
|
enter(node) {
|
|
if (
|
|
t.isCallExpression(node) &&
|
|
t.isIdentifier(node.callee, { name: "make" })
|
|
) {
|
|
reads++;
|
|
expect(
|
|
semanticBindingAtRead(binding, node.callee, state)
|
|
.hasDefiniteInitializer,
|
|
).toBe(true);
|
|
}
|
|
},
|
|
});
|
|
expect(reads).toBe(64);
|
|
expect(
|
|
lookup.mock.calls.filter(([node]) => node === fn).length,
|
|
).toBeLessThanOrEqual(1);
|
|
} finally {
|
|
lookup.mockRestore();
|
|
}
|
|
});
|
|
|
|
it.each([true, false])(
|
|
"orders a module direct-eval activation with early=%s",
|
|
(early) => {
|
|
const { state } = indexed(
|
|
`export {}; ${early ? "eval('open()');" : ""} const make = () => 1; function open() { return make(); } ${early ? "" : "eval('open()');"} open();`,
|
|
);
|
|
const binding = [...state.bindingsById.values()].find(
|
|
(binding) => binding.name === "make",
|
|
);
|
|
if (binding === undefined) throw new Error("Missing make binding");
|
|
let reads = 0;
|
|
traverseJavaScriptAst(state.program, {
|
|
enter(node) {
|
|
if (
|
|
t.isCallExpression(node) &&
|
|
t.isIdentifier(node.callee, { name: "make" })
|
|
) {
|
|
reads++;
|
|
expect(
|
|
semanticBindingAtRead(binding, node.callee, state)
|
|
.hasDefiniteInitializer,
|
|
).toBe(!early);
|
|
}
|
|
},
|
|
});
|
|
expect(reads).toBe(1);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["if(flag){cb=fallback;cb();}", true],
|
|
["if(flag)cb=fallback;cb();", false],
|
|
["cb ||= fallback;cb();", false],
|
|
] as const)(
|
|
"reports initializer coverage at the read: %s",
|
|
(body, expected) => {
|
|
const { state } = indexed(`function choose(cb,flag){${body}}`);
|
|
const binding = [...state.bindingsById.values()].find(
|
|
(binding) => binding.name === "cb",
|
|
);
|
|
if (binding === undefined) throw new Error("Missing cb binding");
|
|
let reads = 0;
|
|
traverseJavaScriptAst(state.program, {
|
|
enter(node) {
|
|
if (
|
|
t.isCallExpression(node) &&
|
|
t.isIdentifier(node.callee, { name: "cb" })
|
|
) {
|
|
reads++;
|
|
expect(
|
|
semanticBindingAtRead(binding, node.callee, state)
|
|
.hasDefiniteInitializer,
|
|
).toBe(expected);
|
|
}
|
|
},
|
|
});
|
|
expect(reads).toBe(1);
|
|
},
|
|
);
|
|
|
|
it("does not reuse a later resolved callback at an earlier read", () => {
|
|
const context = indexed(
|
|
`function fallback(){} function choose(cb){cb();cb=fallback;cb();}`,
|
|
);
|
|
const choose = context.state.program.body[1];
|
|
if (!t.isFunctionDeclaration(choose)) throw new Error("Missing choose");
|
|
const calls = choose.body.body.flatMap((statement) =>
|
|
t.isExpressionStatement(statement) &&
|
|
t.isCallExpression(statement.expression)
|
|
? [statement.expression.callee]
|
|
: [],
|
|
);
|
|
const [before, after] = calls;
|
|
if (before === undefined || after === undefined)
|
|
throw new Error("Missing callback reads");
|
|
expect(context.resolve(after)).toMatchObject({ complete: true });
|
|
expect(context.resolve(before)).toMatchObject({
|
|
complete: false,
|
|
callableIds: [],
|
|
});
|
|
expect(context.resolve(after)).toMatchObject({ complete: true });
|
|
});
|
|
|
|
it("keeps a nested callback read uncertain across a later var initializer", () => {
|
|
const context = indexed(
|
|
`function target(){} function read(){cb();} read(); var cb=target;`,
|
|
);
|
|
const target = context.state.callables.find(
|
|
(callable) => callable.name === "target",
|
|
);
|
|
let read: t.Identifier | undefined;
|
|
traverseJavaScriptAst(context.state.program, {
|
|
enter(node) {
|
|
if (
|
|
t.isCallExpression(node) &&
|
|
t.isIdentifier(node.callee, { name: "cb" })
|
|
)
|
|
read = node.callee;
|
|
},
|
|
});
|
|
if (target === undefined || read === undefined)
|
|
throw new Error("Missing target or callback read");
|
|
expect(context.resolve(read)).toMatchObject({
|
|
callableIds: [target.callableId],
|
|
complete: false,
|
|
});
|
|
});
|
|
|
|
it("keeps same-activation calls after a var initializer resolved", () => {
|
|
const context = indexed(`function target(){} var cb=target; cb();`);
|
|
let read: t.Identifier | undefined;
|
|
traverseJavaScriptAst(context.state.program, {
|
|
enter(node) {
|
|
if (
|
|
t.isCallExpression(node) &&
|
|
t.isIdentifier(node.callee, { name: "cb" })
|
|
)
|
|
read = node.callee;
|
|
},
|
|
});
|
|
if (read === undefined) throw new Error("Missing callback read");
|
|
expect(context.resolve(read)).toMatchObject({
|
|
callableIds: [context.state.callables[0]?.callableId],
|
|
complete: true,
|
|
});
|
|
});
|
|
|
|
it("keeps a hoisted function declaration resolved from a nested read", () => {
|
|
const context = indexed(
|
|
`function read(){target();} read(); function target(){}`,
|
|
);
|
|
const target = context.state.callables.find(
|
|
(callable) => callable.name === "target",
|
|
);
|
|
let read: t.Identifier | undefined;
|
|
traverseJavaScriptAst(context.state.program, {
|
|
enter(node) {
|
|
if (
|
|
t.isCallExpression(node) &&
|
|
t.isIdentifier(node.callee, { name: "target" })
|
|
)
|
|
read = node.callee;
|
|
},
|
|
});
|
|
if (target === undefined || read === undefined)
|
|
throw new Error("Missing target or nested call");
|
|
expect(context.resolve(read)).toMatchObject({
|
|
callableIds: [target.callableId],
|
|
complete: true,
|
|
});
|
|
});
|
|
|
|
it("does not infer hoisting for a conditional function declaration", () => {
|
|
const context = indexed(
|
|
`if (flag) { function target(){} } function read(){target();} read();`,
|
|
);
|
|
let read: t.Identifier | undefined;
|
|
traverseJavaScriptAst(context.state.program, {
|
|
enter(node) {
|
|
if (
|
|
t.isCallExpression(node) &&
|
|
t.isIdentifier(node.callee, { name: "target" })
|
|
)
|
|
read = node.callee;
|
|
},
|
|
});
|
|
if (read === undefined) throw new Error("Missing conditional function read");
|
|
expect(context.resolve(read)).toMatchObject({ complete: false });
|
|
});
|
|
|
|
it("uses class declaration order instead of a stable initializer shortcut", () => {
|
|
const before = indexed("class Target {} new Target();");
|
|
const after = indexed("new Target(); class Target {}");
|
|
const exported = indexed("export class Target {} new Target();");
|
|
const callTarget = (context: ReturnType<typeof indexed>) => {
|
|
let read: t.Identifier | undefined;
|
|
traverseJavaScriptAst(context.state.program, {
|
|
enter(node) {
|
|
if (
|
|
t.isNewExpression(node) &&
|
|
t.isIdentifier(node.callee, { name: "Target" })
|
|
)
|
|
read = node.callee;
|
|
},
|
|
});
|
|
if (read === undefined) throw new Error("Missing class constructor read");
|
|
return context.resolve(read);
|
|
};
|
|
const beforeTarget = before.state.callables.find(
|
|
(callable) => callable.name === "Target",
|
|
);
|
|
if (beforeTarget === undefined) throw new Error("Missing class target");
|
|
expect(callTarget(before)).toMatchObject({
|
|
callableIds: [beforeTarget.callableId],
|
|
complete: true,
|
|
});
|
|
expect(callTarget(after)).toMatchObject({ complete: false });
|
|
expect(callTarget(exported)).toMatchObject({ complete: true });
|
|
});
|
|
|
|
it.each([false, true])(
|
|
"bounds separate branch assignments with intrinsic cycle=%s",
|
|
(cyclic) => {
|
|
const measure = (depth: number) => {
|
|
const context = indexed(`function fallback(){return {};}
|
|
function choose(flag){let a0=${cyclic ? "flag?a0:{make:fallback}" : "{make:fallback}"};
|
|
${Array.from({ length: depth }, (_, i) => `let a${i + 1};if(flag)a${i + 1}=a${i};else a${i + 1}=a${i};`).join("")}
|
|
return a${depth}.make();}`);
|
|
const choose = context.state.program.body[1];
|
|
if (!t.isFunctionDeclaration(choose)) throw new Error("Missing choose");
|
|
const last = choose.body.body.at(-1);
|
|
if (!t.isReturnStatement(last) || !t.isCallExpression(last.argument))
|
|
throw new Error("Missing call");
|
|
const lookups = context.state.scopes.map((scope) =>
|
|
vi.spyOn(scope.bindings, "get"),
|
|
);
|
|
try {
|
|
expect(
|
|
createSemanticReturnedReferences(context.state)(last.argument.callee)
|
|
.references,
|
|
).toHaveLength(1);
|
|
return lookups.reduce(
|
|
(total, lookup) => total + lookup.mock.calls.length,
|
|
0,
|
|
);
|
|
} finally {
|
|
for (const lookup of lookups) lookup.mockRestore();
|
|
}
|
|
};
|
|
const small = measure(8),
|
|
large = measure(12);
|
|
expect(small).toBeGreaterThan(0);
|
|
expect(large).toBeLessThan(3 * small);
|
|
},
|
|
);
|
|
|
|
it.each(["||=", "??="])(
|
|
"retains definite assignment to an empty local: %s",
|
|
(operator) => {
|
|
const { state } = indexed(
|
|
`function choose(){let cb;cb ${operator} fallback;cb();}`,
|
|
);
|
|
const binding = [...state.bindingsById.values()].find(
|
|
(binding) => binding.name === "cb",
|
|
);
|
|
if (binding === undefined) throw new Error("Missing cb binding");
|
|
let reads = 0;
|
|
traverseJavaScriptAst(state.program, {
|
|
enter(node) {
|
|
if (
|
|
t.isCallExpression(node) &&
|
|
t.isIdentifier(node.callee, { name: "cb" })
|
|
) {
|
|
reads++;
|
|
expect(
|
|
semanticBindingAtRead(binding, node.callee, state)
|
|
.hasDefiniteInitializer,
|
|
).toBe(true);
|
|
}
|
|
},
|
|
});
|
|
expect(reads).toBe(1);
|
|
},
|
|
);
|
|
|
|
it("does not memoize alias cycle cuts across entry paths", () => {
|
|
const context = indexed(
|
|
`function fallback(){} let a,b; function setA(){a=flag?b:fallback;} function setB(){b=a;} a(); b();`,
|
|
);
|
|
const callees = context.state.program.body.flatMap((statement) =>
|
|
t.isExpressionStatement(statement) &&
|
|
t.isCallExpression(statement.expression)
|
|
? [statement.expression.callee]
|
|
: [],
|
|
);
|
|
const fallback = context.state.callables.find(
|
|
(callable) => callable.name === "fallback",
|
|
);
|
|
if (fallback === undefined) throw new Error("Missing fallback");
|
|
for (const node of [...callees, ...callees].reverse()) {
|
|
const warm = context.resolve(node);
|
|
const cold = resolveLocalCallables({
|
|
node,
|
|
state: context.state,
|
|
callableById: context.callableById,
|
|
bindingCache: new WeakMap(),
|
|
seenInitializers: new Set(),
|
|
});
|
|
expect(warm).toEqual(cold);
|
|
expect(warm).toMatchObject({
|
|
complete: false,
|
|
cycleDependent: true,
|
|
callableIds: [fallback.callableId],
|
|
});
|
|
}
|
|
});
|
|
|
|
const meshLookups = (writes: number): number => {
|
|
const source = `function f(){} function g(){} let a=f,b=g;
|
|
function run(){${Array.from({ length: writes }, (_, index) =>
|
|
index % 2 === 0 ? "a=b;" : "b=a;",
|
|
).join("")} a();}`;
|
|
const context = indexed(source);
|
|
const run = context.state.program.body[3];
|
|
if (!t.isFunctionDeclaration(run)) throw new Error("Missing run");
|
|
const statement = run.body.body.at(-1);
|
|
if (
|
|
!t.isExpressionStatement(statement) ||
|
|
!t.isCallExpression(statement.expression)
|
|
)
|
|
throw new Error("Missing mesh call");
|
|
const lookups = vi.spyOn(context.bindingCache, "get");
|
|
const result = context.resolve(statement.expression.callee);
|
|
expect(result).toMatchObject({ complete: false, cycleDependent: true });
|
|
expect(
|
|
result.callableIds.map((id) => context.callableById.get(id)?.name).sort(),
|
|
).toEqual(["f", "g"]);
|
|
return lookups.mock.calls.length;
|
|
};
|
|
|
|
it("closes cycles at full-binding reads with bounded lookup growth", () => {
|
|
const six = meshLookups(6);
|
|
const eight = meshLookups(8);
|
|
const ten = meshLookups(10);
|
|
const twelve = meshLookups(12);
|
|
expect(six).toBeGreaterThan(0);
|
|
expect(eight).toBeLessThan(3 * six);
|
|
expect(ten).toBeLessThan(3 * eight);
|
|
expect(twelve).toBeLessThan(3 * ten);
|
|
});
|
|
|
|
it("resolves calls to preceding exported variable initializers and computed method keys", () => {
|
|
const context = indexed(`
|
|
export const pick = () => "target";
|
|
const obj = { [pick()]() { return 1; } };
|
|
pick();
|
|
`);
|
|
const objStatement = context.state.program.body[1];
|
|
if (
|
|
!t.isVariableDeclaration(objStatement) ||
|
|
!t.isVariableDeclarator(objStatement.declarations[0]) ||
|
|
!t.isObjectExpression(objStatement.declarations[0].init)
|
|
)
|
|
throw new Error("Missing obj declaration");
|
|
const method = objStatement.declarations[0].init.properties[0];
|
|
if (!t.isObjectMethod(method) || !t.isCallExpression(method.key))
|
|
throw new Error("Missing computed key call");
|
|
const target = context.state.callables.find(
|
|
(callable) => callable.name === "pick",
|
|
);
|
|
if (target === undefined) throw new Error("Missing target");
|
|
expect(context.resolve(method.key.callee)).toMatchObject({
|
|
complete: true,
|
|
callableIds: [target.callableId],
|
|
});
|
|
|
|
const callStatement = context.state.program.body[2];
|
|
if (
|
|
!t.isExpressionStatement(callStatement) ||
|
|
!t.isCallExpression(callStatement.expression)
|
|
)
|
|
throw new Error("Missing pick call");
|
|
expect(context.resolve(callStatement.expression.callee)).toMatchObject({
|
|
complete: true,
|
|
callableIds: [target.callableId],
|
|
});
|
|
});
|
|
|
|
it("resolves calls in computed method keys before subsequent pattern writes", () => {
|
|
const analysis = analyzeJavaScriptSemantics(`
|
|
function original() {}
|
|
function replacement() {}
|
|
let make = original;
|
|
const obj = { [make()]() { return 1; } };
|
|
if (flag) [make] = [replacement];
|
|
`);
|
|
const original = analysis.callables.find(({ name }) => name === "original");
|
|
expect(original).toBeDefined();
|
|
expect(analysis.callSites[0]).toMatchObject({
|
|
resolution: "exact",
|
|
calleeCallableIds: [original?.callableId],
|
|
});
|
|
});
|
|
|
|
it.each([64, 128])(
|
|
"indexes receiver effects once for %s reads before distinct writes",
|
|
(count) => {
|
|
const source = `const object = {}; ${"object.x;".repeat(count)} ${Array.from({ length: count }, (_, i) => `object.p${i}=1;`).join("")} const make=()=>1; function use(){make();} use();`;
|
|
const file = parseJavaScriptSource(source, "fixture.js");
|
|
if (file === null) throw Error("Invalid fixture");
|
|
const steps = indexJavaScriptBindingsWithReceiversSteps(file.program);
|
|
let step = steps.next();
|
|
while (!step.done) step = steps.next();
|
|
const index = step.value;
|
|
for (const _ of index.indexMemberMutationFactsSteps()) {
|
|
void _;
|
|
}
|
|
const state = index.bindings;
|
|
const object = [...state.bindingsById.values()].find(
|
|
(b) => b.name === "object",
|
|
);
|
|
const make = [...state.bindingsById.values()].find(
|
|
(b) => b.name === "make",
|
|
);
|
|
if (object === undefined || make === undefined)
|
|
throw Error("Missing fixture bindings");
|
|
let visits = 0;
|
|
Object.defineProperty(object, "memberWrites", {
|
|
value: new Proxy(object.memberWrites, {
|
|
get(target, key, receiver) {
|
|
if (typeof key === "string" && /^\d+$/.test(key)) visits++;
|
|
return Reflect.get(target, key, receiver);
|
|
},
|
|
}),
|
|
});
|
|
let checked = 0;
|
|
traverseJavaScriptAst(file.program, {
|
|
enter(node) {
|
|
if (
|
|
t.isCallExpression(node) &&
|
|
t.isIdentifier(node.callee, { name: "make" })
|
|
) {
|
|
expect(
|
|
semanticBindingAtRead(make, node.callee, state)
|
|
.hasDefiniteInitializer,
|
|
).toBe(true);
|
|
checked++;
|
|
}
|
|
},
|
|
});
|
|
expect(checked).toBe(1);
|
|
expect(object.memberWrites.length).toBe(count);
|
|
expect(visits).toBeLessThanOrEqual(count * 2);
|
|
},
|
|
);
|
|
|
|
it("indexes one statement body for distinct captured functions", () => {
|
|
const { state } = indexed(
|
|
`const make=()=>1; ${Array.from({ length: 128 }, (_, i) => `function use${i}(){make();} use${i}();`).join(" ")}`,
|
|
);
|
|
const binding = [...state.bindingsById.values()].find(
|
|
(b) => b.name === "make",
|
|
);
|
|
if (binding === undefined) throw Error("Missing fixture binding");
|
|
const lookup = vi.spyOn(state.program.body, "indexOf");
|
|
let reads = 0;
|
|
try {
|
|
traverseJavaScriptAst(state.program, {
|
|
enter(node) {
|
|
if (
|
|
t.isCallExpression(node) &&
|
|
t.isIdentifier(node.callee, { name: "make" })
|
|
) {
|
|
reads++;
|
|
expect(
|
|
semanticBindingAtRead(binding, node.callee, state)
|
|
.hasDefiniteInitializer,
|
|
).toBe(true);
|
|
}
|
|
},
|
|
});
|
|
expect(reads).toBe(128);
|
|
expect(lookup.mock.calls.length).toBeLessThanOrEqual(1);
|
|
} finally {
|
|
lookup.mockRestore();
|
|
}
|
|
});
|