refactor(javascript): share computed-key callable ownership

Give calls, captures, object operations, promises and effects one callable-owner tracker. Suspend a method while its computed key is evaluated in the enclosing callable; remove the capture-only downstream repair.

Co-authored-by: Alex Desir <adesir089@gmail.com>
This commit is contained in:
morlutoandAlex Desir committed 2026-10-14 15:20:07 +08:00
1 parent 6396851958
commit 76fa5304a8
8 files changed
+245 -107

No files matched your search

@@ -158,6 +158,121 @@ describe("JavaScript semantic analysis: calls 1", () => {
});
});
describe("JavaScript semantic analysis: computed method keys", () => {
it("evaluates computed method keys in the enclosing callable", () => {
const ir = analyzeJavaScriptSemantics(`
function outer() {
const name = "m";
const pick = () => "n";
const obj = { [name]() { return 1; }, [pick()]() { return name; } };
class C { [name]() { return 2; } }
}
`);
const outer = onlyCallable(ir, "outer");
const pickCall = ir.callSites.find(
({ calleeCallableIds }) => calleeCallableIds.length === 1,
);
expect(pickCall).toMatchObject({
callerCallableId: outer.callableId,
resolution: "exact",
});
// Only the method body that reads `name` captures it; the computed keys
// are evaluated outside every method environment.
expect(
ir.closureCaptures.map(({ callableId, bindingId }) => ({
owner: ir.callables.find(
(callable) => callable.callableId === callableId,
)?.kind,
binding: bindingById(ir, bindingId)?.name,
})),
).toEqual([{ owner: "method", binding: "name" }]);
});
it("keeps callables nested in a computed method key in their own frames", () => {
const ir = analyzeJavaScriptSemantics(`
function outer() {
const name = "m";
const pick = () => "n";
return { [(() => pick())()]() { return name; } };
}
`);
const outer = onlyCallable(ir, "outer");
const pick = onlyCallable(ir, "pick");
const keyArrow = ir.callables.find(
({ kind, name }) => kind === "function" && name === null,
);
const method = ir.callables.find(({ kind }) => kind === "method");
expect(keyArrow).toBeDefined();
expect(method).toBeDefined();
const callerOf = (calleeCallableId: string | undefined) =>
ir.callSites.find(({ calleeCallableIds }) =>
calleeCallableIds.includes(calleeCallableId ?? ""),
)?.callerCallableId;
expect(callerOf(pick.callableId)).toBe(keyArrow?.callableId);
expect(callerOf(keyArrow?.callableId)).toBe(outer.callableId);
expect(
ir.closureCaptures.map(({ callableId, bindingId }) => ({
callableId,
binding: bindingById(ir, bindingId)?.name,
})),
).toEqual(
expect.arrayContaining([
{ callableId: keyArrow?.callableId, binding: "pick" },
{ callableId: method?.callableId, binding: "name" },
]),
);
});
it("attributes effects in computed method keys to the enclosing callable", () => {
const ir = analyzeJavaScriptSemantics(`
import { spawn } from "node:child_process";
function outer(config) {
return {
[fetch("/x")]() {},
[setTimeout(tick, 1)]() {},
[Promise.resolve(1)]() {},
[spawn("ls")]() {},
[config.key]() {},
};
}
class C { [fetch("/y")]() {} }
`);
const outer = onlyCallable(ir, "outer");
const owners = (
operations: readonly { readonly ownerCallableId: string | null }[],
) => operations.map(({ ownerCallableId }) => ownerCallableId);
const methodIds = new Set(
ir.callables
.filter(({ kind }) => kind === "method")
.map(({ callableId }) => callableId),
);
expect(methodIds.size).toBe(6);
const classId = onlyCallable(ir, "C").callableId;
expect(owners(ir.requestOperations).sort()).toEqual(
[outer.callableId, classId].sort(),
);
expect(owners(ir.timerOperations)).toEqual([outer.callableId]);
expect(owners(ir.promiseOperations)).toEqual([outer.callableId]);
expect(owners(ir.childProcessSpawns)).toEqual([outer.callableId]);
expect(
ir.objectOperations.filter(({ propertyPath }) =>
propertyPath?.includes("key"),
),
).toEqual([expect.objectContaining({ ownerCallableId: outer.callableId })]);
for (const owner of [
...owners(ir.requestOperations),
...owners(ir.timerOperations),
...owners(ir.promiseOperations),
...owners(ir.childProcessSpawns),
...owners(ir.objectOperations),
])
expect(methodIds.has(owner ?? "")).toBe(false);
});
});
describe("JavaScript semantic analysis: calls 2", () => {
it("does not treat a shadowed Promise binding as the intrinsic", () => {
const ir = analyzeJavaScriptSemantics(`