mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(javascript): deduplicate shared projected source facts
This commit is contained in:
@@ -363,6 +363,27 @@ const projectedSourceFactsAtRead = (
|
||||
)
|
||||
return undefined;
|
||||
const sources: ProjectedSource[] = [];
|
||||
const sourceKeys = new WeakMap<t.Node, Set<string>>();
|
||||
const captureIds = new WeakMap<t.Node, number>();
|
||||
let nextCaptureId = 0;
|
||||
const appendSource = (source: ProjectedSource): void => {
|
||||
const captures = semanticNormalizeCapturedSelections(
|
||||
source.capturedSelections,
|
||||
)?.map(({ at, depth, ownEnumerable }) => {
|
||||
let id = captureIds.get(at);
|
||||
if (id === undefined) {
|
||||
id = nextCaptureId++;
|
||||
captureIds.set(at, id);
|
||||
}
|
||||
return [id, depth, ownEnumerable];
|
||||
});
|
||||
const key = JSON.stringify([source.path, captures]);
|
||||
const keys = sourceKeys.get(source.node) ?? new Set<string>();
|
||||
if (keys.has(key)) return;
|
||||
keys.add(key);
|
||||
sourceKeys.set(source.node, keys);
|
||||
sources.push(source);
|
||||
};
|
||||
for (const effect of writes) {
|
||||
if (!retainEligibility && superseded(effect)) continue;
|
||||
if (
|
||||
@@ -389,7 +410,7 @@ const projectedSourceFactsAtRead = (
|
||||
choice === undefined ? [] : [choice],
|
||||
);
|
||||
if (selection !== null)
|
||||
sources.push({
|
||||
appendSource({
|
||||
node: source.node,
|
||||
path: [...selection.projection, ...suffix],
|
||||
...(selection.capturedSelectionDepth === undefined
|
||||
@@ -415,7 +436,7 @@ const projectedSourceFactsAtRead = (
|
||||
for (const initializer of read.binding.initializers) {
|
||||
if (semanticReferenceIterationUnknown(initializer, state)) return undefined;
|
||||
if (semanticReferenceSourcesInactive(initializer, state)) continue;
|
||||
sources.push({
|
||||
appendSource({
|
||||
node: initializer.node,
|
||||
path: [...initializer.projection, ...path],
|
||||
});
|
||||
@@ -433,7 +454,7 @@ const projectedSourceFactsAtRead = (
|
||||
"identity",
|
||||
);
|
||||
if (projection !== null)
|
||||
sources.push({
|
||||
appendSource({
|
||||
node: initializer.node,
|
||||
path: [...projection, ...suffix],
|
||||
capturedSelections:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createContext, runInContext, runInNewContext } from "node:vm";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
||||
@@ -120,15 +121,34 @@ it.each(
|
||||
.replaceAll("SLOT", value.slot)
|
||||
.replaceAll("READ", value.read),
|
||||
expression: shape.read.replaceAll("READ", value.read),
|
||||
// The public module/delegate owner retains unknown after the copy's child
|
||||
// escapes into the new parent. Factory result selection can exclude it.
|
||||
expected: shared ? !value.host || value.slot !== "make" : value.host,
|
||||
// The new local parent shares the same descendant. Its own slot write
|
||||
// replaces the host value; an independent descendant keeps the old one.
|
||||
expected: shared ? !value.host : value.host,
|
||||
localCall: value.slot === "load",
|
||||
})),
|
||||
),
|
||||
),
|
||||
)(
|
||||
"distinguishes shared and newly allocated descendant receivers: $source",
|
||||
({ source, expression, expected }) => {
|
||||
({ source, expression, expected, localCall }) => {
|
||||
const hostCalls: string[] = [];
|
||||
const module = {
|
||||
require: (specifier: string) => {
|
||||
hostCalls.push(specifier);
|
||||
return {};
|
||||
},
|
||||
};
|
||||
const context = createContext({ module });
|
||||
expect(source.endsWith(expression + ";")).toBe(true);
|
||||
runInContext(source.slice(0, -expression.length - 1), context);
|
||||
if (expected || localCall) {
|
||||
runInContext(expression, context);
|
||||
} else {
|
||||
expect(() => runInContext(expression, context)).toThrow(
|
||||
expect.objectContaining({ name: "TypeError" }),
|
||||
);
|
||||
}
|
||||
expect(hostCalls).toEqual(expected ? ["./shared.cjs"] : []);
|
||||
const result = analyzeJavaScriptStaticSource(source);
|
||||
expect(result.parse_error_count).toBe(0);
|
||||
expect(
|
||||
@@ -143,6 +163,54 @@ it.each(
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{
|
||||
kind: "external call",
|
||||
iterator: false,
|
||||
source:
|
||||
"const child={loader:module};const box={child};external(child);box.child.loader.require('./uncertain.cjs');",
|
||||
},
|
||||
{
|
||||
kind: "custom iterator",
|
||||
iterator: true,
|
||||
source:
|
||||
"const child={loader:module};const box={child};const values=[child];values[Symbol.iterator]=external;for(const selected of values)selected.loader={};box.child.loader.require('./uncertain.cjs');",
|
||||
},
|
||||
])(
|
||||
"retains host eligibility across an opaque shared receiver: $kind",
|
||||
({ source, iterator }) => {
|
||||
const analysis = analyzeJavaScriptStaticSource(source);
|
||||
expect(analysis.references).toContainEqual(
|
||||
expect.objectContaining({ kind: "require", specifier: null }),
|
||||
);
|
||||
// Both executions fit the same unknown input: one retains the host loader,
|
||||
// the other replaces it. Neither outcome can prove the static alternative absent.
|
||||
for (const preserveHost of [true, false]) {
|
||||
const calls: string[] = [];
|
||||
const module = {
|
||||
require: (specifier: string) => {
|
||||
calls.push(specifier);
|
||||
return {};
|
||||
},
|
||||
};
|
||||
const external = iterator
|
||||
? function* (this: readonly { loader: unknown }[]) {
|
||||
yield preserveHost ? { loader: {} } : this[0]!;
|
||||
}
|
||||
: (child: { loader: unknown }) => {
|
||||
if (!preserveHost) child.loader = {};
|
||||
};
|
||||
try {
|
||||
runInNewContext(source, { module, external });
|
||||
} catch (error) {
|
||||
expect(error).toMatchObject({ name: "TypeError" });
|
||||
expect(preserveHost).toBe(false);
|
||||
}
|
||||
expect(calls).toEqual(preserveHost ? ["./uncertain.cjs"] : []);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["module", "{}"])(
|
||||
"selects the later write to a shared projected receiver: %s",
|
||||
(next) => {
|
||||
|
||||
@@ -53,7 +53,7 @@ it("preserves earlier and sibling effects without inventing replaced cached-modu
|
||||
require('node:fs').readFileSync('wrong-direct-loader.json');`,
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "imports.ts"),
|
||||
join(root, "imports.cts"),
|
||||
`import fs = require('node:fs'); const other = require('fs');
|
||||
const read = other.readFileSync; observe(read); read('escaped.json');
|
||||
fs.readFileSync = custom; other.readFileSync('wrong-import-equals.json');`,
|
||||
@@ -65,7 +65,7 @@ it("preserves earlier and sibling effects without inventing replaced cached-modu
|
||||
require('fs').readFileSync('wrong-late-loader.json');`,
|
||||
);
|
||||
await writeFile(
|
||||
join(root, "replacement.ts"),
|
||||
join(root, "replacement.cts"),
|
||||
`import fs = require('fs'); const other=require('fs'); fs={}; fs.readFileSync=custom; other.readFileSync('replacement-kept.json');`,
|
||||
);
|
||||
await writeFile(
|
||||
|
||||
Reference in New Issue
Block a user