Compare commits

...
Author SHA1 Message Date
Kaoru0822andClaude Opus 5.5 99c2f9fb54 perf(javascript): select member-mutation origins after coverage
markValue selected a binding's capture origins, ordering every initializer
against the capture with semanticMutationInitializers, before the coverage
antichain rejected the revisit. On alias-heavy wasm2js output almost every
visit stops at coverage, so most of that work was discarded. Only the local
setter check of a non-delete write needs origins before coverage; select them
after coverage for every other effect.

The semantic phase on pdf.js's openjpeg_nowasm_fallback.js drops from 249.9 s
to 148.6 s with an identical IR digest. Semantic IR and module projections are
byte-identical across 125 real files.

Refs: #2498

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 01:48:24 +00:00
@@ -51,6 +51,10 @@ import {
type PropertyPath = JavaScriptSemanticPropertyPath;
type ValueEffect = "write" | "escape";
type SemanticCaptureOrigins = Pick<
JavaScriptSemanticBindingState,
"initializers" | "referenceInitializers" | "directOrigins"
>;
/** Preserve uncertainty from property writes and references exposed to calls, yielding during traversal and fixpoint iteration. */
@@ -244,6 +248,26 @@ export function* collectSemanticMemberMutationsSteps(
// A capture key already ends the array; a path stays one nested element.
const lifetimeKey = (captureKey: string, path: PropertyPath): string =>
`${captureKey.slice(0, -1)},${JSON.stringify(path)}]`;
// An owner initializer names one copied value; otherwise origins depend on
// where the alias captured the binding.
const captureOrigins = (
binding: JavaScriptSemanticBindingState,
capture: {
readonly originAt: t.Node | undefined;
readonly ownerInitializer?: t.Node;
},
): SemanticCaptureOrigins =>
capture.ownerInitializer !== undefined
? {
initializers: binding.initializers.filter(
({ node }) => node === capture.ownerInitializer,
),
referenceInitializers: [],
directOrigins: [],
}
: capture.originAt === undefined
? binding
: semanticMutationInitializers(binding, capture.originAt, parents);
const pendingReferences: {
readonly initializer: JavaScriptSemanticBindingState["referenceInitializers"][number];
readonly path: PropertyPath;
@@ -441,29 +465,16 @@ export function* collectSemanticMemberMutationsSteps(
index === selectorIndex ? { ...selector, excludedKeys } : key,
);
}
const origins =
current.ownerInitializer !== undefined
? {
initializers: binding.initializers.filter(
({ node }) => node === current.ownerInitializer,
),
referenceInitializers: [],
directOrigins: [],
}
: current.originAt === undefined
? binding
: semanticMutationInitializers(
binding,
current.originAt,
parents,
);
// A setter on the binding's own literal runs on assignment and may
// replace any member of its receiver, so the written key is unknown.
// Deleting the accessor never calls it.
// Deleting the accessor never calls it. Only that check needs origins
// before coverage; most revisits stop there, so others select later.
let origins: SemanticCaptureOrigins | undefined;
if (
effect === "write" &&
!t.isUnaryExpression(mutation, { operator: "delete" })
) {
origins = captureOrigins(binding, current);
const receiver = localSetterReceiver(origins.initializers, path);
if (receiver !== undefined) path = [...receiver, null];
}
@@ -488,6 +499,7 @@ export function* collectSemanticMemberMutationsSteps(
coveringPaths.set(pathState, covering);
}
if (!covering.retain(path)) continue;
origins ??= captureOrigins(binding, current);
// A recorded write invalidates the binding to a non-primitive value, so
// only its first write can target a primitive. Re-evaluating it would
// replay every earlier write and make repeated writes superlinear.