mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(javascript): preserve initializer execution and lexical ownership
This commit is contained in:
@@ -374,6 +374,16 @@ Direct return expressions, including expression-bodied arrows, are evaluated
|
||||
through an execution-free value lattice. Literal object fields and direct
|
||||
return sites are represented in the result. Calls, dynamic spreads, computed
|
||||
keys, and parser recovery remain partial or unknown.
|
||||
Declaration initializers and later assignments share the binding-at-read owner;
|
||||
a declaration-only `var` read before its initializer does not inherit the future
|
||||
value. Earlier `break`/`continue` paths can leave a var initializer unexecuted.
|
||||
Legacy non-strict block-function copies are indexed on their outer variable
|
||||
owner, while strict/module and lexical-shadowing boundaries keep their values.
|
||||
A catch-shadowed var declaration keeps declaration ownership separate from its
|
||||
initializer's lexical assignment. Ordinary functions own an implicit arguments
|
||||
object; arrows retain lexical arguments lookup. Promise source identity uses
|
||||
the same read-position selection and requires an unmodified intrinsic factory.
|
||||
|
||||
Rebound values are evaluated at their read position: saving an incoming parameter
|
||||
before a later assignment preserves caller-dependent uncertainty, while a proven
|
||||
preceding assignment can supply a constant. Unresolved ordering remains unknown.
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { analyzeParsedJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
||||
import {
|
||||
onlyCallable,
|
||||
onlyBinding,
|
||||
} from "./javascriptSemanticAnalysis.fixture.js";
|
||||
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
|
||||
|
||||
const analyze = (source: string, path = "fixture.cjs") => {
|
||||
const file = parseJavaScriptSource(source, path);
|
||||
if (file === null) throw new Error("Expected parsed fixture");
|
||||
return analyzeParsedJavaScriptSemantics(file);
|
||||
};
|
||||
const returned = (body: string) =>
|
||||
onlyCallable(analyze(`exports.read = function read() { ${body} };`), "read")
|
||||
.returnSites[0]?.value;
|
||||
|
||||
it.each([
|
||||
"return value; var value = 1;",
|
||||
"var snapshot = value; var value = 1; return snapshot;",
|
||||
])("selects declaration-only bindings at their actual read: %s", (body) => {
|
||||
expect(returned(body)?.status).toBe("unknown");
|
||||
});
|
||||
|
||||
it.each([
|
||||
"do { break; var value = 1; } while (false); return value;",
|
||||
"do { if (flag) break; var value = 1; } while (false); return value;",
|
||||
"do { continue; var value = 1; } while (false); return value;",
|
||||
"label: { break label; var value = 1; } return value;",
|
||||
])(
|
||||
"does not treat an initializer skipped by abrupt completion as definite: %s",
|
||||
(body) => {
|
||||
expect(returned(body)?.status).toBe("unknown");
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
"var value = 1; return value;",
|
||||
"do { var value = 1; break; } while (false); return value;",
|
||||
"do { for (;;) { break; } var value = 1; } while (false); return value;",
|
||||
"label: { break label; } var value = 1; return value;",
|
||||
"function unused() { return; } var value = 1; return value;",
|
||||
])(
|
||||
"preserves initializers that an earlier local exit does not skip: %s",
|
||||
(body) => {
|
||||
expect(returned(body)).toEqual({ status: "literal", value: 1 });
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
"var value = 1; { function value() {} } exports.read = function read() { return value; };",
|
||||
"exports.read = function read() { var value = 1; if (flag) { function value() {} } return value; };",
|
||||
])(
|
||||
"indexes the legacy block-function write on its outer owner: %s",
|
||||
(source) => {
|
||||
expect(
|
||||
onlyCallable(analyze(source), "read").returnSites[0]?.value.status,
|
||||
).not.toBe("literal");
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
[
|
||||
"fixture.cjs",
|
||||
'"use strict"; var value = 1; { function value() {} } exports.read = function read() { return value; };',
|
||||
],
|
||||
[
|
||||
"fixture.cjs",
|
||||
"let value = 1; { function value() {} } exports.read = function read() { return value; };",
|
||||
],
|
||||
[
|
||||
"fixture.mjs",
|
||||
"var value = 1; { function value() {} } export function read() { return value; }",
|
||||
],
|
||||
])(
|
||||
"does not apply legacy writes across a strict or lexical boundary: %s",
|
||||
(path, source) => {
|
||||
expect(
|
||||
onlyCallable(analyze(source, path), "read").returnSites[0]?.value,
|
||||
).toEqual({ status: "literal", value: 1 });
|
||||
},
|
||||
);
|
||||
|
||||
it("assigns a catch-shadowed var initializer to the active lexical binding", () => {
|
||||
const ir = analyze(`
|
||||
function target() { return "target"; }
|
||||
try { throw 0; } catch (handler) { var handler = target; handler(); }
|
||||
`);
|
||||
const target = onlyCallable(ir, "target");
|
||||
expect(ir.callSites).toContainEqual(
|
||||
expect.objectContaining({
|
||||
resolution: "exact",
|
||||
calleeCallableIds: [target.callableId],
|
||||
}),
|
||||
);
|
||||
const hoisted = ir.bindings.find(
|
||||
({ name, kind }) => name === "handler" && kind === "variable",
|
||||
);
|
||||
expect(hoisted?.value.status).toBe("unknown");
|
||||
});
|
||||
|
||||
it("keeps ordinary arguments local while arrows retain the outer binding", () => {
|
||||
const ir = analyze(`
|
||||
var arguments = { 0: "OUTER" };
|
||||
function ordinary() { const selected = arguments[0]; return selected; }
|
||||
const arrow = () => arguments[0];
|
||||
`);
|
||||
expect(onlyBinding(ir, "selected").value.status).toBe("unknown");
|
||||
expect(onlyCallable(ir, "ordinary").returnSites[0]?.value.status).toBe(
|
||||
"unknown",
|
||||
);
|
||||
expect(onlyCallable(ir, "arrow").returnSites[0]?.value).toEqual({
|
||||
status: "literal",
|
||||
value: "OUTER",
|
||||
});
|
||||
const outer = onlyBinding(ir, "arguments");
|
||||
expect(
|
||||
ir.closureCaptures.some(
|
||||
({ bindingId, callableId }) =>
|
||||
bindingId === outer.bindingId &&
|
||||
callableId === onlyCallable(ir, "ordinary").callableId,
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("does not resolve Promise sources through a future initializer", () => {
|
||||
const early = analyze(
|
||||
`
|
||||
try { task.then(work); } catch (error) {}
|
||||
const task = Promise.resolve(1);
|
||||
`,
|
||||
"fixture.mjs",
|
||||
);
|
||||
expect(
|
||||
early.promiseOperations.find(({ method }) => method === "then"),
|
||||
).toMatchObject({ sourceResolution: "unresolved", sourcePromiseIds: [] });
|
||||
const initialized = analyze(
|
||||
"const task = Promise.resolve(1); task.then(work);",
|
||||
"fixture.mjs",
|
||||
);
|
||||
expect(
|
||||
initialized.promiseOperations.find(({ method }) => method === "then"),
|
||||
).toMatchObject({
|
||||
sourceResolution: "complete",
|
||||
sourcePromiseIds: [expect.any(String)],
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
"globalThis.Promise = { resolve() { return 17; } };",
|
||||
"Promise.resolve = () => 17;",
|
||||
])(
|
||||
"requires the selected intrinsic Promise identity after ambient writes: %s",
|
||||
(write) => {
|
||||
const ir = analyze(
|
||||
`${write} const task = Promise.resolve(1);`,
|
||||
"fixture.mjs",
|
||||
);
|
||||
expect(
|
||||
ir.promiseOperations.filter(({ method }) => method === "resolve"),
|
||||
).toEqual([]);
|
||||
expect(
|
||||
analyze("const task = Promise.resolve(1);", "fixture.mjs")
|
||||
.promiseOperations,
|
||||
).toContainEqual(
|
||||
expect.objectContaining({
|
||||
method: "resolve",
|
||||
sourceResolution: "complete",
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains caught reference identity without tainting inert throws or later replacements", () => {
|
||||
const cases = [
|
||||
["caught.value = 2;", "unknown"],
|
||||
["mutate(caught);", "unknown"],
|
||||
["", "literal"],
|
||||
["caught = {}; caught.value = 2;", "literal"],
|
||||
] as const;
|
||||
for (const [body, status] of cases)
|
||||
expect(
|
||||
returned(
|
||||
`const state = { value: 1 }; try { throw state; } catch (caught) { ${body} } return state.value;`,
|
||||
)?.status,
|
||||
).toBe(status);
|
||||
});
|
||||
|
||||
it("owns a class declaration's self reference separately from its outer variable", () => {
|
||||
const ir = analyze(
|
||||
`
|
||||
class Service { static create() { return new Service(); } }
|
||||
const Original = Service;
|
||||
Service = function Replacement() {};
|
||||
`,
|
||||
"fixture.mjs",
|
||||
);
|
||||
const bindings = ir.bindings.filter(({ name }) => name === "Service");
|
||||
expect(bindings).toHaveLength(2);
|
||||
const create = onlyCallable(ir, "create");
|
||||
expect(
|
||||
ir.closureCaptures.some(
|
||||
({ callableId, bindingId }) =>
|
||||
callableId === create.callableId &&
|
||||
bindingId === bindings.find(({ mutable }) => mutable)?.bindingId,
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
ir.callSites.find(
|
||||
({ callerCallableId }) => callerCallableId === create.callableId,
|
||||
)?.resolution,
|
||||
).toBe("exact");
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import * as t from "@babel/types";
|
||||
import {
|
||||
semanticComputedMethodKey,
|
||||
semanticBranchMayBeSkipped,
|
||||
semanticPriorStatementMayExit,
|
||||
semanticIterationCardinality,
|
||||
} from "./javascriptSemanticEvaluationOrder.js";
|
||||
|
||||
@@ -116,6 +117,7 @@ import type { JavaScriptModuleResolutionMode } from "./javascriptModuleResolutio
|
||||
interface BindPatternInput {
|
||||
readonly iterationSources?: JavaScriptSemanticBindingState["initializers"][number]["iterationSources"];
|
||||
readonly pattern: t.Node;
|
||||
readonly entryBody?: t.BlockStatement;
|
||||
readonly initializer: t.Node | null;
|
||||
readonly scope: JavaScriptSemanticScopeState;
|
||||
readonly state: JavaScriptSemanticAnalysisState;
|
||||
@@ -1067,6 +1069,19 @@ function* collectDefinitionsSteps(
|
||||
node: t.CallExpression;
|
||||
scopeId: string;
|
||||
}[] = [];
|
||||
const displacedVarInitializers: {
|
||||
node: t.VariableDeclarator;
|
||||
scope: JavaScriptSemanticScopeState;
|
||||
owner: JavaScriptSemanticScopeState;
|
||||
}[] = [];
|
||||
const thrownValues: {
|
||||
node: t.ThrowStatement;
|
||||
ancestors: readonly t.Node[];
|
||||
}[] = [];
|
||||
const legacyBlockFunctions: {
|
||||
node: t.FunctionDeclaration;
|
||||
scope: JavaScriptSemanticScopeState;
|
||||
}[] = [];
|
||||
let needsPrototypeFacts = false;
|
||||
const functionBodies: {
|
||||
readonly body: t.BlockStatement;
|
||||
@@ -1117,6 +1132,21 @@ function* collectDefinitionsSteps(
|
||||
parentScope = dynamicScope;
|
||||
}
|
||||
bindOuterDeclaration(node, parentScope, state);
|
||||
if (
|
||||
t.isFunctionDeclaration(node) &&
|
||||
node.id != null &&
|
||||
parentScope.kind === "block"
|
||||
)
|
||||
legacyBlockFunctions.push({ node, scope: parentScope });
|
||||
if (
|
||||
t.isVariableDeclarator(node) &&
|
||||
node.init != null &&
|
||||
t.isIdentifier(node.id)
|
||||
) {
|
||||
const owner = semanticVariableScope(parentScope, parent, state);
|
||||
if (owner !== parentScope)
|
||||
displacedVarInitializers.push({ node, scope: parentScope, owner });
|
||||
}
|
||||
const nested = nestedScope(node, parent, parentScope, state);
|
||||
if (nested !== undefined) {
|
||||
stack.push(nested);
|
||||
@@ -1141,6 +1171,8 @@ function* collectDefinitionsSteps(
|
||||
state,
|
||||
});
|
||||
bindInnerDeclaration(node, parent, scope, state);
|
||||
if (t.isThrowStatement(node))
|
||||
thrownValues.push({ node, ancestors: readAncestors() });
|
||||
// Every supported global eval origin starts with one of these decoded
|
||||
// identifiers, including computed properties and destructured aliases.
|
||||
if (t.isIdentifier(node) && globalEvalRoots.has(node.name))
|
||||
@@ -1180,6 +1212,94 @@ function* collectDefinitionsSteps(
|
||||
stack.pop();
|
||||
},
|
||||
});
|
||||
// A var declaration is instantiated in its variable environment, while its
|
||||
// initializer assigns through the lexical environment at the source site.
|
||||
for (const { node, scope, owner } of displacedVarInitializers) {
|
||||
if (!t.isIdentifier(node.id) || node.init == null) continue;
|
||||
const selected = resolveSemanticBindingFromScope(
|
||||
scope,
|
||||
node.id.name,
|
||||
state,
|
||||
);
|
||||
const declared = owner.bindings.get(node.id.name);
|
||||
if (
|
||||
selected === undefined ||
|
||||
selected === declared ||
|
||||
declared === undefined
|
||||
)
|
||||
continue;
|
||||
for (let index = declared.initializers.length - 1; index >= 0; index--)
|
||||
if (declared.initializers[index]?.node === node.init)
|
||||
declared.initializers.splice(index, 1);
|
||||
addAssignment(
|
||||
node.id,
|
||||
node.init,
|
||||
scope,
|
||||
state,
|
||||
state.conditionallySkippedExpressions.has(node.init),
|
||||
node,
|
||||
);
|
||||
}
|
||||
for (const { node, ancestors } of thrownValues) {
|
||||
let child: t.Node = node;
|
||||
for (let index = ancestors.length - 1; index >= 0; index--) {
|
||||
const parent = ancestors[index]!;
|
||||
if (t.isFunction(parent) || t.isClass(parent)) break;
|
||||
if (
|
||||
t.isTryStatement(parent) &&
|
||||
parent.handler != null &&
|
||||
child === parent.block
|
||||
) {
|
||||
const scope = state.scopeByNode.get(parent.handler);
|
||||
if (scope !== undefined && parent.handler.param != null)
|
||||
bindPattern({
|
||||
pattern: parent.handler.param,
|
||||
initializer: node.argument,
|
||||
scope,
|
||||
state,
|
||||
mutable: true,
|
||||
referenceOnly: true,
|
||||
entryBody: parent.handler.body,
|
||||
});
|
||||
break;
|
||||
}
|
||||
child = parent;
|
||||
}
|
||||
}
|
||||
for (const { node, scope } of legacyBlockFunctions) {
|
||||
if (
|
||||
node.id == null ||
|
||||
semanticStrictnessAt(state.parentsByNode.get(node) ?? node, state) ===
|
||||
"strict"
|
||||
)
|
||||
continue;
|
||||
let outer =
|
||||
scope.parentScopeId === null
|
||||
? undefined
|
||||
: state.scopesById.get(scope.parentScopeId);
|
||||
while (outer !== undefined) {
|
||||
const binding = outer.bindings.get(node.id.name);
|
||||
if (
|
||||
binding !== undefined &&
|
||||
!binding.declarationKinds.has("var") &&
|
||||
binding.kind !== "parameter"
|
||||
)
|
||||
break;
|
||||
if (
|
||||
outer.kind === "function" ||
|
||||
outer.kind === "program" ||
|
||||
outer.kind === "static-block"
|
||||
) {
|
||||
if (binding !== undefined)
|
||||
addAssignment(node.id, node, outer, state, false, node);
|
||||
break;
|
||||
}
|
||||
outer =
|
||||
outer.parentScopeId === null
|
||||
? undefined
|
||||
: state.scopesById.get(outer.parentScopeId);
|
||||
}
|
||||
}
|
||||
if (needsPrototypeFacts)
|
||||
yield* indexPrototypeUncertaintySteps(program, state);
|
||||
// All declarations must exist before resolving writes, including hoisted
|
||||
@@ -1365,7 +1485,11 @@ const writeMayBeSkipped = (
|
||||
t.isStaticBlock(parent)
|
||||
)
|
||||
return false;
|
||||
if (semanticBranchMayBeSkipped(parent, child)) return true;
|
||||
if (
|
||||
semanticBranchMayBeSkipped(parent, child) ||
|
||||
semanticPriorStatementMayExit(parent, child)
|
||||
)
|
||||
return true;
|
||||
child = parent;
|
||||
}
|
||||
return false;
|
||||
@@ -1421,7 +1545,7 @@ const bindInnerDeclaration = (
|
||||
? { declarationKind: parent.kind }
|
||||
: {}),
|
||||
});
|
||||
else if (t.isClassExpression(node) && t.isIdentifier(node.id))
|
||||
else if (t.isClass(node) && t.isIdentifier(node.id))
|
||||
addBinding({
|
||||
state,
|
||||
scope,
|
||||
@@ -1588,7 +1712,8 @@ const mappedArgumentsOwner = (
|
||||
}
|
||||
if (!t.isIdentifier(value)) return undefined;
|
||||
|
||||
if (semanticBindingLookupAt(state, value, "arguments").kind !== "ambient")
|
||||
const lookup = semanticBindingLookupAt(state, value, "arguments");
|
||||
if (lookup.kind !== "implicit-arguments" && lookup.kind !== "ambient")
|
||||
return undefined;
|
||||
let owner: t.Function | undefined;
|
||||
for (
|
||||
@@ -1634,12 +1759,12 @@ const mappedArgumentsOwner = (
|
||||
return {
|
||||
scope,
|
||||
parameters: mapped,
|
||||
strictness: mappedArgumentsStrictness(owner, state),
|
||||
strictness: semanticStrictnessAt(owner, state),
|
||||
};
|
||||
};
|
||||
|
||||
const mappedArgumentsStrictness = (
|
||||
owner: t.Function,
|
||||
const semanticStrictnessAt = (
|
||||
owner: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): MappedArgumentsOwner["strictness"] => {
|
||||
for (
|
||||
@@ -1737,6 +1862,9 @@ const nestedScope = (
|
||||
location: range(switchOwner ?? node),
|
||||
bindingsComplete: true,
|
||||
bindings: new Map(),
|
||||
...(t.isFunction(node) && !t.isArrowFunctionExpression(node)
|
||||
? { implicitArguments: true as const }
|
||||
: {}),
|
||||
};
|
||||
state.scopes.push(scope);
|
||||
state.scopesById.set(scope.scopeId, scope);
|
||||
@@ -1914,6 +2042,9 @@ const bindPattern = (input: BindPatternInput): void => {
|
||||
binding.referenceInitializers.push({
|
||||
node: initializer,
|
||||
projection,
|
||||
...(input.entryBody === undefined
|
||||
? {}
|
||||
: { entryBody: input.entryBody }),
|
||||
...(assignmentReference === undefined ? {} : { assignment: true }),
|
||||
...(copyKind === undefined ? {} : { copyKind }),
|
||||
...(copyProjectionOffset === undefined
|
||||
@@ -2136,7 +2267,8 @@ const addAssignment = (
|
||||
const mutation = writeSite ?? initializer;
|
||||
if (mutation === null) return;
|
||||
const lookup = semanticBindingLookupFromScope(scope, identifier.name, state);
|
||||
if (lookup.kind === "captured") return;
|
||||
if (lookup.kind === "captured" || lookup.kind === "implicit-arguments")
|
||||
return;
|
||||
const binding = "binding" in lookup ? lookup.binding : undefined;
|
||||
conditionallySkipped ||=
|
||||
lookup.kind !== "binding" && lookup.kind !== "ambient";
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
|
||||
import { childNodes } from "./javascriptSemanticTraversal.js";
|
||||
|
||||
const preserveGrouping = { stopAtParentheses: true } as const;
|
||||
|
||||
export type JavaScriptSemanticIterationCardinality =
|
||||
@@ -155,3 +157,94 @@ const optionalChainMayShortCircuit = (expression: t.Node): boolean => {
|
||||
else return false;
|
||||
}
|
||||
};
|
||||
|
||||
interface AbruptCompletionSummary {
|
||||
readonly breaks: ReadonlySet<string | null>;
|
||||
readonly continues: ReadonlySet<string | null>;
|
||||
}
|
||||
const emptyAbruptLabels: ReadonlySet<string | null> = new Set();
|
||||
const emptyAbruptSummary: AbruptCompletionSummary = {
|
||||
breaks: emptyAbruptLabels,
|
||||
continues: emptyAbruptLabels,
|
||||
};
|
||||
const abruptSummaries = new WeakMap<t.Node, AbruptCompletionSummary>();
|
||||
const abruptPrefixes = new WeakMap<
|
||||
t.BlockStatement | t.Program,
|
||||
ReadonlySet<t.Node>
|
||||
>();
|
||||
|
||||
/** Loop and label exits crossing a statement boundary, excluding nested callables. */
|
||||
const abruptSummary = (root: t.Node): AbruptCompletionSummary => {
|
||||
const pending: { node: t.Node; visited: boolean }[] = [
|
||||
{ node: root, visited: false },
|
||||
];
|
||||
while (pending.length > 0) {
|
||||
const { node, visited } = pending.pop()!;
|
||||
if (abruptSummaries.has(node)) continue;
|
||||
const children =
|
||||
t.isFunction(node) || t.isClass(node)
|
||||
? []
|
||||
: childNodes(node).filter(
|
||||
(child) =>
|
||||
t.isStatement(child) ||
|
||||
t.isSwitchCase(child) ||
|
||||
t.isCatchClause(child),
|
||||
);
|
||||
if (!visited) {
|
||||
pending.push({ node, visited: true });
|
||||
for (const child of children)
|
||||
if (!abruptSummaries.has(child))
|
||||
pending.push({ node: child, visited: false });
|
||||
continue;
|
||||
}
|
||||
let breaks: Set<string | null> | undefined,
|
||||
continues: Set<string | null> | undefined;
|
||||
if (t.isBreakStatement(node))
|
||||
(breaks ??= new Set()).add(node.label?.name ?? null);
|
||||
if (t.isContinueStatement(node))
|
||||
(continues ??= new Set()).add(node.label?.name ?? null);
|
||||
for (const child of children) {
|
||||
const summary = abruptSummaries.get(child)!;
|
||||
for (const label of summary.breaks) (breaks ??= new Set()).add(label);
|
||||
for (const label of summary.continues)
|
||||
(continues ??= new Set()).add(label);
|
||||
}
|
||||
if (t.isLoop(node) || t.isSwitchStatement(node)) breaks?.delete(null);
|
||||
if (t.isLoop(node)) continues?.delete(null);
|
||||
if (t.isLabeledStatement(node)) {
|
||||
breaks?.delete(node.label.name);
|
||||
continues?.delete(node.label.name);
|
||||
}
|
||||
abruptSummaries.set(
|
||||
node,
|
||||
(breaks?.size ?? 0) > 0 || (continues?.size ?? 0) > 0
|
||||
? {
|
||||
breaks: breaks ?? emptyAbruptLabels,
|
||||
continues: continues ?? emptyAbruptLabels,
|
||||
}
|
||||
: emptyAbruptSummary,
|
||||
);
|
||||
}
|
||||
return abruptSummaries.get(root)!;
|
||||
};
|
||||
|
||||
/** An earlier sibling may leave this body before reaching the selected statement. */
|
||||
export const semanticPriorStatementMayExit = (
|
||||
parent: t.Node,
|
||||
child: t.Node,
|
||||
): boolean => {
|
||||
if (!t.isBlockStatement(parent) && !t.isProgram(parent)) return false;
|
||||
let skipped = abruptPrefixes.get(parent);
|
||||
if (skipped === undefined) {
|
||||
const prefixes = new Set<t.Node>();
|
||||
let exits = false;
|
||||
for (const statement of parent.body) {
|
||||
if (exits) prefixes.add(statement);
|
||||
const summary = abruptSummary(statement);
|
||||
exits ||= summary.breaks.size > 0 || summary.continues.size > 0;
|
||||
}
|
||||
skipped = prefixes;
|
||||
abruptPrefixes.set(parent, skipped);
|
||||
}
|
||||
return skipped.has(child);
|
||||
};
|
||||
|
||||
@@ -53,7 +53,6 @@ export const createSemanticExpressionShapeResolver = (
|
||||
// Most modules have no named child arguments. Collect source-wide blockers
|
||||
// only when a query actually needs a named binding or ambient undefined.
|
||||
let blocked: boolean | undefined;
|
||||
const blockFunctionNames = new Set<string>();
|
||||
const namedProofBlocked = (): boolean => {
|
||||
if (blocked !== undefined) return blocked;
|
||||
blocked = false;
|
||||
@@ -62,13 +61,6 @@ export const createSemanticExpressionShapeResolver = (
|
||||
blocked ||=
|
||||
isErasedJavaScriptDeclaration(node) ||
|
||||
isUnmodeledJavaScriptRuntimeScope(node);
|
||||
if (
|
||||
javaScriptRuntimeContext(state.program) !== "module" &&
|
||||
t.isFunctionDeclaration(node) &&
|
||||
node.id != null &&
|
||||
!hoistedFunction(node)
|
||||
)
|
||||
blockFunctionNames.add(node.id.name);
|
||||
},
|
||||
});
|
||||
return blocked;
|
||||
@@ -105,13 +97,9 @@ export const createSemanticExpressionShapeResolver = (
|
||||
)
|
||||
return "unknown";
|
||||
}
|
||||
// Annex B may copy a block function into an outer var without an indexed
|
||||
// assignment. Keep same-named vars unknown rather than model that copy.
|
||||
if (
|
||||
namedProofBlocked() ||
|
||||
semanticOpaqueBindingReadMayHaveChanged(binding, node, state) ||
|
||||
(binding.declarationKinds.has("var") &&
|
||||
blockFunctionNames.has(binding.name))
|
||||
semanticOpaqueBindingReadMayHaveChanged(binding, node, state)
|
||||
)
|
||||
return "unknown";
|
||||
const initializer = semanticStableInitializer(binding, state);
|
||||
|
||||
@@ -13,11 +13,12 @@ import {
|
||||
} from "./javascriptAstValues.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
semanticResolutionBlocked,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
||||
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
|
||||
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
|
||||
import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js";
|
||||
import { range } from "./javascriptStaticAnalysisHelpers.js";
|
||||
import {
|
||||
assignedSemanticResultBindings,
|
||||
@@ -140,7 +141,7 @@ const promiseCandidateDetails = (
|
||||
if (
|
||||
t.isNewExpression(node) &&
|
||||
t.isIdentifier(node.callee, { name: "Promise" }) &&
|
||||
isUnshadowedGlobalPromise(node.callee, state)
|
||||
isUnmodifiedGlobalReference(node.callee, state, "Promise")
|
||||
)
|
||||
return { kind: "constructor", method: "new" };
|
||||
if (t.isCallExpression(node) || t.isOptionalCallExpression(node)) {
|
||||
@@ -149,7 +150,7 @@ const promiseCandidateDetails = (
|
||||
const method = semanticStaticPropertyName(member.property, member.computed);
|
||||
if (
|
||||
t.isIdentifier(member.object, { name: "Promise" }) &&
|
||||
isUnshadowedGlobalPromise(member.object, state)
|
||||
isUnmodifiedGlobalReference(node.callee, state, ["Promise", method ?? ""])
|
||||
) {
|
||||
if (method === "all" || method === "allSettled")
|
||||
return { kind: "aggregate", method };
|
||||
@@ -164,13 +165,6 @@ const promiseCandidateDetails = (
|
||||
return null;
|
||||
};
|
||||
|
||||
const isUnshadowedGlobalPromise = (
|
||||
node: t.Identifier,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): boolean =>
|
||||
resolveSemanticBindingState(state, node, "Promise") === undefined &&
|
||||
!semanticResolutionBlocked(state, node, "Promise");
|
||||
|
||||
const isPromiseProducer = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
@@ -447,14 +441,16 @@ const resolvePromiseExpression = (
|
||||
return { promiseIds: [direct.promiseId], status: "complete" };
|
||||
if (!t.isIdentifier(node)) return unresolvedPromise();
|
||||
const binding = resolveSemanticBindingState(state, node, node.name);
|
||||
if (binding === undefined || seenBindings.has(binding.bindingId))
|
||||
return unresolvedPromise();
|
||||
const read = semanticBindingAtRead(binding, node, state.parentsByNode);
|
||||
if (
|
||||
binding === undefined ||
|
||||
binding.mutable ||
|
||||
binding.initializers.length !== 1 ||
|
||||
seenBindings.has(binding.bindingId)
|
||||
read.status !== "ordered" ||
|
||||
!read.hasDefiniteInitializer ||
|
||||
read.binding.initializers.length !== 1
|
||||
)
|
||||
return unresolvedPromise();
|
||||
const initializer = binding.initializers[0];
|
||||
const initializer = read.binding.initializers[0];
|
||||
if (initializer === undefined || initializer.projection.length > 0)
|
||||
return unresolvedPromise();
|
||||
const nestedSeen = new Set(seenBindings);
|
||||
|
||||
@@ -117,6 +117,8 @@ export interface JavaScriptSemanticScopeState {
|
||||
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. */
|
||||
@@ -301,7 +303,14 @@ type SemanticBindingLookup =
|
||||
readonly kind: "binding" | "uncertain-binding";
|
||||
readonly binding: JavaScriptSemanticBindingState;
|
||||
}
|
||||
| { readonly kind: "ambient" | "uncertain-ambient" | "captured" | "unknown" };
|
||||
| {
|
||||
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 = (
|
||||
@@ -314,6 +323,8 @@ export const semanticBindingLookupFromScope = (
|
||||
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(
|
||||
|
||||
@@ -658,12 +658,16 @@ const evaluateExpressionValue = (
|
||||
const binding = resolveSemanticBindingState(context.state, node, node.name);
|
||||
if (binding === undefined)
|
||||
return { status: "unknown", reason: `Unbound identifier ${node.name}.` };
|
||||
const read =
|
||||
const read = semanticBindingAtRead(
|
||||
binding,
|
||||
node,
|
||||
context.state.parentsByNode,
|
||||
);
|
||||
if (
|
||||
read.status === "unresolved" &&
|
||||
binding.mutable &&
|
||||
binding.definitions.some(({ kind }) => kind === "assignment")
|
||||
? semanticBindingAtRead(binding, node, context.state.parentsByNode)
|
||||
: undefined;
|
||||
if (read?.status === "unresolved") {
|
||||
) {
|
||||
const value = evaluateBinding(binding, nestedContext(context));
|
||||
if (
|
||||
value.status === "unknown" ||
|
||||
@@ -676,7 +680,7 @@ const evaluateExpressionValue = (
|
||||
reason: `Binding ${binding.name} has unresolved ordering at this read.`,
|
||||
};
|
||||
}
|
||||
const atRead = read?.binding ?? binding;
|
||||
const atRead = read.binding;
|
||||
if (
|
||||
semanticOpaqueBindingReadMayHaveChanged(
|
||||
binding,
|
||||
|
||||
Reference in New Issue
Block a user