chore(experiment): refresh main before cross-function trials
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
This commit is contained in:
commit
c8d8319367
4 files changed
+83
-20
No files matched your search
@@ -92,3 +92,33 @@ it("escapes constructed fields when a getter query widens its suffix", () => {
|
||||
expect(ir.coverage.resourceLimits).toEqual(["getter-expansion"]);
|
||||
expectRetainedFacts(ir);
|
||||
});
|
||||
|
||||
it.each(["class", "object"] as const)(
|
||||
"bounds branching local aliases inside one %s getter",
|
||||
(receiver) => {
|
||||
const aliases = Array.from(
|
||||
{ length: 10 },
|
||||
(_, index) =>
|
||||
`const a${index + 1} = flag ? a${index}.left : a${index}.right;`,
|
||||
).join("\n");
|
||||
const getter = `get state() { const a0 = this; ${aliases} return a10; }`;
|
||||
const definition =
|
||||
receiver === "class"
|
||||
? `class Request { slot = shared; ${getter} } const request = new Request();`
|
||||
: `const request = { slot: shared, ${getter} };`;
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
import imported from "dep";
|
||||
const shared = { value: 1 };
|
||||
${definition}
|
||||
consume(request.state);
|
||||
const result = shared.value;
|
||||
const unrelated = 42;
|
||||
`);
|
||||
expect(ir.coverage).toMatchObject({
|
||||
status: "partial",
|
||||
resourceLimits: ["getter-expansion"],
|
||||
});
|
||||
expect(onlyBinding(ir, "result").value.status).toBe("unknown");
|
||||
expectRetainedFacts(ir);
|
||||
},
|
||||
);
|
||||
@@ -64,6 +64,21 @@ export class SemanticGetterQuery {
|
||||
|
||||
constructor(readonly state: JavaScriptSemanticAnalysisState) {}
|
||||
|
||||
get widening(): boolean {
|
||||
return this.#fallback;
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
this.#limited = true;
|
||||
return false;
|
||||
}
|
||||
this.#visits++;
|
||||
return true;
|
||||
}
|
||||
|
||||
retain(reference: Reference, escapes = false): void {
|
||||
// Direct fields keep their write suffix; only getter jobs widen omitted paths.
|
||||
this.#references.add({
|
||||
@@ -85,11 +100,7 @@ export class SemanticGetterQuery {
|
||||
const paths = receivers?.get(receiver);
|
||||
const key = JSON.stringify([path, escapes]);
|
||||
if (paths?.has(key)) return true;
|
||||
if (!this.#fallback && this.#visits >= SEMANTIC_GETTER_EXPANSION_LIMIT) {
|
||||
this.#limited = true;
|
||||
return true;
|
||||
}
|
||||
this.#visits++;
|
||||
if (!this.reserveResult()) return true;
|
||||
const selections = paths ?? new Set<string>();
|
||||
const owners = receivers ?? new WeakMap<t.Node, Set<string>>();
|
||||
selections.add(key);
|
||||
|
||||
@@ -75,7 +75,7 @@ const resultBranches = (node: t.Node): readonly t.Node[] | undefined => {
|
||||
|
||||
/** Each walk owns local aliases; getter-to-getter traversal belongs to the query worklist. */
|
||||
class GetterResultWalker {
|
||||
readonly #pending: GetterWork[];
|
||||
readonly #pending: GetterWork[] = [];
|
||||
readonly #aliases = new Map<string, Set<string>>();
|
||||
readonly #scope: string | undefined;
|
||||
|
||||
@@ -84,9 +84,10 @@ class GetterResultWalker {
|
||||
readonly query: SemanticGetterQuery,
|
||||
readonly resolvers: GetterResultResolvers,
|
||||
) {
|
||||
this.#pending = semanticCallableResultExpressions(
|
||||
for (const { node } of semanticCallableResultExpressions(
|
||||
job.invocation.getter,
|
||||
).flatMap(({ node }) => (node === null ? [] : [{ node, path: job.path }]));
|
||||
))
|
||||
if (node !== null) this.#enqueue(() => ({ node, path: job.path }));
|
||||
this.#scope = query.state.scopeByNode.get(job.invocation.getter)?.scopeId;
|
||||
}
|
||||
|
||||
@@ -97,6 +98,25 @@ class GetterResultWalker {
|
||||
}
|
||||
}
|
||||
|
||||
#enqueue(create: () => GetterWork): void {
|
||||
if (!this.query.reserveResult()) return;
|
||||
const work = create();
|
||||
// The fallback escapes receiver roots. Keeping local suffix permutations
|
||||
// here would repeat the precise expansion inside a single getter.
|
||||
this.#pending.push(
|
||||
this.query.widening
|
||||
? {
|
||||
...work,
|
||||
path: [],
|
||||
escapes: true,
|
||||
...(work.privateRead === undefined
|
||||
? {}
|
||||
: { privateRead: { ...work.privateRead, path: [] } }),
|
||||
}
|
||||
: work,
|
||||
);
|
||||
}
|
||||
|
||||
#retain(current: GetterWork, reference: Reference): void {
|
||||
this.query.retain(reference, current.escapes || this.job.escapes);
|
||||
}
|
||||
@@ -149,12 +169,12 @@ class GetterResultWalker {
|
||||
if (previous !== undefined) {
|
||||
// Growing aliases can reach arbitrarily deep children of the same roots.
|
||||
if (current.path.length > previous.pathLength && !current.escapes)
|
||||
this.#pending.push({
|
||||
this.#enqueue(() => ({
|
||||
...current,
|
||||
path: [],
|
||||
aliases: undefined,
|
||||
escapes: true,
|
||||
});
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
const key = JSON.stringify([
|
||||
@@ -195,12 +215,12 @@ class GetterResultWalker {
|
||||
(origin) => !semanticReferenceSourcesInactive(origin, state),
|
||||
),
|
||||
])
|
||||
this.#pending.push({
|
||||
this.#enqueue(() => ({
|
||||
...current,
|
||||
node: initializer.node,
|
||||
path: [...initializer.projection, ...current.path],
|
||||
aliases,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
#ownsThis(expression: t.ThisExpression): boolean {
|
||||
@@ -305,27 +325,28 @@ class GetterResultWalker {
|
||||
t.isPrivateName(expression.property) &&
|
||||
t.isClass(this.job.invocation.owner)
|
||||
) {
|
||||
this.#pending.push({
|
||||
const name = expression.property.id.name;
|
||||
this.#enqueue(() => ({
|
||||
...current,
|
||||
node: expression.object,
|
||||
path: [],
|
||||
privateRead: {
|
||||
node: expression,
|
||||
name: expression.property.id.name,
|
||||
name,
|
||||
path: current.path,
|
||||
},
|
||||
});
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (!this.#superValue(current, expression))
|
||||
this.#pending.push({
|
||||
this.#enqueue(() => ({
|
||||
...current,
|
||||
node: expression.object,
|
||||
path: [
|
||||
semanticStaticPropertyKey(expression.property, expression.computed),
|
||||
...current.path,
|
||||
],
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
#visit(current: GetterWork): void {
|
||||
@@ -340,7 +361,8 @@ class GetterResultWalker {
|
||||
else {
|
||||
const branches = resultBranches(expression);
|
||||
if (branches !== undefined)
|
||||
for (const node of branches) this.#pending.push({ ...current, node });
|
||||
for (const node of branches)
|
||||
this.#enqueue(() => ({ ...current, node }));
|
||||
else if (current.privateRead !== undefined) this.#privateRead(current);
|
||||
else this.#retain(current, { node: expression, path: current.path });
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
export const SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT = 256;
|
||||
/** Maximum active expression nesting during inert semantic evaluation. */
|
||||
export const SEMANTIC_EXPRESSION_DEPTH_LIMIT = 256;
|
||||
/** Distinct getter, receiver and path states retained in one precise query. */
|
||||
/** Getter jobs and local result work retained in one precise query. */
|
||||
export const SEMANTIC_GETTER_EXPANSION_LIMIT = 512;
|
||||
/** Bound normalized strings and transient keys independently of transport limits. */
|
||||
export const SEMANTIC_PRIMITIVE_JSON_BYTES_LIMIT = 1_048_576;
|
||||
@@ -20,7 +20,7 @@ const RESOURCE_LIMIT_REASON: Record<JavaScriptSemanticResourceLimit, string> = {
|
||||
"expression-depth":
|
||||
"Semantic expression depth budget exceeded (maximum 256 nested expressions).",
|
||||
"getter-expansion":
|
||||
"Getter expansion was conservatively bounded (maximum 512 distinct states or a growing receiver cycle).",
|
||||
"Getter expansion was conservatively bounded (maximum 512 scheduled states or a growing receiver cycle).",
|
||||
};
|
||||
|
||||
/** Check UTF-16 length against worst-case JSON escaping without flattening. */
|
||||
|
||||
Reference in new issue
Block a user