diff --git a/src/application/javascript/JavaScriptApplicationSignedZero.test.ts b/src/application/javascript/JavaScriptApplicationSignedZero.test.ts new file mode 100644 index 0000000000..b1f4df0ba4 --- /dev/null +++ b/src/application/javascript/JavaScriptApplicationSignedZero.test.ts @@ -0,0 +1,26 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; + +import { expect, it } from "vitest"; + +import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js"; +import { javascriptApplicationAnalysisResultSchema } from "../../domain/javascript/javascriptApplicationAnalysis.js"; +import { analyzeJavaScriptApplication } from "./JavaScriptApplicationService.js"; + +it("does not expose a resolved positive-zero literal for a negative-zero export", async () => { + const root = await createTestTempDirectory("rea-signed-zero-"); + await writeFile(join(root, "app.js"), "export const value = -0;\n"); + const result = await analyzeJavaScriptApplication({ + input_path: root, + format: "directory", + }); + if (!result.ok) throw result.error; + const analysis = javascriptApplicationAnalysisResultSchema.parse( + result.value.normalized_result, + ); + expect( + analysis.semantic_graph.nodes.filter( + ({ kind, properties }) => kind === "literal" && properties.value === 0, + ), + ).toEqual([]); +}); diff --git a/src/domain/javascript/javascriptSemanticAnalysis.signed_zero.test.ts b/src/domain/javascript/javascriptSemanticAnalysis.signed_zero.test.ts new file mode 100644 index 0000000000..6dfbd0dfe6 --- /dev/null +++ b/src/domain/javascript/javascriptSemanticAnalysis.signed_zero.test.ts @@ -0,0 +1,35 @@ +import { expect, it } from "vitest"; + +import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js"; +import { topLevelBinding } from "./javascriptSemanticAnalysis.fixture.js"; +import { semanticPrimitiveSet } from "./javascriptSemanticPrimitives.js"; + +it.each(["-0", "-0.0", "-0x0", "-0 * 5", "-1 * 0", '+"-0"'])( + "does not publish positive zero evidence for %s", + (expression) => { + const ir = analyzeJavaScriptSemantics( + `export const value = ${expression};`, + ); + expect(topLevelBinding(ir, "value").value).toMatchObject({ + status: "unknown", + }); + }, +); + +it("does not collapse a possible negative-zero branch into positive-zero certainty", () => { + const ir = analyzeJavaScriptSemantics("const value = choice ? -0 : 0;"); + expect(topLevelBinding(ir, "value").value.status).not.toBe("literal"); + expect(semanticPrimitiveSet([-0, 0])).toMatchObject({ status: "unknown" }); +}); + +it.each([ + { expression: "0", value: 0 }, + { expression: "-1", value: -1 }, + { expression: '"-0"', value: "-0" }, +])("preserves representable primitive $expression", ({ expression, value }) => { + const ir = analyzeJavaScriptSemantics(`const value = ${expression};`); + expect(topLevelBinding(ir, "value").value).toEqual({ + status: "literal", + value, + }); +}); diff --git a/src/domain/javascript/javascriptSemanticPrimitives.ts b/src/domain/javascript/javascriptSemanticPrimitives.ts index 29cff687b2..abb1f015c3 100644 --- a/src/domain/javascript/javascriptSemanticPrimitives.ts +++ b/src/domain/javascript/javascriptSemanticPrimitives.ts @@ -21,6 +21,11 @@ export const semanticPrimitiveSet = ( status: "unknown", reason: "Nonfinite numbers are outside the JSON primitive lattice.", }; + if (typeof value === "number" && Object.is(value, -0)) + return { + status: "unknown", + reason: "Negative zero cannot be preserved by JSON primitive evidence.", + }; uniqueValues.add(value); if (uniqueValues.size > SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT) return semanticResourceLimitUnknown("primitive-candidates"); diff --git a/src/domain/javascript/javascriptSemanticValues.ts b/src/domain/javascript/javascriptSemanticValues.ts index 2fdebe5128..d0eb007b5b 100644 --- a/src/domain/javascript/javascriptSemanticValues.ts +++ b/src/domain/javascript/javascriptSemanticValues.ts @@ -171,7 +171,10 @@ const evaluateExpression = ( if (context.expressionDepth > SEMANTIC_EXPRESSION_DEPTH_LIMIT) return semanticResourceLimitUnknown("expression-depth"); const literal = readExactJavaScriptLiteral(node); - if (literal.found) return { status: "literal", value: literal.value }; + if (literal.found) + return typeof literal.value === "number" + ? primitiveSet([literal.value]) + : { status: "literal", value: literal.value }; if (t.isIdentifier(node)) { const binding = resolveSemanticBindingState(context.state, node, node.name); return binding === undefined