fix(javascript): derive loop cardinality from exact literal values

Use the canonical cooked literal value for for-in and for-of string cardinality, preserving possible assignments and prototype uncertainty across quoted and template spellings.

Validation: 473 focused tests across six files, including public CLI assignment Evidence and exported return shapes. Refs #4617
This commit is contained in:
morluto committed 2026-10-17 08:27:39 +08:00
1 parent 746a044e42
commit c3e42385e7
3 files changed
+141 -5

No files matched your search

@@ -547,6 +547,62 @@ describe("nonfinite static values", () => {
});
describe("binding write invalidation", () => {
it.each(["in", "of"])(
"retains prototype uncertainty for an empty template in for-%s",
(operator) => {
const ir = analyzeJavaScriptSemantics(`
String.prototype.extra = "possible inherited key";
let selected = "before";
for (selected ${operator} \`\`) {}
const observed = selected;
`);
expect(
topLevelBinding(ir, "selected").definitions.some(
({ kind }) => kind === "assignment",
),
).toBe(true);
expect(topLevelBinding(ir, "observed").value.status).not.toBe("literal");
},
);
it.each(
[
{ input: '"abc"', empty: false },
{ input: "`abc`", empty: false },
{ input: "`\\u0061`", empty: false },
{ input: '""', empty: true },
{ input: "``", empty: true },
{ input: "`\\\n`", empty: true },
].flatMap((literal) =>
["in", "of"].flatMap((operator) =>
["selected", "[selected]"].map((head) => ({
...literal,
operator,
head,
})),
),
),
)(
"preserves writes for $head $operator $input (empty=$empty)",
({ input, empty, operator, head }) => {
const ir = analyzeJavaScriptSemantics(`
let selected = "before";
for (${head} ${operator} ${input}) {}
const observed = selected;
export function result() { return observed; }
`);
expect(
topLevelBinding(ir, "selected").definitions.some(
({ kind }) => kind === "assignment",
),
).toBe(!empty);
const value = topLevelBinding(ir, "observed").value;
if (empty) expect(value).toEqual({ status: "literal", value: "before" });
else expect(value).not.toEqual({ status: "literal", value: "before" });
expect(onlyCallable(ir, "result").returnSites[0]?.value).toEqual(value);
},
);
it.each([
["let value = 1; value++; const observed = value;", "unknown"],
["let value = 1; --value; const observed = value;", "unknown"],
@@ -19,6 +19,7 @@ export const semanticIterationCardinality = (
prototypeMayAffectEnumeration: boolean | undefined,
): JavaScriptSemanticIterationCardinality => {
const iterable = unwrapJavaScriptExpression(loop.right).node;
const literal = readExactJavaScriptLiteral(iterable);
if (t.isForOfStatement(loop)) {
if (prototypeMayAffectEnumeration !== false) return "unknown";
if (t.isArrayExpression(iterable)) {
@@ -31,8 +32,8 @@ export const semanticIterationCardinality = (
return "nonempty";
return "unknown";
}
if (t.isStringLiteral(iterable))
return iterable.value.length === 0 ? "empty" : "nonempty";
if (literal.found && typeof literal.value === "string")
return literal.value.length === 0 ? "empty" : "nonempty";
return "unknown";
}
if (t.isObjectExpression(iterable)) {
@@ -57,10 +58,14 @@ export const semanticIterationCardinality = (
return "unknown";
return prototypeMayAffectEnumeration === false ? "empty" : "unknown";
}
if (t.isNullLiteral(iterable)) return "empty";
if (t.isStringLiteral(iterable) && iterable.value.length > 0)
if (literal.found && literal.value === null) return "empty";
if (
literal.found &&
typeof literal.value === "string" &&
literal.value.length > 0
)
return "nonempty";
if (readExactJavaScriptLiteral(iterable).found)
if (literal.found)
return prototypeMayAffectEnumeration === false ? "empty" : "unknown";
return "unknown";
};