OR-1 dataflow CPU sketch

Dataflow Graph Renderer Implementation Plan — Phase 5: Frontend — Logical View#

Goal: Cytoscape.js rendering of the logical dataflow graph with coloured circular nodes, edge annotations, and function region boxes.

Architecture: Three TypeScript modules: main.ts (WebSocket client, cytoscape init, graph updates), style.ts (stylesheet definitions), layout.ts (dagre layout config). The frontend receives graph_update JSON from the backend WebSocket and renders it using cytoscape.js with the dagre hierarchical layout.

Tech Stack: TypeScript, cytoscape.js 3.30+, cytoscape-dagre 2.5+, esbuild

Scope: 8 phases from original design (this is phase 5 of 8)

Codebase verified: 2026-02-23


Acceptance Criteria Coverage#

This phase implements and tests:

dataflow-renderer.AC1: Logical view renders dataflow graph#

  • dataflow-renderer.AC1.1 Success: Valid dfasm source renders as a directed graph with nodes and edges
  • dataflow-renderer.AC1.2 Success: Nodes are circular with opcode mnemonic labels centred inside
  • dataflow-renderer.AC1.3 Success: Nodes are coloured by opcode category (arithmetic=blue, logic=green, comparison=amber, routing=purple, memory=orange, io=teal, config=grey)
  • dataflow-renderer.AC1.4 Success: Edges show port annotations (L/R) at target end and branch labels (T/F) at source end for routing ops
  • dataflow-renderer.AC1.5 Success: Function regions render as dashed bounding boxes around their child nodes

Task 1: Create dfgraph/frontend/src/types.ts — shared type definitions#

Verifies: (supports all AC1.x — defines the JSON contract)

Files:

  • Create: /home/orual/Projects/or1-design/dfgraph/frontend/src/types.ts

Implementation:

Define TypeScript interfaces matching the JSON structure produced by dfgraph/graph_json.py (Phase 3). These types are used by all frontend modules.

export interface GraphNode {
  id: string;
  opcode: string;
  category: string;
  colour: string;
  const: number | null;
  pe: number | null;
  iram_offset: number | null;
  ctx: number | null;
  has_error: boolean;
  loc: SourceLoc;
}

export interface SourceLoc {
  line: number;
  column: number;
  end_line: number | null;
  end_column: number | null;
}

export interface AddrInfo {
  offset: number;
  port: string;
  pe: number | null;
}

export interface GraphEdge {
  source: string;
  target: string;
  port: string;
  source_port: string | null;
  has_error: boolean;
  addr?: AddrInfo;
}

export interface GraphRegion {
  tag: string;
  kind: string;
  node_ids: string[];
}

export interface GraphError {
  line: number;
  column: number;
  category: string;
  message: string;
  suggestions: string[];
}

export interface GraphUpdate {
  type: "graph_update";
  stage: string;
  nodes: GraphNode[];
  edges: GraphEdge[];
  regions: GraphRegion[];
  errors: GraphError[];
  parse_error: string | null;
  metadata: {
    stage: string;
    pe_count: number;
    sm_count: number;
  };
}

Verification:

cd /home/orual/Projects/or1-design/dfgraph/frontend
npx esbuild src/types.ts --bundle --outfile=/dev/null --format=esm --target=es2020

Expected: No type errors.

Commit: feat: add TypeScript type definitions for graph JSON

Task 2: Create dfgraph/frontend/src/style.ts — cytoscape stylesheet#

Verifies: dataflow-renderer.AC1.2, dataflow-renderer.AC1.3, dataflow-renderer.AC1.4, dataflow-renderer.AC1.5

Files:

  • Create: /home/orual/Projects/or1-design/dfgraph/frontend/src/style.ts

Implementation:

Export a cytoscape stylesheet array. Key styling rules:

  1. Nodes (AC1.2): shape ellipse, width/height sized to label with padding, centered label via text-valign: center and text-halign: center, label from data(label).

  2. Category colours (AC1.3): background-color from data(colour) — the backend sets the colour per node based on opcode category. The colours are: arithmetic=#4a90d9 (blue), logic=#4caf50 (green), comparison=#ff9800 (amber), routing=#9c27b0 (purple), memory=#ff5722 (orange), config=#9e9e9e (grey).

  3. Edge annotations (AC1.4):

    • Port labels (L/R) at target end via target-label: data(targetLabel) with target-text-offset
    • Branch labels (T/F) at source end for routing ops via source-label: data(sourceLabel) with source-text-offset
    • Constant values shown as node labels (already part of the opcode label)
  4. Function regions (AC1.5): compound parent nodes with $node > node selector — dashed border, light background, label at top.

  5. Error styling: nodes with .error class get red dashed border. Edges with .error class get red dashed line.

