mirror of
https://github.com/morluto/rea
synced 2026-10-26 22:28:27 -04:00
0cc56493be
Select binding facts at reads without mixing whole-binding and temporal values. Share conditional activation rules, preserve partial expression order, and bound immutable ancestry work. Memoize temporal results within each query using initializer identity, capture point, budget and dependency proofs; preserve depth limits and source Evidence. Fixes #3881
148 lines
4.7 KiB
TypeScript
148 lines
4.7 KiB
TypeScript
import { expect, it, vi } from "vitest";
|
|
|
|
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
|
import { onlyBinding } from "./javascriptSemanticAnalysis.fixture.js";
|
|
import type { JavaScriptSemanticIr } from "./javascriptSemanticIr.js";
|
|
|
|
/** Count AST ancestry lookups made while analyzing one source. */
|
|
const ancestryReads = (
|
|
source: string,
|
|
inspect: (ir: JavaScriptSemanticIr) => void,
|
|
): number => {
|
|
const original = WeakMap.prototype.get;
|
|
let reads = 0;
|
|
const get = vi.spyOn(WeakMap.prototype, "get").mockImplementation(function (
|
|
this: WeakMap<object, unknown>,
|
|
key,
|
|
) {
|
|
const value: unknown = original.call(this, key);
|
|
if (
|
|
typeof value === "object" &&
|
|
value !== null &&
|
|
"type" in value &&
|
|
typeof value.type === "string"
|
|
)
|
|
reads++;
|
|
return value;
|
|
});
|
|
try {
|
|
inspect(analyzeJavaScriptSemantics(source));
|
|
return reads;
|
|
} finally {
|
|
get.mockRestore();
|
|
}
|
|
};
|
|
|
|
it.each(["const a=1;", "let a; a=1;"])(
|
|
"bounds ancestry reads for repeated deep lexical references after %s",
|
|
(declaration) => {
|
|
const reads = (depth: number): number =>
|
|
ancestryReads(
|
|
`${declaration} module.exports.value=${"a+(".repeat(depth)}a${")".repeat(depth)};`,
|
|
(ir) => {
|
|
expect(ir.coverage).toEqual({ status: "complete", omittedCount: 0 });
|
|
expect(onlyBinding(ir, "a").value).toEqual({
|
|
status: "literal",
|
|
value: 1,
|
|
});
|
|
},
|
|
);
|
|
|
|
const smaller = reads(80);
|
|
const larger = reads(160);
|
|
expect(smaller).toBeGreaterThan(0);
|
|
expect(larger).toBeLessThan(3 * smaller);
|
|
},
|
|
);
|
|
|
|
it("bounds ancestry reads after a write in the same expression", () => {
|
|
const reads = (depth: number): number =>
|
|
ancestryReads(
|
|
`let a=0; module.exports.value={set:(a=1),value:${"a+(".repeat(depth)}a${")".repeat(depth)}};`,
|
|
(ir) => {
|
|
expect(ir.coverage).toEqual({ status: "complete", omittedCount: 0 });
|
|
expect(onlyBinding(ir, "a").value.status).toBe("ambiguous");
|
|
},
|
|
);
|
|
const smaller = reads(80);
|
|
const larger = reads(160);
|
|
expect(smaller).toBeGreaterThan(0);
|
|
expect(larger).toBeLessThan(3 * smaller);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"object",
|
|
(index: number) => `var s${String(index + 1)}={x:s${String(index)}};`,
|
|
],
|
|
[
|
|
"array",
|
|
(index: number) => `var s${String(index + 1)}=[s${String(index)}];`,
|
|
],
|
|
[
|
|
"sequence-assigned object",
|
|
(index: number) =>
|
|
`var s${String(index + 1)}; 0, s${String(index + 1)}={x:s${String(index)}};`,
|
|
],
|
|
])(
|
|
"keeps the expression depth budget across a chain of %s wrappers",
|
|
(_, wrap) => {
|
|
// Reusing each wrapper's value for the next would nest without limit.
|
|
const nesting = (length: number): number => {
|
|
const ir = analyzeJavaScriptSemantics(
|
|
`var s0={};${Array.from({ length: length - 1 }, (unused, index) => wrap(index)).join("")}`,
|
|
);
|
|
let value = onlyBinding(ir, `s${String(length - 1)}`).value;
|
|
let depth = 0;
|
|
while (value.status === "object" || value.status === "array") {
|
|
const [slot] =
|
|
value.status === "object" ? value.properties : value.items;
|
|
if (slot === undefined) return depth;
|
|
value = slot.value;
|
|
depth++;
|
|
}
|
|
expect(value).toMatchObject({ resourceLimit: "expression-depth" });
|
|
expect(ir.coverage.status).toBe("partial");
|
|
return depth;
|
|
};
|
|
|
|
expect(nesting(40)).toBe(39);
|
|
const bounded = nesting(300);
|
|
expect(bounded).toBeLessThan(256);
|
|
expect(nesting(600)).toBe(bounded);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["an object", "{ value: 1 }", "object"],
|
|
["a self-referential namespace", "s0 || {}", "ambiguous"],
|
|
["an unknown parameter", "seed", "unknown"],
|
|
])(
|
|
"bounds nested closures that alias %s from the enclosing scope",
|
|
(_, root, status) => {
|
|
// Each closure reads its parent's local once live and once at its own
|
|
// capture point; re-deriving the parent each time doubles work per level.
|
|
const reads = (depth: number): number =>
|
|
ancestryReads(
|
|
`function f0(seed){ var s0=${root};${Array.from(
|
|
{ length: depth - 1 },
|
|
(unused, index) =>
|
|
` function f${String(index + 1)}(){ var s${String(index + 1)}=s${String(index)};`,
|
|
).join("")}${"}".repeat(depth)}`,
|
|
(ir) => {
|
|
expect(ir.coverage).toEqual({ status: "complete", omittedCount: 0 });
|
|
expect(onlyBinding(ir, `s${String(depth - 1)}`).value).toEqual(
|
|
onlyBinding(ir, "s1").value,
|
|
);
|
|
expect(onlyBinding(ir, "s1").value.status).toBe(status);
|
|
},
|
|
);
|
|
|
|
const smaller = reads(8);
|
|
const larger = reads(16);
|
|
expect(smaller).toBeGreaterThan(0);
|
|
// Doubling per level would multiply the reads by 256.
|
|
expect(larger).toBeLessThan(8 * smaller);
|
|
},
|
|
);
|