mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 18:05:45 +00:00
fix(javascript): resolve child ownership at receiver reads (#3819)
* fix(javascript): resolve child ownership at receiver reads * fix(javascript): preserve deferred field child candidates --------- Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
1 parent
48c3d2d170
commit
6d2eeeaed0
4 files changed
+560
-26
No files matched your search
@@ -0,0 +1,263 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { analyzeJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
||||
|
||||
const analyze = (source: string) =>
|
||||
analyzeJavaScriptSemantics(
|
||||
`import { spawn } from 'node:child_process';\n${source}`,
|
||||
);
|
||||
|
||||
it("keeps interactions before replacement resolved and later ownership uncertain", () => {
|
||||
const ir = analyze(`
|
||||
let child = spawn('old');
|
||||
child.kill();
|
||||
child = { kill() {}, on() {} };
|
||||
child.kill();
|
||||
child.on('exit', done);
|
||||
const stable = spawn('stable'); stable.kill();
|
||||
`);
|
||||
const [old, stable] = ir.childProcessSpawns;
|
||||
expect(ir.childProcessInteractions).toMatchObject([
|
||||
{ linkedProcessIds: [old!.processId], resolution: "complete" },
|
||||
{ linkedProcessIds: [old!.processId], resolution: "partial" },
|
||||
{ linkedProcessIds: [old!.processId], resolution: "partial" },
|
||||
{ linkedProcessIds: [stable!.processId], resolution: "complete" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("selects a new spawned child at each straight-line receiver read", () => {
|
||||
const ir = analyze(`
|
||||
let child = spawn('old'); child.kill();
|
||||
child = spawn('new'); child.once('exit', done); child.kill();
|
||||
`);
|
||||
const [old, replacement] = ir.childProcessSpawns;
|
||||
expect(ir.childProcessInteractions).toMatchObject([
|
||||
{ linkedProcessIds: [old!.processId], resolution: "complete" },
|
||||
{ linkedProcessIds: [replacement!.processId], resolution: "complete" },
|
||||
{ linkedProcessIds: [replacement!.processId], resolution: "complete" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("selects child ownership before and after a replacement in one expression", () => {
|
||||
const ir = analyze(`
|
||||
let child = spawn('old');
|
||||
(child.kill(), child = spawn('new'));
|
||||
child.kill();
|
||||
`);
|
||||
expect(ir.childProcessInteractions).toMatchObject(
|
||||
ir.childProcessSpawns.map(({ processId }) => ({
|
||||
linkedProcessIds: [processId],
|
||||
resolution: "complete",
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"class Stop { field = child.kill(); }",
|
||||
"class Stop { #field = child.kill(); }",
|
||||
"class Stop { ['field'] = child.kill(); }",
|
||||
"const Stop = class { field = (0, child.kill()); };",
|
||||
"class Stop { field = class { [child.kill()] = 0; }; }",
|
||||
"class Stop { stop() { child.kill(); } }",
|
||||
"class Stop { field = () => child.kill(); }",
|
||||
])(
|
||||
"retains both child candidates across a deferred read: %s",
|
||||
(declaration) => {
|
||||
const ir = analyze(`
|
||||
let child = spawn('old');
|
||||
${declaration}
|
||||
child = spawn('new');
|
||||
new Stop();
|
||||
`);
|
||||
expect(ir.childProcessSpawns).toHaveLength(2);
|
||||
expect(ir.childProcessInteractions[0]?.linkedProcessIds).toHaveLength(2);
|
||||
expect(ir.childProcessInteractions).toEqual([
|
||||
expect.objectContaining({
|
||||
linkedProcessIds: expect.arrayContaining(
|
||||
ir.childProcessSpawns.map(({ processId }) => processId),
|
||||
),
|
||||
resolution: "partial",
|
||||
}),
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it("does not prove an assignment-only child precedes an instance field read", () => {
|
||||
const ir = analyze(`
|
||||
let child;
|
||||
child = spawn('assigned');
|
||||
class Stop { field = child.kill(); }
|
||||
new Stop();
|
||||
`);
|
||||
expect(ir.childProcessInteractions).toMatchObject([
|
||||
{
|
||||
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
|
||||
resolution: "partial",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"class Stop { static field = child.kill(); }",
|
||||
"class Stop { static #field = child.kill(); }",
|
||||
"class Stop { [child.kill()] = 0; }",
|
||||
])("resolves reads evaluated at class definition: %s", (declaration) => {
|
||||
const ir = analyze(`
|
||||
let child = spawn('old');
|
||||
${declaration}
|
||||
child = spawn('new');
|
||||
new Stop();
|
||||
`);
|
||||
expect(ir.childProcessInteractions).toMatchObject([
|
||||
{
|
||||
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
|
||||
resolution: "complete",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it.each(["const", "let"])(
|
||||
"preserves a stable %s child across an instance field read",
|
||||
(kind) => {
|
||||
const ir = analyze(`
|
||||
${kind} child = spawn('stable');
|
||||
class Stop { field = child.kill(); }
|
||||
new Stop();
|
||||
`);
|
||||
expect(ir.childProcessInteractions).toMatchObject([
|
||||
{
|
||||
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
|
||||
resolution: "complete",
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
it("retains a saved alias at its capture and marks a late alias uncertain", () => {
|
||||
const ir = analyze(`
|
||||
let child = spawn('old');
|
||||
const saved = child; const savedAgain = saved;
|
||||
child = { kill() {}, on() {} };
|
||||
const late = child;
|
||||
savedAgain.kill(); saved.on('exit', done); late.kill();
|
||||
`);
|
||||
const child = ir.childProcessSpawns[0]!;
|
||||
expect(ir.childProcessInteractions).toMatchObject([
|
||||
{ linkedProcessIds: [child.processId], resolution: "complete" },
|
||||
{ linkedProcessIds: [child.processId], resolution: "complete" },
|
||||
{ linkedProcessIds: [child.processId], resolution: "partial" },
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"if (flag) child = replacement; child.kill();",
|
||||
"function replace() { child = replacement; } replace(); child.kill();",
|
||||
"function use() { child.kill(); } child = replacement; use();",
|
||||
"child &&= replacement; child.kill();",
|
||||
"child += replacement; child.kill();",
|
||||
"child++; child.kill();",
|
||||
"({ child } = replacement); child.kill();",
|
||||
"for (child of replacements) {} child.kill();",
|
||||
"while (flag) { child.kill(); child = replacement; }",
|
||||
"(child = replacement, child.kill());",
|
||||
])("keeps uncertain child replacement explicit: %s", (body) => {
|
||||
const ir = analyze(`let child = spawn('old'); ${body}`);
|
||||
expect(ir.childProcessInteractions).toHaveLength(1);
|
||||
expect(ir.childProcessInteractions[0]).toMatchObject({
|
||||
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
|
||||
resolution: "partial",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not resolve an interaction before its only spawn assignment", () => {
|
||||
const ir = analyze(`let child; child.kill(); child = spawn('later');`);
|
||||
expect(ir.childProcessInteractions).toMatchObject([
|
||||
{ resolution: "partial" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps conditional spawn assignment and alias cycles uncertain", () => {
|
||||
const ir = analyze(`
|
||||
let child;
|
||||
if (flag) child = spawn('possible');
|
||||
let alias = child; child = alias;
|
||||
alias.kill();
|
||||
`);
|
||||
expect(ir.childProcessInteractions).toHaveLength(1);
|
||||
expect(ir.childProcessInteractions[0]).toMatchObject({
|
||||
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
|
||||
resolution: "partial",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves direct and stable captured children without admitting unrelated receivers", () => {
|
||||
const ir = analyze(`
|
||||
spawn('direct').kill();
|
||||
const child = spawn('captured'); function stop() { child.kill(); }
|
||||
{ const child = { kill() {} }; child.kill(); }
|
||||
`);
|
||||
expect(ir.childProcessInteractions).toHaveLength(2);
|
||||
expect(
|
||||
ir.childProcessInteractions.every(
|
||||
({ resolution }) => resolution === "complete",
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps a default parameter child uncertain when a caller can supply another handle", () => {
|
||||
const ir = analyze(`
|
||||
function stop(child = spawn('fallback')) {
|
||||
child.kill(); child.on('exit', done);
|
||||
}
|
||||
stop({ kill() {}, on() {} });
|
||||
`);
|
||||
expect(ir.childProcessInteractions).toMatchObject([
|
||||
{
|
||||
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
|
||||
resolution: "partial",
|
||||
},
|
||||
{
|
||||
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
|
||||
resolution: "partial",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves the admitted scalar identity of an awaited spawn result", () => {
|
||||
const ir = analyze(`
|
||||
const child = await spawn('stable'); child.kill();
|
||||
`);
|
||||
expect(ir.childProcessInteractions).toMatchObject([
|
||||
{
|
||||
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
|
||||
resolution: "complete",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"let child; function launch() { child = spawn('later'); } const alias = child; alias.kill();",
|
||||
"let child; { const alias = child; alias.kill(); } child = spawn('later');",
|
||||
"{ const alias = child; alias.kill(); } var child = spawn('later');",
|
||||
])("keeps initialization across different bodies uncertain: %s", (source) => {
|
||||
const ir = analyze(source);
|
||||
expect(ir.childProcessInteractions).toMatchObject([
|
||||
{
|
||||
linkedProcessIds: [ir.childProcessSpawns[0]!.processId],
|
||||
resolution: "partial",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves an unconditional assignment before the read in the same function body", () => {
|
||||
const ir = analyze(`function stop() {
|
||||
let child; child = spawn('first'); child.kill();
|
||||
child = spawn('second'); child.kill();
|
||||
}`);
|
||||
expect(ir.childProcessInteractions).toMatchObject(
|
||||
ir.childProcessSpawns.map(({ processId }) => ({
|
||||
linkedProcessIds: [processId],
|
||||
resolution: "complete",
|
||||
})),
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user