mirror of
https://github.com/morluto/rea.git
synced 2026-11-12 01:41:21 +00:00
feat(analysis): make resource budgets recoverable across CLI and MCP (#9706)
* refactor(analysis): centralize resource budgets and recovery facts * feat(analysis): expose ASAR and Ghidra runtime admission controls * feat(artifacts): expose reader admission and preserve limited recognition * feat(javascript): expose recoverable semantic precision budgets * feat(web): add recoverable source-map budgets and compact retained views * feat(web): make historical capture decoding budgets recoverable * fix(desktop): enforce selected view policies across capture and export * feat(runtime): make Frida and LLDB retention budgets selectable * feat(native): expose diagnostic budgets and preserve exhaustion causes * feat(managed): expose recoverable snapshot and representation budgets * feat(metadata): make Go and Rust inspection budgets recoverable * feat(wasm): expose representation and producer admissions * feat(evm): expose per-call worker resource budgets * feat(android): make resource and string-pool omissions recoverable * feat(producers): expose command and recorded-capture budgets * docs(analysis): record recovery contracts and integrated validation * fix(docs): link calibration oracle from published verification pages * fix(docs): restore desktop task anchors and receipt links * test(analysis): align fixtures with selectable budget contracts * fix(analysis): remove source-location projector dependency cycle * test(analysis): refresh budget recovery and verifier regressions * test(analysis): preserve format and publication recovery expectations * test(windows): align conformance with owned runtime admission * fix(desktop): preserve selected alias kinds in exported state * test(analysis): align provider fixtures with recoverable budgets * test(desktop): validate emitted alias metadata structurally * fix(ghidra): classify bounded Windows endpoint refusals * test(cli): assert selectable artifact admission recovery * fix(cli): preserve capture provenance in environment builds
This commit is contained in:
1 parent
55b6d2ca20
commit
cb4e5cfbff
515 files changed
+21066
-3637
No files matched your search
@@ -1284,7 +1284,9 @@ it("reports nested bridge depth truncation while retaining shallow paths", () =>
|
||||
expect(result.electron.call_paths).toContainEqual(
|
||||
expect.objectContaining({ entry: "desktop.shallow", channel: "shallow" }),
|
||||
);
|
||||
expect(result.limitations.join(" ")).toContain("depth 128");
|
||||
expect(result.limitations.join(" ")).toContain(
|
||||
"semantic_precision.wrapper_depth=128",
|
||||
);
|
||||
});
|
||||
it("preserves conditions when a wrapper dispatches through conditional external methods", () => {
|
||||
const result = analyzeJavaScriptStaticSource(`
|
||||
|
||||
@@ -263,7 +263,7 @@ export function* collectElectronWrappedCallPathsSteps(
|
||||
): Generator<void, readonly ElectronCallPath[]> {
|
||||
// Bounds repeated interprocedural expansion independently of bundle bytes.
|
||||
// Exhaustion is reported as unknown coverage, never as a negative finding.
|
||||
let remainingVisits = 1_000_000;
|
||||
let remainingVisits = state.precision.wrapper_visits;
|
||||
const prefixes = new ElectronCallPathBlockPrefixes(
|
||||
() => --remainingVisits >= 0,
|
||||
state.parentsByNode,
|
||||
@@ -820,9 +820,9 @@ export function* collectElectronWrappedCallPathsSteps(
|
||||
readonly fn: SymbolicFunction;
|
||||
}> {
|
||||
if (active.has(value)) return;
|
||||
if (active.size >= 128) {
|
||||
if (active.size >= state.precision.wrapper_depth) {
|
||||
accumulator.unknownFindings += 1;
|
||||
accumulator.electronCallPathLimitReached = true;
|
||||
(accumulator.electronCallPathLimits ??= new Set()).add("wrapper_depth");
|
||||
return;
|
||||
}
|
||||
const nested = new Set(active);
|
||||
@@ -1580,7 +1580,9 @@ export function* collectElectronWrappedCallPathsSteps(
|
||||
recordBridgeExposures();
|
||||
if (remainingVisits < 0 || resolver.truncated) {
|
||||
accumulator.unknownFindings += 1;
|
||||
accumulator.electronCallPathLimitReached = true;
|
||||
const limits = (accumulator.electronCallPathLimits ??= new Set());
|
||||
if (remainingVisits < 0) limits.add("wrapper_visits");
|
||||
for (const limit of resolver.resourceLimits) limits.add(limit);
|
||||
} else
|
||||
// A dynamic site's channels are enumerated only when the complete trace
|
||||
// reached it with literal channels, no other invocation outcome, and no
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
javaScriptSemanticPrecisionSchema,
|
||||
} from "./javascriptSemanticPrecision.js";
|
||||
import { z } from "zod";
|
||||
|
||||
/** Complete static checkpoint / per-script result transport ceiling. */
|
||||
@@ -200,6 +204,15 @@ export const javaScriptAnalysisCoverageLimits = (
|
||||
|
||||
/** Optional operational choices; omission keeps them out of Evidence parameters. */
|
||||
export const javaScriptAnalysisResourceControlsSchema = z.strictObject({
|
||||
semantic_precision: javaScriptSemanticPrecisionSchema.optional().describe(
|
||||
`Per-source evaluator budgets. Omitted fields preserve defaults: ${Object.entries(
|
||||
JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
)
|
||||
.map(([name, value]) => `${name} ${String(value)}`)
|
||||
.join(
|
||||
", ",
|
||||
)}. Raising these does not raise graph retention, worker memory, transfer or parent result budgets; cyclic and unsupported values remain unknown.`,
|
||||
),
|
||||
max_semantic_nodes: z
|
||||
.number()
|
||||
.int()
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
staticObjectCopyPropertyReader,
|
||||
staticObjectPropertyNames,
|
||||
} from "./javascriptAstValues.js";
|
||||
import { SEMANTIC_EXPRESSION_DEPTH_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
|
||||
it("keeps string spread indices separate from named descriptor fields", () => {
|
||||
for (const text of ["", "x", "😀", "x".repeat(1_000_000)]) {
|
||||
@@ -145,17 +144,17 @@ it("limits unary-plus keys to transparent numeric literals", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("bounds nested inline object spreads without losing a shallow final property", () => {
|
||||
it("reads nested inline object spreads beyond the former evaluator depth cap", () => {
|
||||
let object = t.objectExpression([
|
||||
t.objectProperty(t.identifier("stdio"), t.stringLiteral("inherit")),
|
||||
]);
|
||||
for (let depth = 0; depth < SEMANTIC_EXPRESSION_DEPTH_LIMIT; depth++)
|
||||
for (let depth = 0; depth < 1_024; depth++)
|
||||
object = t.objectExpression([t.spreadElement(object)]);
|
||||
expect(staticObjectProperty(object, "stdio").status).toBe("explicit");
|
||||
expect(
|
||||
staticObjectProperty(t.objectExpression([t.spreadElement(object)]), "stdio")
|
||||
.status,
|
||||
).toBe("dynamic");
|
||||
).toBe("explicit");
|
||||
});
|
||||
|
||||
it("collects complete own-key sets through bounded literal spreads", () => {
|
||||
@@ -222,17 +221,17 @@ it("retains known keys alongside unresolved object membership", () => {
|
||||
).toEqual({ status: "partial", names: ["direct"] });
|
||||
});
|
||||
|
||||
it("does not claim a complete key set after exceeding spread depth", () => {
|
||||
it("keeps complete key sets across deeply nested finite inline spreads", () => {
|
||||
let object = t.objectExpression([
|
||||
t.objectProperty(t.identifier("query"), t.objectExpression([])),
|
||||
]);
|
||||
for (let depth = 0; depth < SEMANTIC_EXPRESSION_DEPTH_LIMIT; depth++)
|
||||
for (let depth = 0; depth < 1_024; depth++)
|
||||
object = t.objectExpression([t.spreadElement(object)]);
|
||||
expect(staticObjectPropertyNames(object).status).toBe("complete");
|
||||
expect(
|
||||
staticObjectPropertyNames(t.objectExpression([t.spreadElement(object)]))
|
||||
.status,
|
||||
).toBe("partial");
|
||||
).toBe("complete");
|
||||
});
|
||||
|
||||
it("enumerates a wide inline spread without a variadic call overflow", () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as t from "@babel/types";
|
||||
|
||||
import type { JavaScriptSemanticPrimitive } from "./javascriptSemanticValueTypes.js";
|
||||
import { SEMANTIC_EXPRESSION_DEPTH_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
import { semanticArrayIndex } from "./javascriptSemanticPropertyPaths.js";
|
||||
|
||||
/** Computed definition keys use the surrounding evaluation environment. */
|
||||
@@ -304,14 +303,22 @@ export const isObjectPrototypeSetter = (
|
||||
!property.shorthand &&
|
||||
key === "__proto__";
|
||||
|
||||
/** Expand only bounded literal spreads, preserving unknown writes in order. */
|
||||
/** Iterate literal syntax once, preserving unknown writes and prototype order. */
|
||||
const appendStaticObjectMembers = (
|
||||
members: StaticObjectMember[],
|
||||
object: t.ObjectExpression,
|
||||
depth: number,
|
||||
isNullish?: (node: t.Node) => boolean,
|
||||
): void => {
|
||||
for (const property of object.properties) {
|
||||
const pending = [{ object, depth, next: 0 }];
|
||||
while (pending.length > 0) {
|
||||
const frame = pending[pending.length - 1]!;
|
||||
const property = frame.object.properties[frame.next++];
|
||||
if (property === undefined) {
|
||||
pending.pop();
|
||||
continue;
|
||||
}
|
||||
const depth = frame.depth;
|
||||
if (!t.isSpreadElement(property)) {
|
||||
const key = semanticStaticPropertyKey(property.key, property.computed);
|
||||
if (
|
||||
@@ -337,34 +344,36 @@ const appendStaticObjectMembers = (
|
||||
members.push({ kind: "property", property, key });
|
||||
continue;
|
||||
}
|
||||
const { node: spread, depth: wrappers } = unwrapJavaScriptExpression(
|
||||
const nested = appendEnumerableSourceMembers(
|
||||
members,
|
||||
property.argument,
|
||||
depth,
|
||||
isNullish,
|
||||
);
|
||||
appendEnumerableSourceMembers(members, spread, depth + wrappers, isNullish);
|
||||
if (nested !== undefined) pending.push({ ...nested, next: 0 });
|
||||
}
|
||||
};
|
||||
|
||||
/** Copy own enumerable facts; inherited properties and prototype syntax are not writes. */
|
||||
/** Record atomic own-key facts or return literal syntax for iterative copying. */
|
||||
const appendEnumerableSourceMembers = (
|
||||
members: StaticObjectMember[],
|
||||
source: t.Node,
|
||||
depth: number,
|
||||
isNullish?: (node: t.Node) => boolean,
|
||||
): void => {
|
||||
):
|
||||
| { readonly object: t.ObjectExpression; readonly depth: number }
|
||||
| undefined => {
|
||||
const { node: spread, depth: wrappers } = unwrapJavaScriptExpression(source);
|
||||
if (t.isObjectExpression(spread)) {
|
||||
const nextDepth = depth + wrappers + 1;
|
||||
if (
|
||||
nextDepth > SEMANTIC_EXPRESSION_DEPTH_LIMIT ||
|
||||
spread.properties.some((entry) =>
|
||||
t.isObjectMethod(entry, { kind: "get" }),
|
||||
)
|
||||
) {
|
||||
members.push(UNKNOWN_STATIC_OBJECT_MEMBER);
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
appendStaticObjectMembers(members, spread, nextDepth, isNullish);
|
||||
return;
|
||||
return { object: spread, depth: depth + wrappers + 1 };
|
||||
}
|
||||
if (t.isArrayExpression(spread)) {
|
||||
let firstSpread = -1;
|
||||
@@ -384,7 +393,7 @@ const appendEnumerableSourceMembers = (
|
||||
elements: spread.elements,
|
||||
firstSpread,
|
||||
});
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
const primitive = primitiveOwnKeys(spread, isNullish);
|
||||
if (primitive === "indices") {
|
||||
@@ -394,7 +403,7 @@ const appendEnumerableSourceMembers = (
|
||||
? literal.value.length
|
||||
: null;
|
||||
if (length !== 0) members.push({ kind: "primitive-indices", length });
|
||||
return;
|
||||
return undefined;
|
||||
}
|
||||
if (
|
||||
primitive === "none" ||
|
||||
@@ -402,8 +411,9 @@ const appendEnumerableSourceMembers = (
|
||||
t.isArrowFunctionExpression(spread) ||
|
||||
t.isRegExpLiteral(spread)
|
||||
)
|
||||
return;
|
||||
return undefined;
|
||||
members.push(UNKNOWN_STATIC_OBJECT_MEMBER);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const staticObjectMembers = (
|
||||
@@ -485,7 +495,9 @@ export const staticObjectCopyPropertyReader = (
|
||||
): ((name: string) => StaticObjectProperty) => {
|
||||
if (source === undefined) return indexStaticObjectProperties("missing");
|
||||
const members: StaticObjectMember[] = [];
|
||||
appendEnumerableSourceMembers(members, source, 0, isNullish);
|
||||
const nested = appendEnumerableSourceMembers(members, source, 0, isNullish);
|
||||
if (nested !== undefined)
|
||||
appendStaticObjectMembers(members, nested.object, nested.depth, isNullish);
|
||||
return indexStaticObjectProperties(members);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from "./javascriptAstValues.js";
|
||||
import { collectJavaScriptIdentityWritesSteps } from "./javascriptElectronMemberWrites.js";
|
||||
import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js";
|
||||
import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
import { createSemanticModuleInterop } from "./javascriptModuleInterop.js";
|
||||
import {
|
||||
semanticRequireOriginCandidate,
|
||||
@@ -187,7 +186,7 @@ const canonicalWebpackInteropBindings = (
|
||||
const node = pending.pop();
|
||||
if (node === undefined || seen.has(node)) continue;
|
||||
seen.add(node);
|
||||
if (seen.size > SEMANTIC_ALIAS_SELECTION_LIMIT) {
|
||||
if (seen.size > state.precision.alias_selections) {
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
semanticEffectTiming,
|
||||
} from "./javascriptSemanticMutationInitializers.js";
|
||||
import { javaScriptRuntimeContext } from "./javascriptSourceParser.js";
|
||||
import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
|
||||
const exportInvocations = new WeakMap<
|
||||
JavaScriptSemanticAnalysisState,
|
||||
@@ -92,7 +91,7 @@ const copiedOwnFields = (
|
||||
while (pending.length > 0) {
|
||||
const node = unwrapJavaScriptExpression(pending.pop()!).node;
|
||||
if (seen.has(node)) continue;
|
||||
if (seen.size >= SEMANTIC_ALIAS_SELECTION_LIMIT) {
|
||||
if (seen.size >= state.precision.alias_selections) {
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
return unknown;
|
||||
}
|
||||
@@ -132,7 +131,7 @@ const copiedOwnFields = (
|
||||
return unknown;
|
||||
if (
|
||||
seen.size + pending.length + selected.length >
|
||||
SEMANTIC_ALIAS_SELECTION_LIMIT
|
||||
state.precision.alias_selections
|
||||
) {
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
return unknown;
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
type SemanticCapturedSelection,
|
||||
} from "./javascriptSemanticProjectedSources.js";
|
||||
import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js";
|
||||
import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
@@ -96,7 +95,7 @@ export const createSemanticElectronAppPathChangeQuery = (
|
||||
const node = unwrapJavaScriptExpression(candidate.node).node;
|
||||
// Close the finite source graph after the precise context budget is
|
||||
// exhausted. Widening alone is never evidence of an Electron origin.
|
||||
if (!widened && scheduled >= SEMANTIC_ALIAS_SELECTION_LIMIT) {
|
||||
if (!widened && scheduled >= state.precision.alias_selections) {
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
widened = true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { JavaScriptSemanticPropertyPath } from "./javascriptSemanticPropertyPaths.js";
|
||||
import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
import { JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS } from "./javascriptSemanticPrecision.js";
|
||||
|
||||
type PropertyKey = JavaScriptSemanticPropertyPath[number];
|
||||
|
||||
@@ -76,7 +76,10 @@ export class SemanticAliasSelections {
|
||||
#calls = 0;
|
||||
#limited = false;
|
||||
|
||||
constructor(readonly onLimit: () => void) {}
|
||||
constructor(
|
||||
readonly onLimit: () => void,
|
||||
readonly maxSelections: number = JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS.alias_selections,
|
||||
) {}
|
||||
|
||||
get limited(): boolean {
|
||||
return this.#limited;
|
||||
@@ -95,10 +98,7 @@ export class SemanticAliasSelections {
|
||||
);
|
||||
const covered = paths.find((prior) => pathCovers(prior, candidate));
|
||||
if (covered !== undefined) return covered;
|
||||
if (
|
||||
existing !== undefined &&
|
||||
++this.#calls > SEMANTIC_ALIAS_SELECTION_LIMIT
|
||||
) {
|
||||
if (existing !== undefined && ++this.#calls > this.maxSelections) {
|
||||
this.#limited = true;
|
||||
this.#paths = new WeakMap();
|
||||
this.onLimit();
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
resolveJavaScriptSemanticPrecision,
|
||||
type JavaScriptSemanticPrecisionInput,
|
||||
} from "./javascriptSemanticPrecision.js";
|
||||
import { semanticPatternPropertyReadSources } from "./javascriptSemanticIterationSources.js";
|
||||
import {
|
||||
createSemanticOpaqueGlobalObserver,
|
||||
@@ -182,11 +186,12 @@ interface AddBindingInput {
|
||||
/** Recover lexical bindings, aliases, constants, and module links. */
|
||||
export const analyzeJavaScriptSemantics = (
|
||||
source: string,
|
||||
precision?: JavaScriptSemanticPrecisionInput,
|
||||
): JavaScriptSemanticIr => {
|
||||
const file = parseJavaScriptSource(source);
|
||||
return file === null
|
||||
? failedJavaScriptSemanticIr()
|
||||
: analyzeParsedJavaScriptSemantics(file);
|
||||
: analyzeParsedJavaScriptSemantics(file, "import", precision);
|
||||
};
|
||||
|
||||
/** Recover lexical references, optionally for one name, without evaluating values or provenance. */
|
||||
@@ -406,8 +411,9 @@ const semanticPathsOverlap = (
|
||||
/** Share the lexical index with bounded, demand-driven boundary analyses. */
|
||||
export function* indexJavaScriptBindingsSteps(
|
||||
program: t.Program,
|
||||
precision?: JavaScriptSemanticPrecisionInput,
|
||||
): Generator<void, JavaScriptSemanticAnalysisState> {
|
||||
const state = createState(program);
|
||||
const state = createState(program, "import", precision);
|
||||
yield* collectDefinitionsSteps(program, state);
|
||||
yield* indexSemanticAmbientWritesSteps(state);
|
||||
return state;
|
||||
@@ -417,9 +423,15 @@ export function* indexJavaScriptBindingsSteps(
|
||||
export const analyzeParsedJavaScriptSemantics = (
|
||||
file: ParsedJavaScriptSource,
|
||||
staticImportResolutionMode: JavaScriptModuleResolutionMode = "import",
|
||||
precision?: JavaScriptSemanticPrecisionInput,
|
||||
): JavaScriptSemanticIr =>
|
||||
completeSemanticSteps(
|
||||
analyzeParsedJavaScriptSemanticsSteps(file, staticImportResolutionMode),
|
||||
analyzeParsedJavaScriptSemanticsSteps(
|
||||
file,
|
||||
staticImportResolutionMode,
|
||||
undefined,
|
||||
precision,
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -432,8 +444,13 @@ export function* analyzeParsedJavaScriptSemanticsSteps(
|
||||
file: ParsedJavaScriptSource,
|
||||
staticImportResolutionMode: JavaScriptModuleResolutionMode = "import",
|
||||
expressionNodeBudget?: number,
|
||||
precision?: JavaScriptSemanticPrecisionInput,
|
||||
): Generator<void, JavaScriptSemanticIr> {
|
||||
const state = createState(file.program, staticImportResolutionMode);
|
||||
const state = createState(
|
||||
file.program,
|
||||
staticImportResolutionMode,
|
||||
precision,
|
||||
);
|
||||
yield* collectDefinitionsSteps(file.program, state);
|
||||
yield* indexSemanticAmbientWritesSteps(state);
|
||||
yield* indexSemanticMemberMutationFactsSteps(state);
|
||||
@@ -467,6 +484,7 @@ export function* analyzeParsedJavaScriptSemanticsSteps(
|
||||
...new Set([...state.valueResourceLimits, ...state.getterResourceLimits]),
|
||||
].sort(),
|
||||
[...state.returnedReferenceWidenings].map(range),
|
||||
state.precision,
|
||||
);
|
||||
return {
|
||||
schema: "JavaScriptSemanticIR",
|
||||
@@ -486,14 +504,14 @@ export function* analyzeParsedJavaScriptSemanticsSteps(
|
||||
]
|
||||
: []),
|
||||
...(state.valueResourceLimits.has("alias-selections")
|
||||
? [semanticResourceLimitReason("alias-selections")]
|
||||
? [semanticResourceLimitReason("alias-selections", state.precision)]
|
||||
: []),
|
||||
...(state.returnedReferenceWidenings.size > 0
|
||||
? [RETURNED_REFERENCE_WIDENING_LIMITATION]
|
||||
: []),
|
||||
...[...state.getterResourceLimits]
|
||||
.sort()
|
||||
.map(semanticResourceLimitReason),
|
||||
.map((limit) => semanticResourceLimitReason(limit, state.precision)),
|
||||
...(parserPartial
|
||||
? [
|
||||
"The parser recovered from syntax errors; affected bindings are partial.",
|
||||
@@ -537,8 +555,9 @@ export interface JavaScriptReceiverIndex extends JavaScriptBindingIndex {
|
||||
export function* indexJavaScriptBindingsWithReceiversSteps(
|
||||
program: t.Program,
|
||||
hostContext?: "commonjs-wrapper",
|
||||
precision?: JavaScriptSemanticPrecisionInput,
|
||||
): Generator<void, JavaScriptReceiverIndex> {
|
||||
const state = createState(program);
|
||||
const state = createState(program, "import", precision);
|
||||
if (hostContext === "commonjs-wrapper") {
|
||||
const scope = currentSemanticScope(state.scopes);
|
||||
const binding: JavaScriptSemanticBindingState = {
|
||||
@@ -1154,6 +1173,7 @@ const bindingOrigin = (
|
||||
const createState = (
|
||||
program: t.Program,
|
||||
staticImportResolutionMode: JavaScriptModuleResolutionMode = "import",
|
||||
precision?: JavaScriptSemanticPrecisionInput,
|
||||
): JavaScriptSemanticAnalysisState => {
|
||||
const root: JavaScriptSemanticScopeState = {
|
||||
scopeId: semanticScopeId("program", program),
|
||||
@@ -1164,6 +1184,7 @@ const createState = (
|
||||
bindings: new Map(),
|
||||
};
|
||||
return {
|
||||
precision: resolveJavaScriptSemanticPrecision(precision),
|
||||
arrayIterationUnknown: false,
|
||||
memberMutationFactsComplete: false,
|
||||
program,
|
||||
|
||||
@@ -269,7 +269,7 @@ const directLiteral = (
|
||||
} => {
|
||||
const literal = readExactJavaScriptLiteral(node);
|
||||
if (!literal.found) return {};
|
||||
const parsed = semanticPrimitiveSet([literal.value]);
|
||||
const parsed = semanticPrimitiveSet([literal.value], state.precision);
|
||||
if (parsed.status === "literal") return { literal: parsed.value };
|
||||
if (
|
||||
parsed.status === "unknown" &&
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { semanticResourceLimitFacts } from "./javascriptSemanticResourceLimits.js";
|
||||
import type { JavaScriptSourceRange } from "./javascriptStaticAnalysisTypes.js";
|
||||
import type { JavaScriptSemanticResourceLimit } from "./javascriptSemanticValueTypes.js";
|
||||
import type { ApplicationCoverage } from "./javascriptApplicationEvidenceSchemas.js";
|
||||
import {
|
||||
SEMANTIC_EXPRESSION_DEPTH_LIMIT,
|
||||
SEMANTIC_GETTER_EXPANSION_LIMIT,
|
||||
SEMANTIC_ALIAS_SELECTION_LIMIT,
|
||||
SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT,
|
||||
SEMANTIC_PRIMITIVE_JSON_BYTES_LIMIT,
|
||||
} from "./javascriptSemanticResourceLimits.js";
|
||||
JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
type JavaScriptSemanticPrecision,
|
||||
} from "./javascriptSemanticPrecision.js";
|
||||
|
||||
/** Exact retention state for one callable's direct returns. */
|
||||
export type JavaScriptSemanticReturnCoverage = {
|
||||
@@ -23,18 +21,21 @@ export type JavaScriptSemanticCoverage =
|
||||
readonly status: "complete";
|
||||
readonly omittedCount: 0;
|
||||
readonly resourceLimits?: readonly JavaScriptSemanticResourceLimit[];
|
||||
readonly precision?: JavaScriptSemanticPrecision;
|
||||
readonly widenedReturnedReferences?: readonly JavaScriptSourceRange[];
|
||||
}
|
||||
| {
|
||||
readonly status: "partial";
|
||||
readonly omittedCount: 0 | null;
|
||||
readonly resourceLimits?: readonly JavaScriptSemanticResourceLimit[];
|
||||
readonly precision?: JavaScriptSemanticPrecision;
|
||||
readonly widenedReturnedReferences?: readonly JavaScriptSourceRange[];
|
||||
}
|
||||
| {
|
||||
readonly status: "failed";
|
||||
readonly omittedCount: null;
|
||||
readonly resourceLimits?: readonly JavaScriptSemanticResourceLimit[];
|
||||
readonly precision?: JavaScriptSemanticPrecision;
|
||||
readonly widenedReturnedReferences?: readonly JavaScriptSourceRange[];
|
||||
};
|
||||
|
||||
@@ -47,15 +48,22 @@ export const semanticCoverage = (
|
||||
parserPartial: boolean,
|
||||
resourceLimits: readonly JavaScriptSemanticResourceLimit[] = [],
|
||||
widenedReturnedReferences: readonly JavaScriptSourceRange[] = [],
|
||||
precision: JavaScriptSemanticPrecision = JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
): JavaScriptSemanticCoverage => {
|
||||
const widened =
|
||||
widenedReturnedReferences.length > 0 ? { widenedReturnedReferences } : {};
|
||||
const details = {
|
||||
...(widenedReturnedReferences.length > 0
|
||||
? { widenedReturnedReferences }
|
||||
: {}),
|
||||
...(precision === JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS
|
||||
? {}
|
||||
: { precision }),
|
||||
};
|
||||
if (resourceLimits.length > 0)
|
||||
return {
|
||||
status: "partial",
|
||||
omittedCount: null,
|
||||
resourceLimits,
|
||||
...widened,
|
||||
...details,
|
||||
};
|
||||
return {
|
||||
status:
|
||||
@@ -63,7 +71,7 @@ export const semanticCoverage = (
|
||||
? "partial"
|
||||
: "complete",
|
||||
omittedCount: 0,
|
||||
...widened,
|
||||
...details,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -72,40 +80,22 @@ export const semanticCoverageResourceLimits = (
|
||||
coverage: JavaScriptSemanticCoverage,
|
||||
): readonly JavaScriptSemanticResourceLimit[] => coverage.resourceLimits ?? [];
|
||||
|
||||
const semanticResourceBounds: Record<
|
||||
JavaScriptSemanticResourceLimit,
|
||||
Pick<ApplicationCoverage["limits"][number], "value" | "unit">
|
||||
> = {
|
||||
"primitive-candidates": {
|
||||
value: SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT,
|
||||
unit: "items",
|
||||
},
|
||||
"primitive-bytes": {
|
||||
value: SEMANTIC_PRIMITIVE_JSON_BYTES_LIMIT,
|
||||
unit: "bytes",
|
||||
},
|
||||
"expression-depth": {
|
||||
value: SEMANTIC_EXPRESSION_DEPTH_LIMIT,
|
||||
unit: "depth",
|
||||
},
|
||||
"getter-expansion": {
|
||||
value: SEMANTIC_GETTER_EXPANSION_LIMIT,
|
||||
unit: "items",
|
||||
},
|
||||
"alias-selections": {
|
||||
value: SEMANTIC_ALIAS_SELECTION_LIMIT,
|
||||
unit: "items",
|
||||
},
|
||||
};
|
||||
|
||||
/** Map semantic evaluator bounds to their caller-visible application coverage. */
|
||||
/** Effective evaluator facts, rather than reconstructed default-only bounds. */
|
||||
export const semanticResourceLimitCoverage = (
|
||||
resourceLimits: readonly JavaScriptSemanticResourceLimit[],
|
||||
precision: JavaScriptSemanticPrecision = JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
): ApplicationCoverage["limits"] =>
|
||||
resourceLimits.map((resourceLimit) => ({
|
||||
name: `javascript_semantic_${resourceLimit.replaceAll("-", "_")}`,
|
||||
...semanticResourceBounds[resourceLimit],
|
||||
}));
|
||||
resourceLimits.map((resourceLimit) => {
|
||||
const { value, unit } = semanticResourceLimitFacts(
|
||||
resourceLimit,
|
||||
precision,
|
||||
);
|
||||
return {
|
||||
name: `javascript_semantic_${resourceLimit.replaceAll("-", "_")}`,
|
||||
value,
|
||||
unit,
|
||||
};
|
||||
});
|
||||
|
||||
/** Classify one callable's direct-return coverage. */
|
||||
export const semanticReturnCoverage = (
|
||||
|
||||
@@ -220,7 +220,7 @@ const collectDefaults = (
|
||||
if (candidate === undefined) return;
|
||||
const literal = readExactJavaScriptLiteral(node.right);
|
||||
const value: JavaScriptSemanticValue = literal.found
|
||||
? semanticPrimitiveSet([literal.value])
|
||||
? semanticPrimitiveSet([literal.value], context.state.precision)
|
||||
: { status: "unknown", reason: "Default is not an exact primitive." };
|
||||
recordSemanticValueLimit(context.state, value);
|
||||
output.push({
|
||||
|
||||
@@ -188,7 +188,7 @@ function* collectExpressionFlowsSteps(
|
||||
const expression = unwrapJavaScriptExpression(returnValue).node;
|
||||
const exact = readExactJavaScriptLiteral(expression);
|
||||
if (exact.found) {
|
||||
const value = semanticPrimitiveSet([exact.value]);
|
||||
const value = semanticPrimitiveSet([exact.value], state.precision);
|
||||
if (value.status === "literal" && literalReturns.length < budget)
|
||||
literalReturns.push({
|
||||
location: range(expression),
|
||||
@@ -270,7 +270,9 @@ function* collectExpressionFlowsSteps(
|
||||
(t.isSequenceExpression(node) && index !== values.length - 1);
|
||||
const operand = unwrapJavaScriptExpression(value).node;
|
||||
const exact = readExactJavaScriptLiteral(operand);
|
||||
const literal = exact.found ? semanticPrimitiveSet([exact.value]) : null;
|
||||
const literal = exact.found
|
||||
? semanticPrimitiveSet([exact.value], state.precision)
|
||||
: null;
|
||||
if (
|
||||
literal?.status === "unknown" &&
|
||||
literal.resourceLimit === "primitive-bytes"
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
isUnshadowedGlobal,
|
||||
semanticBindingAtRead,
|
||||
} from "./javascriptSemanticMutationInitializers.js";
|
||||
import { SEMANTIC_EXPRESSION_DEPTH_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
semanticStableInitializer,
|
||||
@@ -66,7 +65,7 @@ export const createSemanticExpressionShapeResolver = (
|
||||
const unwrapped = unwrapJavaScriptExpression(node);
|
||||
node = unwrapped.node;
|
||||
depth += unwrapped.depth;
|
||||
if (depth > SEMANTIC_EXPRESSION_DEPTH_LIMIT) {
|
||||
if (depth > state.precision.expression_depth) {
|
||||
state.valueResourceLimits.add("expression-depth");
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type * as t from "@babel/types";
|
||||
import type { JavaScriptSemanticObjectReference as Reference } from "./javascriptSemanticObjectReference.js";
|
||||
import type { JavaScriptSemanticPropertyPath as PropertyPath } from "./javascriptSemanticPropertyPaths.js";
|
||||
import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js";
|
||||
import { SEMANTIC_GETTER_EXPANSION_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
|
||||
export interface GetterInvocation {
|
||||
readonly getter: t.ObjectMethod | t.ClassMethod;
|
||||
@@ -71,7 +70,10 @@ export class SemanticGetterQuery {
|
||||
/** Charge retained local work and getter jobs to the same precise query. */
|
||||
reserveResult(): boolean {
|
||||
if (this.#limited) return false;
|
||||
if (!this.#fallback && this.#visits >= SEMANTIC_GETTER_EXPANSION_LIMIT) {
|
||||
if (
|
||||
!this.#fallback &&
|
||||
this.#visits >= this.state.precision.getter_expansions
|
||||
) {
|
||||
this.#limited = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -67,7 +67,6 @@ import {
|
||||
semanticScopeExitEffect,
|
||||
} from "./javascriptSemanticMutationInitializers.js";
|
||||
import { semanticCoercionOperands } from "./javascriptSemanticLanguageInvocations.js";
|
||||
import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
import {
|
||||
semanticKnownArrayIteration,
|
||||
semanticReferenceSourcesInactive,
|
||||
@@ -148,7 +147,7 @@ const localAccessorMethods = (
|
||||
if (paths.has(key)) continue;
|
||||
paths.add(key);
|
||||
examined.set(node, paths);
|
||||
if (++selections > SEMANTIC_ALIAS_SELECTION_LIMIT) {
|
||||
if (++selections > state.precision.alias_selections) {
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
return undefined;
|
||||
}
|
||||
@@ -407,7 +406,7 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
]);
|
||||
if (examined.has(identity)) continue;
|
||||
examined.add(identity);
|
||||
if (examined.size > SEMANTIC_ALIAS_SELECTION_LIMIT) {
|
||||
if (examined.size > state.precision.alias_selections) {
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
return [{ node: root, path: [] }];
|
||||
}
|
||||
@@ -810,7 +809,7 @@ export function* collectSemanticMemberMutationsSteps(
|
||||
if (exact.has(selection)) return;
|
||||
if (
|
||||
exact.size > 0 &&
|
||||
++additionalAlternatives > SEMANTIC_ALIAS_SELECTION_LIMIT
|
||||
++additionalAlternatives > state.precision.alias_selections
|
||||
) {
|
||||
projectionsWidened = true;
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
||||
import { onlyBinding } from "./javascriptSemanticAnalysis.fixture.js";
|
||||
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
||||
import { semanticResourceLimitCoverage } from "./javascriptSemanticCoverage.js";
|
||||
import { javaScriptSemanticPrecisionSchema } from "./javascriptSemanticPrecision.js";
|
||||
|
||||
const alternatives = (start: number, count: number): string =>
|
||||
count === 1
|
||||
? JSON.stringify(`value_${String(start)}`)
|
||||
: `(flag ? ${alternatives(start, count / 2)} : ${alternatives(start + count / 2, count / 2)})`;
|
||||
|
||||
it("recovers the same finite candidate set with a source-owned precision budget", () => {
|
||||
const source = `export const selected = ${alternatives(0, 512)};`;
|
||||
const bounded = analyzeJavaScriptSemantics(source);
|
||||
expect(onlyBinding(bounded, "selected").value).toMatchObject({
|
||||
status: "unknown",
|
||||
resourceLimit: "primitive-candidates",
|
||||
reason: expect.stringContaining(
|
||||
"semantic_precision.primitive_candidates=256",
|
||||
),
|
||||
});
|
||||
const recovered = analyzeJavaScriptSemantics(source, {
|
||||
primitive_candidates: 512,
|
||||
});
|
||||
const value = onlyBinding(recovered, "selected").value;
|
||||
expect(value.status).toBe("union");
|
||||
if (value.status !== "union")
|
||||
throw new Error("Expected finite primitive alternatives");
|
||||
expect(value.values).toHaveLength(512);
|
||||
expect(value.values).toContain("value_511");
|
||||
expect(recovered.coverage.resourceLimits).toBeUndefined();
|
||||
// A later source does not inherit a previous source's caller choice.
|
||||
expect(
|
||||
onlyBinding(analyzeJavaScriptSemantics(source), "selected").value,
|
||||
).toMatchObject({
|
||||
status: "unknown",
|
||||
resourceLimit: "primitive-candidates",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves exact effective precision when an increased budget still exhausts", () => {
|
||||
const source = `const selected = ${alternatives(0, 512)};`;
|
||||
const result = analyzeJavaScriptSemantics(source, {
|
||||
primitive_candidates: 300,
|
||||
});
|
||||
expect(onlyBinding(result, "selected").value).toMatchObject({
|
||||
reason: expect.stringContaining(
|
||||
"semantic_precision.primitive_candidates=300",
|
||||
),
|
||||
});
|
||||
expect(
|
||||
semanticResourceLimitCoverage(
|
||||
result.coverage.resourceLimits ?? [],
|
||||
result.coverage.precision,
|
||||
),
|
||||
).toContainEqual({
|
||||
name: "javascript_semantic_primitive_candidates",
|
||||
value: 300,
|
||||
unit: "items",
|
||||
});
|
||||
});
|
||||
|
||||
it("distinguishes wrapper work recovery from unchanged unsupported or cyclic syntax", () => {
|
||||
const source = `const e = require("electron"); e.ipcMain.on("first", event => event.sender.copy());`;
|
||||
const bounded = analyzeJavaScriptStaticSource(source, { wrapper_visits: 1 });
|
||||
expect(
|
||||
bounded.analysis_coverage?.examination.electron_call_paths_limited,
|
||||
).toBe(true);
|
||||
expect(bounded.limitations).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining("semantic_precision.wrapper_visits=1"),
|
||||
]),
|
||||
);
|
||||
const recovered = analyzeJavaScriptStaticSource(source, {
|
||||
wrapper_visits: 10_000,
|
||||
});
|
||||
expect(
|
||||
recovered.analysis_coverage?.examination.electron_call_paths_limited,
|
||||
).toBe(false);
|
||||
expect((recovered.electron.call_paths ?? []).length).toBeGreaterThan(0);
|
||||
const cycle = analyzeJavaScriptSemantics(
|
||||
"const a = b; const b = a; const selected = a;",
|
||||
{ primitive_candidates: 512 },
|
||||
);
|
||||
expect(onlyBinding(cycle, "selected").value).toMatchObject({
|
||||
status: "unknown",
|
||||
});
|
||||
expect(onlyBinding(cycle, "selected").value).not.toHaveProperty(
|
||||
"resourceLimit",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ primitive_candidates: 0 },
|
||||
{ primitive_bytes: -1 },
|
||||
{ wrapper_visits: 1.5 },
|
||||
{ expression_depth: Number.POSITIVE_INFINITY },
|
||||
])(
|
||||
"rejects invalid precision without silently replacing the caller choice: %o",
|
||||
(input) =>
|
||||
expect(javaScriptSemanticPrecisionSchema.safeParse(input).success).toBe(
|
||||
false,
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/** Per-source evaluator work, distinct from retained graph size and worker memory. */
|
||||
export const JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS = {
|
||||
primitive_candidates: 256,
|
||||
primitive_bytes: 1_048_576,
|
||||
expression_depth: 256,
|
||||
getter_expansions: 512,
|
||||
alias_selections: 512,
|
||||
wrapper_visits: 1_000_000,
|
||||
wrapper_depth: 128,
|
||||
} as const;
|
||||
|
||||
const work = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER);
|
||||
export const javaScriptSemanticPrecisionSchema = z.strictObject({
|
||||
primitive_candidates: work.exactOptional(),
|
||||
primitive_bytes: work
|
||||
.exactOptional()
|
||||
.describe(
|
||||
`Conservative JSON string-payload allowance per evaluated candidate set; default ${String(JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS.primitive_bytes)}. Admission charges six bytes per UTF-16 code unit plus quotes before creating concatenated strings, so ordinary ASCII can exhaust this estimate before its actual JSON encoding reaches the selected byte count. Independent of graph, checkpoint and worker-transfer budgets.`,
|
||||
),
|
||||
expression_depth: work.exactOptional(),
|
||||
getter_expansions: work.exactOptional(),
|
||||
alias_selections: work.exactOptional(),
|
||||
wrapper_visits: work.exactOptional(),
|
||||
wrapper_depth: work.exactOptional(),
|
||||
});
|
||||
export type JavaScriptSemanticPrecisionInput = z.output<
|
||||
typeof javaScriptSemanticPrecisionSchema
|
||||
>;
|
||||
export type JavaScriptSemanticPrecision = {
|
||||
readonly [Key in keyof typeof JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS]: number;
|
||||
};
|
||||
|
||||
/** Resolve once at source admission; consumers share this exact policy. */
|
||||
export const resolveJavaScriptSemanticPrecision = (
|
||||
input?: JavaScriptSemanticPrecisionInput,
|
||||
): JavaScriptSemanticPrecision =>
|
||||
input === undefined
|
||||
? JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS
|
||||
: { ...JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS, ...input };
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
type JavaScriptSemanticPrecision,
|
||||
} from "./javascriptSemanticPrecision.js";
|
||||
import type {
|
||||
JavaScriptSemanticPrimitive,
|
||||
JavaScriptSemanticValue,
|
||||
@@ -5,7 +9,6 @@ import type {
|
||||
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
||||
import { semanticPrimitiveKey } from "./javascriptSemanticProvenance.js";
|
||||
import {
|
||||
SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT,
|
||||
exceedsSemanticPrimitiveStringByteBudget,
|
||||
semanticResourceLimitUnknown,
|
||||
} from "./javascriptSemanticResourceLimits.js";
|
||||
@@ -13,6 +16,7 @@ import {
|
||||
/** Normalize one bounded collection of possible primitive values. */
|
||||
export const semanticPrimitiveSet = (
|
||||
values: readonly JavaScriptSemanticPrimitive[],
|
||||
precision: JavaScriptSemanticPrecision = JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
): JavaScriptSemanticValue => {
|
||||
let uniqueValues: Set<JavaScriptSemanticPrimitive> | undefined;
|
||||
for (const value of values) {
|
||||
@@ -30,13 +34,13 @@ export const semanticPrimitiveSet = (
|
||||
// or canonical-key allocation. Check strings before retaining the value.
|
||||
if (values.length === 1)
|
||||
return typeof value === "string" &&
|
||||
exceedsSemanticPrimitiveStringByteBudget([value])
|
||||
? semanticResourceLimitUnknown("primitive-bytes")
|
||||
exceedsSemanticPrimitiveStringByteBudget([value], precision)
|
||||
? semanticResourceLimitUnknown("primitive-bytes", precision)
|
||||
: { status: "literal", value };
|
||||
uniqueValues ??= new Set<JavaScriptSemanticPrimitive>();
|
||||
uniqueValues.add(value);
|
||||
if (uniqueValues.size > SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT)
|
||||
return semanticResourceLimitUnknown("primitive-candidates");
|
||||
if (uniqueValues.size > precision.primitive_candidates)
|
||||
return semanticResourceLimitUnknown("primitive-candidates", precision);
|
||||
}
|
||||
if (uniqueValues === undefined) return { status: "union", values: [] };
|
||||
if (
|
||||
@@ -44,9 +48,10 @@ export const semanticPrimitiveSet = (
|
||||
[...uniqueValues].flatMap((value) =>
|
||||
typeof value === "string" ? [value] : [],
|
||||
),
|
||||
precision,
|
||||
)
|
||||
)
|
||||
return semanticResourceLimitUnknown("primitive-bytes");
|
||||
return semanticResourceLimitUnknown("primitive-bytes", precision);
|
||||
const unique = [...uniqueValues]
|
||||
.map((value) => ({ key: semanticPrimitiveKey(value), value }))
|
||||
.sort((left, right) => compareUnicodeCodePoints(left.key, right.key))
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
semanticArrayPropertyReferences,
|
||||
type JavaScriptSemanticObjectReference,
|
||||
} from "./javascriptSemanticObjectReferences.js";
|
||||
import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
import {
|
||||
semanticBindingAtRead,
|
||||
semanticDefiniteEffectPrecedesReference,
|
||||
@@ -115,7 +114,7 @@ const hasFreshProjectedReceiver = (
|
||||
if (paths.has(key)) return false;
|
||||
paths.add(key);
|
||||
examined.set(node, paths);
|
||||
if (++selections > SEMANTIC_ALIAS_SELECTION_LIMIT) {
|
||||
if (++selections > state.precision.alias_selections) {
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
return false;
|
||||
}
|
||||
@@ -138,7 +137,7 @@ const hasFreshProjectedReceiver = (
|
||||
if (sources.length === 0) return false;
|
||||
if (
|
||||
selections + pending.length + sources.length >
|
||||
SEMANTIC_ALIAS_SELECTION_LIMIT
|
||||
state.precision.alias_selections
|
||||
) {
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
return false;
|
||||
|
||||
@@ -53,7 +53,6 @@ import {
|
||||
semanticAliasSelectionChoices,
|
||||
type SemanticAliasSelectionPath,
|
||||
} from "./javascriptSemanticAliasSelections.js";
|
||||
import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
import {
|
||||
semanticRequireOriginReaders,
|
||||
type JavaScriptRequireLoaderFact,
|
||||
@@ -413,7 +412,7 @@ const selectedRequireSourcesAt = (
|
||||
if (paths.has(key)) continue;
|
||||
paths.add(key);
|
||||
seen.set(node, paths);
|
||||
if (++count > SEMANTIC_ALIAS_SELECTION_LIMIT) {
|
||||
if (++count > state.precision.alias_selections) {
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
return { loader: "unknown", sourceRelative: false };
|
||||
}
|
||||
@@ -1179,7 +1178,7 @@ const hasCommonJsModuleSource = (
|
||||
// Keep depths separate and join finite key alternatives at each source read.
|
||||
const selections = new SemanticAliasSelections(() => {
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
});
|
||||
}, state.precision.alias_selections);
|
||||
const contexts = new WeakMap<
|
||||
t.Node,
|
||||
Map<
|
||||
@@ -1228,10 +1227,10 @@ const hasCommonJsModuleSource = (
|
||||
// Consumption contexts must not provide an unbounded second path to
|
||||
// expansion beyond the shared selector budget.
|
||||
if (exhausted) {
|
||||
if (++drained > SEMANTIC_ALIAS_SELECTION_LIMIT) return "incomplete";
|
||||
if (++drained > state.precision.alias_selections) return "incomplete";
|
||||
} else if (
|
||||
(byMode.size > 1 || context.identities.size > 0) &&
|
||||
++additionalStates > SEMANTIC_ALIAS_SELECTION_LIMIT
|
||||
++additionalStates > state.precision.alias_selections
|
||||
) {
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
if (includeUnknown) return "possible";
|
||||
|
||||
@@ -3,28 +3,53 @@ import type {
|
||||
JavaScriptSemanticValue,
|
||||
} from "./javascriptSemanticValueTypes.js";
|
||||
|
||||
/** Maximum distinct alternatives retained in one normalized primitive value. */
|
||||
export const SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT = 256;
|
||||
/** Maximum active expression nesting during inert semantic evaluation. */
|
||||
export const SEMANTIC_EXPRESSION_DEPTH_LIMIT = 256;
|
||||
/** Getter jobs and local result work retained in one precise query. */
|
||||
export const SEMANTIC_GETTER_EXPANSION_LIMIT = 512;
|
||||
/** Additional selector states retained by one precise alias query. */
|
||||
export const SEMANTIC_ALIAS_SELECTION_LIMIT = 512;
|
||||
/** Bound normalized strings and transient keys independently of transport limits. */
|
||||
export const SEMANTIC_PRIMITIVE_JSON_BYTES_LIMIT = 1_048_576;
|
||||
import {
|
||||
JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
type JavaScriptSemanticPrecision,
|
||||
} from "./javascriptSemanticPrecision.js";
|
||||
|
||||
const RESOURCE_LIMIT_REASON: Record<JavaScriptSemanticResourceLimit, string> = {
|
||||
"primitive-candidates":
|
||||
"Primitive candidate budget exceeded (maximum 256 alternatives).",
|
||||
"primitive-bytes":
|
||||
"Primitive string-byte budget exceeded (maximum 1048576 estimated JSON bytes).",
|
||||
"expression-depth":
|
||||
"Semantic expression depth budget exceeded (maximum 256 nested expressions).",
|
||||
"getter-expansion":
|
||||
"Getter expansion was conservatively bounded (maximum 512 scheduled states or a growing receiver cycle).",
|
||||
"alias-selections":
|
||||
"Alias selection expansion was conservatively bounded (maximum 512 additional selector states); selected paths remain unknown.",
|
||||
const resourceControls = {
|
||||
"primitive-candidates": ["primitive_candidates", "items", "alternatives"],
|
||||
"primitive-bytes": ["primitive_bytes", "bytes", "estimated JSON bytes"],
|
||||
"expression-depth": ["expression_depth", "depth", "nested expressions"],
|
||||
"getter-expansion": ["getter_expansions", "items", "scheduled states"],
|
||||
"alias-selections": [
|
||||
"alias_selections",
|
||||
"items",
|
||||
"additional selector states",
|
||||
],
|
||||
} as const;
|
||||
|
||||
/** One owner maps exhaustion classes to controls and effective coverage facts. */
|
||||
export const semanticResourceLimitFacts = (
|
||||
resourceLimit: JavaScriptSemanticResourceLimit,
|
||||
precision: JavaScriptSemanticPrecision = JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
) => {
|
||||
const [control, unit, displayUnit] = resourceControls[resourceLimit];
|
||||
return {
|
||||
control,
|
||||
unit,
|
||||
displayUnit,
|
||||
value: precision[control],
|
||||
defaultValue: JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS[control],
|
||||
};
|
||||
};
|
||||
|
||||
/** Exact caller control and effective value at the evaluator boundary. */
|
||||
export const semanticResourceLimitReason = (
|
||||
resourceLimit: JavaScriptSemanticResourceLimit,
|
||||
precision: JavaScriptSemanticPrecision = JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
): string => {
|
||||
const { control, displayUnit, value, defaultValue } =
|
||||
semanticResourceLimitFacts(resourceLimit, precision);
|
||||
const prefixes = {
|
||||
"primitive-candidates": `Primitive candidate budget exceeded (maximum ${String(precision.primitive_candidates)} alternatives).`,
|
||||
"primitive-bytes": `Primitive string-byte budget exceeded (maximum ${String(precision.primitive_bytes)} estimated JSON bytes).`,
|
||||
"expression-depth": `Semantic expression depth budget exceeded (maximum ${String(precision.expression_depth)} nested expressions).`,
|
||||
"getter-expansion": `Getter expansion was conservatively bounded (maximum ${String(precision.getter_expansions)} scheduled states or a growing receiver cycle).`,
|
||||
"alias-selections": `Alias selection expansion was conservatively bounded (maximum ${String(precision.alias_selections)} additional selector states); selected paths remain unknown.`,
|
||||
};
|
||||
return `${prefixes[resourceLimit]} Semantic ${resourceLimit} budget reached: semantic_precision.${control}=${String(value)} ${displayUnit} (default ${String(defaultValue)}). Increase semantic_precision.${control} on the next analysis call (analyze_javascript_application or analyze_web_bundle), or pass --semantic-precision JSON on the CLI; no REA restart is required. Worker memory, transfer and graph-retention limits remain independent. Cyclic or unsupported evaluations remain unknown regardless of budget.`;
|
||||
};
|
||||
|
||||
/** Check UTF-16 length against worst-case JSON escaping without flattening. */
|
||||
@@ -34,11 +59,12 @@ const jsonStringBytesFromLength = (codeUnits: number): number =>
|
||||
/** Bound string payload before JSON key serialization or candidate creation. */
|
||||
export const exceedsSemanticPrimitiveStringByteBudget = (
|
||||
strings: Iterable<string>,
|
||||
precision: JavaScriptSemanticPrecision = JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
): boolean => {
|
||||
let total = 0;
|
||||
for (const value of strings) {
|
||||
total += jsonStringBytesFromLength(value.length);
|
||||
if (total > SEMANTIC_PRIMITIVE_JSON_BYTES_LIMIT) return true;
|
||||
if (total > precision.primitive_bytes) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -47,6 +73,7 @@ export const exceedsSemanticPrimitiveStringByteBudget = (
|
||||
export const exceedsSemanticPrimitiveAdditionByteBudget = (
|
||||
left: readonly (string | number | boolean | null)[],
|
||||
right: readonly (string | number | boolean | null)[],
|
||||
precision: JavaScriptSemanticPrecision = JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
): boolean => {
|
||||
let total = 0;
|
||||
for (const leftValue of left) {
|
||||
@@ -55,7 +82,7 @@ export const exceedsSemanticPrimitiveAdditionByteBudget = (
|
||||
continue;
|
||||
const length = String(leftValue).length + String(rightValue).length;
|
||||
total += jsonStringBytesFromLength(length);
|
||||
if (total > SEMANTIC_PRIMITIVE_JSON_BYTES_LIMIT) return true;
|
||||
if (total > precision.primitive_bytes) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
@@ -65,27 +92,24 @@ export const exceedsSemanticPrimitiveAdditionByteBudget = (
|
||||
export const exceedsSemanticTemplateByteBudget = (
|
||||
prefixes: readonly string[],
|
||||
suffixes: readonly string[],
|
||||
precision: JavaScriptSemanticPrecision = JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
): boolean => {
|
||||
let total = 0;
|
||||
for (const prefix of prefixes) {
|
||||
for (const suffix of suffixes) {
|
||||
total += jsonStringBytesFromLength(prefix.length + suffix.length);
|
||||
if (total > SEMANTIC_PRIMITIVE_JSON_BYTES_LIMIT) return true;
|
||||
if (total > precision.primitive_bytes) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/** Make an explicit unknown value tagged with the semantic resource bound. */
|
||||
/** Make an explicit unknown value tagged with its effective semantic bound. */
|
||||
export const semanticResourceLimitUnknown = (
|
||||
resourceLimit: JavaScriptSemanticResourceLimit,
|
||||
precision: JavaScriptSemanticPrecision = JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
): JavaScriptSemanticValue => ({
|
||||
status: "unknown",
|
||||
reason: RESOURCE_LIMIT_REASON[resourceLimit],
|
||||
reason: semanticResourceLimitReason(resourceLimit, precision),
|
||||
resourceLimit,
|
||||
});
|
||||
|
||||
/** Display label for one stable resource classification. */
|
||||
export const semanticResourceLimitReason = (
|
||||
resourceLimit: JavaScriptSemanticResourceLimit,
|
||||
): string => RESOURCE_LIMIT_REASON[resourceLimit];
|
||||
@@ -31,7 +31,6 @@ import {
|
||||
semanticShiftCapturedSelections,
|
||||
type SemanticCapturedSelection,
|
||||
} from "./javascriptSemanticProjectedSources.js";
|
||||
import { SEMANTIC_ALIAS_SELECTION_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
import {
|
||||
semanticPropertyPathKeyMatches,
|
||||
type JavaScriptSemanticPropertyPath,
|
||||
@@ -184,7 +183,7 @@ export const createSemanticReturnedReferences = (
|
||||
const selections = new SemanticAliasSelections(() => {
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
result.precision = "widened";
|
||||
});
|
||||
}, state.precision.alias_selections);
|
||||
const enqueue = (
|
||||
node: t.Node,
|
||||
path: Candidate["path"],
|
||||
@@ -255,7 +254,7 @@ export const createSemanticReturnedReferences = (
|
||||
!contextsWidened &&
|
||||
(capturedSelections !== undefined || invocationContext !== undefined) &&
|
||||
paths.size > 0 &&
|
||||
++additionalContexts > SEMANTIC_ALIAS_SELECTION_LIMIT
|
||||
++additionalContexts > state.precision.alias_selections
|
||||
) {
|
||||
contextsWidened = true;
|
||||
state.valueResourceLimits.add("alias-selections");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JavaScriptSemanticPrecision } from "./javascriptSemanticPrecision.js";
|
||||
import type { SemanticPropertyReadSource } from "./javascriptSemanticIterationSources.js";
|
||||
import * as t from "@babel/types";
|
||||
import {
|
||||
@@ -149,6 +150,7 @@ export type SemanticPrototypeOwner = (typeof semanticPrototypeOwners)[number];
|
||||
|
||||
/** Shared state for semantic collection and evaluation. */
|
||||
export interface JavaScriptSemanticAnalysisState {
|
||||
readonly precision: JavaScriptSemanticPrecision;
|
||||
arrayIterationUnknown: boolean;
|
||||
/** Receiver isolation requires the completed member-effect inventory. */
|
||||
memberMutationFactsComplete: boolean;
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
indexJavaScriptBindingsSteps,
|
||||
} from "./javascriptSemanticAnalysis.js";
|
||||
import { SemanticDependencyProofs } from "./javascriptSemanticDependencies.js";
|
||||
import { SEMANTIC_EXPRESSION_DEPTH_LIMIT } from "./javascriptSemanticResourceLimits.js";
|
||||
import { JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS } from "./javascriptSemanticPrecision.js";
|
||||
import type { JavaScriptSemanticValue } from "./javascriptSemanticValueTypes.js";
|
||||
import {
|
||||
clearSemanticValueCaches,
|
||||
@@ -62,7 +62,8 @@ it("reuses dependency-aware read snapshots across a reconvergent value chain", (
|
||||
|
||||
it("preserves the remaining depth budget when reusing a read snapshot", () => {
|
||||
const depth = 12;
|
||||
const nesting = SEMANTIC_EXPRESSION_DEPTH_LIMIT - depth;
|
||||
const nesting =
|
||||
JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS.expression_depth - depth;
|
||||
const source = `let a0=1;${Array.from({ length: depth }, (_, index) => `let a${index + 1}=a${index}+a${index};`).join("")}
|
||||
const root={shallow:a${depth},deep:${"[".repeat(nesting)}a${depth}${"]".repeat(nesting)},after:a${depth}};
|
||||
${Array.from({ length: depth + 1 }, (_, index) => `a${index}=0;`).join("")}`;
|
||||
@@ -95,7 +96,7 @@ ${Array.from({ length: depth + 1 }, (_, index) => `a${index}=0;`).join("")}`;
|
||||
});
|
||||
|
||||
it("bounds reconvergent read work when the expression depth budget is exhausted", () => {
|
||||
const depth = SEMANTIC_EXPRESSION_DEPTH_LIMIT;
|
||||
const depth = JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS.expression_depth;
|
||||
const source = `let a0=1;${Array.from({ length: depth }, (_, index) => `let a${index + 1}=a${index}+a${index};`).join("")}
|
||||
const saved=a${depth};${Array.from({ length: depth + 1 }, (_, index) => `a${index}=0;`).join("")}`;
|
||||
const parsed = parseJavaScriptSource(source, "snapshot-limit.js");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JavaScriptSemanticPrecision } from "./javascriptSemanticPrecision.js";
|
||||
import {
|
||||
semanticBindingMayHaveOpaqueEffect,
|
||||
semanticBindingCanBeRebound,
|
||||
@@ -61,8 +62,6 @@ import {
|
||||
semanticPrimitiveSet as primitiveSet,
|
||||
} from "./javascriptSemanticPrimitives.js";
|
||||
import {
|
||||
SEMANTIC_EXPRESSION_DEPTH_LIMIT,
|
||||
SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT,
|
||||
exceedsSemanticPrimitiveAdditionByteBudget,
|
||||
exceedsSemanticTemplateByteBudget,
|
||||
semanticResourceLimitUnknown,
|
||||
@@ -650,18 +649,24 @@ const evaluateExpressionValue = (
|
||||
node: t.Node,
|
||||
context: EvaluationContext,
|
||||
): JavaScriptSemanticValue => {
|
||||
if (context.expressionDepth > SEMANTIC_EXPRESSION_DEPTH_LIMIT) {
|
||||
if (context.expressionDepth > context.state.precision.expression_depth) {
|
||||
context.dependence.lowest = EVERY_CALLER;
|
||||
return semanticResourceLimitUnknown("expression-depth");
|
||||
return semanticResourceLimitUnknown(
|
||||
"expression-depth",
|
||||
context.state.precision,
|
||||
);
|
||||
}
|
||||
const unwrapped = unwrapJavaScriptExpression(node);
|
||||
if (unwrapped.depth > 0) {
|
||||
if (
|
||||
context.expressionDepth + unwrapped.depth >
|
||||
SEMANTIC_EXPRESSION_DEPTH_LIMIT
|
||||
context.state.precision.expression_depth
|
||||
) {
|
||||
context.dependence.lowest = EVERY_CALLER;
|
||||
return semanticResourceLimitUnknown("expression-depth");
|
||||
return semanticResourceLimitUnknown(
|
||||
"expression-depth",
|
||||
context.state.precision,
|
||||
);
|
||||
}
|
||||
return evaluateExpression(unwrapped.node, {
|
||||
...context,
|
||||
@@ -669,7 +674,8 @@ const evaluateExpressionValue = (
|
||||
});
|
||||
}
|
||||
const literal = readExactJavaScriptLiteral(node);
|
||||
if (literal.found) return primitiveSet([literal.value]);
|
||||
if (literal.found)
|
||||
return primitiveSet([literal.value], context.state.precision);
|
||||
if (
|
||||
t.isFunctionExpression(node) ||
|
||||
t.isFunctionDeclaration(node) ||
|
||||
@@ -722,6 +728,7 @@ const evaluateExpressionValue = (
|
||||
branch.value ??
|
||||
evaluateExpression(branch.node, nestedContext(context)),
|
||||
),
|
||||
context.state.precision,
|
||||
);
|
||||
if (t.isObjectExpression(node)) return evaluateObject(node, context);
|
||||
if (t.isArrayExpression(node)) return evaluateArray(node, context);
|
||||
@@ -746,8 +753,17 @@ const evaluateTemplate = (
|
||||
status: "unknown",
|
||||
reason: "Template text has no cooked representation.",
|
||||
};
|
||||
if (exceedsSemanticTemplateByteBudget(candidates, [text]))
|
||||
return semanticResourceLimitUnknown("primitive-bytes");
|
||||
if (
|
||||
exceedsSemanticTemplateByteBudget(
|
||||
candidates,
|
||||
[text],
|
||||
context.state.precision,
|
||||
)
|
||||
)
|
||||
return semanticResourceLimitUnknown(
|
||||
"primitive-bytes",
|
||||
context.state.precision,
|
||||
);
|
||||
candidates = candidates.map((prefix) => `${prefix}${text}`);
|
||||
const expression = node.expressions[index];
|
||||
if (expression === undefined) continue;
|
||||
@@ -760,16 +776,31 @@ const evaluateTemplate = (
|
||||
reason: "Template expression is not a bounded primitive.",
|
||||
};
|
||||
}
|
||||
if (candidates.length > SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT / values.length)
|
||||
return semanticResourceLimitUnknown("primitive-candidates");
|
||||
if (
|
||||
candidates.length >
|
||||
context.state.precision.primitive_candidates / values.length
|
||||
)
|
||||
return semanticResourceLimitUnknown(
|
||||
"primitive-candidates",
|
||||
context.state.precision,
|
||||
);
|
||||
const suffixes = values.map((value) => String(value));
|
||||
if (exceedsSemanticTemplateByteBudget(candidates, suffixes))
|
||||
return semanticResourceLimitUnknown("primitive-bytes");
|
||||
if (
|
||||
exceedsSemanticTemplateByteBudget(
|
||||
candidates,
|
||||
suffixes,
|
||||
context.state.precision,
|
||||
)
|
||||
)
|
||||
return semanticResourceLimitUnknown(
|
||||
"primitive-bytes",
|
||||
context.state.precision,
|
||||
);
|
||||
candidates = candidates.flatMap((prefix) =>
|
||||
suffixes.map((suffix) => `${prefix}${suffix}`),
|
||||
);
|
||||
}
|
||||
return primitiveSet(candidates);
|
||||
return primitiveSet(candidates, context.state.precision);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1032,7 +1063,7 @@ const evaluateAddition = (
|
||||
current = parent.node.right;
|
||||
break;
|
||||
}
|
||||
value = addPrimitiveValues(parent.left, value);
|
||||
value = addPrimitiveValues(parent.left, value, context.state.precision);
|
||||
pending.pop();
|
||||
}
|
||||
}
|
||||
@@ -1041,6 +1072,7 @@ const evaluateAddition = (
|
||||
const addPrimitiveValues = (
|
||||
leftValue: JavaScriptSemanticValue,
|
||||
rightValue: JavaScriptSemanticValue,
|
||||
precision: JavaScriptSemanticPrecision,
|
||||
): JavaScriptSemanticValue => {
|
||||
const left = primitiveCandidates(leftValue);
|
||||
const right = primitiveCandidates(rightValue);
|
||||
@@ -1049,10 +1081,10 @@ const addPrimitiveValues = (
|
||||
if (isSemanticResourceLimit(rightValue)) return rightValue;
|
||||
return { status: "unknown", reason: "Non-primitive addition." };
|
||||
}
|
||||
if (left.length > SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT / right.length)
|
||||
return semanticResourceLimitUnknown("primitive-candidates");
|
||||
if (exceedsSemanticPrimitiveAdditionByteBudget(left, right))
|
||||
return semanticResourceLimitUnknown("primitive-bytes");
|
||||
if (left.length > precision.primitive_candidates / right.length)
|
||||
return semanticResourceLimitUnknown("primitive-candidates", precision);
|
||||
if (exceedsSemanticPrimitiveAdditionByteBudget(left, right, precision))
|
||||
return semanticResourceLimitUnknown("primitive-bytes", precision);
|
||||
const values = left.flatMap((leftValue) =>
|
||||
right.map((rightValue) =>
|
||||
typeof leftValue === "string" || typeof rightValue === "string"
|
||||
@@ -1060,7 +1092,7 @@ const addPrimitiveValues = (
|
||||
: Number(leftValue) + Number(rightValue),
|
||||
),
|
||||
);
|
||||
return primitiveSet(values);
|
||||
return primitiveSet(values, precision);
|
||||
};
|
||||
|
||||
const evaluateUnary = (
|
||||
@@ -1074,11 +1106,20 @@ const evaluateUnary = (
|
||||
return { status: "unknown", reason: "Non-primitive unary operand." };
|
||||
}
|
||||
if (node.operator === "!")
|
||||
return primitiveSet(argument.map((value) => !value));
|
||||
return primitiveSet(
|
||||
argument.map((value) => !value),
|
||||
context.state.precision,
|
||||
);
|
||||
if (node.operator === "+")
|
||||
return primitiveSet(argument.map((value) => Number(value)));
|
||||
return primitiveSet(
|
||||
argument.map((value) => Number(value)),
|
||||
context.state.precision,
|
||||
);
|
||||
if (node.operator === "-")
|
||||
return primitiveSet(argument.map((value) => -Number(value)));
|
||||
return primitiveSet(
|
||||
argument.map((value) => -Number(value)),
|
||||
context.state.precision,
|
||||
);
|
||||
return { status: "unknown", reason: `Unsupported unary ${node.operator}.` };
|
||||
};
|
||||
|
||||
@@ -1248,25 +1289,28 @@ const provenanceForExpression = (
|
||||
node: t.Node,
|
||||
context: EvaluationContext,
|
||||
): JavaScriptBindingProvenance => {
|
||||
if (context.expressionDepth > SEMANTIC_EXPRESSION_DEPTH_LIMIT) {
|
||||
if (context.expressionDepth > context.state.precision.expression_depth) {
|
||||
context.dependence.lowest = EVERY_CALLER;
|
||||
context.state.valueResourceLimits.add("expression-depth");
|
||||
return semanticUnresolvedProvenance(
|
||||
"unknown",
|
||||
semanticResourceLimitReason("expression-depth"),
|
||||
semanticResourceLimitReason("expression-depth", context.state.precision),
|
||||
);
|
||||
}
|
||||
const unwrapped = unwrapJavaScriptExpression(node);
|
||||
if (unwrapped.depth > 0) {
|
||||
if (
|
||||
context.expressionDepth + unwrapped.depth >
|
||||
SEMANTIC_EXPRESSION_DEPTH_LIMIT
|
||||
context.state.precision.expression_depth
|
||||
) {
|
||||
context.dependence.lowest = EVERY_CALLER;
|
||||
context.state.valueResourceLimits.add("expression-depth");
|
||||
return semanticUnresolvedProvenance(
|
||||
"unknown",
|
||||
semanticResourceLimitReason("expression-depth"),
|
||||
semanticResourceLimitReason(
|
||||
"expression-depth",
|
||||
context.state.precision,
|
||||
),
|
||||
);
|
||||
}
|
||||
return provenanceForExpression(unwrapped.node, {
|
||||
@@ -1406,6 +1450,7 @@ const isLocallyCreatedValue = (node: t.Node): boolean =>
|
||||
|
||||
const mergeValues = (
|
||||
values: readonly JavaScriptSemanticValue[],
|
||||
precision: JavaScriptSemanticPrecision,
|
||||
): JavaScriptSemanticValue => {
|
||||
const resourceLimit = values.find(isSemanticResourceLimit);
|
||||
if (resourceLimit !== undefined) return resourceLimit;
|
||||
@@ -1415,7 +1460,7 @@ const mergeValues = (
|
||||
const merged: JavaScriptSemanticValue = values.every(
|
||||
(value) => value.status === "union" || value.status === "literal",
|
||||
)
|
||||
? primitiveSet(primitives)
|
||||
? primitiveSet(primitives, precision)
|
||||
: { status: "ambiguous", reason: "Branches have incompatible values." };
|
||||
if (values.length > 0 && values.every(semanticValueDefinitelyDefined))
|
||||
definitelyDefinedValues.add(merged);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { JavaScriptSemanticPrecisionInput } from "./javascriptSemanticPrecision.js";
|
||||
import type {
|
||||
JavaScriptSemanticProjectionCapacity,
|
||||
JavaScriptSemanticFileProjection,
|
||||
@@ -38,6 +39,7 @@ export interface JavaScriptSourceAnalysisFailure {
|
||||
|
||||
/** Operational choices for source analysis, independent of transport framing. */
|
||||
export interface JavaScriptSourceAnalysisLimits {
|
||||
readonly semanticPrecision?: JavaScriptSemanticPrecisionInput;
|
||||
readonly heapMb: number;
|
||||
readonly timeoutMs: number;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS,
|
||||
type JavaScriptSemanticPrecisionInput,
|
||||
} from "./javascriptSemanticPrecision.js";
|
||||
import { createXhrOpenRequestQuery } from "./javascriptNetworkRequests.js";
|
||||
import { uniqueSortedBy } from "../canonicalOrdering.js";
|
||||
import * as t from "@babel/types";
|
||||
@@ -86,28 +90,37 @@ import {
|
||||
/** Parse one JavaScript artifact and recover static structure only. */
|
||||
export const analyzeJavaScriptStaticSource = (
|
||||
source: string,
|
||||
precision?: JavaScriptSemanticPrecisionInput,
|
||||
): JavaScriptStaticAnalysis => {
|
||||
const file = parseJavaScriptSource(source);
|
||||
return file === null
|
||||
? failedJavaScriptStaticAnalysis()
|
||||
: analyzeParsedJavaScriptStaticSource(source, file);
|
||||
: analyzeParsedJavaScriptStaticSource(source, file, precision);
|
||||
};
|
||||
|
||||
/** Recover static structure from an already parsed JavaScript artifact. */
|
||||
export const analyzeParsedJavaScriptStaticSource = (
|
||||
source: string,
|
||||
file: ParsedJavaScriptSource,
|
||||
precision?: JavaScriptSemanticPrecisionInput,
|
||||
): JavaScriptStaticAnalysis =>
|
||||
completeSemanticSteps(analyzeParsedJavaScriptStaticSourceSteps(source, file));
|
||||
completeSemanticSteps(
|
||||
analyzeParsedJavaScriptStaticSourceSteps(source, file, precision),
|
||||
);
|
||||
|
||||
/** Static analysis that yields during its source traversal; results are unchanged. */
|
||||
export function* analyzeParsedJavaScriptStaticSourceSteps(
|
||||
source: string,
|
||||
file: ParsedJavaScriptSource,
|
||||
precision?: JavaScriptSemanticPrecisionInput,
|
||||
): Generator<void, JavaScriptStaticAnalysis> {
|
||||
const accumulator = createJavaScriptAnalysisAccumulator(source.length);
|
||||
// One definitions pass serves receiver classification and binding lookup.
|
||||
const index = yield* indexJavaScriptBindingsWithReceiversSteps(file.program);
|
||||
const index = yield* indexJavaScriptBindingsWithReceiversSteps(
|
||||
file.program,
|
||||
undefined,
|
||||
precision,
|
||||
);
|
||||
const { bindings, receivers } = index;
|
||||
yield* index.indexAmbientWritesSteps();
|
||||
if (hasPossibleCommonJsDelegateWrite(bindings))
|
||||
@@ -254,18 +267,21 @@ const finalizeStaticAnalysis = (
|
||||
examination: {
|
||||
status:
|
||||
resourceLimits.length > 0 ||
|
||||
accumulator.electronCallPathLimitReached === true
|
||||
(accumulator.electronCallPathLimits?.size ?? 0) > 0
|
||||
? "limited"
|
||||
: "complete",
|
||||
semantic_resource_limits: resourceLimits,
|
||||
...(bindings.precision === JAVASCRIPT_SEMANTIC_PRECISION_DEFAULTS
|
||||
? {}
|
||||
: { semantic_precision: bindings.precision }),
|
||||
electron_call_paths_limited:
|
||||
accumulator.electronCallPathLimitReached === true,
|
||||
(accumulator.electronCallPathLimits?.size ?? 0) > 0,
|
||||
},
|
||||
applicability: { electron: electronObserved ? "observed" : "unknown" },
|
||||
};
|
||||
const limitations = [
|
||||
...(bindings.valueResourceLimits.has("alias-selections")
|
||||
? [semanticResourceLimitReason("alias-selections")]
|
||||
? [semanticResourceLimitReason("alias-selections", bindings.precision)]
|
||||
: []),
|
||||
...(bindings.returnedReferenceWidenings.size > 0
|
||||
? [RETURNED_REFERENCE_WIDENING_LIMITATION]
|
||||
@@ -275,11 +291,12 @@ const finalizeStaticAnalysis = (
|
||||
"Bundled Electron call paths use static argument and closure substitutions. Conditions, ambient helper overrides, runtime reachability and arbitrary side effects remain unproven.",
|
||||
]
|
||||
: []),
|
||||
...(accumulator.electronCallPathLimitReached
|
||||
? [
|
||||
"Electron wrapper expansion reached its resource budget (1000000 visits/substitutions or depth 128); additional paths remain unknown.",
|
||||
]
|
||||
: []),
|
||||
...[...(accumulator.electronCallPathLimits ?? [])]
|
||||
.sort()
|
||||
.map(
|
||||
(control) =>
|
||||
`Electron wrapper expansion reached semantic_precision.${control}=${String(bindings.precision[control])}. Increase semantic_precision.${control} on the next analyze_javascript_application call (--semantic-precision JSON); no REA restart is required. Worker memory and transfer admission remain independent; cyclic or unsupported paths remain unknown.`,
|
||||
),
|
||||
...(parserErrors === 0
|
||||
? []
|
||||
: [
|
||||
|
||||
@@ -59,7 +59,7 @@ export interface JavaScriptAnalysisAccumulator {
|
||||
)[];
|
||||
readonly appPathChanges: LocatedJavaScriptFinding<ElectronAppPathChangeFinding>[];
|
||||
electronCallPaths?: readonly ElectronCallPath[];
|
||||
electronCallPathLimitReached?: boolean;
|
||||
electronCallPathLimits?: Set<"wrapper_visits" | "wrapper_depth">;
|
||||
readonly references: LocatedJavaScriptFinding<JavaScriptStaticReference>[];
|
||||
readonly endpoints: LocatedJavaScriptFinding<JavaScriptStaticEndpoint>[];
|
||||
/** Target and receiver identities awaiting the shared mutation index. */
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { javaScriptSemanticPrecisionSchema } from "./javascriptSemanticPrecision.js";
|
||||
import { z } from "zod";
|
||||
import { javaScriptSemanticResourceLimitSchema } from "./javascriptSemanticValueTypes.js";
|
||||
|
||||
@@ -31,6 +32,7 @@ export const javaScriptStaticCoverageSchema = z.strictObject({
|
||||
examination: z.strictObject({
|
||||
status: z.enum(["complete", "limited", "not-examined"]),
|
||||
semantic_resource_limits: z.array(javaScriptSemanticResourceLimitSchema),
|
||||
semantic_precision: javaScriptSemanticPrecisionSchema.optional(),
|
||||
electron_call_paths_limited: z.boolean(),
|
||||
}),
|
||||
applicability: z.strictObject({ electron: z.enum(["observed", "unknown"]) }),
|
||||
|
||||
@@ -67,8 +67,6 @@ interface SymbolicReadBoundary {
|
||||
}
|
||||
type SymbolicNamespace = Extract<SymbolicValue, { kind: "namespace" }>;
|
||||
|
||||
const SYMBOLIC_DEPTH_LIMIT = 128;
|
||||
|
||||
const emptyEnvironment: SymbolicEnvironment = { values: new Map() };
|
||||
|
||||
const containsOwnedReturn = (root: t.Node): boolean => {
|
||||
@@ -120,9 +118,13 @@ export class JavaScriptSymbolicBindings {
|
||||
readonly tableAliases: ReadonlySet<string>;
|
||||
readonly modules: Map<string, SymbolicValue>;
|
||||
}> = [];
|
||||
#remainingEvaluations = 1_000_000;
|
||||
#remainingEvaluations: number;
|
||||
#depth = 0;
|
||||
truncated = false;
|
||||
readonly resourceLimits = new Set<"wrapper_visits" | "wrapper_depth">();
|
||||
|
||||
get truncated(): boolean {
|
||||
return this.resourceLimits.size > 0;
|
||||
}
|
||||
unresolved = false;
|
||||
|
||||
constructor(
|
||||
@@ -132,6 +134,7 @@ export class JavaScriptSymbolicBindings {
|
||||
indexJavaScriptElectronIdentitySteps(program, state),
|
||||
),
|
||||
) {
|
||||
this.#remainingEvaluations = state.precision.wrapper_visits;
|
||||
this.#electronIdentity = electronIdentity;
|
||||
const shapes = createSemanticExpressionShapeResolver(state);
|
||||
const defineProperty = createGlobalReferenceReader(state, [
|
||||
@@ -218,9 +221,11 @@ export class JavaScriptSymbolicBindings {
|
||||
#admitEvaluation(): boolean {
|
||||
if (
|
||||
--this.#remainingEvaluations < 0 ||
|
||||
this.#depth >= SYMBOLIC_DEPTH_LIMIT
|
||||
this.#depth >= this.state.precision.wrapper_depth
|
||||
) {
|
||||
this.truncated = true;
|
||||
this.resourceLimits.add(
|
||||
this.#remainingEvaluations < 0 ? "wrapper_visits" : "wrapper_depth",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -269,7 +274,7 @@ export class JavaScriptSymbolicBindings {
|
||||
: 0);
|
||||
if (cost > this.#remainingEvaluations) {
|
||||
this.#remainingEvaluations = 0;
|
||||
this.truncated = true;
|
||||
this.resourceLimits.add("wrapper_visits");
|
||||
return symbolicUnknown("symbolic condition resource limit");
|
||||
}
|
||||
this.#remainingEvaluations -= cost;
|
||||
@@ -319,7 +324,7 @@ export class JavaScriptSymbolicBindings {
|
||||
first === undefined ? undefined : this.#completionGroups.get(first);
|
||||
if (group === undefined || group.members.size !== values.length) return [];
|
||||
if (values.length > this.#remainingEvaluations) {
|
||||
this.truncated = true;
|
||||
this.resourceLimits.add("wrapper_visits");
|
||||
this.#remainingEvaluations = 0;
|
||||
return [];
|
||||
}
|
||||
@@ -981,7 +986,7 @@ export class JavaScriptSymbolicBindings {
|
||||
current = current.parent
|
||||
) {
|
||||
if (--this.#remainingEvaluations < 0) {
|
||||
this.truncated = true;
|
||||
this.resourceLimits.add("wrapper_visits");
|
||||
return false;
|
||||
}
|
||||
if (current.activation === activation) return true;
|
||||
@@ -1000,7 +1005,7 @@ export class JavaScriptSymbolicBindings {
|
||||
current = current.caller
|
||||
) {
|
||||
if (--this.#remainingEvaluations < 0) {
|
||||
this.truncated = true;
|
||||
this.resourceLimits.add("wrapper_visits");
|
||||
return undefined;
|
||||
}
|
||||
const point = current.point;
|
||||
@@ -1031,7 +1036,7 @@ export class JavaScriptSymbolicBindings {
|
||||
const boundary =
|
||||
read === undefined ? undefined : this.#namespaceReadBoundary(value, read);
|
||||
if (this.#remainingEvaluations <= 0) {
|
||||
this.truncated = true;
|
||||
this.resourceLimits.add("wrapper_visits");
|
||||
return undefined;
|
||||
}
|
||||
const sourcePoint = boundary?.node[boundary.edge];
|
||||
@@ -1049,7 +1054,7 @@ export class JavaScriptSymbolicBindings {
|
||||
if (commonThrough != null) {
|
||||
if (writes.length > this.#remainingEvaluations) {
|
||||
this.#remainingEvaluations = 0;
|
||||
this.truncated = true;
|
||||
this.resourceLimits.add("wrapper_visits");
|
||||
return undefined;
|
||||
}
|
||||
this.#remainingEvaluations -= writes.length;
|
||||
@@ -1147,7 +1152,7 @@ export class JavaScriptSymbolicBindings {
|
||||
) {
|
||||
if (variants.length > this.#remainingEvaluations) {
|
||||
this.#remainingEvaluations = 0;
|
||||
this.truncated = true;
|
||||
this.resourceLimits.add("wrapper_visits");
|
||||
} else {
|
||||
this.#remainingEvaluations -= variants.length;
|
||||
const exits = variants as readonly SymbolicNamespace[];
|
||||
@@ -1277,8 +1282,13 @@ export class JavaScriptSymbolicBindings {
|
||||
...(unresolvedFallthrough ? { conditionUnresolved: true } : {}),
|
||||
});
|
||||
for (const statement of statements) {
|
||||
if (--this.#remainingEvaluations < 0 || depth >= 128) {
|
||||
this.truncated = true;
|
||||
if (
|
||||
--this.#remainingEvaluations < 0 ||
|
||||
depth >= this.state.precision.wrapper_depth
|
||||
) {
|
||||
this.resourceLimits.add(
|
||||
this.#remainingEvaluations < 0 ? "wrapper_visits" : "wrapper_depth",
|
||||
);
|
||||
return {
|
||||
values: [...possible, symbolicUnknown("symbolic resource limit")],
|
||||
fallsThrough: false,
|
||||
|
||||
Reference in new issue
Block a user