import cytoscape from "cytoscape";

export const stylesheet: cytoscape.Stylesheet[] = [
  {
    selector: "node",
    style: {
      shape: "ellipse",
      width: "label",
      height: "label",
      padding: "12px",
      "text-valign": "center",
      "text-halign": "center",
      label: "data(label)",
      "font-size": 11,
      "font-family": "monospace",
      color: "#fff",
      "text-outline-width": 0,
      "background-color": "data(colour)",
      "border-width": 2,
      "border-color": "data(colour)",
    },
  },
  {
    selector: "$node > node",
    style: {
      shape: "roundrectangle",
      "border-style": "dashed",
      "border-width": 2,
      "border-color": "#888",
      "background-color": "rgba(200, 200, 200, 0.08)",
      padding: "24px",
      "text-valign": "top",
      "text-halign": "center",
      label: "data(label)",
      "font-size": 12,
      color: "#666",
    },
  },
  {
    selector: "edge",
    style: {
      "curve-style": "bezier",
      "target-arrow-shape": "triangle",
      "target-arrow-color": "#999",
      "line-color": "#999",
      width: 2,
      "target-label": "data(targetLabel)",
      "target-text-offset": 18,
      "target-text-margin-y": -10,
      "source-label": "data(sourceLabel)",
      "source-text-offset": 18,
      "source-text-margin-y": -10,
      "font-size": 9,
      "font-family": "monospace",
      color: "#666",
      "text-background-color": "#fff",
      "text-background-opacity": 0.8,
      "text-background-padding": "2px",
    },
  },
  {
    selector: "node.error",
    style: {
      "border-style": "dashed",
      "border-width": 3,
      "border-color": "#e53935",
    },
  },
  {
    selector: "edge.error",
    style: {
      "line-style": "dashed",
      "line-color": "#e53935",
      "target-arrow-color": "#e53935",
    },
  },
];

Verification:

cd /home/orual/Projects/or1-design/dfgraph/frontend
npx esbuild src/style.ts --bundle --outfile=/dev/null --format=esm --target=es2020

Expected: No errors.

Commit: feat: add cytoscape stylesheet for logical view

Task 3: Create dfgraph/frontend/src/layout.ts — dagre layout configuration#

Verifies: dataflow-renderer.AC1.1, dataflow-renderer.AC1.5

Files:

  • Create: /home/orual/Projects/or1-design/dfgraph/frontend/src/layout.ts

Implementation:

Export a function that returns the dagre layout options. The layout should be top-to-bottom hierarchical with reasonable spacing for small-to-medium graphs (5–50 nodes).

export function logicalLayout(): object {
  return {
    name: "dagre",
    rankDir: "TB",
    nodeSep: 60,
    rankSep: 80,
    edgeSep: 20,
    animate: false,
  };
}

Verification:

cd /home/orual/Projects/or1-design/dfgraph/frontend
npx esbuild src/layout.ts --bundle --outfile=/dev/null --format=esm --target=es2020

Expected: No errors.

Commit: feat: add dagre layout configuration

Task 4: Rewrite dfgraph/frontend/src/main.ts — WebSocket client and graph rendering#

Verifies: dataflow-renderer.AC1.1, dataflow-renderer.AC1.2, dataflow-renderer.AC1.3, dataflow-renderer.AC1.4, dataflow-renderer.AC1.5

Files:

  • Modify: /home/orual/Projects/or1-design/dfgraph/frontend/src/main.ts (replace Phase 1 placeholder)

Implementation:

Replace the minimal placeholder from Phase 1 with the full WebSocket client and graph rendering logic.

Key responsibilities:

  1. Initialize cytoscape with the stylesheet and an empty graph
  2. Connect to WebSocket at ws://${location.host}/ws
  3. On graph_update message, convert JSON to cytoscape elements and re-render
  4. Handle reconnection on WebSocket close

Converting backend JSON to cytoscape elements:

  • Each GraphNode becomes a cytoscape node element with data: { id, label, colour, category, ... }
  • The label should include the opcode mnemonic, plus constant value if present (e.g., "const\n42")
  • Each GraphEdge becomes a cytoscape edge element with data: { source, target, targetLabel, sourceLabel }
    • targetLabel = edge port ("L" or "R")
    • sourceLabel = branch label for routing ops — check if source node is a routing op, and if source_port is set, use "T" for L and "F" for R
  • Each GraphRegion with kind: "function" becomes a compound parent node, and its node_ids nodes get parent set to the region's tag
  • Nodes with has_error: true get the error class
  • Edges with has_error: true get the error class
