Compare commits

...
Author SHA1 Message Date
Kaoru0822andClaude Opus 5.5 a262e1c220 test(javascript): pin mesh call targets and cycle dependence
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:09:30 +00:00
Kaoru0822andClaude Opus 5.5 840d707257 docs(javascript): state the quadratic bound for full-read alias meshes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:08:06 +00:00
Kaoru0822andClaude Opus 5.5 91a863c73a fix(javascript): close full-read alias cycles at the binding in call resolution
#4243 moved local call resolution from binding-level to initializer-level
cycle detection. Results inside a cycle are cycle-dependent and never cached,
so a mesh of mutual reassignments whose reads select every write enumerated
every initializer order: 12 writes took 1.5 s and each further pair ~35x more.

A read that selects every initializer resolves the same candidates wherever
the chain re-enters it, and a longer chain only cuts more of them, so closing
that cycle at the binding adds no callable and keeps the mesh linear (12
writes: 5 ms). Position-narrowed reads keep their per-initializer cycles.

Refs: #4348

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:00:11 +00:00
2 changed files with 48 additions and 1 deletions

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 =