Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 34 additions & 21 deletions src/dataflow/attach.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* param→contracted out of the L3 CFG; '@formal_in:N' at L4, statement→'line:col'.
*/

import type { CfgEdge, GraphNode, PdgEdge, ProgramGraphs } from "../schema/graphs";
import type { CfgEdge, GraphNode, GraphSelector, PdgEdge, ProgramGraphs } from "../schema/graphs";
import type { TSApplication, TSCallable, TSParamEdge } from "../schema";

interface LocalIds {
Expand Down Expand Up @@ -80,7 +80,14 @@ function spanOf(n: GraphNode): { start: [number, number]; end: [number, number];
// L3 — body statements + cfg/cdg/ddg on the callable (bare local ids)
// ----------------------------------------------------------------------------------------------

function emitL3(li: LocalIds, nodes: GraphNode[], cfgEdges: CfgEdge[] | undefined, pdgEdges: PdgEdge[] | undefined): void {
function emitL3(
li: LocalIds,
nodes: GraphNode[],
cfgEdges: CfgEdge[] | undefined,
pdgEdges: PdgEdge[] | undefined,
emitCdg: boolean,
emitDdg: boolean,
): void {
const c = li.callable;
// Grow body with entry/exit + statement nodes (params are contracted out; call nodes from L1 win).
for (const n of nodes) {
Expand All @@ -105,11 +112,11 @@ function emitL3(li: LocalIds, nodes: GraphNode[], cfgEdges: CfgEdge[] | undefine
const cdg: Array<{ src: string; dst: string }> = [];
const ddg: Array<{ src: string; dst: string; var?: string; prov: string[] }> = [];
for (const e of pdgEdges) {
if (e.type === "CDG") {
if (e.type === "CDG" && emitCdg) {
const src = l3(li, e.source);
const dst = l3(li, e.target);
if (src !== dst) cdg.push({ src, dst });
} else if (e.type === "DDG") {
} else if (e.type === "DDG" && emitDdg) {
if (e.target === li.exitId) continue; // formal-out routing → deferred to L4 (→ @formal_out)
// prov = the def-use METHOD: `solveDefUse` computes forward may-reaching-definitions over
// k-limited access paths with a flow-insensitive copy/field-alias substrate (defuse.ts). It
Expand All @@ -122,36 +129,38 @@ function emitL3(li: LocalIds, nodes: GraphNode[], cfgEdges: CfgEdge[] | undefine
ddg.push({ src: l3(li, e.source), dst: l3(li, e.target), var: e.var, prov: ["reaching-defs"] });
}
}
c.cdg = dedupe(cdg, (e) => `${e.src}\0${e.dst}`).sort(cmp2);
c.ddg = dedupe(ddg, (e) => `${e.src}\0${e.dst}\0${e.var ?? ""}`).sort(cmpDdg);
if (emitCdg) c.cdg = dedupe(cdg, (e) => `${e.src}\0${e.dst}`).sort(cmp2);
if (emitDdg) c.ddg = dedupe(ddg, (e) => `${e.src}\0${e.dst}\0${e.var ?? ""}`).sort(cmpDdg);
}
}

// ----------------------------------------------------------------------------------------------
// L4 — synthetic vertices + summary (callable) + param_in/param_out (application)
// ----------------------------------------------------------------------------------------------

function emitL4(root: TSApplication, pg: ProgramGraphs, info: Map<string, LocalIds>): void {
// Formal vertices + the deferred formal-out-routing ddg edges, per callable.
function emitL4(root: TSApplication, pg: ProgramGraphs, info: Map<string, LocalIds>, emitDdg: boolean): void {
// Formal vertices + the deferred formal-out-routing DDG edges, per callable.
for (const [sig, fg] of Object.entries(pg.functions)) {
const li = info.get(sig);
if (!li) continue;
const c = li.callable;
for (const [nodeId, n] of li.paramN) c.body[`@formal_in:${n}`] = { kind: "formal_in", of: li.paramName.get(nodeId) };
if (li.exitId >= 0) c.body["@formal_out"] = { kind: "formal_out", of: "$ret" };
if (!c.summary) c.summary = [];
// return/global → EXIT ddg edges re-target @formal_out (syntactic routing; L4-placed vertex).
for (const e of fg.pdg?.edges ?? []) {
if (e.type === "DDG" && e.target === li.exitId) {
(c.ddg as Array<{ src: string; dst: string; var?: string; prov: string[] }>).push({
src: l3(li, e.source),
dst: "@formal_out",
var: e.var,
prov: ["reaching-defs"],
});
// Return/global → EXIT DDG edges re-target @formal_out (syntactic routing; L4-placed vertex).
if (emitDdg) {
for (const e of fg.pdg?.edges ?? []) {
if (e.type === "DDG" && e.target === li.exitId) {
c.ddg?.push({
src: l3(li, e.source),
dst: "@formal_out",
var: e.var,
prov: ["reaching-defs"],
});
}
}
c.ddg?.sort(cmpDdg);
}
(c.ddg as Array<{ src: string; dst: string; var?: string; prov: string[] }>)?.sort?.(cmpDdg);
}

// Cross-function SDG edges → param_in/param_out (app) + summary (callable) + actual vertices.
Expand Down Expand Up @@ -232,8 +241,9 @@ export function applyDataflow(
idBySig: Map<string, string>,
callableBySig: Map<string, TSCallable>,
level: number,
selectors: readonly GraphSelector[],
): void {
if (level < 3) return;
if (level < 3 || selectors.length === 0) return;

const info = new Map<string, LocalIds>();
for (const [sig, fg] of Object.entries(pg.functions)) {
Expand All @@ -243,13 +253,16 @@ export function applyDataflow(
info.set(sig, buildLocalIds(canId, callable, fg.cfg.nodes));
}

const wantCfg = selectors.includes("cfg");
const wantCdg = selectors.includes("pdg");
const wantDdg = wantCdg || selectors.includes("dfg");
for (const [sig, fg] of Object.entries(pg.functions)) {
const li = info.get(sig);
if (!li) continue;
emitL3(li, fg.cfg?.nodes ?? [], fg.cfg?.edges, fg.pdg?.edges);
emitL3(li, fg.cfg?.nodes ?? [], wantCfg ? fg.cfg?.edges : undefined, fg.pdg?.edges, wantCdg, wantDdg);
}

if (level >= 4) emitL4(root, pg, info);
if (level >= 4 && selectors.includes("sdg")) emitL4(root, pg, info, wantDdg);
}

// ----------------------------------------------------------------------------------------------
Expand Down
26 changes: 9 additions & 17 deletions src/dataflow/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,34 +204,26 @@ export async function buildProgramGraphs(
const { summaries, ddg, sccCount, largest } = await composeWavefront(datas, callSites, extraction.pool, log);
log.debug(`program graphs: ${sccCount} SCCs, largest ${largest}`);

// Emission per --graphs selector.
const wantCfg = opts.graphs.includes("cfg");
const wantPdg = opts.graphs.includes("pdg");
const wantDfg = opts.graphs.includes("dfg");
const wantSdg = opts.graphs.includes("sdg");

// Build the complete compute IR. Output selectors are applied only when this substrate is
// attached to the analysis tree; DDG, SDG, and body-node identity all depend on CFG nodes.
const functions: Record<string, FunctionGraphs> = {};
for (const [sig, data] of [...datas.entries()].sort(([a], [b]) => a.localeCompare(b))) {
const fg: FunctionGraphs = {};
if (wantCfg) fg.cfg = { nodes: data.nodes, edges: data.edges };
if (wantPdg || wantDfg) {
const edges: PdgEdge[] = [];
if (wantPdg) edges.push(...data.cdg);
edges.push(...(ddg.get(sig) ?? []));
fg.pdg = {
const edges: PdgEdge[] = [...data.cdg, ...(ddg.get(sig) ?? [])];
functions[sig] = {
cfg: { nodes: data.nodes, edges: data.edges },
pdg: {
edges: edges.sort(
(a, b) =>
a.source - b.source ||
a.target - b.target ||
a.type.localeCompare(b.type) ||
(a.var ?? "").localeCompare(b.var ?? ""),
),
};
}
functions[sig] = fg;
},
};
}

const sdg_edges = wantSdg ? assembleSdg(datas, callSites, summaries) : [];
const sdg_edges = assembleSdg(datas, callSites, summaries);

persistSummaries(opts, symbol_table, callables, summaries, log);

Expand Down
2 changes: 1 addition & 1 deletion src/schema/emit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export function finalizeAnalysis(
// L3/L4 — grow body{} + cfg/cdg/ddg/summary on callables and param_in/param_out on the app.
let k_limit: number | undefined;
if (level >= 3 && pg) {
applyDataflow(root, pg, idBySig, callableBySig, level);
applyDataflow(root, pg, idBySig, callableBySig, level, opts.graphs);
k_limit = pg.k_limit;
}

Expand Down
6 changes: 3 additions & 3 deletions src/schema/graphs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* The level-3 `program_graphs` contract — CFG / PDG (CDG+DDG) / SDG, per the cross-language
* dataflow-graphs spec. Emitted as an optional top-level section of analysis.json, only at
* `-a 3`, scoped by `--graphs`.
* The level-3 `program_graphs` compute IR — complete CFG / PDG (CDG+DDG) / SDG data per the
* cross-language dataflow-graphs spec. `--graphs` is applied only when this IR is attached to the
* analysis tree, so selecting DFG or SDG never removes the CFG node substrate those graphs need.
*
* Node identity is the invariant that makes everything joinable: every node is keyed by
* `(signature, node_id)` where `signature` is the SAME signatureOf() key used by symbol_table
Expand Down
45 changes: 38 additions & 7 deletions test/dataflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ import { describe, expect, test } from "bun:test";
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
import { analyze } from "../src/core";
import { analyze, type AnalysisResult } from "../src/core";
import { backwardSlice } from "../src/dataflow";
import type { AnalysisOptions } from "../src/options";
import type { CfgEdge, FunctionCfg, ProgramGraphs, SdgEdge } from "../src/schema";
import { forEachCallable, type CfgEdge, type FunctionCfg, type GraphSelector, type ProgramGraphs, type SdgEdge, type TSCallable } from "../src/schema";

const FIXTURE = path.resolve(import.meta.dir, "fixtures/dataflow-app");

function options(level: 1 | 2 | 3, cacheDir: string, jobs: number): AnalysisOptions {
function options(level: 1 | 2 | 3, cacheDir: string, jobs: number, graphs: GraphSelector[]): AnalysisOptions {
return {
input: FIXTURE,
output: null,
Expand All @@ -26,7 +26,7 @@ function options(level: 1 | 2 | 3, cacheDir: string, jobs: number): AnalysisOpti
neo4jPassword: "",
neo4jDatabase: null,
analysisLevel: level,
graphs: ["cfg", "dfg", "pdg", "sdg"],
graphs,
graphFieldDepth: 3,
jobs,
targetFiles: null,
Expand All @@ -39,16 +39,31 @@ function options(level: 1 | 2 | 3, cacheDir: string, jobs: number): AnalysisOpti
};
}

async function run(level: 1 | 2 | 3, jobs = 1): Promise<Awaited<ReturnType<typeof analyze>>> {
// returns the full AnalysisResult — the program-graph IR rides on it, not on the tree
async function run(
level: 1 | 2 | 3,
jobs = 1,
graphs: GraphSelector[] = ["cfg", "dfg", "pdg", "sdg"],
): Promise<AnalysisResult> {
// Returns the full AnalysisResult — the program-graph IR rides on it, not on the tree.
const cacheDir = fs.mkdtempSync(path.join(os.tmpdir(), "cants-dataflow-test-"));
try {
return await analyze(options(level, cacheDir, jobs));
return await analyze(options(level, cacheDir, jobs, graphs));
} finally {
fs.rmSync(cacheDir, { recursive: true, force: true });
}
}

function callableOf(result: AnalysisResult, signature: string): TSCallable {
let found: TSCallable | undefined;
for (const mod of Object.values(result.internal.symbol_table)) {
forEachCallable(mod, (callable) => {
if (callable.signature === signature) found = callable;
});
}
if (!found) throw new Error(`no callable for ${signature}`);
return found;
}

const pg = (await run(3)).program_graphs as ProgramGraphs;

const cfgOf = (sig: string): FunctionCfg => {
Expand Down Expand Up @@ -386,6 +401,22 @@ describe("determinism and gating", () => {
expect(pg.schema_version).toBe("1.0.0");
expect(pg.k_limit).toBe(3);
});

test("--graphs selects attached fields without deleting compute dependencies", async () => {
const dfg = await run(3, 1, ["dfg"]);
const dfgCallable = callableOf(dfg, "src/flow.sumTo");
expect(dfg.program_graphs?.functions["src/flow.sumTo"]?.cfg?.nodes.length).toBeGreaterThan(0);
expect(dfgCallable.body["@entry"]).toBeDefined();
expect(dfgCallable.cfg).toBeUndefined();
expect(dfgCallable.cdg).toBeUndefined();
expect(dfgCallable.ddg?.length).toBeGreaterThan(0);

const cfg = await run(3, 1, ["cfg"]);
const cfgCallable = callableOf(cfg, "src/flow.sumTo");
expect(cfgCallable.cfg?.length).toBeGreaterThan(0);
expect(cfgCallable.cdg).toBeUndefined();
expect(cfgCallable.ddg).toBeUndefined();
});
});

// ------------------------------------------------------------------------------------------------
Expand Down