fix(javascript): preserve initializer execution and lexical ownership

This commit is contained in:
morluto
2026-10-24 02:28:12 +08:00
parent 5cbcd58550
commit fba4faf88a
8 changed files with 488 additions and 41 deletions
+10
View File
@@ -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,