OR-1 dataflow CPU sketch
1import cytoscape from "cytoscape";
2import elk from "cytoscape-elk";
3import svg from "cytoscape-svg";
4import type { GraphUpdate, GraphNode, GraphEdge, GraphRegion } from "@common/types";
5import { stylesheet } from "./style";
6import { logicalLayout, physicalLayout } from "@common/layout";
7import { exportSvg, exportPng, copyPng } from "@common/export";
8
9cytoscape.use(elk);
10cytoscape.use(svg);
11
12const ROUTING_CATEGORY = "routing";
13
14const cy = cytoscape({
15 container: document.getElementById("graph"),
16 style: stylesheet,
17 elements: [],
18 maxZoom: 2.0,
19 minZoom: 0.1,
20});
21
22function buildLabel(node: GraphNode): string {
23 if (node.label) return node.label;
24 if (node.const !== null) {
25 return `${node.opcode}\n${node.const}`;
26 }
27 return node.opcode;
28}
29
30function buildPhysicalLabel(node: GraphNode): string {
31 if (node.label) return node.label;
32 const lines: string[] = [];
33 if (node.const !== null) {
34 lines.push(`${node.opcode}=${node.const}`);
35 } else {
36 lines.push(node.opcode);
37 }
38 if (node.iram_offset !== null) lines.push(`iram:${node.iram_offset}`);
39 if (node.ctx !== null) lines.push(`ctx:${node.ctx}`);
40 return lines.join("\n");
41}
42
43function buildElements(update: GraphUpdate): cytoscape.ElementDefinition[] {
44 const elements: cytoscape.ElementDefinition[] = [];
45 const regionParents = new Map<string, string>();
46
47 for (const region of update.regions) {
48 if (region.kind === "function") {
49 elements.push({
50 data: { id: region.tag, label: region.tag },
51 });
52 for (const nodeId of region.node_ids) {
53 regionParents.set(nodeId, region.tag);
54 }
55 }
56 }
57
58 for (const node of update.nodes) {
59 const el: cytoscape.ElementDefinition = {
60 data: {
61 id: node.id,
62 label: buildLabel(node),
63 colour: node.colour,
64 category: node.category,
65 pe: node.pe,
66 iram_offset: node.iram_offset,
67 ctx: node.ctx,
68 },
69 classes: node.has_error ? "error" : undefined,
70 };
71 const parent = regionParents.get(node.id);
72 if (parent) {
73 el.data.parent = parent;
74 }
75 elements.push(el);
76 }
77
78 for (const edge of update.edges) {
79 const sourceNode = update.nodes.find((n) => n.id === edge.source);
80 let sourceLabel: string | undefined;
81 if (sourceNode && sourceNode.category === ROUTING_CATEGORY && edge.source_port) {
82 sourceLabel = edge.source_port === "L" ? "T" : "F";
83 }
84
85 const edgeClasses = [
86 edge.has_error ? "error" : undefined,
87 edge.synthetic ? "synthetic" : undefined,
88 ].filter(Boolean).join(" ") || undefined;
89
90 elements.push({
91 data: {
92 id: `${edge.source}->${edge.target}:${edge.port}`,
93 source: edge.source,
94 target: edge.target,
95 targetLabel: edge.port,
96 sourceLabel: sourceLabel ?? "",
97 },
98 classes: edgeClasses,
99 });
100 }
101
102 return elements;
103}
104
105function buildPhysicalElements(update: GraphUpdate): cytoscape.ElementDefinition[] {
106 const elements: cytoscape.ElementDefinition[] = [];
107
108 // Build set of edge targets for seed const detection
109 const edgeTargets = new Set<string>();
110 for (const edge of update.edges) {
111 edgeTargets.add(edge.target);
112 }
113
114 const seedConstIds = new Set<string>();
115 const nodePeMap = new Map<string, number | null>();
116 const peIds = new Set<number>();
117
118 for (const node of update.nodes) {
119 nodePeMap.set(node.id, node.pe);
120 if (node.pe !== null) peIds.add(node.pe);
121
122 if (node.category === "config" && node.opcode === "const" && !edgeTargets.has(node.id)) {
123 seedConstIds.add(node.id);
124 }
125 }
126
127 // Create PE cluster parent nodes
128 for (const peId of peIds) {
129 elements.push({
130 data: { id: `pe-${peId}`, label: `PE ${peId}` },
131 classes: "pe-cluster",
132 });
133 }
134
135 // Create operation nodes
136 for (const node of update.nodes) {
137 const isSeedConst = seedConstIds.has(node.id);
138 const el: cytoscape.ElementDefinition = {
139 data: {
140 id: node.id,
141 label: buildPhysicalLabel(node),
142 colour: node.colour,
143 category: node.category,
144 pe: node.pe,
145 iram_offset: node.iram_offset,
146 ctx: node.ctx,
147 },
148 classes:
149 [isSeedConst ? "seed-const" : undefined, node.has_error ? "error" : undefined].filter(Boolean).join(" ") ||
150 undefined,
151 };
152 // Synthetic SM nodes float freely (not parented to any PE cluster)
153 if (!node.synthetic && !isSeedConst && node.pe !== null) {
154 el.data.parent = `pe-${node.pe}`;
155 }
156 elements.push(el);
157 }
158
159 // Scan cross-PE edges (excluding seed const sources) to find PE pairs
160 type PePair = { srcPe: number; tgtPe: number };
161 const pePairKey = (src: number, tgt: number) => `${src}->${tgt}`;
162 const crossPePairs = new Map<string, PePair>();
163 const busEdgeCounts = new Map<string, number>();
164
165 for (const edge of update.edges) {
166 if (seedConstIds.has(edge.source)) continue;
167 const srcPe = nodePeMap.get(edge.source) ?? null;
168 const tgtPe = nodePeMap.get(edge.target) ?? null;
169 if (srcPe !== null && tgtPe !== null && srcPe !== tgtPe) {
170 const key = pePairKey(srcPe, tgtPe);
171 if (!crossPePairs.has(key)) {
172 crossPePairs.set(key, { srcPe, tgtPe });
173 }
174 busEdgeCounts.set(key, (busEdgeCounts.get(key) ?? 0) + 1);
175 }
176 }
177
178 // Create port nodes for each directed PE pair
179 for (const [, pair] of crossPePairs) {
180 elements.push({
181 data: {
182 id: `port-${pair.srcPe}-to-${pair.tgtPe}-exit`,
183 parent: `pe-${pair.srcPe}`,
184 },
185 classes: "port-node",
186 });
187 elements.push({
188 data: {
189 id: `port-${pair.srcPe}-to-${pair.tgtPe}-entry`,
190 parent: `pe-${pair.tgtPe}`,
191 },
192 classes: "port-node",
193 });
194 }
195
196 // Create bus edges (one per PE pair)
197 for (const [key, pair] of crossPePairs) {
198 const count = busEdgeCounts.get(key) ?? 1;
199 elements.push({
200 data: {
201 id: `bus-${pair.srcPe}-to-${pair.tgtPe}`,
202 source: `port-${pair.srcPe}-to-${pair.tgtPe}-exit`,
203 target: `port-${pair.srcPe}-to-${pair.tgtPe}-entry`,
204 label: count > 1 ? `×${count}` : "",
205 },
206 classes: "physical bus-segment",
207 });
208 }
209
210 // Create edges
211 for (const edge of update.edges) {
212 const srcPe = nodePeMap.get(edge.source) ?? null;
213 const tgtPe = nodePeMap.get(edge.target) ?? null;
214 const errorClass = edge.has_error ? " error" : "";
215 const syntheticClass = edge.synthetic ? " synthetic" : "";
216
217 if (edge.synthetic) {
218 // Synthetic edges (e.g., SM request/return) — never bundled into bus
219 elements.push({
220 data: {
221 id: `${edge.source}->${edge.target}:${edge.port}`,
222 source: edge.source,
223 target: edge.target,
224 targetLabel: edge.port,
225 sourceLabel: "",
226 },
227 classes: `physical synthetic${errorClass}`,
228 });
229 } else if (seedConstIds.has(edge.source)) {
230 // Seed const: direct edge, never bundled
231 elements.push({
232 data: {
233 id: `${edge.source}->${edge.target}:${edge.port}`,
234 source: edge.source,
235 target: edge.target,
236 targetLabel: edge.port,
237 sourceLabel: "",
238 },
239 classes: `seed-edge physical${errorClass}`,
240 });
241 } else if (srcPe !== null && tgtPe !== null && srcPe !== tgtPe) {
242 // Cross-PE: split into exit segment + entry segment (bus already created)
243 elements.push({
244 data: {
245 id: `${edge.source}->exit-${srcPe}-to-${tgtPe}:${edge.port}`,
246 source: edge.source,
247 target: `port-${srcPe}-to-${tgtPe}-exit`,
248 sourceLabel: "",
249 targetLabel: "",
250 },
251 classes: `physical exit-segment${errorClass}`,
252 });
253 elements.push({
254 data: {
255 id: `entry-${srcPe}-to-${tgtPe}->${edge.target}:${edge.port}`,
256 source: `port-${srcPe}-to-${tgtPe}-entry`,
257 target: edge.target,
258 targetLabel: edge.port,
259 },
260 classes: `physical entry-segment${errorClass}`,
261 });
262 } else {
263 // Intra-PE or unplaced
264 elements.push({
265 data: {
266 id: `${edge.source}->${edge.target}:${edge.port}`,
267 source: edge.source,
268 target: edge.target,
269 targetLabel: edge.port,
270 },
271 classes: `physical intra-pe${errorClass}`,
272 });
273 }
274 }
275
276 return elements;
277}
278
279function getRequiredElement(id: string): HTMLElement {
280 const element = document.getElementById(id);
281 if (!element) throw new Error(`Required element with id "${id}" not found`);
282 return element;
283}
284
285function updateErrorPanel(update: GraphUpdate): void {
286 const panel = getRequiredElement("error-panel");
287 const list = getRequiredElement("error-list");
288 const count = getRequiredElement("error-count");
289 const overlay = getRequiredElement("parse-error-overlay");
290
291 // Handle parse error (AC5.5)
292 if (update.parse_error) {
293 overlay.textContent = update.parse_error;
294 overlay.classList.add("visible");
295 panel.classList.remove("visible");
296 return;
297 }
298 overlay.classList.remove("visible");
299
300 // Handle pipeline errors
301 if (update.errors.length === 0) {
302 panel.classList.remove("visible");
303 list.innerHTML = "";
304 return;
305 }
306
307 count.textContent = `${update.errors.length} error${update.errors.length > 1 ? "s" : ""}`;
308 list.innerHTML = "";
309
310 for (const error of update.errors) {
311 const li = document.createElement("li");
312 li.className = "error-item";
313
314 // Create line:column span
315 const lineSpan = document.createElement("span");
316 lineSpan.className = "error-line";
317 lineSpan.textContent = `L${error.line}:${error.column}`;
318 li.appendChild(lineSpan);
319
320 // Create category span
321 const categorySpan = document.createElement("span");
322 categorySpan.className = "error-category";
323 categorySpan.textContent = `[${error.category}]`;
324 li.appendChild(categorySpan);
325
326 // Create message text node (safe from XSS)
327 const messageNode = document.createTextNode(error.message);
328 li.appendChild(messageNode);
329
330 if (error.suggestions.length > 0) {
331 for (const suggestion of error.suggestions) {
332 const span = document.createElement("span");
333 span.className = "error-suggestion";
334 span.textContent = `→ ${suggestion}`;
335 li.appendChild(span);
336 }
337 }
338
339 li.addEventListener("click", () => {
340 // Highlight related nodes in the graph
341 cy.nodes().unselect();
342 cy.nodes().forEach((node) => {
343 if (node.hasClass("error")) {
344 node.select();
345 }
346 });
347 });
348
349 list.appendChild(li);
350 }
351
352 panel.classList.add("visible");
353}
354
355type ViewMode = "logical" | "physical";
356let currentView: ViewMode = "logical";
357let latestUpdate: GraphUpdate | null = null;
358
359function routeEdges(selector?: string): void {
360 const nodes = cy.nodes().not(":parent").not(".port-node");
361 const nodePositions: Array<{ x: number; y: number }> = [];
362 nodes.forEach((n) => {
363 nodePositions.push({ x: n.position("x"), y: n.position("y") });
364 });
365
366 let leftCount = 0;
367 let rightCount = 0;
368
369 const edges = selector ? cy.edges(selector) : cy.edges();
370 edges.forEach((edge) => {
371 const sy = edge.source().position("y");
372 const ty = edge.target().position("y");
373 const sx = edge.source().position("x");
374 const tx = edge.target().position("x");
375 const span = Math.abs(ty - sy);
376
377 if (span < 80) {
378 edge.style({ "curve-style": "bezier" });
379 return;
380 }
381
382 const blocked = nodePositions.some((p) => {
383 return p.y > Math.min(sy, ty) + 25 && p.y < Math.max(sy, ty) - 25;
384 });
385
386 if (!blocked) {
387 edge.style({ "curve-style": "bezier" });
388 return;
389 }
390
391 const avgX = (sx + tx) / 2;
392 const centerX = (Math.min(...nodePositions.map((p) => p.x)) + Math.max(...nodePositions.map((p) => p.x))) / 2;
393 const goLeft = avgX >= centerX;
394
395 const baseOffset = 30;
396 const stagger = goLeft ? 12 * leftCount : 12 * rightCount;
397 const offset = (goLeft ? -1 : 1) * (baseOffset + stagger);
398
399 if (goLeft) leftCount++;
400 else rightCount++;
401
402 edge.style({
403 "curve-style": "unbundled-bezier",
404 "control-point-distances": [offset * 0.6, offset, offset * 0.6],
405 "control-point-weights": [0.2, 0.5, 0.8],
406 });
407 });
408}
409
410function renderLogical(update: GraphUpdate): void {
411 cy.batch(() => {
412 cy.elements().remove();
413 cy.add(buildElements(update));
414 });
415 const layout = cy.layout(logicalLayout());
416 layout.on("layoutstop", () => {
417 routeEdges();
418 cy.fit(undefined, 40);
419 });
420 layout.run();
421}
422
423function renderPhysical(update: GraphUpdate): void {
424 cy.batch(() => {
425 cy.elements().remove();
426 cy.add(buildPhysicalElements(update));
427 });
428 const layout = cy.layout(physicalLayout());
429 layout.on("layoutstop", () => {
430 routeEdges(".intra-pe, .seed-edge");
431 cy.fit(undefined, 40);
432 });
433 layout.run();
434}
435
436function renderUpdate(update: GraphUpdate): void {
437 latestUpdate = update;
438 if (currentView === "physical" && update.stage !== "allocate") {
439 currentView = "logical";
440 const toggleBtn = document.getElementById("view-toggle");
441 if (toggleBtn) {
442 toggleBtn.textContent = "Physical View";
443 }
444 }
445 if (currentView === "logical") {
446 renderLogical(update);
447 } else {
448 renderPhysical(update);
449 }
450 updateErrorPanel(update);
451}
452
453function setupToggleButton(): void {
454 const toggleBtn = document.getElementById("view-toggle");
455 if (toggleBtn) {
456 toggleBtn.addEventListener("click", () => {
457 if (!latestUpdate) return;
458 if (currentView === "logical") {
459 if (latestUpdate.stage !== "allocate") {
460 // Physical view not available
461 return;
462 }
463 currentView = "physical";
464 toggleBtn.textContent = "Logical View";
465 renderPhysical(latestUpdate);
466 } else {
467 currentView = "logical";
468 toggleBtn.textContent = "Physical View";
469 renderLogical(latestUpdate);
470 }
471 });
472 }
473}
474
475function setupExportButtons(): void {
476 document.getElementById("export-svg")?.addEventListener("click", () => exportSvg(cy));
477 document.getElementById("export-png")?.addEventListener("click", () => exportPng(cy));
478 document.getElementById("copy-png")?.addEventListener("click", () => copyPng(cy));
479}
480
481function connect(): void {
482 const protocol = location.protocol === "https:" ? "wss:" : "ws:";
483 const ws = new WebSocket(`${protocol}//${location.host}/ws`);
484
485 ws.onmessage = (event: MessageEvent) => {
486 const update: GraphUpdate = JSON.parse(event.data);
487 if (update.type === "graph_update") {
488 renderUpdate(update);
489 }
490 };
491
492 ws.onerror = (event: Event) => {
493 console.error("WebSocket error:", event);
494 };
495
496 ws.onclose = () => {
497 setTimeout(connect, 2000);
498 };
499}
500
501setupToggleButton();
502setupExportButtons();
503connect();