diff --git a/.deciduous/deciduous.db b/.deciduous/deciduous.db new file mode 100644 --- /dev/null +++ b/.deciduous/deciduous.db diff --git a/dfgraph/pipeline.py b/dfgraph/pipeline.py --- a/dfgraph/pipeline.py +++ b/dfgraph/pipeline.py @@ -11,6 +11,7 @@ from typing import Optional from lark import Lark +from lark.exceptions import UnexpectedInput from pathlib import Path from asm.ir import IRGraph @@ -85,7 +86,7 @@ # Stage 1: Parse try: tree = _get_parser().parse(source) - except Exception as exc: + except UnexpectedInput as exc: return PipelineResult( graph=None, stage=PipelineStage.PARSE_ERROR, diff --git a/dfgraph/frontend/package-lock.json b/dfgraph/frontend/package-lock.json --- a/dfgraph/frontend/package-lock.json +++ b/dfgraph/frontend/package-lock.json @@ -9,13 +9,13 @@ "version": "0.1.0", "dependencies": { "cytoscape": "^3.30.0", - "cytoscape-dagre": "^2.5.0", "cytoscape-elk": "^2.3.0", "cytoscape-svg": "^0.4.0" }, "devDependencies": { "@types/cytoscape": "^3.21.0", - "esbuild": "^0.24.0" + "esbuild": "^0.24.0", + "typescript": "^5.7.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -460,18 +460,6 @@ "node": ">=0.10" } }, - "node_modules/cytoscape-dagre": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/cytoscape-dagre/-/cytoscape-dagre-2.5.0.tgz", - "integrity": "sha512-VG2Knemmshop4kh5fpLO27rYcyUaaDkRw+6PiX4bstpB+QFt0p2oauMrsjVbUamGWQ6YNavh7x2em2uZlzV44g==", - "license": "MIT", - "dependencies": { - "dagre": "^0.8.5" - }, - "peerDependencies": { - "cytoscape": "^3.2.22" - } - }, "node_modules/cytoscape-elk": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/cytoscape-elk/-/cytoscape-elk-2.3.0.tgz", @@ -491,16 +479,6 @@ "license": "GNU GPLv3", "peerDependencies": { "cytoscape": "^3.2.0" - } - }, - "node_modules/dagre": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz", - "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", - "license": "MIT", - "dependencies": { - "graphlib": "^2.1.8", - "lodash": "^4.17.15" } }, "node_modules/elkjs": { @@ -550,20 +528,19 @@ "@esbuild/win32-x64": "0.24.2" } }, - "node_modules/graphlib": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz", - "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.15" + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" } - }, - "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT" } } } diff --git a/dfgraph/frontend/package.json b/dfgraph/frontend/package.json --- a/dfgraph/frontend/package.json +++ b/dfgraph/frontend/package.json @@ -5,16 +5,17 @@ "type": "module", "scripts": { "build": "esbuild src/main.ts --bundle --outfile=dist/bundle.js --format=esm --target=es2020", - "watch": "esbuild src/main.ts --bundle --outfile=dist/bundle.js --format=esm --target=es2020 --watch" + "watch": "esbuild src/main.ts --bundle --outfile=dist/bundle.js --format=esm --target=es2020 --watch", + "typecheck": "tsc --noEmit" }, "dependencies": { "cytoscape": "^3.30.0", - "cytoscape-dagre": "^2.5.0", "cytoscape-elk": "^2.3.0", "cytoscape-svg": "^0.4.0" }, "devDependencies": { "@types/cytoscape": "^3.21.0", - "esbuild": "^0.24.0" + "esbuild": "^0.24.0", + "typescript": "^5.7.0" } } diff --git a/dfgraph/frontend/src/cytoscape-dagre.d.ts b/dfgraph/frontend/src/cytoscape-dagre.d.ts deleted file mode 100644 --- a/dfgraph/frontend/src/cytoscape-dagre.d.ts +++ /dev/null @@ -1,1 +0,0 @@ -declare module 'cytoscape-dagre'; diff --git a/dfgraph/frontend/src/cytoscape-plugins.d.ts b/dfgraph/frontend/src/cytoscape-plugins.d.ts new file mode 100644 --- /dev/null +++ b/dfgraph/frontend/src/cytoscape-plugins.d.ts @@ -0,0 +1,15 @@ +import "cytoscape"; + +declare module "cytoscape" { + interface Core { + svg(options?: { full?: boolean; scale?: number; bg?: string }): string; + png(options?: { + full?: boolean; + output?: "base64uri" | "base64" | "blob" | "blob-promise"; + scale?: number; + bg?: string; + maxWidth?: number; + maxHeight?: number; + }): string | Blob | Promise; + } +} diff --git a/dfgraph/frontend/src/cytoscape-svg.d.ts b/dfgraph/frontend/src/cytoscape-svg.d.ts --- a/dfgraph/frontend/src/cytoscape-svg.d.ts +++ b/dfgraph/frontend/src/cytoscape-svg.d.ts @@ -1,5 +1,4 @@ declare module "cytoscape-svg" { - import cytoscape from "cytoscape"; const ext: cytoscape.Ext; export default ext; } diff --git a/dfgraph/frontend/src/export.ts b/dfgraph/frontend/src/export.ts --- a/dfgraph/frontend/src/export.ts +++ b/dfgraph/frontend/src/export.ts @@ -12,30 +12,26 @@ } export function exportSvg(cy: cytoscape.Core): void { - const svgContent: string = (cy as any).svg({ full: true }); + const svgContent = cy.svg({ full: true }); const blob = new Blob([svgContent], { type: "image/svg+xml" }); downloadBlob(blob, "dfgraph.svg"); } -export function exportPng(cy: cytoscape.Core): void { - const pngPromise: Promise = (cy as any).png({ +export async function exportPng(cy: cytoscape.Core): Promise { + const blob = await (cy.png({ full: true, output: "blob-promise", scale: 2, - }); - pngPromise.then((blob: Blob) => { - downloadBlob(blob, "dfgraph.png"); - }); + }) as Promise); + downloadBlob(blob, "dfgraph.png"); } -export function copyPng(cy: cytoscape.Core): void { - const pngPromise: Promise = (cy as any).png({ +export async function copyPng(cy: cytoscape.Core): Promise { + const blob = await (cy.png({ full: true, output: "blob-promise", scale: 2, - }); - pngPromise.then((blob: Blob) => { - const item = new ClipboardItem({ "image/png": blob }); - navigator.clipboard.write([item]); - }); + }) as Promise); + const item = new ClipboardItem({ "image/png": blob }); + await navigator.clipboard.write([item]); } diff --git a/dfgraph/frontend/src/layout.ts b/dfgraph/frontend/src/layout.ts --- a/dfgraph/frontend/src/layout.ts +++ b/dfgraph/frontend/src/layout.ts @@ -1,4 +1,6 @@ -export function logicalLayout(): object { +import cytoscape from "cytoscape"; + +export function logicalLayout(): cytoscape.LayoutOptions { return { name: "elk", elk: { @@ -15,25 +17,27 @@ "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", }, animate: false, - }; + } as cytoscape.LayoutOptions; } -export function physicalLayout(): object { +export function physicalLayout(): cytoscape.LayoutOptions { return { name: "elk", elk: { algorithm: "layered", "elk.direction": "DOWN", - "elk.spacing.nodeNode": 35, - "elk.layered.spacing.nodeNodeBetweenLayers": 40, - "elk.spacing.edgeNode": 20, - "elk.spacing.edgeNodeBetweenLayers": 15, - "elk.spacing.edgeEdge": 10, - "elk.spacing.edgeEdgeBetweenLayers": 10, - "elk.edgeRouting": "ORTHOGONAL", + "elk.hierarchyHandling": "INCLUDE_CHILDREN", + "elk.spacing.nodeNode": 40, + "elk.layered.spacing.nodeNodeBetweenLayers": 50, + "elk.spacing.edgeNode": 25, + "elk.spacing.edgeNodeBetweenLayers": 20, + "elk.spacing.edgeEdge": 12, + "elk.spacing.edgeEdgeBetweenLayers": 12, + "elk.edgeRouting": "POLYLINE", + "elk.padding": "[top=20,left=20,bottom=20,right=20]", "elk.layered.crossingMinimization.strategy": "LAYER_SWEEP", "elk.layered.nodePlacement.strategy": "NETWORK_SIMPLEX", }, animate: false, - }; + } as cytoscape.LayoutOptions; } diff --git a/dfgraph/frontend/src/main.ts b/dfgraph/frontend/src/main.ts --- a/dfgraph/frontend/src/main.ts +++ b/dfgraph/frontend/src/main.ts @@ -151,18 +151,24 @@ targetLabel: edge.port, sourceLabel: "", }, - classes: (isCrossPe ? "cross-pe" : "intra-pe") + (edge.has_error ? " error" : ""), + classes: "physical " + (isCrossPe ? "cross-pe" : "intra-pe") + (edge.has_error ? " error" : ""), }); } return elements; } +function getRequiredElement(id: string): HTMLElement { + const element = document.getElementById(id); + if (!element) throw new Error(`Required element with id "${id}" not found`); + return element; +} + function updateErrorPanel(update: GraphUpdate): void { - const panel = document.getElementById("error-panel")!; - const list = document.getElementById("error-list")!; - const count = document.getElementById("error-count")!; - const overlay = document.getElementById("parse-error-overlay")!; + const panel = getRequiredElement("error-panel"); + const list = getRequiredElement("error-list"); + const count = getRequiredElement("error-count"); + const overlay = getRequiredElement("parse-error-overlay"); // Handle parse error (AC5.5) if (update.parse_error) { @@ -186,9 +192,22 @@ for (const error of update.errors) { const li = document.createElement("li"); li.className = "error-item"; - li.innerHTML = `L${error.line}:${error.column}` - + `[${error.category}]` - + error.message; + + // Create line:column span + const lineSpan = document.createElement("span"); + lineSpan.className = "error-line"; + lineSpan.textContent = `L${error.line}:${error.column}`; + li.appendChild(lineSpan); + + // Create category span + const categorySpan = document.createElement("span"); + categorySpan.className = "error-category"; + categorySpan.textContent = `[${error.category}]`; + li.appendChild(categorySpan); + + // Create message text node (safe from XSS) + const messageNode = document.createTextNode(error.message); + li.appendChild(messageNode); if (error.suggestions.length > 0) { for (const suggestion of error.suggestions) { @@ -293,7 +312,6 @@ }); const layout = cy.layout(physicalLayout()); layout.on("layoutstop", () => { - routeEdges(); cy.fit(undefined, 40); }); layout.run(); @@ -353,6 +371,10 @@ if (update.type === "graph_update") { renderUpdate(update); } + }; + + ws.onerror = (event: Event) => { + console.error("WebSocket error:", event); }; ws.onclose = () => { diff --git a/dfgraph/frontend/src/style.ts b/dfgraph/frontend/src/style.ts --- a/dfgraph/frontend/src/style.ts +++ b/dfgraph/frontend/src/style.ts @@ -1,6 +1,6 @@ import cytoscape from "cytoscape"; -export const stylesheet: cytoscape.Stylesheet[] = [ +export const stylesheet: cytoscape.StylesheetStyle[] = [ { selector: "node", style: { @@ -31,7 +31,8 @@ "border-style": "dashed", "border-width": 1, "border-color": "#888", - "background-color": "rgba(200, 200, 200, 0.06)", + "background-color": "#c8c8c8", + "background-opacity": 0.06, padding: "20px", "text-valign": "top", "text-halign": "center", @@ -86,31 +87,38 @@ width: "label", height: "label", "border-width": 1.5, - "border-color": "#5c6bc0", - "background-color": "rgba(92, 107, 192, 0.04)", + "border-color": "#7986cb", + "background-color": "#e8eaf6", + "background-opacity": 0.5, padding: "16px", "text-valign": "top", "text-halign": "center", label: "data(label)", "font-size": 9, "font-weight": "bold", - color: "#5c6bc0", + color: "#3949ab", }, }, { selector: "edge.cross-pe", style: { width: 1.5, - "line-color": "#5c6bc0", - "target-arrow-color": "#5c6bc0", + "line-color": "#e57373", + "target-arrow-color": "#e57373", }, }, { selector: "edge.intra-pe", style: { width: 0.75, - "line-color": "#999", - "target-arrow-color": "#999", + "line-color": "#616161", + "target-arrow-color": "#616161", + }, + }, + { + selector: "edge.physical", + style: { + "curve-style": "bezier", }, }, ]; diff --git a/dfgraph/frontend/src/types.ts b/dfgraph/frontend/src/types.ts --- a/dfgraph/frontend/src/types.ts +++ b/dfgraph/frontend/src/types.ts @@ -1,4 +1,4 @@ -export interface GraphNode { +export type GraphNode = { id: string; opcode: string; category: string; @@ -9,45 +9,45 @@ ctx: number | null; has_error: boolean; loc: SourceLoc; -} +}; -export interface SourceLoc { +export type SourceLoc = { line: number; column: number; end_line: number | null; end_column: number | null; -} +}; -export interface AddrInfo { +export type AddrInfo = { offset: number; port: string; pe: number | null; -} +}; -export interface GraphEdge { +export type GraphEdge = { source: string; target: string; port: string; source_port: string | null; has_error: boolean; addr?: AddrInfo; -} +}; -export interface GraphRegion { +export type GraphRegion = { tag: string; kind: string; node_ids: string[]; -} +}; -export interface GraphError { +export type GraphError = { line: number; column: number; category: string; message: string; suggestions: string[]; -} +}; -export interface GraphUpdate { +export type GraphUpdate = { type: "graph_update"; stage: string; nodes: GraphNode[]; @@ -60,4 +60,4 @@ pe_count: number; sm_count: number; }; -} +};