Files
reverse-engineering-anything/src/domain/javascript/javascriptSymbolicBindings.ts
T
Kaoru0822andClaude Opus 5.5 705058f3e2 fix(javascript): count one IPC observation per site-resolved channel (#5810)
* fix(javascript): count one IPC observation per site-resolved channel

A send with a local constant channel was recorded twice: once unresolved
by the syntax pass and once resolved by wrapper substitution, because
duplicates were keyed on the channel. A channel the call site determines
now resolves that observation in place. A channel supplied through a
wrapper argument stays a candidate beside the unresolved observation.
Invocation identity now follows the call that supplies the wrapper's
literal arguments, so one wrapper call reached through several entries
is recorded once.

Fixes #5710

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(javascript): keep wrapper invocations distinct while removing repeats

Taking the nearest literal call as invocation identity collapsed separate
outer calls that pass different handlers through one wrapper call. Entry
identity is restored, and a finding is a repeat only when an identical
finding was reached through the same trailing local calls. A registration
replaces its unresolved source observation only when the site also
determines its handler, so a parameter handler keeps the generic source
registration beside per-invocation candidates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(javascript): compare site-determined IPC handlers by value

In-place resolution compared handler representations, so a constant
channel with a named local handler (recorded as an identifier by the
syntax pass and as its function by substitution) stayed counted twice.
The handler is now site-determined when it evaluates to the same function
in the site's lexical environment. A replaced observation also removes
the syntax pass's unknown-channel contribution, so a fully resolved file
is no longer reported as partial.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(javascript): keep closures over invocation values as distinct IPC handlers

A handler node created in different wrapper activations may capture
different values, so the same function node alone does not prove the call
site determines the registration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(javascript): guard optional call paths in closure handler test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(javascript): decide IPC handler identity from activation captures

A closure is one site-determined handler only when it reads no binding of a
function this trace activated, such as a wrapper parameter or factory local.
Unused bindings in the activation no longer split a known listener, and
closures over outer-call values stay distinct when a shorter entry traced
the same wrapper call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(javascript): treat implicit captures and bundle modules precisely in IPC handler identity

An arrow handler reading its enclosing function's arguments or this captures
that activation. A bundle module factory runs once per module load, so its
bindings are shared by every trace rather than per-invocation captures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(javascript): attribute an arrow handler's inherited context to its owner

An arrow inherits this and arguments from the nearest non-arrow function, so
an activated arrow inside that owner does not make the handler depend on
the traced call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(javascript): skip suffix scans for per-invocation IPC handlers

Check activation capture before scanning recorded call chains, keep the
chains of capture-dependent handlers out of the index, append chains in
place, and treat an arrow's inherited super and new.target as captures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(javascript): supersede nested-entry IPC records and share module-init factories

A shorter entry traced alone no longer swallows the outer calls that invoke
the same wrapper sequence: the first fuller path takes over its record and
separate outer calls stay distinct invocations. A factory activation called
at module scope runs once per load, so its closure is shared unless the
trace itself makes that call. The resolver now owns bundle module-factory
recognition instead of an environment marker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 05:06:50 -07:00

1489 lines
50 KiB
TypeScript

import * as t from "@babel/types";
import {
semanticStaticPropertyKey,
semanticWriteSiteTarget,
semanticWriteTargets,
runtimeFunctionParameters,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import { javascriptBootstrapAt } from "./javascriptBootstrap.js";
import { ELECTRON_MODULE } from "./javascriptElectronMemberWrites.js";
import {
indexJavaScriptElectronIdentitySteps,
type JavaScriptElectronIdentity,
} from "./javascriptSemanticAnalysis.js";
import { semanticArrayIndex } from "./javascriptSemanticPropertyPaths.js";
import { symbolicLogicalRight } from "./javascriptSymbolicControlFlow.js";
import {
moduleFactory,
modulePropertyName,
} from "./javascriptStaticAnalysisHelpers.js";
import {
completeSemanticSteps,
traverseJavaScriptAst,
} from "./javascriptSemanticTraversal.js";
import {
resolveSemanticBindingState,
semanticResolutionBlocked,
type JavaScriptSemanticAnalysisState,
} from "./javascriptSemanticState.js";
import {
symbolicCondition,
symbolicUnion,
symbolicUnknown,
symbolicVariants,
symbolicTruth,
type SymbolicCondition,
type SymbolicMetadata,
type SymbolicValue,
type SymbolicEnvironment,
type SymbolicFunction,
} from "./javascriptSymbolicValues.js";
interface PropertyWrite {
readonly path: readonly string[];
readonly value: t.Node;
readonly site: t.Node;
}
interface SymbolicReturnFlow extends SymbolicMetadata {
readonly values: readonly SymbolicValue[];
readonly fallsThrough: boolean;
}
interface SymbolicPropertyRead {
readonly node: t.Node;
readonly environment: SymbolicEnvironment;
}
interface SymbolicReadBoundary {
readonly node: t.Node;
readonly edge: "start" | "end";
}
type SymbolicNamespace = Extract<SymbolicValue, { kind: "namespace" }>;
const emptyEnvironment: SymbolicEnvironment = { values: new Map() };
const isObjectPrototypeSetter = (
property: t.ObjectMethod | t.ObjectProperty,
name: string,
): boolean =>
t.isObjectProperty(property) &&
!property.computed &&
!property.shorthand &&
name === "__proto__";
const containsOwnedReturn = (root: t.Node): boolean => {
let found = false;
traverseJavaScriptAst(root, {
skip: (node) => found || t.isFunction(node) || t.isClass(node),
enter: (node) => {
if (t.isReturnStatement(node)) found = true;
},
});
return found;
};
/** Demand-driven substitutions for local bindings, closures and literal module tables. */
export class JavaScriptSymbolicBindings {
readonly #writes = new Map<string, PropertyWrite[]>();
readonly #environments = new WeakMap<t.Node, SymbolicEnvironment>();
readonly #cache = new WeakMap<
SymbolicEnvironment,
WeakMap<t.Node, SymbolicValue>
>();
readonly #active = new Set<t.Node>();
readonly #activeFunctions = new Set<t.Function>();
readonly #exportAliases = new Map<string, string>();
readonly #moduleBindings = new Set<string>();
readonly #completedModules = new WeakMap<
SymbolicEnvironment,
SymbolicValue
>();
readonly #completionGroups = new WeakMap<
SymbolicValue,
{
readonly members: ReadonlySet<SymbolicValue>;
readonly common: SymbolicNamespace;
}
>();
readonly #propertyActive = new WeakMap<object, Set<string>>();
readonly #literalPropertyIndex = new WeakMap<
t.ObjectExpression,
ReadonlyMap<string, t.ObjectMethod | t.ObjectProperty> | null
>();
readonly #mutated = new Set<string>();
readonly #electronIdentity: JavaScriptElectronIdentity;
readonly #namespaceOwners = new Map<string, t.Function>();
readonly #moduleFactories = new WeakSet<t.Function>();
readonly #unsafeExports = new Set<string>();
readonly #aliasesByBinding = new Map<string, string[]>();
readonly #bootstrapTables: Array<{
readonly tableAliases: ReadonlySet<string>;
readonly modules: Map<string, SymbolicValue>;
}> = [];
#remainingEvaluations = 1_000_000;
#depth = 0;
truncated = false;
unresolved = false;
constructor(
readonly state: JavaScriptSemanticAnalysisState,
program: t.Program,
electronIdentity = completeSemanticSteps(
indexJavaScriptElectronIdentitySteps(program, state),
),
) {
this.#electronIdentity = electronIdentity;
this.#indexBindingAliases();
traverseJavaScriptAst(program, {
enter: (node) => {
if (t.isCallExpression(node)) this.#indexBootstrap(node);
},
});
traverseJavaScriptAst(program, {
enter: (node) => {
const target = semanticWriteSiteTarget(node);
if (target !== null) this.#indexWriteSite(target, node);
if (
t.isCallExpression(node) &&
t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.object, { name: "Object" }) &&
!resolveSemanticBindingState(state, node.callee.object, "Object") &&
semanticStaticPropertyKey(
node.callee.property,
node.callee.computed,
) === "defineProperty"
) {
const [target, key, descriptor] = node.arguments;
if (
t.isIdentifier(target) &&
t.isStringLiteral(key) &&
t.isObjectExpression(descriptor)
) {
const value = descriptor.properties.find(
(p) =>
t.isObjectProperty(p) &&
semanticStaticPropertyKey(p.key, p.computed) === "value",
);
if (t.isObjectProperty(value))
this.#addWrite(target, [key.value], value.value, node);
}
}
},
});
for (const table of this.#bootstrapTables)
this.#invalidateBootstrapTable(table.tableAliases, table.modules);
}
/** Whether a function is a recognized bundle module factory, which runs once per module load. */
isModuleFactory(node: t.Node): boolean {
return t.isFunction(node) && this.#moduleFactories.has(node);
}
environmentAt(node: t.Node): SymbolicEnvironment {
let current: t.Node | undefined = node;
while (current) {
const found = this.#environments.get(current);
if (found) return found;
current = this.state.parentsByNode.get(current);
}
return emptyEnvironment;
}
evaluate(
input: t.Node | null | undefined,
environment?: SymbolicEnvironment,
): SymbolicValue {
if (!input) return { kind: "literal", value: undefined };
if (--this.#remainingEvaluations < 0 || this.#depth >= 128) {
this.truncated = true;
return symbolicUnknown("symbolic resource limit");
}
const node = unwrapJavaScriptExpression(input).node;
const env = environment ?? this.environmentAt(node);
const cache = this.#cache.get(env) ?? new WeakMap<t.Node, SymbolicValue>();
this.#cache.set(env, cache);
const cached = cache.get(node);
if (cached) return cached;
if (this.#active.has(node)) return symbolicUnknown("cyclic binding");
this.#active.add(node);
this.#depth += 1;
try {
const value = this.#evaluate(node, env);
cache.set(node, value);
return value;
} finally {
this.#active.delete(node);
this.#depth -= 1;
}
}
#withConditions(
value: SymbolicValue,
source: SymbolicMetadata,
): SymbolicValue {
const conditions = source.conditions ?? [];
if (conditions.length === 0 && !source.conditionUnresolved) return value;
const variants = symbolicVariants(value);
// Charge the representation expansion before allocating copies. A long
// sequence of early exits otherwise builds unbounded predicate prefixes.
for (const variant of variants) {
const cost =
1 +
(conditions.length
? conditions.length + (variant.conditions?.length ?? 0)
: 0);
if (cost > this.#remainingEvaluations) {
this.#remainingEvaluations = 0;
this.truncated = true;
return symbolicUnknown("symbolic condition resource limit");
}
this.#remainingEvaluations -= cost;
}
return symbolicUnion(
variants.map((variant) => ({
...variant,
...(conditions.length
? { conditions: [...(variant.conditions ?? []), ...conditions] }
: {}),
...(source.conditionUnresolved
? { conditionUnresolved: true as const }
: {}),
})),
);
}
property(
value: SymbolicValue,
key: string,
read?: SymbolicPropertyRead,
): SymbolicValue {
let result: SymbolicValue;
if (value.kind === "union")
result = symbolicUnion([
...this.#commonProperty(value.values, key, read),
...value.values.map((variant) => this.property(variant, key, read)),
]);
else result = this.#property(value, key, read);
return this.#withConditions(result, value);
}
#commonProperty(
values: readonly SymbolicValue[],
key: string,
read?: SymbolicPropertyRead,
): readonly SymbolicValue[] {
const first = values[0];
const group =
first === undefined ? undefined : this.#completionGroups.get(first);
if (group === undefined || group.members.size !== values.length) return [];
if (values.length > this.#remainingEvaluations) {
this.truncated = true;
this.#remainingEvaluations = 0;
return [];
}
this.#remainingEvaluations -= values.length;
// Only the complete, original partition proves an unconditional fact.
// Added predicates or a subset of exits cannot borrow this proof.
if (!values.every((value) => group.members.has(value))) return [];
const common = this.property(group.common, key, read);
return common.kind === "unknown" ? [] : [common];
}
#property(
value: SymbolicValue,
key: string,
read?: SymbolicPropertyRead,
): SymbolicValue {
const active = this.#propertyActive.get(value) ?? new Set<string>();
if (active.has(key)) return symbolicUnknown("cyclic property");
active.add(key);
this.#propertyActive.set(value, active);
try {
return this.#propertyValue(value, key, read);
} finally {
active.delete(key);
}
}
#propertyValue(
value: SymbolicValue,
key: string,
read?: SymbolicPropertyRead,
): SymbolicValue {
if (value.kind === "external") {
const path = [...value.path, key];
if (
ELECTRON_MODULE.test(path[0] ?? "") &&
!this.#electronIdentity.intactExport(path)
) {
return symbolicUnknown("overwritten Electron API member");
}
return { kind: "external", path };
}
if (value.kind === "record") {
const property = value.values.get(key);
if (
key === "exports" &&
property?.kind === "namespace" &&
this.#namespaceOwners.has(property.bindingId)
) {
// Preserve namespace identity for members, not a known runtime value.
return { ...property, runtimeValueUnknown: true };
}
return property ?? symbolicUnknown("missing property");
}
if (value.kind === "namespace") {
if (
this.#mutated.has(value.bindingId) ||
this.#unsafeExports.has(`${value.bindingId}:${key}`)
)
return symbolicUnknown("ambiguous export writes");
const available = this.#namespaceWrites(value, read, key);
if (available === undefined) {
this.unresolved = true;
return symbolicUnknown("module export read boundary is unavailable");
}
const replacement = available.findLast(
(write) => write.path.length === 0,
);
if (replacement)
return this.property(
this.evaluate(replacement.value, value.environment),
key,
read,
);
const writes = available.filter(
(w) => w.path.length === 1 && w.path[0] === key,
);
const write = writes.at(-1);
return write
? this.evaluate(write.value, value.environment)
: symbolicUnknown("unresolved module export");
}
if (value.kind === "object") {
let index = this.#literalPropertyIndex.get(value.node);
if (index === undefined) {
const properties = new Map<string, t.ObjectMethod | t.ObjectProperty>();
index = properties;
for (const property of value.node.properties) {
const name = t.isSpreadElement(property)
? null
: semanticStaticPropertyKey(property.key, property.computed);
if (name === null || t.isSpreadElement(property)) {
index = null;
break;
}
if (isObjectPrototypeSetter(property, name)) continue;
properties.set(name, property);
}
this.#literalPropertyIndex.set(value.node, index);
}
if (index !== null) {
const property = index.get(key);
if (property !== undefined)
return this.#objectPropertyValue(property, value.environment);
if (key === "__proto__") {
const prototypeSetter = [...value.node.properties]
.reverse()
.filter(
(candidate): candidate is t.ObjectMethod | t.ObjectProperty =>
!t.isSpreadElement(candidate),
)
.find((candidate) => isObjectPrototypeSetter(candidate, key));
if (prototypeSetter !== undefined)
return this.#objectPropertyValue(
prototypeSetter,
value.environment,
);
}
return symbolicUnknown("missing property");
}
for (const p of [...value.node.properties].reverse()) {
if (t.isSpreadElement(p)) {
const spread = this.evaluate(p.argument, value.environment);
const found = this.property(spread, key, {
node: p.argument,
environment: value.environment,
});
if (found.kind !== "unknown") return found;
if (
this.unknownMembers(spread) > 0 ||
this.members(spread).includes(key)
)
return found;
} else if (semanticStaticPropertyKey(p.key, p.computed) === key) {
return this.#objectPropertyValue(p, value.environment);
} else if (semanticStaticPropertyKey(p.key, p.computed) === null)
return symbolicUnknown("dynamic property overwrite");
}
}
if (value.kind === "array") {
const index = semanticArrayIndex(key);
if (index !== null)
return value.values[index] ?? symbolicUnknown("missing argument");
}
if (value.kind === "iterator" || value.kind === "promise") {
this.unresolved = true;
return symbolicUnknown(`${value.kind} members are not modeled`);
}
return symbolicUnknown("unresolved property");
}
#objectPropertyValue(
property: t.ObjectMethod | t.ObjectProperty,
environment: SymbolicEnvironment,
): SymbolicValue {
return t.isObjectMethod(property) && property.kind !== "method"
? symbolicUnknown("accessor property")
: this.evaluate(
t.isObjectProperty(property) ? property.value : property,
environment,
);
}
members(
value: SymbolicValue,
active = new Set<SymbolicValue>(),
): readonly string[] {
if (active.has(value)) return [];
active.add(value);
if (value.kind === "union")
return [...new Set(value.values.flatMap((v) => this.members(v, active)))];
if (value.kind === "record") return [...value.values.keys()];
if (value.kind === "namespace") {
const available = this.#namespaceWrites(value);
if (available === undefined) return [];
const replacement = available.findLast(
(write) => write.path.length === 0,
);
return replacement
? this.members(
this.evaluate(replacement.value, value.environment),
active,
)
: [
...new Set(
available.flatMap((w) => (w.path.length === 1 ? w.path : [])),
),
];
}
if (value.kind !== "object") return [];
return [
...new Set(
value.node.properties.flatMap((p) =>
t.isSpreadElement(p)
? this.members(this.evaluate(p.argument, value.environment), active)
: [semanticStaticPropertyKey(p.key, p.computed)].filter(
(k): k is string =>
k !== null && !isObjectPrototypeSetter(p, k),
),
),
),
];
}
unknownMembers(
value: SymbolicValue,
active = new Set<SymbolicValue>(),
): number {
if (active.has(value)) return 1;
active.add(value);
if (value.kind === "union")
return value.values.reduce(
(sum, v) => sum + this.unknownMembers(v, active),
0,
);
if (value.kind === "record") return 0;
if (value.kind === "namespace") {
if (this.#mutated.has(value.bindingId)) return 1;
const available = this.#namespaceWrites(value);
if (available === undefined) return 1;
const replacement = available.findLast(
(write) => write.path.length === 0,
);
return replacement
? this.unknownMembers(
this.evaluate(replacement.value, value.environment),
active,
)
: 0;
}
if (value.kind !== "object") return 1;
return value.node.properties.reduce(
(sum, p) =>
sum +
(t.isSpreadElement(p)
? this.unknownMembers(
this.evaluate(p.argument, value.environment),
new Set(active),
)
: Number(semanticStaticPropertyKey(p.key, p.computed) === null)),
0,
);
}
/** Calling a generator creates an iterator; advancing it is not modeled. */
callBody(fn: SymbolicFunction): t.Function["body"] | undefined {
return fn.node.generator ? undefined : fn.node.body;
}
bind(
fn: SymbolicFunction,
args: readonly SymbolicValue[],
invocationNode?: t.Node,
caller?: SymbolicEnvironment,
): SymbolicEnvironment {
const values = new Map<string, SymbolicValue>();
runtimeFunctionParameters(fn.node).forEach((param, index) => {
const target = t.isRestElement(param) ? param.argument : param;
if (!t.isIdentifier(target)) return;
const binding = resolveSemanticBindingState(
this.state,
target,
target.name,
);
if (binding)
values.set(
binding.bindingId,
t.isRestElement(param)
? { kind: "array", values: args.slice(index) }
: (args[index] ?? symbolicUnknown("runtime parameter")),
);
});
const scopeId =
this.state.scopeByNode.get(fn.node)?.scopeId ??
this.state.scopeByNode.get(fn.node.body)?.scopeId;
return {
values,
activation: {},
parent: fn.environment,
...(caller === undefined ? {} : { caller }),
point: invocationNode ?? fn.node,
scopeNode: fn.node,
...(scopeId === undefined ? {} : { scopeId }),
};
}
arguments(
node: t.CallExpression | t.OptionalCallExpression,
env: SymbolicEnvironment,
): readonly SymbolicValue[] {
return node.arguments.flatMap((a) => {
if (!t.isSpreadElement(a)) return [this.evaluate(a, env)];
const value = this.evaluate(a.argument, env);
return value.kind === "array"
? value.values
: [symbolicUnknown("dynamic spread arguments")];
});
}
#evaluate(node: t.Node, env: SymbolicEnvironment): SymbolicValue {
if (
t.isStringLiteral(node) ||
t.isNumericLiteral(node) ||
t.isBooleanLiteral(node)
)
return { kind: "literal", value: node.value };
if (t.isNullLiteral(node)) return { kind: "literal", value: null };
if (t.isFunction(node))
return { kind: "function", node, environment: env, origins: [] };
if (t.isObjectExpression(node))
return { kind: "object", node, environment: env };
if (t.isIdentifier(node)) return this.#identifier(node, env);
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
const key = semanticStaticPropertyKey(node.property, node.computed);
return key === null
? symbolicUnknown("dynamic member")
: this.property(this.evaluate(node.object, env), key, {
node,
environment: env,
});
}
if (t.isSequenceExpression(node))
return this.evaluate(node.expressions.at(-1), env);
if (t.isAssignmentExpression(node)) {
if (node.operator === "=") return this.evaluate(node.right, env);
if (
node.operator === "&&=" ||
node.operator === "||=" ||
node.operator === "??="
)
return this.#logicalValue(
node.operator === "&&="
? "&&"
: node.operator === "||="
? "||"
: "??",
node.left,
node.right,
env,
true,
);
return symbolicUnknown("compound assignment result");
}
if (t.isAwaitExpression(node))
return this.#awaited(this.evaluate(node.argument, env));
if (t.isUnaryExpression(node)) {
const value = this.evaluate(node.argument, env);
if (node.operator === "void")
return { kind: "literal", value: undefined };
const truth = symbolicTruth(value);
if (node.operator === "!" && truth !== undefined)
return { kind: "literal", value: !truth };
}
if (t.isConditionalExpression(node)) {
const truth = symbolicTruth(this.evaluate(node.test, env));
return truth === undefined
? symbolicUnion([
symbolicCondition(this.evaluate(node.consequent, env), {
node: node.test,
positive: true,
}),
symbolicCondition(this.evaluate(node.alternate, env), {
node: node.test,
positive: false,
}),
])
: this.evaluate(truth ? node.consequent : node.alternate, env);
}
if (t.isLogicalExpression(node))
return this.#logicalValue(node.operator, node.left, node.right, env);
if (t.isCallExpression(node) || t.isOptionalCallExpression(node))
return this.#call(node, env);
return symbolicUnknown(`unsupported ${node.type}`);
}
#logicalValue(
operator: t.LogicalExpression["operator"],
leftNode: t.Node,
rightNode: t.Node,
env: SymbolicEnvironment,
assignment = false,
): SymbolicValue {
const value = this.evaluate(leftNode, env);
let left = value;
if (assignment) {
// A mutable namespace supplies member identity, not a callable root value.
// Assignment results must not resolve their own write through that identity.
const alternatives = symbolicUnion(
symbolicVariants(value).map((variant) =>
variant.kind === "namespace" && variant.runtimeValueUnknown
? this.#withConditions(
symbolicUnknown("mutable module exports value"),
variant,
)
: variant,
),
);
left =
value.kind === "union"
? this.#withConditions(alternatives, value)
: alternatives;
}
const right = symbolicLogicalRight(operator, left);
if (right !== undefined)
return right ? this.evaluate(rightNode, env) : left;
const leftCondition: SymbolicCondition = {
node: leftNode,
positive:
operator === "&&" ? false : operator === "||" ? true : "not-nullish",
};
const rightCondition: SymbolicCondition = {
node: leftNode,
positive:
operator === "&&" ? true : operator === "||" ? false : "nullish",
};
return symbolicUnion([
symbolicCondition(left, leftCondition),
symbolicCondition(this.evaluate(rightNode, env), rightCondition),
]);
}
#identifier(node: t.Identifier, env: SymbolicEnvironment): SymbolicValue {
const binding = resolveSemanticBindingState(this.state, node, node.name);
if (
!binding &&
node.name === "undefined" &&
!semanticResolutionBlocked(this.state, node, node.name)
)
return { kind: "literal", value: undefined };
if (!binding)
return !semanticResolutionBlocked(this.state, node, node.name) &&
["require", "navigator", "String", "Boolean"].includes(node.name)
? { kind: "external", path: [node.name] }
: symbolicUnknown("unbound identifier");
if (binding.definitions.some((d) => d.kind === "assignment"))
return symbolicUnknown("reassigned binding");
// Electron member identity is checked at the selected property. A write
// to one method must not erase the module origin or unaffected siblings.
const electronOrigin = this.#electronIdentity.exportPath(node);
if (electronOrigin === undefined && this.#mutated.has(binding.bindingId))
return symbolicUnknown("dynamic member writes");
for (let e: SymbolicEnvironment | undefined = env; e; e = e.parent) {
const value = e.values.get(binding.bindingId);
if (value) return value;
}
if (electronOrigin === undefined && this.#writes.has(binding.bindingId))
return symbolicUnknown("overwritten object members");
const initializer =
binding.initializers.length === 1 ? binding.initializers[0] : undefined;
if (!initializer)
return symbolicUnknown("missing or ambiguous initializer");
const boundary = this.#readBoundary(binding.scopeId, env, node);
const point =
boundary === undefined ? undefined : boundary.node[boundary.edge];
if (
!initializer.entryBody &&
!binding.definitions.every(({ kind }) => kind === "function") &&
(initializer.node.end == null ||
point == null ||
initializer.node.end > point)
) {
this.unresolved = true;
return symbolicUnknown("initializer not available at this read");
}
if (this.state.conditionallySkippedExpressions.has(initializer.node))
return symbolicUnknown("conditional initializer");
let value = this.evaluate(initializer.node, env);
for (const key of initializer.projection) {
if (key === null) return symbolicUnknown("dynamic destructuring");
value = this.property(value, String(key), {
node: initializer.node,
environment: env,
});
}
return value;
}
#readBoundary(
bindingScopeId: string,
environment: SymbolicEnvironment,
readNode: t.Node,
): SymbolicReadBoundary | undefined {
let current: SymbolicEnvironment | undefined = environment;
let child: SymbolicEnvironment | undefined;
while (current) {
if (this.#scopeContains(bindingScopeId, current.scopeId)) {
const point = child?.point ?? readNode;
if (current.completedAt !== undefined)
return { node: current.completedAt, edge: "end" };
if (current.scopeNode && !this.#nodeWithin(point, current.scopeNode))
return undefined;
return { node: point, edge: "start" };
}
child = current;
current = current.parent;
}
let outermost = environment;
while (outermost.parent) outermost = outermost.parent;
const point = outermost.point ?? environment.point ?? readNode;
return { node: point, edge: "start" };
}
#scopeContains(bindingScopeId: string, environmentScopeId?: string): boolean {
let scopeId: string | null = bindingScopeId;
while (scopeId !== null) {
if (scopeId === environmentScopeId) return true;
scopeId = this.state.scopesById.get(scopeId)?.parentScopeId ?? null;
}
return false;
}
#nodeWithin(node: t.Node, ancestor?: t.Node): boolean {
if (!ancestor) return false;
let current: t.Node | undefined = node;
while (current) {
if (current === ancestor) return true;
current = this.state.parentsByNode.get(current);
}
return false;
}
#completedEnvironment(
environment: SymbolicEnvironment,
activation: SymbolicEnvironment,
returnPoint: t.Node,
): SymbolicEnvironment | undefined {
if (environment === activation)
return { ...environment, completedAt: returnPoint };
if (environment.parent === undefined) return undefined;
const parent = this.#completedEnvironment(
environment.parent,
activation,
returnPoint,
);
return parent === undefined ? undefined : { ...environment, parent };
}
#awaited(value: SymbolicValue): SymbolicValue {
return symbolicUnion(
symbolicVariants(value).map((variant) =>
variant.kind === "promise"
? this.#withConditions(this.#awaited(variant.value), variant)
: variant,
),
);
}
#completeReturnedValue(
value: SymbolicValue,
activation: SymbolicEnvironment,
fallbackPoint: t.Node,
origins: readonly t.Node[],
): SymbolicValue {
if (value.kind === "union")
return symbolicUnion(
value.values.map((variant) =>
this.#completeReturnedValue(
variant,
activation,
fallbackPoint,
origins,
),
),
);
if (value.kind !== "function" && value.kind !== "promise") return value;
const { returnPoint, ...returned } = value;
const point = returnPoint ?? fallbackPoint;
if (returned.kind === "promise")
return {
...returned,
value: this.#completeReturnedValue(
returned.value,
activation,
point,
origins,
),
};
const environment = this.#completedEnvironment(
returned.environment,
activation,
point,
);
return {
...returned,
environment: environment ?? returned.environment,
origins: [...origins, ...returned.origins],
};
}
#namespaceReadBoundary(
value: SymbolicNamespace,
read: SymbolicPropertyRead,
): SymbolicReadBoundary | undefined {
const scopeId = value.environment.scopeId;
const activation = value.environment.activation;
if (scopeId === undefined || activation === undefined) return undefined;
const captured = this.#hasActivation(read.environment, activation);
// Replaying a factory-owned initializer/spread reads at capture, even
// when a later closure observes it after the activation completed.
if (
captured &&
this.#owningFunction(read.node) === value.environment.scopeNode
)
return { node: read.node, edge: "start" };
const caller = this.#namespaceCallerBoundary(
value,
read.environment,
activation,
);
if (caller !== undefined) return caller;
if (!captured) return undefined;
const boundary = this.#readBoundary(scopeId, read.environment, read.node);
return boundary !== undefined &&
this.#owningFunction(boundary.node) === value.environment.scopeNode
? boundary
: undefined;
}
#hasActivation(
environment: SymbolicEnvironment,
activation: object,
): boolean {
for (
let current: SymbolicEnvironment | undefined = environment;
current;
current = current.parent
) {
if (--this.#remainingEvaluations < 0) {
this.truncated = true;
return false;
}
if (current.activation === activation) return true;
}
return false;
}
#namespaceCallerBoundary(
value: SymbolicNamespace,
environment: SymbolicEnvironment,
activation: object,
): SymbolicReadBoundary | undefined {
for (
let current = environment;
current.caller !== undefined;
current = current.caller
) {
if (--this.#remainingEvaluations < 0) {
this.truncated = true;
return undefined;
}
const point = current.point;
if (
point !== undefined &&
this.#owningFunction(point) === value.environment.scopeNode &&
this.#hasActivation(current.caller, activation)
)
return { node: point, edge: "start" };
}
return undefined;
}
#owningFunction(node: t.Node): t.Function | undefined {
let current: t.Node | undefined = node;
while (current !== undefined && !t.isFunction(current))
current = this.state.parentsByNode.get(current);
return current;
}
#namespaceWrites(
value: SymbolicNamespace,
read?: SymbolicPropertyRead,
selectedKey?: string,
): readonly PropertyWrite[] | undefined {
const writes = this.#writes.get(value.bindingId) ?? [];
const completedAt = value.environment.completedAt;
const boundary =
read === undefined ? undefined : this.#namespaceReadBoundary(value, read);
if (this.#remainingEvaluations <= 0) {
this.truncated = true;
return undefined;
}
const sourcePoint = boundary?.node[boundary.edge];
if (boundary !== undefined && sourcePoint == null) return undefined;
if (completedAt !== undefined && completedAt.end == null) return undefined;
if (sourcePoint == null && completedAt === undefined)
return read === undefined ? writes : undefined;
// An owned snapshot cannot read beyond either its capture or this exit.
const point = Math.min(
sourcePoint ?? Infinity,
completedAt?.end ?? Infinity,
);
const commonThrough = value.environment.commonThrough?.end;
const changed = new Set<string>();
if (commonThrough != null) {
if (writes.length > this.#remainingEvaluations) {
this.#remainingEvaluations = 0;
this.truncated = true;
return undefined;
}
this.#remainingEvaluations -= writes.length;
for (const write of writes) {
if (write.site.end == null) return undefined;
if (
write.site.end > completedAt!.end! &&
write.site.end <= commonThrough
) {
if (write.path.length === 0) return [];
changed.add(write.path[0]!);
}
}
}
return writes.filter(
(write) =>
write.site.end != null &&
write.site.end <= point &&
(changed.size === 0 ||
(write.path.length > 0
? !changed.has(write.path[0]!)
: selectedKey !== undefined && !changed.has(selectedKey))),
);
}
#completeNamespace(
value: SymbolicNamespace,
point: t.Node,
commonThrough?: t.Node,
): SymbolicNamespace {
const values = new Map(value.environment.values);
const environment: SymbolicEnvironment = {
...value.environment,
values,
completedAt: point,
...(commonThrough === undefined ? {} : { commonThrough }),
};
const completed: SymbolicNamespace = { ...value, environment };
// A completed activation owns coherent views of both parameter aliases.
// Merely copying completedAt loses that proof when `exports` escapes.
// This map has only the recognized factory's module/exports/loader slots.
for (const [bindingId, slot] of value.environment.values) {
if (slot === value) values.set(bindingId, completed);
else if (
this.#moduleBindings.has(bindingId) &&
this.#exportAliases.get(bindingId) === value.bindingId &&
slot.kind === "record" &&
slot.values.get("exports") === value
) {
values.set(bindingId, {
...slot,
values: new Map(slot.values).set("exports", completed),
});
}
}
return completed;
}
#moduleExports(value: SymbolicNamespace): SymbolicValue {
const factory = this.#namespaceOwners.get(value.bindingId);
if (factory === undefined) return value;
const cached = this.#completedModules.get(value.environment);
if (cached !== undefined) return cached;
if (this.#activeFunctions.has(factory))
return symbolicUnknown("recursive module completion");
this.#activeFunctions.add(factory);
try {
// Factory return values are ignored by the loader. Each reachable exit
// still owns a distinct initialization boundary and branch predicates.
const flow = this.#returnFlow(
t.isBlockStatement(factory.body) ? factory.body.body : [],
value.environment,
(statement) => this.#completeNamespace(value, statement),
);
const completed = symbolicUnion([
...flow.values,
...(flow.fallsThrough
? [
this.#withConditions(
this.#completeNamespace(value, factory.body),
flow,
),
]
: []),
]);
const variants = symbolicVariants(completed);
if (
variants.length > 1 &&
variants.every(
(variant) =>
variant.kind === "namespace" &&
variant.environment.completedAt?.end != null &&
!variant.conditionUnresolved,
)
) {
if (variants.length > this.#remainingEvaluations) {
this.#remainingEvaluations = 0;
this.truncated = true;
} else {
this.#remainingEvaluations -= variants.length;
const exits = variants as readonly SymbolicNamespace[];
let first = exits[0]!.environment.completedAt!;
let last = first;
for (const exit of exits) {
const point = exit.environment.completedAt!;
if (point.end! < first.end!) first = point;
if (point.end! > last.end!) last = point;
}
const common = this.#completeNamespace(value, first, last);
const group = { members: new Set(variants), common };
for (const variant of variants)
this.#completionGroups.set(variant, group);
}
}
this.#completedModules.set(value.environment, completed);
return completed;
} finally {
this.#activeFunctions.delete(factory);
}
}
#call(
node: t.CallExpression | t.OptionalCallExpression,
env: SymbolicEnvironment,
): SymbolicValue {
const callee = this.evaluate(node.callee, env);
const args = this.arguments(node, env);
return symbolicUnion(
symbolicVariants(callee).map((value) => {
let result: SymbolicValue;
if (value.kind === "loader") {
const key = args[0];
const loaded =
key?.kind === "literal"
? (value.modules.get(String(key.value)) ??
symbolicUnknown("missing module"))
: symbolicUnknown("dynamic module");
result =
loaded.kind === "namespace" ? this.#moduleExports(loaded) : loaded;
} else if (
value.kind === "external" &&
value.path.join(".") === "require"
) {
const spec = args[0];
result =
spec?.kind === "literal" && typeof spec.value === "string"
? { kind: "external", path: [spec.value] }
: symbolicUnknown("dynamic require");
} else if (
value.kind !== "function" ||
this.#activeFunctions.has(value.node)
) {
result = symbolicUnknown("unresolved or recursive call");
} else if (this.callBody(value) === undefined) {
// Calling a generator creates an iterator without running its body.
result = { kind: "iterator" };
} else {
this.#activeFunctions.add(value.node);
try {
const bound = this.bind(value, args, node, env);
const returned = t.isBlockStatement(value.node.body)
? this.#returns(value.node.body.body, bound)
: this.evaluate(value.node.body, bound);
const completed = this.#completeReturnedValue(
returned,
bound,
value.node.body,
[...value.origins, node],
);
// Async calls return a truthy Promise; only await selects its value.
result = value.node.async
? { kind: "promise", value: this.#awaited(completed) }
: completed;
} finally {
this.#activeFunctions.delete(value.node);
}
}
return this.#withConditions(result, value);
}),
);
}
#returns(
statements: readonly t.Statement[],
env: SymbolicEnvironment,
): SymbolicValue {
const flow = this.#returnFlow(statements, env, (statement) => {
const value = this.evaluate(statement.argument, env);
return symbolicUnion(
symbolicVariants(value).map((variant) =>
variant.kind === "function" || variant.kind === "promise"
? { ...variant, returnPoint: statement }
: variant,
),
);
});
return symbolicUnion([
...flow.values,
...(flow.fallsThrough
? [this.#withConditions({ kind: "literal", value: undefined }, flow)]
: []),
]);
}
#returnFlow(
statements: readonly t.Statement[],
env: SymbolicEnvironment,
returnedValue: (statement: t.ReturnStatement) => SymbolicValue,
depth = 0,
): SymbolicReturnFlow {
const possible: SymbolicValue[] = [];
let unresolvedFallthrough = false;
const continuationConditions: SymbolicCondition[] = [];
const guardCurrentPath = (value: SymbolicValue): SymbolicValue =>
this.#withConditions(value, {
conditions: continuationConditions,
...(unresolvedFallthrough ? { conditionUnresolved: true } : {}),
});
for (const statement of statements) {
if (--this.#remainingEvaluations < 0 || depth >= 128) {
this.truncated = true;
return {
values: [...possible, symbolicUnknown("symbolic resource limit")],
fallsThrough: false,
};
}
if (t.isThrowStatement(statement))
return {
values: [...possible, symbolicUnknown("wrapper exits by throwing")],
fallsThrough: false,
};
if (t.isReturnStatement(statement)) {
return {
values: [...possible, guardCurrentPath(returnedValue(statement))],
fallsThrough: false,
};
}
if (t.isBlockStatement(statement)) {
const flow = this.#returnFlow(
statement.body,
env,
returnedValue,
depth + 1,
);
possible.push(...flow.values.map(guardCurrentPath));
if (!flow.fallsThrough)
return { values: possible, fallsThrough: false };
continuationConditions.push(...(flow.conditions ?? []));
unresolvedFallthrough ||= flow.conditionUnresolved === true;
}
if (t.isIfStatement(statement)) {
const truth = symbolicTruth(this.evaluate(statement.test, env));
const branches =
truth === undefined
? [statement.consequent, statement.alternate]
: [truth ? statement.consequent : statement.alternate];
const flows = branches.map((branch) =>
this.#returnFlow(
branch ? [branch] : [],
env,
returnedValue,
depth + 1,
),
);
possible.push(
...flows.flatMap((flow, index) =>
flow.values.map((value) => {
const guarded = guardCurrentPath(value);
return truth === undefined
? symbolicCondition(guarded, {
node: statement.test,
positive: index === 0,
})
: guarded;
}),
),
);
if (flows.every((flow) => !flow.fallsThrough))
return { values: possible, fallsThrough: false };
if (truth !== undefined) {
continuationConditions.push(...(flows[0]?.conditions ?? []));
unresolvedFallthrough ||= flows[0]?.conditionUnresolved === true;
} else {
const fallthrough = flows.flatMap((flow, index) =>
flow.fallsThrough ? [{ flow, index }] : [],
);
if (
fallthrough.length === 1 &&
fallthrough[0]?.flow.values.length === 0
)
continuationConditions.push({
node: statement.test,
positive: fallthrough[0].index === 0,
});
else if (
fallthrough.length > 0 &&
(fallthrough.length < flows.length ||
fallthrough.some(({ flow }) => flow.values.length > 0))
)
unresolvedFallthrough = true;
}
}
if (
t.isLoop(statement) ||
t.isSwitchStatement(statement) ||
t.isTryStatement(statement) ||
t.isLabeledStatement(statement) ||
t.isWithStatement(statement)
) {
if (containsOwnedReturn(statement)) {
possible.push(
symbolicUnknown(
`unsupported return control flow: ${statement.type}`,
),
);
unresolvedFallthrough = true;
}
}
}
return {
values: possible,
fallsThrough: true,
...(continuationConditions.length
? { conditions: continuationConditions }
: {}),
...(unresolvedFallthrough ? { conditionUnresolved: true } : {}),
};
}
#indexBootstrap(call: t.CallExpression): void {
const bootstrap = javascriptBootstrapAt(call, this.state);
if (!bootstrap) return;
const modules = new Map<string, SymbolicValue>();
const tableAliases = new Set([bootstrap.tableBindingId]);
const pendingAliases = [bootstrap.tableBindingId];
while (pendingAliases.length > 0) {
const source = pendingAliases.pop()!;
for (const alias of this.#aliasesByBinding.get(source) ?? [])
if (!tableAliases.has(alias)) {
tableAliases.add(alias);
pendingAliases.push(alias);
}
}
for (const property of bootstrap.table.properties) {
const factory = moduleFactory(property);
if (!factory) continue;
this.#moduleFactories.add(factory);
const moduleKey = modulePropertyName(property);
const values = new Map<string, SymbolicValue>();
const environment: SymbolicEnvironment = {
values,
activation: {},
point: factory.body,
scopeNode: factory,
...(this.state.scopeByNode.get(factory)?.scopeId === undefined
? {}
: { scopeId: this.state.scopeByNode.get(factory)!.scopeId }),
};
const [first, second, third] = runtimeFunctionParameters(factory);
const binding = (n: t.Node | undefined) =>
t.isIdentifier(n)
? resolveSemanticBindingState(this.state, n, n.name)?.bindingId
: undefined;
const moduleId = binding(first);
const exportsId = binding(second);
const loaderId = binding(third);
const namespaceId = `${String(factory.start)}:exports`;
this.#namespaceOwners.set(namespaceId, factory);
const namespace: SymbolicValue = {
kind: "namespace",
bindingId: namespaceId,
environment,
};
if (moduleId) {
values.set(moduleId, {
kind: "record",
values: new Map([["exports", namespace]]),
});
this.#exportAliases.set(moduleId, namespaceId);
this.#moduleBindings.add(moduleId);
}
if (exportsId) {
values.set(exportsId, namespace);
this.#exportAliases.set(exportsId, namespaceId);
}
if (loaderId) values.set(loaderId, { kind: "loader", modules });
this.#environments.set(factory, environment);
modules.set(moduleKey, namespace);
}
this.#bootstrapTables.push({ tableAliases, modules });
}
#indexBindingAliases(): void {
for (const binding of this.state.bindingsById.values()) {
const initializer = binding.initializers[0];
if (
!initializer ||
initializer.entryBody !== undefined ||
initializer.projection.length !== 0 ||
!t.isIdentifier(initializer.node)
)
continue;
const source = resolveSemanticBindingState(
this.state,
initializer.node,
initializer.node.name,
);
if (!source) continue;
const aliases = this.#aliasesByBinding.get(source.bindingId) ?? [];
aliases.push(binding.bindingId);
this.#aliasesByBinding.set(source.bindingId, aliases);
}
}
#invalidateBootstrapTable(
tableAliases: ReadonlySet<string>,
modules: Map<string, SymbolicValue>,
): void {
let invalidateAll = false;
const invalidSlots = new Set<string>();
for (const bindingId of tableAliases) {
if (this.#mutated.has(bindingId)) invalidateAll = true;
for (const definition of this.state.bindingsById.get(bindingId)
?.definitions ?? [])
if (definition.kind === "assignment") invalidateAll = true;
for (const write of this.#writes.get(bindingId) ?? []) {
if (write.path.length === 0) invalidateAll = true;
else invalidSlots.add(write.path[0]!);
}
}
if (invalidateAll) modules.clear();
else for (const key of invalidSlots) modules.delete(key);
}
#indexWriteSite(target: t.Node, site: t.Node): void {
const direct = unwrapJavaScriptExpression(target).node;
if (
(t.isIdentifier(direct) || t.isMemberExpression(direct)) &&
!t.isForOfStatement(site) &&
!t.isForInStatement(site)
) {
// Retain the operator so stored writes use the same result semantics as
// inline assignments, rather than treating every write as its RHS.
this.#indexWrite(direct, site, site);
return;
}
for (const leaf of semanticWriteTargets(target))
this.#indexWrite(leaf, site, site, false);
}
#indexWrite(target: t.Node, value: t.Node, site: t.Node, exact = true): void {
const path: string[] = [];
let root = unwrapJavaScriptExpression(target).node;
let dynamic = !exact;
while (t.isMemberExpression(root)) {
const key = semanticStaticPropertyKey(root.property, root.computed);
if (key === null) dynamic = true;
else path.unshift(key);
root = unwrapJavaScriptExpression(root.object).node;
}
if (dynamic && t.isIdentifier(root)) {
const binding = resolveSemanticBindingState(this.state, root, root.name);
if (binding)
this.#mutated.add(
this.#exportAliases.get(binding.bindingId) ?? binding.bindingId,
);
return;
}
if (t.isIdentifier(root)) this.#addWrite(root, path, value, site);
}
#addWrite(
root: t.Identifier,
path: readonly string[],
value: t.Node,
site: t.Node,
): void {
const binding = resolveSemanticBindingState(this.state, root, root.name);
if (!binding) return;
const alias = this.#exportAliases.get(binding.bindingId);
if (this.#moduleBindings.has(binding.bindingId) && path[0] !== "exports")
return;
const key = alias ?? binding.bindingId;
const selected = this.#moduleBindings.has(binding.bindingId)
? path.slice(1)
: path;
if (alias) {
const owner = this.#namespaceOwners.get(alias);
for (
let parent = this.state.parentsByNode.get(site);
parent && parent !== owner;
parent = this.state.parentsByNode.get(parent)
) {
if (
t.isFunction(parent) ||
t.isIfStatement(parent) ||
t.isConditionalExpression(parent) ||
t.isLogicalExpression(parent) ||
t.isLoop(parent) ||
t.isSwitchCase(parent) ||
t.isTryStatement(parent)
) {
if (selected.length === 0) this.#mutated.add(key);
else this.#unsafeExports.add(`${key}:${selected[0] ?? ""}`);
return;
}
}
}
const writes = this.#writes.get(key) ?? [];
writes.push({ path: selected, value, site });
this.#writes.set(key, writes);
}
}