Skip to content

Commit 734a503

Browse files
committed
generate evaluation forest graphs
1 parent 96c00d0 commit 734a503

File tree

5 files changed

+588
-0
lines changed

5 files changed

+588
-0
lines changed

deno.lock

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/evaluator/arenaEvaluator.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,47 @@ export class ArenaEvaluatorWasm implements Evaluator {
144144
this.fromArena(this.$.rightOf(id)),
145145
);
146146
}
147+
148+
dumpArena() {
149+
const nodes: Array<
150+
| { id: number; kind: "terminal"; sym: string }
151+
| { id: number; kind: "non-terminal"; left: number; right: number }
152+
> = [];
153+
154+
for (let id = 0;; id++) {
155+
const k = this.$.kindOf(id);
156+
// kindOf returns 0 for uninitialised slots; once we hit the first zero we
157+
// have traversed the allocated prefix because ids are assigned densely.
158+
if (k === 0) break;
159+
160+
if (k === (ArenaKind.Terminal as number)) {
161+
let sym: string;
162+
switch (this.$.symOf(id) as ArenaSym) {
163+
case ArenaSym.S:
164+
sym = "S";
165+
break;
166+
case ArenaSym.K:
167+
sym = "K";
168+
break;
169+
case ArenaSym.I:
170+
sym = "I";
171+
break;
172+
default:
173+
sym = "?";
174+
}
175+
nodes.push({ id, kind: "terminal", sym });
176+
} /* Non-terminal */ else {
177+
nodes.push({
178+
id,
179+
kind: "non-terminal",
180+
left: this.$.leftOf(id),
181+
right: this.$.rightOf(id),
182+
});
183+
}
184+
}
185+
186+
return { nodes } as const;
187+
}
147188
}
148189

