Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a262e1c220 | ||
|
|
840d707257 | ||
|
|
91a863c73a |
No files matched your search
@@ -82,6 +82,29 @@ it("bounds shared partial callback alias expansion by AST size", () => {
|
||||
expect(large.referenceReads).toBeLessThan(3 * small.referenceReads);
|
||||
});
|
||||
|
||||
it("bounds reassignment meshes by write pairs when reads select every write", () => {
|
||||
const measure = (writes: number) => {
|
||||
const context = indexed(`function f(){} function g(){} let a = f, b = g;
|
||||
function run(){ ${Array.from({ length: writes }, (_, i) => (i % 2 === 0 ? "a=b;" : "b=a;")).join("")} a(); }`);
|
||||
const run = context.state.program.body.at(-1);
|
||||
if (!t.isFunctionDeclaration(run)) throw new Error("Missing run");
|
||||
const last = run.body.body.at(-1);
|
||||
if (!t.isExpressionStatement(last) || !t.isCallExpression(last.expression))
|
||||
throw new Error("Missing call");
|
||||
const reads = vi.spyOn(context.bindingCache, "get");
|
||||
const result = context.resolve(last.expression.callee);
|
||||
expect(result).toMatchObject({ complete: false, cycleDependent: true });
|
||||
expect(
|
||||
result.callableIds.map((id) => context.callableById.get(id)?.name).sort(),
|
||||
).toEqual(["f", "g"]);
|
||||
return reads.mock.calls.length;
|
||||
};
|
||||
const small = measure(6);
|
||||
const large = measure(10);
|
||||
expect(small).toBeGreaterThan(0);
|
||||
expect(large).toBeLessThan(3 * small);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["if(flag){cb=fallback;cb();}", true],
|
||||
["if(flag)cb=fallback;cb();", false],
|
||||
|
||||
@@ -33,6 +33,8 @@ interface CallResolutionContext {
|
||||
readonly state: JavaScriptSemanticAnalysisState;
|
||||
readonly callableById: ReadonlyMap<string, JavaScriptSemanticCallable>;
|
||||
readonly seenInitializers: ReadonlySet<JavaScriptSemanticInitializer>;
|
||||
/** Bindings on the active chain whose read selected every initializer. */
|
||||
readonly seenBindings: ReadonlySet<JavaScriptSemanticBindingState>;
|
||||
readonly bindingCache: WeakMap<t.Identifier, LocalCallableResolution>;
|
||||
}
|
||||
|
||||
@@ -53,6 +55,7 @@ export const resolveLocalCallables = (
|
||||
state: input.state,
|
||||
callableById: input.callableById,
|
||||
seenInitializers: input.seenInitializers,
|
||||
seenBindings: new Set(),
|
||||
bindingCache: input.bindingCache,
|
||||
});
|
||||
|
||||
@@ -103,7 +106,28 @@ const resolveIdentifier = (
|
||||
node,
|
||||
context.state.parentsByNode,
|
||||
);
|
||||
const candidates = resolveBindingCallables(read.binding, context);
|
||||
// A read that selects every initializer resolves the same candidates
|
||||
// wherever the chain re-enters it, and a longer chain can only cut more of
|
||||
// them, so re-entry adds no target. Closing that cycle at the binding bounds
|
||||
// an alias mesh by pairs of writes instead of every initializer order.
|
||||
// Position-narrowed reads keep their per-initializer cycles.
|
||||
const selectsAll = read.binding === binding;
|
||||
if (selectsAll && context.seenBindings.has(binding))
|
||||
return {
|
||||
callableIds: [],
|
||||
complete: false,
|
||||
cycleDependent: true,
|
||||
reason: `Call target initializer cycle at ${binding.name}.`,
|
||||
};
|
||||
const candidates = resolveBindingCallables(
|
||||
read.binding,
|
||||
selectsAll
|
||||
? {
|
||||
...context,
|
||||
seenBindings: new Set(context.seenBindings).add(binding),
|
||||
}
|
||||
: context,
|
||||
);
|
||||
// Knowing every local candidate does not prove that any write replaced the
|
||||
// incoming value, or that a write in another activation precedes this read.
|
||||
const covered =
|
||||
|
||||
Reference in new issue
Block a user