import * as t from "@babel/types"; import type { JavaScriptSemanticAnalysisState, JavaScriptSemanticBindingState, } from "./javascriptSemanticState.js"; import { semanticPropertyPathKeyMatches, type JavaScriptSemanticPropertyPath, } from "./javascriptSemanticPropertyPaths.js"; import { unwrapJavaScriptExpression } from "./javascriptAstValues.js"; import { semanticObjectPropertyReferences, semanticArrayPropertyReferences, type JavaScriptSemanticObjectReference, } from "./javascriptSemanticObjectReferences.js"; import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js"; import { semanticBindingAtRead, semanticDefiniteEffectPrecedesReference, semanticEffectTiming, semanticTopLevelEffectFollowsCapture, } from "./javascriptSemanticMutationInitializers.js"; import { semanticCopiedReferencePath, semanticReferenceIterationUnknown, semanticReferenceSourcesInactive, } from "./javascriptSemanticReferenceSources.js"; import { semanticStoredSourceSelection } from "./javascriptSemanticIterationSources.js"; import { semanticAliasSelectionChoices, type SemanticAliasSelectionPath, } from "./javascriptSemanticAliasSelections.js"; export interface SemanticCapturedSelection { readonly at: t.Node; readonly depth: number; /** Object spread/rest reads only an own enumerable slot at this depth. */ readonly ownEnumerable?: true; } /** The next origin crossed owns a repeated copy boundary at the same depth. */ export const semanticNormalizeCapturedSelections = ( selections: readonly SemanticCapturedSelection[] | undefined, pathLength?: number, ): readonly SemanticCapturedSelection[] | undefined => { const byDepth = new Map(); for (const capture of selections ?? []) if ( capture.depth > 0 && (pathLength === undefined || capture.depth <= pathLength) ) byDepth.set(capture.depth, capture); const normalized = [...byDepth.values()].sort( (left, right) => left.depth - right.depth, ); return normalized.length ? normalized : undefined; }; /** Keep capture boundaries aligned as a selected prefix is added or consumed. */ export const semanticShiftCapturedSelections = ( selections: readonly SemanticCapturedSelection[] | undefined, delta: number, ): readonly SemanticCapturedSelection[] | undefined => { const shifted = selections?.flatMap((selection) => selection.depth + delta > 0 ? [{ ...selection, depth: selection.depth + delta }] : [], ); return semanticNormalizeCapturedSelections(shifted); }; /** Record an object/array property selected when a shallow copy was made. */ export const semanticCapturedSelectionsAfterPropertyRead = ( selections: readonly SemanticCapturedSelection[] | undefined, source: Pick< JavaScriptSemanticObjectReference, "path" | "capturedSelectionAt" | "ownEnumerable" >, ): readonly SemanticCapturedSelection[] => [ ...(semanticShiftCapturedSelections(selections, source.path.length - 1) ?? []), ...(source.capturedSelectionAt === undefined ? [] : [ { at: source.capturedSelectionAt, depth: source.path.length, ...(source.ownEnumerable ? { ownEnumerable: true as const } : {}), }, ]), ]; interface ProjectedSource { readonly node: t.Node; readonly path: SemanticAliasSelectionPath; readonly capturedSelections?: readonly SemanticCapturedSelection[]; } /** A fresh parent does not prove a descendant receiver is a fresh allocation. */ const hasFreshProjectedReceiver = ( root: t.Node, path: JavaScriptSemanticPropertyPath, state: JavaScriptSemanticAnalysisState, ): boolean => { const pending = [{ node: root, path }]; const examined = new WeakMap>(); let selections = 0; while (pending.length > 0) { const current = pending.pop()!; const node = unwrapJavaScriptExpression(current.node).node; const key = JSON.stringify(current.path); const paths = examined.get(node) ?? new Set(); if (paths.has(key)) return false; paths.add(key); examined.set(node, paths); if (++selections > SEMANTIC_ALIAS_SELECTION_LIMIT) { state.valueResourceLimits.add("alias-selections"); return false; } if (current.path.length === 0) { if ( !t.isObjectExpression(node) && !t.isArrayExpression(node) && !t.isFunction(node) && !t.isClassExpression(node) ) return false; continue; } const [selected, ...remaining] = current.path; const sources = t.isObjectExpression(node) ? semanticObjectPropertyReferences(node, [selected!], state) : t.isArrayExpression(node) ? semanticArrayPropertyReferences(node, [selected!]) : []; if (sources.length === 0) return false; if ( selections + pending.length + sources.length > SEMANTIC_ALIAS_SELECTION_LIMIT ) { state.valueResourceLimits.add("alias-selections"); return false; } pending.push( ...sources.map((source) => ({ node: source.node, path: [...source.path, ...remaining], })), ); } return true; }; /** Select retained assignment and copy sources at one projected member read. */ export const semanticProjectedSourcesAtRead = ( ...arguments_: Parameters ): readonly ProjectedSource[] | undefined => semanticProjectedSourceFactsAtRead(...arguments_)?.sources; /** Retain the selected receiver's possible owners after its current value becomes opaque. */ export const semanticProjectedSourceEligibilityAtRead = ( binding: JavaScriptSemanticBindingState, reference: t.Node, path: SemanticAliasSelectionPath, state: JavaScriptSemanticAnalysisState, memberReadAt: t.Node = reference, capturedSelections: readonly SemanticCapturedSelection[] = [], ): readonly ProjectedSource[] | undefined => projectedSourceFactsAtRead( binding, reference, path, state, false, memberReadAt, capturedSelections, true, )?.sources; /** Keep imported origins subject to the same selected writes as other values. */ // Projection alternatives are introduced by callers. An ordinary property path // remains ordinary while the owner selects stored and copied source prefixes. export function semanticProjectedSourceFactsAtRead( binding: JavaScriptSemanticBindingState, reference: t.Node, path: JavaScriptSemanticPropertyPath, state: JavaScriptSemanticAnalysisState, includeUnknown: boolean, memberReadAt?: t.Node, capturedSelections?: readonly SemanticCapturedSelection[], retainEligibility?: boolean, ): | { readonly sources: readonly (Omit & { readonly path: JavaScriptSemanticPropertyPath; })[]; readonly retainsOrigin: boolean; } | undefined; export function semanticProjectedSourceFactsAtRead( ...arguments_: Parameters ): ReturnType; export function semanticProjectedSourceFactsAtRead( ...arguments_: Parameters ): ReturnType { return projectedSourceFactsAtRead(...arguments_); } const projectedSourceFactsAtRead = ( binding: JavaScriptSemanticBindingState, reference: t.Node, path: SemanticAliasSelectionPath, state: JavaScriptSemanticAnalysisState, includeUnknown: boolean, memberReadAt: t.Node = reference, capturedSelections: readonly SemanticCapturedSelection[] = [], retainEligibility = false, ): | { readonly sources: readonly ProjectedSource[]; readonly retainsOrigin: boolean; } | undefined => { const read = semanticBindingAtRead(binding, reference, state.parentsByNode); const captured = [ ...read.binding.initializers, ...read.binding.referenceInitializers, ]; const normalizedCaptures = semanticNormalizeCapturedSelections( capturedSelections, path.length, ); const captureForDepth = (depth: number): t.Node | undefined => normalizedCaptures?.find((capture) => capture.depth >= depth)?.at; const selectedAt = (depth: number): t.Node => captureForDepth(depth) ?? memberReadAt; const samePrefix = ( prefix: JavaScriptSemanticBindingState["memberWrites"][number], effect: JavaScriptSemanticBindingState["memberWrites"][number], ): boolean => prefix.path.length < effect.path.length && prefix.path.every( (key, index) => key !== null && typeof key !== "object" && effect.path[index] !== null && typeof effect.path[index] !== "object" && String(key) === String(effect.path[index]), ); const targetsReplacedPrefix = ( effect: JavaScriptSemanticBindingState["memberWrites"][number], ): boolean => capturedSelections.length > 0 && binding.memberWrites.some((replacement) => { const capture = captureForDepth(replacement.path.length); const receiverAt = effect.receiverAt ?? effect.node; return ( capture !== undefined && receiverAt !== undefined && replacement.receiverIdentity === "definite" && t.isAssignmentExpression(replacement.node, { operator: "=" }) && samePrefix(replacement, effect) && semanticTopLevelEffectFollowsCapture( capture, replacement.node, state.parentsByNode, ) && semanticDefiniteEffectPrecedesReference( replacement.node, receiverAt, state.parentsByNode, ) && hasFreshProjectedReceiver( replacement.node.right, effect.path.slice(replacement.path.length, -1), state, ) ); }); const matches = ( effect: JavaScriptSemanticBindingState["memberWrites"][number], ) => (effect.receiverInitializers === undefined || effect.receiverInitializers.length === 0 || captured.length === 0 || effect.receiverInitializers.some((receiver) => captured.some( (source) => source.node === receiver.node && JSON.stringify(source.projection) === JSON.stringify(receiver.projection), ), )) && path.length > 0 && effect.path.length <= path.length && effect.path.every((key, index) => { const selected = path[index]; return ( selected !== undefined && semanticAliasSelectionChoices(selected).some( (choice) => choice === null || typeof choice === "object" || semanticPropertyPathKeyMatches(key, String(choice)), ) ); }) && semanticEffectTiming(selectedAt(effect.path.length), effect.node, state) !== "after" && !targetsReplacedPrefix(effect); const writes = binding.memberWrites.filter(matches); let replacement: | JavaScriptSemanticBindingState["memberWrites"][number] | undefined; for (const effect of writes) { if ( effect.receiverIdentity === "definite" && t.isAssignmentExpression(effect.node, { operator: "=" }) && effect.path.every( (key, index) => key !== null && typeof key !== "object" && path[index] !== null && typeof path[index] !== "object" && String(key) === String(path[index]), ) && semanticDefiniteEffectPrecedesReference( effect.node, selectedAt(effect.path.length), state.parentsByNode, ) && (replacement === undefined || semanticDefiniteEffectPrecedesReference( replacement.node!, effect.node, state.parentsByNode, ) || (replacement.node === effect.node && (replacement.assignedSources?.[0]?.target?.start ?? -1) < (effect.assignedSources?.[0]?.target?.start ?? -1))) ) replacement = effect; } const superseded = ( effect: JavaScriptSemanticBindingState["memberWrites"][number], ) => replacement !== undefined && effect.node !== undefined && effect !== replacement && (semanticEffectTiming(replacement.node!, effect.node, state) === "before" || (effect.node === replacement.node && (effect.assignedSources?.[0]?.target?.start ?? -1) < (replacement.assignedSources?.[0]?.target?.start ?? -1))); if ( includeUnknown && binding.escapedPaths.some( (effect) => matches(effect) && !superseded(effect), ) ) return undefined; const sources: ProjectedSource[] = []; for (const effect of writes) { if (!retainEligibility && superseded(effect)) continue; if ( !t.isAssignmentExpression(effect.node) || !["=", "||=", "&&=", "??="].includes(effect.node.operator) ) { if (includeUnknown) return undefined; continue; } if (effect.assignedSources === undefined) { if (includeUnknown) return undefined; continue; } const remaining = path.slice(effect.path.length); for (const source of effect.assignedSources) { if (semanticReferenceIterationUnknown(source, state)) return undefined; if (semanticReferenceSourcesInactive(source, state)) continue; const [key, ...suffix] = remaining; for (const choice of key === undefined ? [undefined] : semanticAliasSelectionChoices(key)) { const selection = semanticStoredSourceSelection( source, choice === undefined ? [] : [choice], ); if (selection !== null) sources.push({ node: source.node, path: [...selection.projection, ...suffix], ...(selection.capturedSelectionDepth === undefined ? {} : { capturedSelections: [ { at: source.node, depth: selection.capturedSelectionDepth, ...(selection.ownEnumerable ? { ownEnumerable: true as const } : {}), }, ], }), }); } } } if (!retainEligibility && replacement !== undefined) return { sources, retainsOrigin: false }; for (const initializer of read.binding.initializers) { if (semanticReferenceIterationUnknown(initializer, state)) return undefined; if (semanticReferenceSourcesInactive(initializer, state)) continue; sources.push({ node: initializer.node, path: [...initializer.projection, ...path], }); } for (const initializer of read.binding.referenceInitializers) { if (semanticReferenceIterationUnknown(initializer, state)) return undefined; if (semanticReferenceSourcesInactive(initializer, state)) continue; const [key, ...suffix] = path; for (const choice of key === undefined ? [undefined] : semanticAliasSelectionChoices(key)) { const projection = semanticCopiedReferencePath( initializer, choice === undefined ? [] : [choice], "identity", ); if (projection !== null) sources.push({ node: initializer.node, path: [...projection, ...suffix], capturedSelections: initializer.copyProjectionOffset === undefined ? [] : [ { at: initializer.node, depth: projection.length + suffix.length - path.length + 1, ...(initializer.copyKind === "object-rest" ? { ownEnumerable: true as const } : {}), }, ], }); } } return { sources, retainsOrigin: true }; };