|
| 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