149190
export async function initArenaEvaluator(wasmPath: string) {

scripts/genSvg.ts

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// Usage: deno run -A scripts/genSvg.ts <symbolCount>
2+
3+
import type { EvaluationPath, GlobalInfo } from "./types.ts";
4+
import {
5+
getNodeLabel,
6+
isValidEvaluationPath,
7+
isValidGlobalInfo,
8+
} from "./types.ts";
9+
10+
const [nRaw] = Deno.args;
11+
12+
if (!nRaw) {
13+
console.error("Usage: deno run -A scripts/genSvg.ts <symbolCount>");
14+
Deno.exit(1);
15+
}
16+
17+
const n = Number.parseInt(nRaw, 10);
18+
19+
if (!Number.isFinite(n) || n <= 0) {
20+
console.error(
21+
`symbolCount must be a positive integer; received \`${nRaw}\`.`,
22+
);
23+
Deno.exit(1);
24+
}
25+
26+
const outputDir = `forest${n}_svg`;
27+
await Deno.mkdir(outputDir, { recursive: true });
28+
29+
console.error(`Step 1: Generating forest JSONL...`);
30+
31+
const genForest = new Deno.Command(Deno.execPath(), {
32+
args: ["run", "-A", "scripts/generateforest.ts", String(n)],
33+
stdout: "piped",
34+
});
35+
const forestProc = genForest.spawn();
36+
const forestOut = await forestProc.output();
37+
38+
// Parse the JSONL output
39+
const lines = new TextDecoder().decode(forestOut.stdout).trim().split("\n");
40+
const paths: EvaluationPath[] = [];
41+
let globalInfo: GlobalInfo | null = null;
42+
43+
for (const line of lines) {
44+
if (!line.trim()) continue;
45+
const data = JSON.parse(line);
46+
if (data.type === "global") {
47+
if (isValidGlobalInfo(data)) {
48+
globalInfo = data;
49+
} else {
50+
console.error("Invalid global info structure:", data);
51+
Deno.exit(1);
52+
}
53+
} else {
54+
if (isValidEvaluationPath(data)) {
55+
paths.push(data);
56+
} else {
57+
console.error("Invalid evaluation path structure:", data);
58+
Deno.exit(1);
59+
}
60+
}
61+
}
62+
63+
if (!globalInfo) {
64+
console.error("No global info found in JSONL output");
65+
Deno.exit(1);
66+
}
67+
68+
console.error(`Step 1 complete. Found ${paths.length} evaluation paths`);
69+
console.error(
70+
`Global info contains ${Object.keys(globalInfo.labels).length} labels`,
71+
);
72+
73+
console.error(`Step 2: Grouping paths by sink...`);
74+
75+
// Group paths by sink
76+
const sinkGroups = new Map<number, EvaluationPath[]>();
77+
for (const path of paths) {
78+
if (!sinkGroups.has(path.sink)) {
79+
sinkGroups.set(path.sink, []);
80+
}
81+
sinkGroups.get(path.sink)!.push(path);
82+
}
83+
84+
console.error(`Found ${sinkGroups.size} unique sinks`);
85+
86+
console.error(`Step 3: Generating DOT files for each sink...`);
87+
88+
// Generate DOT file for each sink
89+
let dotFileCount = 0;
90+
for (const [sinkId, sinkPaths] of sinkGroups) {
91+
const sinkLabel = getNodeLabel(globalInfo, sinkId);
92+
const dotPath = `${outputDir}/sink_${sinkId}_${
93+
sinkLabel.replace(/[^a-zA-Z0-9]/g, "_")
94+
}.dot`;
95+
96+
// Collect all nodes and edges for this sink
97+
const nodes = new Set<number>();
98+
const edges = new Set<string>();
99+
100+
for (const path of sinkPaths) {
101+
nodes.add(path.source);
102+
nodes.add(path.sink);
103+
for (const step of path.steps) {
104+
nodes.add(step.from);
105+
nodes.add(step.to);
106+
edges.add(`${step.from} -> ${step.to}`);
107+
}
108+
}
109+
110+
// Generate DOT content
111+
let dotContent = `digraph "Sink_${sinkId}_${sinkLabel}" {\n`;
112+
dotContent += ` rankdir=TB;\n`;
113+
dotContent +=
114+
` node [shape=box, style=filled, fontname="Arial", fontsize=10];\n`;
115+
dotContent += ` edge [fontname="Arial", fontsize=8];\n\n`;
116+
117+
// Add nodes
118+
for (const nodeId of nodes) {
119+
const label = getNodeLabel(globalInfo, nodeId);
120+
121+
const isSource = sinkPaths.some((p) => p.source === nodeId);
122+
const isSink = nodeId === sinkId;
123+
124+
let color = "lightgray";
125+
if (isSource && isSink) {
126+
color = "lightblue"; // Self-reducing
127+
} else if (isSource) {
128+
color = "lightgreen"; // Source only
129+
} else if (isSink) {
130+
color = "lightcoral"; // Sink only
131+
}
132+
133+
dotContent += ` ${nodeId} [label="${label}", fillcolor="${color}"];\n`;
134+
}
135+
136+
dotContent += `\n`;
137+
138+
// Add edges
139+
for (const edge of edges) {
140+
dotContent += ` ${edge};\n`;
141+
}
142+
143+
dotContent += `}\n`;
144+
145+
await Deno.writeTextFile(dotPath, dotContent);
146+
dotFileCount++;
147+
}
148+
149+
console.error(`Step 3 complete. Generated ${dotFileCount} DOT files`);
150+
151+
console.error(`Step 4: Running neato to generate SVG...`);
152+
153+
// Run neato on all DOT files
154+
const dotFiles = [];
155+
for await (const entry of Deno.readDir(outputDir)) {
156+
if (entry.isFile && entry.name.endsWith(".dot")) {
157+
dotFiles.push(`${outputDir}/${entry.name}`);
158+
}
159+
}
160+
161+
const CONCURRENCY = 64;
162+
let idx = 0;
163+
164+
async function runNeato(dotPath: string) {
165+
const svgPath = dotPath.replace(/\.dot$/, ".svg");
166+
const neato = new Deno.Command("neato", {
167+
args: [
168+
"-Tsvg",
169+
"-Goverlap=scale",
170+
"-Gsplines=true",
171+
"-Gnodesep=1.0",
172+
"-Granksep=2.0",
173+
dotPath,
174+
"-o",
175+
svgPath,
176+
],
177+
stdout: "null",
178+
stderr: "null",
179+
});
180+
const { success } = await neato.output();
181+
if (!success) {
182+
console.error(`neato failed for ${dotPath}`);
183+
}
184+
}
185+
186+
while (idx < dotFiles.length) {
187+
const batch = dotFiles.slice(idx, idx + CONCURRENCY);
188+
await Promise.all(batch.map(runNeato));
189+
idx += CONCURRENCY;
190+
console.error(
191+
`Processed ${
192+
Math.min(idx, dotFiles.length)
193+
} / ${dotFiles.length} SVG files (${
194+
((Math.min(idx, dotFiles.length) / dotFiles.length) * 100).toFixed(2)
195+
}%)`,
196+
);
197+
}
198+
199+
console.log(`Generated ${dotFiles.length} SVG files in ${outputDir}`);

0 commit comments

Comments
 (0)