Files
rea/src/domain/javascript/javascriptSemanticState.ts
T

630 lines
24 KiB
TypeScript

import type { JavaScriptOpaqueScopeEffects } from "./javascriptSemanticOpaqueEffects.js";
import type { SemanticIterationSource } from "./javascriptSemanticIterationSources.js";
import * as t from "@babel/types";
import {
semanticStaticPropertyKey,
staticJavaScriptTruthiness,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import {
semanticBindingAtRead,
semanticEffectTiming,
} from "./javascriptSemanticMutationInitializers.js";
import type {
JavaScriptSemanticPropertyPath,
SemanticPropertyPathCoverage,
} from "./javascriptSemanticPropertyPaths.js";
import type {
JavaScriptModuleOrigin,
JavaScriptSemanticDefinition,
JavaScriptSemanticModuleLink,
JavaScriptSemanticCallable,
JavaScriptSemanticScope,
JavaScriptSemanticReference,
} from "./javascriptSemanticIr.js";
import type { JavaScriptModuleResolutionMode } from "./javascriptModuleResolutionMode.js";
import type {
JavaScriptSemanticResourceLimit,
JavaScriptSemanticValue,
} from "./javascriptSemanticValueTypes.js";
/** Internal initializer plus a destructuring/member projection. */
export interface JavaScriptSemanticInitializer {
readonly node: t.Node;
readonly projection: readonly (string | number | null)[];
readonly entryBody?: t.BlockStatement;
readonly iterationSources?: readonly {
readonly node: t.Node;
readonly projection: JavaScriptSemanticPropertyPath;
}[];
}
/** Additional references from destructuring copies and possible defaults. */
interface JavaScriptSemanticReferenceInitializer extends JavaScriptSemanticInitializer {
/** This source is a value replacement, rather than a declaration fallback. */
readonly assignment?: true;
readonly copyKind?: "object-rest" | "array-rest";
readonly copyProjectionOffset?: number;
readonly copyExcludedKeys?: readonly string[];
readonly copyStartIndex?: number;
readonly fallbackSources?: readonly JavaScriptSemanticInitializer[];
readonly requiredSources?: readonly JavaScriptSemanticInitializer[];
}
/** Mutable binding state used only while constructing the immutable IR. */
export interface JavaScriptSemanticMemberEffect {
readonly path: JavaScriptSemanticPropertyPath;
readonly node: t.Node | undefined;
/** Selected RHS values retained by the shared pattern-store owner. */
readonly assignedSources?: readonly SemanticIterationSource[];
/** Binding values whose receiver identities were selected for this effect. */
readonly receiverInitializers?: readonly JavaScriptSemanticInitializer[];
/** Proof that the write selects this captured receiver, not just a possible alias. */
readonly receiverIdentity?: "definite" | "possible";
/** Capture of this binding used while following an aliased effect receiver. */
readonly receiverAt?: t.Node;
}
export type JavaScriptSemanticEffectTiming = "before" | "after" | "unknown";
export interface JavaScriptSemanticReferenceEffect {
readonly kind: "binding-write" | "member-write" | "escape" | "ambient-write";
readonly path?: JavaScriptSemanticPropertyPath;
readonly timing: JavaScriptSemanticEffectTiming;
}
/** Source-ordered effects relevant to one binding or projected member use. */
export interface JavaScriptSemanticReferenceEffectsAtSite {
readonly bindingId: string | null;
readonly resolution: JavaScriptSemanticReference["resolution"];
/** Whether alias-aware member and escape effects were collected for this source. */
readonly memberEffectsIndexed: boolean;
readonly effects: readonly JavaScriptSemanticReferenceEffect[];
}
export interface JavaScriptSemanticRequiredModuleEffect extends JavaScriptSemanticMemberEffect {
readonly kind: "write" | "escape";
}
export interface JavaScriptSemanticBindingState {
readonly hostOrigin?: "commonjs-wrapper";
readonly bindingId: string;
readonly scopeId: string;
readonly name: string;
kind: JavaScriptSemanticDefinition["kind"];
/** Source declaration form retained internally for CommonJS wrapper bindings. */
declarationKinds: Set<"var" | "let" | "const" | "using" | "await using">;
mutable: boolean;
readonly memberWrites: JavaScriptSemanticMemberEffect[];
readonly escapedPaths: JavaScriptSemanticMemberEffect[];
readonly definitions: JavaScriptSemanticDefinition[];
readonly initializers: JavaScriptSemanticInitializer[];
readonly referenceInitializers: JavaScriptSemanticReferenceInitializer[];
readonly directOrigins: JavaScriptModuleOrigin[];
/** An ECMAScript import that exists at runtime, excluding type-only and import-equals syntax. */
runtimeImport: boolean;
}
/** Mutable lexical-scope state used only during AST recovery. */
export interface JavaScriptSemanticScopeState {
readonly scopeId: string;
readonly parentScopeId: string | null;
readonly kind: JavaScriptSemanticScope["kind"];
readonly location: JavaScriptSemanticScope["location"];
bindingsComplete: boolean;
readonly bindings: Map<string, JavaScriptSemanticBindingState>;
/** Object environment introduced by `with`, active only for its body. */
readonly dynamicWithStatement?: t.WithStatement;
/** Ordinary functions own `arguments`; arrow functions inherit it. */
readonly implicitArguments?: true;
}
/** Shared state for semantic collection and evaluation. */
export interface JavaScriptSemanticAnalysisState {
arrayIterationUnknown: boolean;
readonly program: t.Program;
readonly opaqueEffects: JavaScriptOpaqueScopeEffects;
/** Effective loader for static imports in this source artifact. */
readonly staticImportResolutionMode: JavaScriptModuleResolutionMode;
/** Bounds encountered during this analysis, independent of later projections. */
readonly valueResourceLimits: Set<JavaScriptSemanticResourceLimit>;
/** Getter query bounds encountered while resolving object and class properties. */
readonly getterResourceLimits: Set<JavaScriptSemanticResourceLimit>;
/** Callee sites whose possible local returns needed a broader projection. */
readonly returnedReferenceWidenings: Set<t.Node>;
readonly parentsByNode: WeakMap<t.Node, t.Node>;
readonly scopes: JavaScriptSemanticScopeState[];
readonly scopesById: Map<string, JavaScriptSemanticScopeState>;
/** Scope boundaries and descendant scopes resolved on demand. */
readonly scopeByNode: WeakMap<t.Node, JavaScriptSemanticScopeState>;
readonly bindingsById: Map<string, JavaScriptSemanticBindingState>;
/** Resolved identifier writes retained at their exact source sites. */
readonly bindingWrites: Map<string, t.Node[]>;
/** Source evidence that Object.prototype may supply a `require` property. */
prototypeRequireMutationObserved: boolean | undefined;
/** Source evidence that Object.prototype may add inherited enumerable keys. */
prototypeEnumerationMayAffect: boolean | undefined;
/** Effects on a cached require exports object, independent of local projections. */
readonly requiredModuleEffects: Map<
string,
JavaScriptSemanticRequiredModuleEffect[]
>;
readonly callables: JavaScriptSemanticCallable[];
readonly callableNodesById: Map<string, t.Node>;
readonly moduleLinks: JavaScriptSemanticModuleLink[];
/** Exact calls already represented by binding or dependency links. */
readonly linkedRequireCalls: WeakSet<t.CallExpression>;
readonly moduleLinkBindings: WeakMap<
JavaScriptSemanticModuleLink,
JavaScriptSemanticBindingState
>;
/** Default expressions capture their value at the export statement. */
readonly defaultExportExpressions: WeakMap<
JavaScriptSemanticModuleLink,
t.Node
>;
/** Initializer expressions whose write can be skipped by enclosing control flow. */
readonly conditionallySkippedExpressions: WeakSet<t.Node>;
/** Unbound identifier writes retain their source sites for capture ordering. */
readonly ambientBindingWrites: Map<string, t.Node[]>;
/** Source-local writes, tagged by global, import.meta or CJS module identity. */
readonly ambientWrites: SemanticPropertyPathCoverage;
/** Ambient object identities passed to calls whose effects are not evaluated. */
readonly ambientEscapes: SemanticPropertyPathCoverage;
/** Capture sites keep later opaque effects from changing earlier copies. */
readonly ambientEscapeEffects: JavaScriptSemanticMemberEffect[];
/** Source sites distinguish replacement before an ambient identity capture from later writes. */
readonly ambientWriteEffects: JavaScriptSemanticMemberEffect[];
}
/** Record one producer outcome without traversing or retaining its value graph. */
export const recordSemanticValueLimit = (
state: JavaScriptSemanticAnalysisState,
value: JavaScriptSemanticValue,
): void => {
if (value.status === "unknown" && value.resourceLimit !== undefined)
state.valueResourceLimits.add(value.resourceLimit);
};
/** One parsed source owns its optional indexes and their per-source lifetime. */
export interface JavaScriptBindingIndex {
readonly sourceType: t.Program["sourceType"];
readonly bindings: JavaScriptSemanticAnalysisState;
indexAmbientWritesSteps(): Generator<void, void>;
indexMemberMutationFactsSteps(): Generator<void, void>;
/** Resolve certain lexical ownership without collecting mutation reports. */
bindingForReferenceAt(
referenceNode: t.Node,
): JavaScriptSemanticBindingState | undefined;
effectsForReferenceAt(
site: t.Node,
referenceNode: t.Node,
propertyPath?: JavaScriptSemanticPropertyPath,
): JavaScriptSemanticReferenceEffectsAtSite;
/** Stop at the first relevant prior/uncertain effect without retaining a report. */
referenceMayHaveChangedAt(
site: t.Node,
referenceNode: t.Node,
propertyPath?: JavaScriptSemanticPropertyPath,
): boolean;
referencesSteps(): Generator<void, readonly JavaScriptSemanticReference[]>;
}
/**
* Return one unconditional source initializer for a declared lexical binding.
* Repeated variable declarations without another initializer do not write it.
* This establishes a stable source fact only; it does not prove immutability or
* that the declaration has executed at a particular program point.
*/
export const semanticStableInitializer = (
binding: JavaScriptSemanticBindingState | undefined,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticBindingState["initializers"][number] | undefined => {
const [initializer] = binding?.initializers ?? [];
const [definition] = binding?.definitions ?? [];
return binding !== undefined &&
((definition?.kind === "variable" &&
binding.definitions.every(({ kind }) => kind === "variable")) ||
(binding.definitions.length === 1 &&
(definition?.kind === "function" || definition?.kind === "class"))) &&
binding.initializers.length === 1 &&
initializer !== undefined &&
!state.conditionallySkippedExpressions.has(initializer.node)
? initializer
: undefined;
};
/** Return the active scope from a non-empty construction stack. */
export const currentSemanticScope = (
stack: readonly JavaScriptSemanticScopeState[],
): JavaScriptSemanticScopeState => {
const scope = stack.at(-1);
if (scope === undefined) throw new TypeError("Missing semantic scope");
return scope;
};
/** Create a stable source-position identity for one lexical scope. */
export const semanticScopeId = (
kind: JavaScriptSemanticScope["kind"],
node: t.Node,
): string =>
`scope:${kind}:${String(node.start ?? -1)}:${String(node.end ?? -1)}`;
/** Create the canonical identity used by one binding in a lexical scope. */
export const semanticBindingId = (scopeId: string, name: string): string =>
`${scopeId}:binding:${encodeURIComponent(name)}`;
/** Resolve one lexical name from the scope containing a node. */
export const resolveSemanticBindingState = (
state: JavaScriptSemanticAnalysisState,
node: t.Node,
name: string,
): JavaScriptSemanticBindingState | undefined => {
const lookup = semanticBindingLookupAt(state, node, name);
return lookup.kind === "binding" ? lookup.binding : undefined;
};
/** Report whether an omitted or dynamic binding blocks a trustworthy miss. */
export const semanticResolutionBlocked = (
state: JavaScriptSemanticAnalysisState,
node: t.Node,
name: string,
): boolean => {
const lookup = semanticBindingLookupAt(state, node, name);
return lookup.kind !== "binding" && lookup.kind !== "ambient";
};
const semanticScopeForNode = (
state: JavaScriptSemanticAnalysisState,
node: t.Node,
): JavaScriptSemanticScopeState | undefined => {
let candidate: t.Node | undefined = node;
while (candidate !== undefined) {
const scope = state.scopeByNode.get(candidate);
if (scope !== undefined) {
// Repeated references in a deep expression share this ancestry. Cache
// the traversed path without eagerly indexing nodes never resolved.
let descendant: t.Node | undefined = node;
while (descendant !== undefined && descendant !== candidate) {
state.scopeByNode.set(descendant, scope);
descendant = state.parentsByNode.get(descendant);
}
return scope;
}
candidate = state.parentsByNode.get(candidate);
}
return undefined;
};
type SemanticBindingLookup =
| {
readonly kind: "binding" | "uncertain-binding";
readonly binding: JavaScriptSemanticBindingState;
}
| {
readonly kind:
| "ambient"
| "uncertain-ambient"
| "captured"
| "unknown"
| "implicit-arguments";
};
/** Retain both a dynamic lookup's uncertainty and its possible outer write owner. */
export const semanticBindingLookupFromScope = (
scope: JavaScriptSemanticScopeState | undefined,
name: string,
state: JavaScriptSemanticAnalysisState,
): SemanticBindingLookup => {
let uncertain = false;
while (scope !== undefined) {
const binding = scope.bindings.get(name);
if (binding !== undefined)
return { kind: uncertain ? "uncertain-binding" : "binding", binding };
if (name === "arguments" && scope.implicitArguments === true)
return { kind: "implicit-arguments" };
if (!scope.bindingsComplete) {
if (scope.dynamicWithStatement === undefined) return { kind: "unknown" };
const lookup = semanticWithRequireLookup(
state,
scope.dynamicWithStatement,
name,
);
if (lookup === "captures") return { kind: "captured" };
if (lookup === "unknown") uncertain = true;
}
scope =
scope.parentScopeId === null
? undefined
: state.scopesById.get(scope.parentScopeId);
}
return { kind: uncertain ? "uncertain-ambient" : "ambient" };
};
/** Return certain and possible binding ownership without discarding uncertainty. */
export const semanticBindingLookupAt = (
state: JavaScriptSemanticAnalysisState,
node: t.Node,
name: string,
): SemanticBindingLookup =>
semanticBindingLookupFromScope(
semanticScopeForNode(state, node),
name,
state,
);
/** Resolve a certain lexical binding from an explicitly selected scope. */
export const resolveSemanticBindingFromScope = (
scope: JavaScriptSemanticScopeState,
name: string,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticBindingState | undefined => {
const lookup = semanticBindingLookupFromScope(scope, name, state);
return lookup.kind === "binding" ? lookup.binding : undefined;
};
type SemanticWithRequireLookup = "captures" | "skips" | "unknown";
const semanticUnscopablesKey = Symbol("semantic-unscopables-key");
/** Classify only source-visible own `require` entries in a literal with object. */
const semanticWithRequireLookup = (
state: JavaScriptSemanticAnalysisState,
statement: t.WithStatement,
name: string,
): SemanticWithRequireLookup => {
if (name !== "require") return "unknown";
const object = unwrapJavaScriptExpression(statement.object).node;
if (!t.isObjectExpression(object)) return "unknown";
let ownRequire = false;
let hasUnknownOwnProperties = false;
let prototypeIsNull = false;
let unknownPrototype = false;
for (const property of object.properties) {
if (t.isSpreadElement(property)) {
hasUnknownOwnProperties = true;
continue;
}
const key = semanticWithPropertyName(
property.key,
property.computed,
state,
);
if (key === "require") ownRequire = true;
else if (
key === "__proto__" &&
t.isObjectProperty(property) &&
!property.computed &&
!property.shorthand
) {
const prototype = unwrapJavaScriptExpression(property.value).node;
if (t.isNullLiteral(prototype)) prototypeIsNull = true;
else unknownPrototype = true;
} else if (key === null && property.computed) {
hasUnknownOwnProperties = true;
}
}
if (ownRequire) {
const unscopablesValue = finalSemanticWithSymbolProperty(object, state);
if (unscopablesValue === "unknown") return "unknown";
if (unscopablesValue === undefined)
return prototypeIsNull ||
(!unknownPrototype && state.prototypeRequireMutationObserved === false)
? "captures"
: "unknown";
const unscopables = unwrapJavaScriptExpression(unscopablesValue).node;
if (!t.isObjectExpression(unscopables))
return isSemanticWithPrimitive(unscopables) ? "captures" : "unknown";
const entry = finalSemanticWithStringProperty(unscopables, "require");
if (entry === "unknown") return "unknown";
if (entry === undefined) {
const prototype = semanticWithPrototypeKind(unscopables);
return prototype === "null" ||
(prototype === "ordinary" &&
state.prototypeRequireMutationObserved === false)
? "captures"
: "unknown";
}
const truthiness = staticJavaScriptTruthiness(entry);
return truthiness === true
? "skips"
: truthiness === false
? "captures"
: "unknown";
}
if (hasUnknownOwnProperties || unknownPrototype) return "unknown";
if (prototypeIsNull) return "skips";
return state.prototypeRequireMutationObserved === false ? "skips" : "unknown";
};
const semanticWithPropertyName = (
key: t.Node,
computed: boolean,
state: JavaScriptSemanticAnalysisState,
): string | typeof semanticUnscopablesKey | null => {
if (
computed &&
t.isMemberExpression(key) &&
!key.computed &&
t.isIdentifier(key.object, { name: "Symbol" }) &&
semanticStaticPropertyKey(key.property, false) === "unscopables" &&
resolveSemanticBindingState(state, key.object, "Symbol") === undefined &&
!semanticResolutionBlocked(state, key.object, "Symbol")
)
return semanticUnscopablesKey;
return semanticStaticPropertyKey(key, computed);
};
const finalSemanticWithSymbolProperty = (
object: t.ObjectExpression,
state: JavaScriptSemanticAnalysisState,
): t.Node | undefined | "unknown" => {
for (let index = object.properties.length - 1; index >= 0; index -= 1) {
const property = object.properties[index];
if (property === undefined || t.isSpreadElement(property)) return "unknown";
const name = semanticWithPropertyName(
property.key,
property.computed,
state,
);
if (name === semanticUnscopablesKey)
return t.isObjectProperty(property) ? property.value : "unknown";
if (name === null && property.computed) return "unknown";
}
return undefined;
};
const semanticWithPrototypeKind = (
object: t.ObjectExpression,
): "null" | "ordinary" | "other" => {
for (const property of object.properties) {
if (
t.isObjectProperty(property) &&
!property.computed &&
!property.shorthand &&
t.isIdentifier(property.key, { name: "__proto__" })
)
return t.isNullLiteral(unwrapJavaScriptExpression(property.value).node)
? "null"
: "other";
}
return "ordinary";
};
const finalSemanticWithStringProperty = (
object: t.ObjectExpression,
sought: string,
): t.Node | undefined | "unknown" => {
for (let index = object.properties.length - 1; index >= 0; index -= 1) {
const property = object.properties[index];
if (property === undefined || t.isSpreadElement(property)) return "unknown";
const name = semanticStaticPropertyKey(property.key, property.computed);
if (name === sought)
return t.isObjectProperty(property) ? property.value : "unknown";
if (name === null && property.computed) return "unknown";
}
return undefined;
};
const isSemanticWithPrimitive = (node: t.Node): boolean =>
t.isNullLiteral(node) ||
t.isBooleanLiteral(node) ||
t.isStringLiteral(node) ||
t.isNumericLiteral(node) ||
t.isBigIntLiteral(node) ||
(t.isUnaryExpression(node, { operator: "-" }) &&
(t.isNumericLiteral(node.argument) || t.isBigIntLiteral(node.argument)));
/** Select the function/program owner for a var declaration. */
export const semanticVariableScope = (
scope: JavaScriptSemanticScopeState,
parent: t.Node | null,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticScopeState => {
if (!(parent !== null && t.isVariableDeclaration(parent, { kind: "var" })))
return scope;
let candidate = scope;
while (candidate.kind === "block" || candidate.kind === "catch") {
const outer =
candidate.parentScopeId === null
? undefined
: state.scopesById.get(candidate.parentScopeId);
if (outer === undefined) break;
candidate = outer;
}
return candidate;
};
/** Identify a global name only when no lexical or dynamic environment shadows it. */
export const isUnshadowedGlobal = (
node: t.Node,
state: JavaScriptSemanticAnalysisState,
name: string,
): boolean =>
t.isIdentifier(node, { name }) &&
(resolveSemanticBindingState(state, node, name) === undefined
? !semanticResolutionBlocked(state, node, name)
: isUnmodifiedCommonJsWrapperBinding(node, name, state));
/** A program-level `var` redeclaration still denotes its untouched CJS wrapper parameter. */
export const isUnmodifiedCommonJsWrapperBinding = (
node: t.Node,
name: string,
state: JavaScriptSemanticAnalysisState,
): boolean => {
const binding = commonJsWrapperBindingDeclaration(node, name, state);
return (
binding !== undefined &&
!semanticBindingHasPriorValueWrite(node, binding, state)
);
};
/** Retained value and pattern stores share one source-ordered write boundary. */
export const semanticBindingHasPriorValueWrite = (
site: t.Node,
binding: JavaScriptSemanticBindingState,
state: JavaScriptSemanticAnalysisState,
): boolean => {
if (binding.definitions.some(({ kind }) => kind === "function")) return true;
const writes = state.bindingWrites.get(binding.bindingId) ?? [];
const read = semanticBindingAtRead(binding, site, state.parentsByNode);
const retained =
read.status === "ordered"
? new Set([
...read.binding.initializers,
...read.binding.referenceInitializers,
])
: undefined;
const representedWrites = new Set<t.Node>();
const retainedWrites = new Set<t.Node>();
for (const source of [
...binding.initializers,
...binding.referenceInitializers,
]) {
let owner: t.Node | undefined = source.node;
while (owner !== undefined && !writes.includes(owner))
owner = state.parentsByNode.get(owner);
if (owner !== undefined) {
representedWrites.add(owner);
if (retained === undefined || retained.has(source))
retainedWrites.add(owner);
} else if (
(retained === undefined || retained.has(source)) &&
semanticEffectTiming(site, source.node, state) !== "after"
)
return true;
}
return writes.some(
(write) =>
(!representedWrites.has(write) || retainedWrites.has(write)) &&
semanticEffectTiming(site, write, state) !== "after",
);
};
/** Program `var` redeclarations retain wrapper identity until a source write. */
export const commonJsWrapperBindingDeclaration = (
node: t.Node,
name: string,
state: JavaScriptSemanticAnalysisState,
): JavaScriptSemanticBindingState | undefined => {
// Parser sourceType separates explicit modules from script/CommonJS source;
// specifier resolution mode does not establish that distinction.
if (
state.program.sourceType !== "script" ||
!["module", "exports", "require", "__dirname", "__filename"].includes(
name,
) ||
!t.isIdentifier(node, { name })
)
return undefined;
const binding = resolveSemanticBindingState(state, node, name);
if (binding === undefined) return undefined;
const scope = state.scopesById.get(binding.scopeId);
return scope?.kind === "program" &&
binding.declarationKinds.size > 0 &&
[...binding.declarationKinds].every((kind) => kind === "var")
? binding
: undefined;
};