import cytoscape from "cytoscape";
import dagre from "cytoscape-dagre";
import type { GraphUpdate, GraphNode, GraphEdge, GraphRegion } from "./types";
import { stylesheet } from "./style";
import { logicalLayout } from "./layout";

cytoscape.use(dagre);

const ROUTING_CATEGORY = "routing";

const cy = cytoscape({
  container: document.getElementById("graph"),
  style: stylesheet,
  elements: [],
});

function buildLabel(node: GraphNode): string {
  if (node.const !== null) {
    return `${node.opcode}\n${node.const}`;
  }
  return node.opcode;
}

function buildElements(
  update: GraphUpdate
): cytoscape.ElementDefinition[] {
  const elements: cytoscape.ElementDefinition[] = [];
  const regionParents = new Map<string, string>();

  for (const region of update.regions) {
    if (region.kind === "function") {
      elements.push({
        data: { id: region.tag, label: region.tag },
      });
      for (const nodeId of region.node_ids) {
        regionParents.set(nodeId, region.tag);
      }
    }
  }

  for (const node of update.nodes) {
    const el: cytoscape.ElementDefinition = {
      data: {
        id: node.id,
        label: buildLabel(node),
        colour: node.colour,
        category: node.category,
        pe: node.pe,
        iram_offset: node.iram_offset,
        ctx: node.ctx,
      },
      classes: node.has_error ? "error" : undefined,
    };
    const parent = regionParents.get(node.id);
    if (parent) {
      el.data.parent = parent;
    }
    elements.push(el);
  }

  for (const edge of update.edges) {
    const sourceNode = update.nodes.find((n) => n.id === edge.source);
    let sourceLabel: string | undefined;
    if (sourceNode && sourceNode.category === ROUTING_CATEGORY && edge.source_port) {
      sourceLabel = edge.source_port === "L" ? "T" : "F";
    }

    elements.push({
      data: {
        id: `${edge.source}->${edge.target}:${edge.port}`,
        source: edge.source,
        target: edge.target,
        targetLabel: edge.port,
        sourceLabel: sourceLabel ?? "",
      },
      classes: edge.has_error ? "error" : undefined,
    });
  }

  return elements;
}

function renderGraph(update: GraphUpdate): void {
  cy.batch(() => {
    cy.elements().remove();
    cy.add(buildElements(update));
  });
  cy.layout(logicalLayout()).run();
  cy.fit(undefined, 40);
}

function connect(): void {
  const protocol = location.protocol === "https:" ? "wss:" : "ws:";
  const ws = new WebSocket(`${protocol}//${location.host}/ws`);

  ws.onmessage = (event: MessageEvent) => {
    const update: GraphUpdate = JSON.parse(event.data);
    if (update.type === "graph_update") {
      renderGraph(update);
    }
  };

  ws.onclose = () => {
    setTimeout(connect, 2000);
  };
}

connect();

Verification:

cd /home/orual/Projects/or1-design/dfgraph/frontend
npm run build
ls -la dist/bundle.js

Expected: dist/bundle.js is created without errors.

Commit: feat: implement WebSocket client and logical view rendering

Task 5: Manual verification of the logical view#

Files: None (manual testing)

Step 1: Create a test dfasm file

Create a temporary test file (e.g., /tmp/test_render.dfasm) with a simple program. Use a valid program from the existing test suite. A minimal example:

@system pe=2 sm=1

$main {
  &a = add @const_1 @const_2
  &b = sub &a @const_3
  &out = pass &b

  &const_1 = const 10
  &const_2 = const 20
  &const_3 = const 5
}

(Adapt this to match actual dfasm syntax — check tests/test_e2e.py for known-working examples.)

Step 2: Build the frontend

cd /home/orual/Projects/or1-design/dfgraph/frontend
npm run build

Step 3: Start the server

cd /home/orual/Projects/or1-design
python -m dfgraph /tmp/test_render.dfasm

Expected: Browser opens showing an interactive graph.

Step 4: Verify visual elements

Check visually:

  • Nodes are circular with opcode labels (AC1.2)
  • Nodes have category-appropriate colours (AC1.3)
  • Edges have arrowheads and port labels (AC1.4)
  • Function region ($main) has a dashed bounding box (AC1.5)

Step 5: Run full test suite

cd /home/orual/Projects/or1-design
python -m pytest tests/ -v

Expected: All tests pass.