diff --git a/packages/bugc-react/package.json b/packages/bugc-react/package.json index e3e448fa81..d2c740b10b 100644 --- a/packages/bugc-react/package.json +++ b/packages/bugc-react/package.json @@ -36,6 +36,7 @@ "devDependencies": { "@testing-library/dom": "^10.0.0", "@testing-library/react": "^16.0.0", + "@types/dagre": "^0.7.52", "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", "jsdom": "^26.0.0", diff --git a/packages/playground/package.json b/packages/playground/package.json index 5eef477069..421045b95f 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -15,14 +15,13 @@ }, "dependencies": { "@ethdebug/bugc": "^0.1.0-0", + "@ethdebug/bugc-react": "^0.1.0-0", "@monaco-editor/react": "^4.6.0", - "@types/dagre": "^0.7.52", "dagre": "^0.8.5", "monaco-editor": "^0.52.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-flow-renderer": "^10.3.17", - "vis-network": "^9.1.9" + "react-flow-renderer": "^10.3.17" }, "devDependencies": { "@types/react": "^18.2.43", diff --git a/packages/playground/src/compiler/CompilerOutput.tsx b/packages/playground/src/compiler/CompilerOutput.tsx index 865765bd6b..e911ac24c8 100644 --- a/packages/playground/src/compiler/CompilerOutput.tsx +++ b/packages/playground/src/compiler/CompilerOutput.tsx @@ -1,13 +1,23 @@ import { useState } from "react"; import type { CompileResult } from "./types"; -import { AstView } from "../visualization/AstView"; -import { IrView } from "../visualization/IrView"; -import { CfgView } from "../visualization/CfgView"; -import { BytecodeView } from "../visualization/BytecodeView"; +import { + AstView, + IrView, + CfgView, + BytecodeView, + type SourceRange, +} from "@ethdebug/bugc-react"; import { ErrorView } from "./ErrorView"; -import type { SourceRange } from "../visualization/debugUtils"; import "./CompilerOutput.css"; +// CSS for bugc-react components +import "@ethdebug/bugc-react/src/components/variables.css"; +import "@ethdebug/bugc-react/src/components/AstView.css"; +import "@ethdebug/bugc-react/src/components/BytecodeView.css"; +import "@ethdebug/bugc-react/src/components/CfgView.css"; +import "@ethdebug/bugc-react/src/components/EthdebugTooltip.css"; +import "@ethdebug/bugc-react/src/components/IrView.css"; + interface CompilerOutputProps { result: CompileResult; onOpcodeHover?: (ranges: SourceRange[]) => void; diff --git a/packages/playground/src/compiler/types.ts b/packages/playground/src/compiler/types.ts index 9222fbd7ee..0a0ca021ae 100644 --- a/packages/playground/src/compiler/types.ts +++ b/packages/playground/src/compiler/types.ts @@ -1,25 +1,6 @@ -import type { Ast, Ir, Evm } from "@ethdebug/bugc"; - -export interface BytecodeOutput { - runtime: Uint8Array; - create?: Uint8Array; - runtimeInstructions: Evm.Instruction[]; - createInstructions?: Evm.Instruction[]; -} - -export interface SuccessfulCompileResult { - success: true; - ast: Ast.Program; - ir: Ir.Module; - bytecode: BytecodeOutput; - warnings: string[]; -} - -export interface FailedCompileResult { - success: false; - error: string; - ast?: Ast.Program; - warnings?: string[]; -} - -export type CompileResult = SuccessfulCompileResult | FailedCompileResult; +export type { + BytecodeOutput, + SuccessfulCompileResult, + FailedCompileResult, + CompileResult, +} from "@ethdebug/bugc-react"; diff --git a/packages/playground/src/editor/Editor.tsx b/packages/playground/src/editor/Editor.tsx index 48bd362227..75ede919cf 100644 --- a/packages/playground/src/editor/Editor.tsx +++ b/packages/playground/src/editor/Editor.tsx @@ -1,12 +1,10 @@ import MonacoEditor, { type OnMount } from "@monaco-editor/react"; import { useEffect, useRef } from "react"; -import { registerBugLanguage } from "./bugLanguage"; -import type { editor as MonacoEditor_Type } from "monaco-editor"; +import * as monaco from "monaco-editor"; +import { registerBugLanguage } from "@ethdebug/bugc-react"; +import type { SourceRange } from "@ethdebug/bugc-react"; -export interface SourceRange { - offset: number; - length: number; -} +export type { SourceRange } from "@ethdebug/bugc-react"; interface EditorProps { value: string; @@ -21,13 +19,11 @@ export function Editor({ language = "bug", highlightedRanges = [], }: EditorProps) { - const editorRef = useRef( - null, - ); + const editorRef = useRef(null); const decorationsRef = useRef([]); useEffect(() => { - registerBugLanguage(); + registerBugLanguage(monaco); }, []); useEffect(() => { diff --git a/packages/playground/src/editor/bugLanguage.ts b/packages/playground/src/editor/bugLanguage.ts deleted file mode 100644 index a93fe34dac..0000000000 --- a/packages/playground/src/editor/bugLanguage.ts +++ /dev/null @@ -1,192 +0,0 @@ -import * as monaco from "monaco-editor"; - -export function registerBugLanguage() { - // Register the BUG language - monaco.languages.register({ id: "bug" }); - - // Set language configuration - monaco.languages.setLanguageConfiguration("bug", { - comments: { - lineComment: "//", - blockComment: ["/*", "*/"], - }, - brackets: [ - ["{", "}"], - ["[", "]"], - ["(", ")"], - ], - autoClosingPairs: [ - { open: "{", close: "}" }, - { open: "[", close: "]" }, - { open: "(", close: ")" }, - { open: '"', close: '"' }, - { open: "'", close: "'" }, - ], - surroundingPairs: [ - { open: "{", close: "}" }, - { open: "[", close: "]" }, - { open: "(", close: ")" }, - { open: '"', close: '"' }, - { open: "'", close: "'" }, - ], - }); - - // Set token provider - monaco.languages.setMonarchTokensProvider("bug", { - keywords: [ - "name", - "define", - "struct", - "storage", - "code", - "let", - "if", - "else", - "for", - "while", - "return", - "break", - "continue", - "true", - "false", - ], - - typeKeywords: [ - "uint256", - "int256", - "uint128", - "int128", - "uint64", - "int64", - "uint32", - "int32", - "uint16", - "int16", - "uint8", - "int8", - "address", - "bool", - "bytes32", - "bytes", - "mapping", - "array", - ], - - operators: [ - "=", - ">", - "<", - "!", - "~", - "?", - ":", - "==", - "<=", - ">=", - "!=", - "&&", - "||", - "++", - "--", - "+", - "-", - "*", - "/", - "&", - "|", - "^", - "%", - "<<", - ">>", - ">>>", - "+=", - "-=", - "*=", - "/=", - "&=", - "|=", - "^=", - "%=", - "<<=", - ">>=", - ">>>=", - ], - - // Define symbols for the @symbols reference - symbols: /[=>](?!@symbols)/, "@brackets"], - [ - /@symbols/, - { - cases: { - "@operators": "operator", - "@default": "", - }, - }, - ], - - // Storage slot syntax - [/\[\d+\]/, "number.slot"], - ], - - comment: [ - [/[^/*]+/, "comment"], - [/\/\*/, "comment", "@push"], - [/\*\//, "comment", "@pop"], - [/[/*]/, "comment"], - ], - - string: [ - [/[^\\"]+/, "string"], - [/\\./, "string.escape"], - [/"/, { token: "string.quote", bracket: "@close", next: "@pop" }], - ], - - whitespace: [ - [/[ \t\r\n]+/, "white"], - [/\/\*/, "comment", "@comment"], - [/\/\/.*$/, "comment"], - ], - }, - }); -} diff --git a/packages/playground/src/visualization/AstView.css b/packages/playground/src/visualization/AstView.css deleted file mode 100644 index 3536b032fb..0000000000 --- a/packages/playground/src/visualization/AstView.css +++ /dev/null @@ -1,15 +0,0 @@ -.ast-view { - height: 100%; - overflow: auto; -} - -.ast-json { - margin: 0; - padding: 1rem; - font-family: "Consolas", "Monaco", "Courier New", monospace; - font-size: 0.875rem; - line-height: 1.5; - color: #cccccc; - white-space: pre; - overflow: auto; -} diff --git a/packages/playground/src/visualization/AstView.tsx b/packages/playground/src/visualization/AstView.tsx deleted file mode 100644 index 7263e0c5dd..0000000000 --- a/packages/playground/src/visualization/AstView.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { Ast } from "@ethdebug/bugc"; -import "./AstView.css"; - -interface AstViewProps { - ast: Ast.Program; -} - -export function AstView({ ast }: AstViewProps) { - // Format AST as JSON, excluding parent references to avoid circular structure - const astJson = JSON.stringify( - ast, - (key, value) => { - if (key === "parent") return undefined; - return value; - }, - 2, - ); - - return ( -
-
{astJson}
-
- ); -} diff --git a/packages/playground/src/visualization/BytecodeView.css b/packages/playground/src/visualization/BytecodeView.css deleted file mode 100644 index bb352dbdf4..0000000000 --- a/packages/playground/src/visualization/BytecodeView.css +++ /dev/null @@ -1,111 +0,0 @@ -.bytecode-view { - height: 100%; - overflow: auto; -} - -.bytecode-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem; - background-color: #2d2d30; - border-bottom: 1px solid #3e3e42; - position: sticky; - top: 0; - z-index: 1; -} - -.bytecode-header h3 { - margin: 0; - font-size: 1rem; - color: #cccccc; -} - -.bytecode-stats { - display: flex; - gap: 1rem; - font-size: 0.813rem; - color: #969696; -} - -.bytecode-content { - padding: 1rem; -} - -.bytecode-section { - margin-bottom: 2rem; -} - -.bytecode-section h4 { - margin: 0 0 0.5rem 0; - font-size: 0.875rem; - color: #cccccc; -} - -.bytecode-hex, -.bytecode-disassembly { - margin: 0; - padding: 1rem; - background-color: #2d2d30; - border: 1px solid #3e3e42; - border-radius: 4px; - font-family: "Consolas", "Monaco", "Courier New", monospace; - font-size: 0.813rem; - line-height: 1.5; - color: #cccccc; - overflow-x: auto; - white-space: pre-wrap; - word-break: break-all; -} - -.bytecode-disassembly { - white-space: pre; - word-break: normal; -} - -.bytecode-separator { - margin: 2rem 1rem; - border: none; - border-top: 1px solid #3e3e42; -} - -.bytecode-disassembly-interactive { - padding: 1rem; - background-color: #2d2d30; - border: 1px solid #3e3e42; - border-radius: 4px; - font-family: "Consolas", "Monaco", "Courier New", monospace; - font-size: 0.813rem; - line-height: 1.5; - overflow-x: auto; -} - -.opcode-line { - display: flex; - gap: 1rem; - padding: 0.125rem 0.5rem; - border-radius: 3px; - transition: background-color 0.15s ease; -} - -.opcode-line.has-debug-info:hover { - background-color: #3e3e42; -} - -.opcode-line .pc { - color: #858585; - min-width: 3rem; - text-align: right; -} - -.opcode-line .opcode { - color: #4ec9b0; - min-width: 6rem; - font-weight: 500; -} - -.opcode-line .immediates { - color: #b5cea8; -} - -/* Debug icon and spacer styles moved to EthdebugTooltip.css */ diff --git a/packages/playground/src/visualization/BytecodeView.tsx b/packages/playground/src/visualization/BytecodeView.tsx deleted file mode 100644 index 85b2e10c3e..0000000000 --- a/packages/playground/src/visualization/BytecodeView.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import type { BytecodeOutput } from "../compiler/types"; -import type { Evm } from "@ethdebug/bugc"; -import { extractSourceRange, type SourceRange } from "./debugUtils"; -import { EthdebugTooltip, useEthdebugTooltip } from "./EthdebugTooltip"; -import "./EthdebugTooltip.css"; -import "./BytecodeView.css"; - -interface BytecodeViewProps { - bytecode: BytecodeOutput; - onOpcodeHover?: (ranges: SourceRange[]) => void; -} - -function InstructionsView({ - instructions, - onOpcodeHover, -}: { - instructions: Evm.Instruction[]; - onOpcodeHover?: (ranges: SourceRange[]) => void; -}) { - const { - tooltip, - setTooltip, - showTooltip, - pinTooltip, - hideTooltip, - closeTooltip, - } = useEthdebugTooltip(); - - let pc = 0; - - const handleOpcodeMouseEnter = (sourceRanges: SourceRange[]) => { - onOpcodeHover?.(sourceRanges); - }; - - const handleOpcodeMouseLeave = () => { - onOpcodeHover?.([]); - }; - - const handleDebugIconMouseEnter = ( - e: React.MouseEvent, - instruction: Evm.Instruction, - ) => { - if (instruction.debug?.context) { - showTooltip(e, JSON.stringify(instruction.debug.context, null, 2)); - } - }; - - const handleDebugIconClick = ( - e: React.MouseEvent, - instruction: Evm.Instruction, - ) => { - if (instruction.debug?.context) { - pinTooltip(e, JSON.stringify(instruction.debug.context, null, 2)); - } - }; - - return ( -
- {instructions.map((instruction, idx) => { - const currentPc = pc; - pc += 1 + (instruction.immediates?.length || 0); - - const sourceRanges = extractSourceRange(instruction.debug?.context); - const hasDebugInfo = !!instruction.debug?.context; - - return ( -
handleOpcodeMouseEnter(sourceRanges)} - onMouseLeave={handleOpcodeMouseLeave} - > - {hasDebugInfo ? ( - handleDebugIconMouseEnter(e, instruction)} - onMouseLeave={hideTooltip} - onClick={(e) => handleDebugIconClick(e, instruction)} - > - ℹ - - ) : ( - - )} - {currentPc.toString().padStart(4, "0")} - {instruction.mnemonic} - {instruction.immediates && instruction.immediates.length > 0 && ( - - 0x - {instruction.immediates - .map((b) => b.toString(16).padStart(2, "0")) - .join("")} - - )} -
- ); - })} - -
- ); -} - -export function BytecodeView({ bytecode, onOpcodeHover }: BytecodeViewProps) { - const runtimeHex = Array.from(bytecode.runtime) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); - - const constructorHex = bytecode.create - ? Array.from(bytecode.create) - .map((b) => b.toString(16).padStart(2, "0")) - .join("") - : null; - - return ( -
- {bytecode.create && ( - <> -
-

Constructor Bytecode

-
- Size: {bytecode.create.length / 2} bytes -
-
- -
-
-

Hex

-
{constructorHex}
-
- -
-

Instructions

- {bytecode.createInstructions && ( - - )} -
-
- -
- - )} - -
-

{bytecode.create ? "Runtime Bytecode" : "EVM Bytecode"}

-
- Size: {bytecode.runtime.length / 2} bytes -
-
- -
-
-

Hex

-
{runtimeHex}
-
- -
-

Instructions

- -
-
-
- ); -} diff --git a/packages/playground/src/visualization/CfgView.css b/packages/playground/src/visualization/CfgView.css deleted file mode 100644 index 20ce254b0f..0000000000 --- a/packages/playground/src/visualization/CfgView.css +++ /dev/null @@ -1,195 +0,0 @@ -.cfg-view { - display: flex; - flex-direction: column; - height: 100%; -} - -.cfg-header { - padding: 1rem; - border-bottom: 1px solid var(--border-color); - background: var(--bg-secondary); -} - -.cfg-header h3 { - margin: 0; - font-size: 1.1rem; - color: var(--text-primary); -} - -.cfg-content { - flex: 1; - display: flex; - min-height: 0; -} - -.cfg-graph { - flex: 1; - position: relative; -} - -.cfg-sidebar { - width: 400px; - border-left: 1px solid var(--border-color); - padding: 1rem; - overflow-y: auto; - background: #f5f5f5; - color: #333; -} - -.cfg-sidebar h4 { - margin: 0 0 1rem 0; - font-size: 1rem; - color: #333; - display: flex; - justify-content: space-between; - align-items: center; -} - -.cfg-sidebar h5 { - margin: 0.5rem 0; - font-size: 0.9rem; - color: #666; -} - -.cfg-sidebar-close { - background: none; - border: none; - font-size: 1.5rem; - cursor: pointer; - color: #666; - padding: 0; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 4px; - transition: background-color 0.2s; -} - -.cfg-sidebar-close:hover { - background-color: #e0e0e0; - color: #333; -} - -/* Custom node styles */ -.cfg-node { - background: white; - border: 2px solid #2196f3; - border-radius: 8px; - padding: 10px 15px; - min-width: 120px; - text-align: center; - cursor: pointer; - transition: all 0.2s ease; -} - -.cfg-node.entry { - border-color: #4caf50; - background: #e8f5e9; -} - -.cfg-node.selected { - border-width: 3px; - box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.3); -} - -.cfg-node:hover { - transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); -} - -.cfg-node-header { - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - margin-bottom: 4px; -} - -.cfg-node-header strong { - font-family: "Courier New", Courier, monospace; - font-size: 14px; -} - -.entry-badge { - background: #4caf50; - color: white; - font-size: 10px; - padding: 2px 6px; - border-radius: 4px; - text-transform: uppercase; -} - -.cfg-node-stats { - font-size: 12px; - color: #666; -} - -/* Instruction display */ -.block-instructions { - margin-top: 1rem; -} - -.instruction-list { - background: #ffffff; - border: 1px solid #ddd; - border-radius: 4px; - padding: 1rem; - margin: 0; - font-family: "Courier New", Courier, monospace; - font-size: 0.85rem; - line-height: 1.4; - overflow-x: auto; - color: #333; -} - -.instruction { - margin: 0.25rem 0; - padding: 0.125rem 0; -} - -.instruction.terminator { - margin-top: 0.5rem; - padding-top: 0.5rem; - border-top: 1px dashed #ddd; - color: #d32f2f; - font-weight: bold; -} - -/* React Flow overrides */ -.cfg-view .react-flow__attribution { - display: none; -} - -.cfg-view .react-flow__edge-path { - stroke-width: 2; -} - -.cfg-view .react-flow__edge-text { - font-size: 12px; - font-weight: 600; -} - -.cfg-view .react-flow__handle { - width: 8px; - height: 8px; - background: #2196f3; - border: 2px solid white; -} - -.cfg-view .react-flow__handle-top { - top: -4px; -} - -.cfg-view .react-flow__handle-bottom { - bottom: -4px; -} - -.cfg-view .react-flow__handle-left { - left: -4px; -} - -.cfg-view .react-flow__handle-right { - right: -4px; -} diff --git a/packages/playground/src/visualization/CfgView.tsx b/packages/playground/src/visualization/CfgView.tsx deleted file mode 100644 index cdb869f0be..0000000000 --- a/packages/playground/src/visualization/CfgView.tsx +++ /dev/null @@ -1,457 +0,0 @@ -import { useMemo, useCallback, useState, useEffect } from "react"; -import ReactFlow, { - type Node, - type Edge, - Controls, - Background, - useNodesState, - useEdgesState, - Handle, - Position, - type NodeProps, - MarkerType, - useReactFlow, - ReactFlowProvider, -} from "react-flow-renderer"; -import dagre from "dagre"; -import "react-flow-renderer/dist/style.css"; -import type { Ir } from "@ethdebug/bugc"; -import "./CfgView.css"; - -interface CfgViewProps { - ir: Ir.Module; - showComparison?: boolean; - comparisonIr?: Ir.Module; -} - -interface BlockNodeData { - label: string; - block: Ir.Block; - isEntry: boolean; - instructionCount: number; - functionName?: string; -} - -function BlockNode({ data, selected }: NodeProps) { - return ( -
- - -
- - {data.functionName}::{data.label} - - {data.isEntry && entry} -
-
- {data.instructionCount} instruction - {data.instructionCount !== 1 ? "s" : ""} -
- - -
- ); -} - -const nodeTypes = { - block: BlockNode, -}; - -function CfgViewContent({ ir }: CfgViewProps) { - const [selectedNode, setSelectedNode] = useState(null); - const { fitView } = useReactFlow(); - - const { initialNodes, initialEdges } = useMemo(() => { - const nodes: Node[] = []; - const edges: Edge[] = []; - - const processFunction = (func: Ir.Function, funcName: string) => { - const blockEntries = Array.from(func.blocks.entries()); - - // Create nodes with function name prefix to ensure unique IDs - blockEntries.forEach(([blockId, block]) => { - const nodeId = `${funcName}:${blockId}`; - - nodes.push({ - id: nodeId, - type: "block", - position: { x: 0, y: 0 }, // Will be set by dagre - data: { - label: blockId, - block, - isEntry: blockId === func.entry, - instructionCount: block.instructions.length + 1, // +1 for terminator - functionName: funcName, - }, - }); - }); - - // Create edges with function name prefix - blockEntries.forEach(([blockId, block]) => { - const sourceId = `${funcName}:${blockId}`; - const term = block.terminator; - - if (term.kind === "jump") { - const targetId = `${funcName}:${term.target}`; - edges.push({ - id: `${sourceId}-${targetId}`, - source: sourceId, - target: targetId, - sourceHandle: "bottom", - targetHandle: "top", - markerEnd: { - type: MarkerType.ArrowClosed, - }, - }); - } else if (term.kind === "branch") { - const trueTargetId = `${funcName}:${term.trueTarget}`; - const falseTargetId = `${funcName}:${term.falseTarget}`; - - edges.push({ - id: `${sourceId}-${trueTargetId}-true`, - source: sourceId, - target: trueTargetId, - sourceHandle: "bottom", - targetHandle: "top", - label: "true", - labelBgStyle: { fill: "#e8f5e9" }, - style: { stroke: "#4caf50" }, - markerEnd: { - type: MarkerType.ArrowClosed, - color: "#4caf50", - }, - }); - edges.push({ - id: `${sourceId}-${falseTargetId}-false`, - source: sourceId, - target: falseTargetId, - sourceHandle: "bottom", - targetHandle: "top", - label: "false", - labelBgStyle: { fill: "#ffebee" }, - style: { stroke: "#f44336" }, - markerEnd: { - type: MarkerType.ArrowClosed, - color: "#f44336", - }, - }); - } else if (term.kind === "call") { - // Handle call terminator - const continuationId = `${funcName}:${term.continuation}`; - edges.push({ - id: `${sourceId}-${continuationId}-call-cont`, - source: sourceId, - target: continuationId, - sourceHandle: "bottom", - targetHandle: "top", - label: `after ${term.function}()`, - labelBgStyle: { fill: "#f3e8ff" }, - style: { stroke: "#9333ea" }, - markerEnd: { - type: MarkerType.ArrowClosed, - color: "#9333ea", - }, - }); - } - }); - }; - - // Process all functions - if (ir.functions) { - for (const [funcName, func] of ir.functions.entries()) { - processFunction(func, funcName); - } - } - - if (ir.create) { - processFunction(ir.create, "create"); - } - - processFunction(ir.main, "main"); - - // Add call edges between blocks and functions - const allFunctions = new Map(); - if (ir.functions) { - ir.functions.forEach((func, name) => allFunctions.set(name, func)); - } - if (ir.create) { - allFunctions.set("create", ir.create); - } - allFunctions.set("main", ir.main); - - // Call instructions are now terminators, so we don't need this loop anymore - - // Apply dagre layout - const dagreGraph = new dagre.graphlib.Graph(); - dagreGraph.setDefaultEdgeLabel(() => ({})); - dagreGraph.setGraph({ - rankdir: "TB", - nodesep: 80, - ranksep: 120, - edgesep: 50, - }); - - nodes.forEach((node) => { - dagreGraph.setNode(node.id, { width: 200, height: 80 }); - }); - - edges.forEach((edge) => { - dagreGraph.setEdge(edge.source, edge.target); - }); - - dagre.layout(dagreGraph); - - // Apply the computed positions - const layoutedNodes = nodes.map((node) => { - const nodeWithPosition = dagreGraph.node(node.id); - return { - ...node, - position: { - x: nodeWithPosition.x - 100, // Center the node - y: nodeWithPosition.y - 40, - }, - }; - }); - - return { initialNodes: layoutedNodes, initialEdges: edges }; - }, [ir]); - - const [nodesState, setNodes, onNodesChange] = useNodesState(initialNodes); - const [edgesState, setEdges, onEdgesChange] = useEdgesState(initialEdges); - - // Update nodes and edges when IR changes - useEffect(() => { - setNodes(initialNodes); - setEdges(initialEdges); - // Auto-fit view after a short delay to ensure layout is complete - setTimeout(() => { - fitView({ padding: 0.2, minZoom: 0.1, maxZoom: 2 }); - }, 50); - }, [initialNodes, initialEdges, setNodes, setEdges, fitView]); - - const onNodeClick = useCallback( - (_event: React.MouseEvent, node: Node) => { - setSelectedNode(node.id); - }, - [], - ); - - const selectedBlock = useMemo(() => { - if (!selectedNode) return null; - const node = nodesState.find( - (n: Node) => n.id === selectedNode, - ); - return node?.data.block ?? null; - }, [selectedNode, nodesState]); - - const selectedBlockName = useMemo(() => { - if (!selectedNode || selectedNode.includes("-label")) return null; - // Extract the display name from the node ID (e.g., "main:entry" -> "main::entry") - return selectedNode.replace(":", "::"); - }, [selectedNode]); - - const formatInstruction = useCallback((inst: Ir.Instruction): string => { - // Recreate the formatting logic from IrFormatter since methods are private - const formatValue = (value: unknown): string => { - if (typeof value === "bigint") return value.toString(); - if (typeof value === "string") return JSON.stringify(value); - if (typeof value === "boolean") return value.toString(); - - const val = value as { - kind?: string; - value?: unknown; - id?: string | number; - name?: string; - }; - if (!val.kind) return "?"; - - switch (val.kind) { - case "const": - return String(val.value || "?"); - case "temp": - return `%${val.id || "?"}`; - case "local": - return `$${val.name || "?"}`; - default: - return "?"; - } - }; - - switch (inst.kind) { - case "const": - return `${inst.dest} = ${inst.value}`; - case "binary": - return `${inst.dest} = ${formatValue(inst.left)} ${inst.op} ${formatValue(inst.right)}`; - case "unary": - return `${inst.dest} = ${inst.op}${formatValue(inst.operand)}`; - case "read": - if (inst.location === "storage" && inst.slot) { - return `${inst.dest} = storage[${formatValue(inst.slot)}]`; - } - return `${inst.dest} = read.${inst.location}`; - case "write": - if (inst.location === "storage" && inst.slot) { - return `storage[${formatValue(inst.slot)}] = ${formatValue(inst.value)}`; - } - return `write.${inst.location} = ${formatValue(inst.value)}`; - case "env": { - const envInst = inst; - switch (envInst.op) { - case "msg_sender": - return `${envInst.dest} = msg.sender`; - case "msg_value": - return `${envInst.dest} = msg.value`; - case "msg_data": - return `${envInst.dest} = msg.data`; - case "block_timestamp": - return `${envInst.dest} = block.timestamp`; - case "block_number": - return `${envInst.dest} = block.number`; - default: - return `${envInst.dest} = ${envInst.op}`; - } - } - case "hash": - return `${inst.dest} = keccak256(${formatValue(inst.value)})`; - case "cast": - return `${inst.dest} = cast ${formatValue(inst.value)} to ${inst.targetType.kind}`; - case "compute_slot": { - if (inst.slotKind === "mapping") { - const mappingInst = inst as Ir.Instruction.ComputeSlot.Mapping; - return `${mappingInst.dest} = compute_slot[mapping](${formatValue(mappingInst.base)}, ${formatValue(mappingInst.key)})`; - } else if (inst.slotKind === "array") { - const arrayInst = inst as Ir.Instruction.ComputeSlot.Array; - return `${arrayInst.dest} = compute_slot[array](${formatValue(arrayInst.base)})`; - } else if (inst.slotKind === "field") { - const fieldInst = inst as Ir.Instruction.ComputeSlot.Field; - return `${fieldInst.dest} = compute_slot[field](${formatValue(fieldInst.base)}, offset_${fieldInst.fieldOffset})`; - } - // This should never be reached due to exhaustive checking - const _exhaustive: never = inst; - void _exhaustive; - return `unknown compute_slot`; - } - // Call instruction removed - calls are now block terminators - default: { - const unknownInst = inst as { dest?: string; kind?: string }; - return `${unknownInst.dest || "?"} = ${unknownInst.kind || "unknown"}(...)`; - } - } - }, []); - - const formatTerminator = useCallback((term: Ir.Block.Terminator): string => { - const formatValue = (value: unknown): string => { - if (typeof value === "bigint") return value.toString(); - if (typeof value === "string") return JSON.stringify(value); - if (typeof value === "boolean") return value.toString(); - - const val = value as { - kind?: string; - value?: unknown; - id?: string | number; - name?: string; - }; - if (!val.kind) return "?"; - - switch (val.kind) { - case "const": - return String(val.value || "?"); - case "temp": - return `%${val.id || "?"}`; - case "local": - return `$${val.name || "?"}`; - default: - return "?"; - } - }; - - switch (term.kind) { - case "jump": - return `jump ${term.target}`; - case "branch": - return `branch ${formatValue(term.condition)} ? ${term.trueTarget} : ${term.falseTarget}`; - case "return": - return term.value ? `return ${formatValue(term.value)}` : "return void"; - case "call": { - const args = term.arguments.map(formatValue).join(", "); - const callPart = term.dest - ? `${term.dest} = call ${term.function}(${args})` - : `call ${term.function}(${args})`; - return `${callPart} -> ${term.continuation}`; - } - default: - return `unknown terminator`; - } - }, []); - - return ( -
-
-

Control Flow Graph

-
-
-
- - - - -
- {selectedBlock && selectedBlockName && ( -
-

- Block {selectedBlockName} - -

-
-
Instructions:
-
-                {selectedBlock.instructions.map(
-                  (inst: Ir.Instruction, i: number) => (
-                    
- {formatInstruction(inst)} -
- ), - )} -
- {formatTerminator(selectedBlock.terminator)} -
-
-
-
- )} -
-
- ); -} - -export function CfgView(props: CfgViewProps) { - return ( - - - - ); -} diff --git a/packages/playground/src/visualization/EthdebugTooltip.css b/packages/playground/src/visualization/EthdebugTooltip.css deleted file mode 100644 index 4c903eee20..0000000000 --- a/packages/playground/src/visualization/EthdebugTooltip.css +++ /dev/null @@ -1,71 +0,0 @@ -.ethdebug-tooltip { - position: fixed; - z-index: 1000; - background-color: #1e1e1e; - border: 1px solid #3e3e42; - border-radius: 4px; - padding: 0.5rem; - max-width: 600px; - max-height: 400px; - overflow: auto; - pointer-events: none; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); -} - -.ethdebug-tooltip.pinned { - pointer-events: auto; - border-color: #569cd6; - box-shadow: 0 4px 16px rgba(86, 156, 214, 0.3); -} - -.ethdebug-tooltip pre { - margin: 0; - font-family: "Courier New", monospace; - font-size: 0.75rem; - color: #d4d4d4; - white-space: pre-wrap; - word-break: break-word; -} - -.debug-info-icon { - color: #569cd6; - cursor: pointer; - padding: 0.125rem 0.25rem; - border-radius: 3px; - transition: all 0.15s ease; - user-select: none; - display: inline-block; - min-width: 1.2rem; - text-align: center; -} - -.debug-info-icon:hover { - background-color: rgba(86, 156, 214, 0.15); - color: #7cb6f0; -} - -.debug-info-spacer { - display: inline-block; - min-width: 1.2rem; - padding: 0.125rem 0.25rem; -} - -.tooltip-close-btn { - position: absolute; - top: 0.25rem; - right: 0.25rem; - background: transparent; - border: none; - color: #d4d4d4; - font-size: 1.5rem; - line-height: 1; - cursor: pointer; - padding: 0.125rem 0.25rem; - border-radius: 3px; - transition: all 0.15s ease; -} - -.tooltip-close-btn:hover { - background-color: rgba(255, 255, 255, 0.1); - color: #ffffff; -} diff --git a/packages/playground/src/visualization/EthdebugTooltip.tsx b/packages/playground/src/visualization/EthdebugTooltip.tsx deleted file mode 100644 index c9d9aab55a..0000000000 --- a/packages/playground/src/visualization/EthdebugTooltip.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { useState, useRef, useEffect } from "react"; -import "./EthdebugTooltip.css"; - -export interface TooltipData { - content: string; - x: number; - y: number; - pinned?: boolean; -} - -interface EthdebugTooltipProps { - tooltip: TooltipData | null; - onUpdate?: (tooltip: TooltipData) => void; - onClose?: () => void; -} - -export function EthdebugTooltip({ - tooltip, - onUpdate, - onClose, -}: EthdebugTooltipProps) { - const tooltipRef = useRef(null); - - useEffect(() => { - if (tooltip && tooltipRef.current) { - const tooltipRect = tooltipRef.current.getBoundingClientRect(); - const viewportWidth = window.innerWidth; - const viewportHeight = window.innerHeight; - - let { x, y } = tooltip; - - // Adjust horizontal position if tooltip goes off right edge - if (x + tooltipRect.width > viewportWidth) { - x = viewportWidth - tooltipRect.width - 10; - } - - // Adjust horizontal position if tooltip goes off left edge - if (x < 10) { - x = 10; - } - - // Adjust vertical position if tooltip goes off bottom edge - if (y + tooltipRect.height > viewportHeight) { - y = viewportHeight - tooltipRect.height - 10; - } - - // Adjust vertical position if tooltip goes off top edge - if (y < 10) { - y = 10; - } - - // Update position if it changed - if (x !== tooltip.x || y !== tooltip.y) { - onUpdate?.({ ...tooltip, x, y }); - } - } - }, [tooltip, onUpdate]); - - if (!tooltip) { - return null; - } - - return ( -
- {tooltip.pinned && ( - - )} -
{tooltip.content}
-
- ); -} - -export function useEthdebugTooltip() { - const [tooltip, setTooltip] = useState(null); - - const showTooltip = (e: React.MouseEvent, content: string) => { - const rect = e.currentTarget.getBoundingClientRect(); - setTooltip({ - content, - x: rect.left, - y: rect.bottom, - pinned: false, - }); - }; - - const pinTooltip = (e: React.MouseEvent, content: string) => { - const rect = e.currentTarget.getBoundingClientRect(); - setTooltip({ - content, - x: rect.left, - y: rect.bottom, - pinned: true, - }); - }; - - const hideTooltip = () => { - if (!tooltip?.pinned) { - setTooltip(null); - } - }; - - const closeTooltip = () => { - setTooltip(null); - }; - - return { - tooltip, - setTooltip, - showTooltip, - pinTooltip, - hideTooltip, - closeTooltip, - }; -} diff --git a/packages/playground/src/visualization/IrView.css b/packages/playground/src/visualization/IrView.css deleted file mode 100644 index 91eff2220b..0000000000 --- a/packages/playground/src/visualization/IrView.css +++ /dev/null @@ -1,140 +0,0 @@ -.ir-view { - height: 100%; - overflow: auto; -} - -.ir-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem; - background-color: #2d2d30; - border-bottom: 1px solid #3e3e42; - position: sticky; - top: 0; - z-index: 1; -} - -.ir-header h3 { - margin: 0; - font-size: 1rem; - color: #cccccc; -} - -.ir-stats { - display: flex; - gap: 1rem; - font-size: 0.813rem; - color: #969696; -} - -.ir-content { - padding: 1rem; - font-family: "Consolas", "Monaco", "Courier New", monospace; - font-size: 0.875rem; - line-height: 1.6; - color: #cccccc; -} - -.section-label { - color: #6a9955; - font-weight: bold; - margin-top: 1.5rem; - margin-bottom: 0.5rem; - font-size: 0.938rem; -} - -.section-label:first-child { - margin-top: 0; -} - -.ir-function { - margin-bottom: 2rem; -} - -.function-header h4 { - margin: 0 0 0.5rem 0; - color: #dcdcaa; - font-size: 1rem; - font-weight: bold; -} - -.ir-block { - margin-bottom: 1rem; - padding-left: 1rem; -} - -.block-header { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 0.25rem; - color: #4ec9b0; -} - -.entry-badge { - background-color: rgba(78, 201, 176, 0.2); - color: #4ec9b0; - padding: 0.125rem 0.375rem; - border-radius: 3px; - font-size: 0.688rem; - font-weight: bold; -} - -.block-body { - padding-left: 1rem; -} - -.ir-instruction, -.ir-terminator, -.ir-phi { - display: flex; - align-items: flex-start; - gap: 0.5rem; - padding: 0.125rem 0; - line-height: 1.6; -} - -.ir-instruction:hover, -.ir-terminator:hover, -.ir-phi:hover { - background-color: rgba(255, 255, 255, 0.03); -} - -.instruction-operation, -.terminator-operation, -.phi-operation { - flex: 1; -} - -.ir-terminator { - color: #c586c0; - font-weight: 500; -} - -.ir-phi { - color: #9cdcfe; - font-style: italic; -} - -.hoverable-part { - display: inline; - transition: background-color 0.15s ease; -} - -.hoverable-part.has-debug { - cursor: pointer; - border-bottom: 1px dotted rgba(86, 156, 214, 0.4); -} - -.hoverable-part.has-debug:hover { - background-color: rgba(86, 156, 214, 0.15); - border-bottom-color: rgba(86, 156, 214, 0.8); -} - -.debug-info-icon.inline { - display: inline; - margin-left: 0.25rem; - font-size: 0.75rem; - vertical-align: super; -} diff --git a/packages/playground/src/visualization/IrView.tsx b/packages/playground/src/visualization/IrView.tsx deleted file mode 100644 index 6fad402b34..0000000000 --- a/packages/playground/src/visualization/IrView.tsx +++ /dev/null @@ -1,834 +0,0 @@ -import { Ir } from "@ethdebug/bugc"; -import { useMemo } from "react"; -import type { SourceRange } from "./debugUtils"; -import { - extractInstructionDebug, - extractTerminatorDebug, - extractPhiDebug, - formatMultiLevelDebug, - extractAllSourceRanges, - extractOperandSourceRanges, -} from "./irDebugUtils"; -import { EthdebugTooltip, useEthdebugTooltip } from "./EthdebugTooltip"; -import "./IrView.css"; - -interface IrViewProps { - ir: Ir.Module; - onOpcodeHover?: (ranges: SourceRange[]) => void; -} - -interface HoverablePart { - text: string; - ranges: SourceRange[]; - className?: string; -} - -// Component for a hoverable part of an instruction -function HoverablePart({ - part, - onHover, - onLeave, - onDebugIconHover, - showDebugIcon, -}: { - part: HoverablePart; - onHover: (ranges: SourceRange[]) => void; - onLeave: () => void; - onDebugIconHover?: (e: React.MouseEvent) => void; - showDebugIcon?: boolean; -}) { - return ( - 0 ? "has-debug" : ""}`} - onMouseEnter={() => onHover(part.ranges)} - onMouseLeave={onLeave} - > - {part.text} - {showDebugIcon && part.ranges.length > 0 && onDebugIconHover && ( - - ℹ - - )} - - ); -} - -// Format a value (temp, const) - matches Formatter.formatValue -function formatValue(value: Ir.Value | bigint | string | boolean): string { - if (typeof value === "bigint") { - return value.toString(); - } - if (typeof value === "string") { - if (value.startsWith("0x")) { - return value; - } - return JSON.stringify(value); - } - if (typeof value === "boolean") { - return value.toString(); - } - - switch (value.kind) { - case "const": - return formatValue(value.value); - case "temp": - return `%${value.id}`; - default: - return "?"; - } -} - -// Format a type -function formatType(type: Ir.Type): string { - return type.kind; -} - -// Format destination with type - matches Formatter.destWithType -function formatDest(dest: string, type?: Ir.Type): string { - const prefix = dest.startsWith("t") ? "%" : "^"; - const formattedDest = `${prefix}${dest}`; - return type ? `${formattedDest}: ${formatType(type)}` : formattedDest; -} - -// Component for rendering an instruction with hoverable parts -function InstructionRenderer({ - instruction, - onHover, - onLeave, - showTooltip, - pinTooltip, - hideTooltip, -}: { - instruction: Ir.Instruction; - onHover: (ranges: SourceRange[]) => void; - onLeave: () => void; - showTooltip: (e: React.MouseEvent, content: string) => void; - pinTooltip: (e: React.MouseEvent, content: string) => void; - hideTooltip: () => void; -}) { - const debugInfo = extractInstructionDebug(instruction); - const operationRanges = debugInfo.operation?.context - ? extractAllSourceRanges({ operation: debugInfo.operation, operands: [] }) - : []; - - const parts: (HoverablePart | string)[] = []; - - // Helper to add a hoverable operand - const addOperand = (label: string, text: string, className?: string) => { - const ranges = extractOperandSourceRanges(debugInfo, label); - parts.push({ text, ranges, className }); - }; - - // Helper to add plain text - const add = (text: string) => { - parts.push(text); - }; - - // Build instruction representation - matches Formatter.formatInstruction - switch (instruction.kind) { - case "const": - add(`${formatDest(instruction.dest, instruction.type)} = const `); - addOperand("value", formatValue(instruction.value)); - break; - - case "allocate": - add( - `${formatDest(instruction.dest, Ir.Type.Scalar.uint256)} = allocate.${instruction.location}, size=`, - ); - addOperand("size", formatValue(instruction.size)); - break; - - case "binary": - add(`${formatDest(instruction.dest)} = ${instruction.op} `); - addOperand("left", formatValue(instruction.left)); - add(", "); - addOperand("right", formatValue(instruction.right)); - break; - - case "unary": - add(`${formatDest(instruction.dest)} = ${instruction.op} `); - addOperand("operand", formatValue(instruction.operand)); - break; - - case "env": - add(`${formatDest(instruction.dest)} = env ${instruction.op}`); - break; - - case "hash": - add(`${formatDest(instruction.dest)} = hash `); - addOperand("value", formatValue(instruction.value)); - break; - - case "cast": - add(`${formatDest(instruction.dest, instruction.targetType)} = cast `); - addOperand("value", formatValue(instruction.value)); - add(` to ${formatType(instruction.targetType)}`); - break; - - case "length": - add(`${formatDest(instruction.dest)} = length `); - addOperand("object", formatValue(instruction.object)); - break; - - case "compute_slot": { - const base = formatValue(instruction.base); - add(`${formatDest(instruction.dest, Ir.Type.Scalar.uint256)} = slot[`); - addOperand("base", base); - if ("key" in instruction && instruction.key) { - add("].mapping["); - addOperand("key", formatValue(instruction.key)); - add("]"); - } else if ( - "slotKind" in instruction && - instruction.slotKind === "array" - ) { - add("].array"); - } else if ("fieldOffset" in instruction) { - add(`].field[${instruction.fieldOffset}]`); - } else { - add("]"); - } - break; - } - - case "compute_offset": { - const base = formatValue(instruction.base); - const dest = instruction.dest.startsWith("t") - ? `%${instruction.dest}` - : instruction.dest; - add(`${dest} = offset[`); - addOperand("base", base); - if ("index" in instruction && instruction.index) { - if (instruction.stride === 32) { - add("].array["); - addOperand("index", formatValue(instruction.index)); - add("]"); - } else { - add("].array[index: "); - addOperand("index", formatValue(instruction.index)); - add(`, stride: ${instruction.stride}]`); - } - } else if ("offset" in instruction && instruction.offset) { - add("].byte["); - addOperand("offset", formatValue(instruction.offset)); - add("]"); - } else if ("fieldOffset" in instruction) { - add(`].field[${instruction.fieldOffset}]`); - } else { - add("]"); - } - break; - } - - case "read": { - const location = instruction.location; - const isDefaultOffset = - !instruction.offset || - (instruction.offset.kind === "const" && - instruction.offset.value === 0n); - const isDefaultLength = - !instruction.length || - (instruction.length.kind === "const" && - instruction.length.value === 32n); - - add(`${formatDest(instruction.dest, instruction.type)} = `); - - if (location === "storage" || location === "transient") { - const slot = instruction.slot ? formatValue(instruction.slot) : "0"; - if (isDefaultOffset && isDefaultLength) { - add(`${location}[`); - addOperand("slot", slot); - add("*]"); - } else { - add(`${location}[slot: `); - addOperand("slot", slot); - if (!isDefaultOffset && instruction.offset) { - add(", offset: "); - addOperand("offset", formatValue(instruction.offset)); - } - if (!isDefaultLength && instruction.length) { - add(", length: "); - addOperand("length", formatValue(instruction.length)); - } - add("]"); - } - } else { - if (instruction.offset) { - const offset = formatValue(instruction.offset); - if (isDefaultLength) { - add(`${location}[`); - addOperand("offset", offset); - add("*]"); - } else { - add(`${location}[offset: `); - addOperand("offset", offset); - const length = instruction.length - ? formatValue(instruction.length) - : "32"; - add(", length: "); - addOperand("length", length); - add("]"); - } - } else { - add(`${location}[]`); - } - } - break; - } - - case "write": { - const location = instruction.location; - const value = formatValue(instruction.value); - const isDefaultOffset = - !instruction.offset || - (instruction.offset.kind === "const" && - instruction.offset.value === 0n); - const isDefaultLength = - !instruction.length || - (instruction.length.kind === "const" && - instruction.length.value === 32n); - - if (location === "storage" || location === "transient") { - const slot = instruction.slot ? formatValue(instruction.slot) : "0"; - if (isDefaultOffset && isDefaultLength) { - add(`${location}[`); - addOperand("slot", slot); - add("*] = "); - } else { - add(`${location}[slot: `); - addOperand("slot", slot); - if (!isDefaultOffset && instruction.offset) { - add(", offset: "); - addOperand("offset", formatValue(instruction.offset)); - } - if (!isDefaultLength && instruction.length) { - add(", length: "); - addOperand("length", formatValue(instruction.length)); - } - add("] = "); - } - } else { - if (instruction.offset) { - const offset = formatValue(instruction.offset); - if (isDefaultLength) { - add(`${location}[`); - addOperand("offset", offset); - add("*] = "); - } else { - add(`${location}[offset: `); - addOperand("offset", offset); - const length = instruction.length - ? formatValue(instruction.length) - : "32"; - add(", length: "); - addOperand("length", length); - add("] = "); - } - } else { - add(`${location}[] = `); - } - } - addOperand("value", value); - break; - } - - default: - add(`; unknown instruction: ${(instruction as { kind?: string }).kind}`); - } - - const hasAnyDebug = - operationRanges.length > 0 || - debugInfo.operands.some((op) => op.debug?.context); - - const handleDebugIconHover = (e: React.MouseEvent) => { - const content = formatMultiLevelDebug(debugInfo); - showTooltip(e, content); - }; - - const handleDebugIconClick = (e: React.MouseEvent) => { - const content = formatMultiLevelDebug(debugInfo); - pinTooltip(e, content); - }; - - return ( -
- {hasAnyDebug && ( - - ℹ - - )} - {!hasAnyDebug && } - onHover(operationRanges)} - onMouseLeave={onLeave} - > - {parts.map((part, idx) => - typeof part === "string" ? ( - {part} - ) : ( - - ), - )} - -
- ); -} - -// Component for rendering a terminator with hoverable parts -function TerminatorRenderer({ - terminator, - onHover, - onLeave, - showTooltip, - pinTooltip, - hideTooltip, -}: { - terminator: Ir.Block.Terminator; - onHover: (ranges: SourceRange[]) => void; - onLeave: () => void; - showTooltip: (e: React.MouseEvent, content: string) => void; - pinTooltip: (e: React.MouseEvent, content: string) => void; - hideTooltip: () => void; -}) { - const debugInfo = extractTerminatorDebug(terminator); - const operationRanges = debugInfo.operation?.context - ? extractAllSourceRanges({ operation: debugInfo.operation, operands: [] }) - : []; - - const parts: (HoverablePart | string)[] = []; - - const addOperand = (label: string, text: string, className?: string) => { - const ranges = extractOperandSourceRanges(debugInfo, label); - parts.push({ text, ranges, className }); - }; - - const add = (text: string) => { - parts.push(text); - }; - - switch (terminator.kind) { - case "jump": - add(`jump ${terminator.target}`); - break; - - case "branch": - add("branch "); - addOperand("condition", formatValue(terminator.condition)); - add(` ? ${terminator.trueTarget} : ${terminator.falseTarget}`); - break; - - case "return": - if (terminator.value) { - add("return "); - addOperand("value", formatValue(terminator.value)); - } else { - add("return void"); - } - break; - - case "call": - if (terminator.dest) { - add(`${terminator.dest} = `); - } - add(`call ${terminator.function}(`); - terminator.arguments.forEach((arg, idx) => { - if (idx > 0) add(", "); - addOperand(`arg[${idx}]`, formatValue(arg)); - }); - add(`) -> ${terminator.continuation}`); - break; - } - - const hasAnyDebug = - operationRanges.length > 0 || - debugInfo.operands.some((op) => op.debug?.context); - - const handleDebugIconHover = (e: React.MouseEvent) => { - const content = formatMultiLevelDebug(debugInfo); - showTooltip(e, content); - }; - - const handleDebugIconClick = (e: React.MouseEvent) => { - const content = formatMultiLevelDebug(debugInfo); - pinTooltip(e, content); - }; - - return ( -
- {hasAnyDebug && ( - - ℹ - - )} - {!hasAnyDebug && } - onHover(operationRanges)} - onMouseLeave={onLeave} - > - {parts.map((part, idx) => - typeof part === "string" ? ( - {part} - ) : ( - - ), - )} - -
- ); -} - -// Component for rendering a phi node -function PhiRenderer({ - phi, - onHover, - onLeave, - showTooltip, - pinTooltip, - hideTooltip, -}: { - phi: Ir.Block.Phi; - onHover: (ranges: SourceRange[]) => void; - onLeave: () => void; - showTooltip: (e: React.MouseEvent, content: string) => void; - pinTooltip: (e: React.MouseEvent, content: string) => void; - hideTooltip: () => void; -}) { - const debugInfo = extractPhiDebug(phi); - const operationRanges = debugInfo.operation?.context - ? extractAllSourceRanges({ operation: debugInfo.operation, operands: [] }) - : []; - - const hasAnyDebug = - operationRanges.length > 0 || - debugInfo.operands.some((op) => op.debug?.context); - - const handleDebugIconHover = (e: React.MouseEvent) => { - const content = formatMultiLevelDebug(debugInfo); - showTooltip(e, content); - }; - - const handleDebugIconClick = (e: React.MouseEvent) => { - const content = formatMultiLevelDebug(debugInfo); - pinTooltip(e, content); - }; - - const parts: (HoverablePart | string)[] = []; - const add = (text: string) => parts.push(text); - - const dest = phi.dest.startsWith("t") ? `%${phi.dest}` : `^${phi.dest}`; - const typeStr = phi.type ? `: ${formatType(phi.type)}` : ""; - add(`${dest}${typeStr} = phi `); - - const sources = Array.from(phi.sources.entries()); - sources.forEach(([pred, value], idx) => { - if (idx > 0) add(", "); - - const label = `from ${pred}`; - const ranges = extractOperandSourceRanges(debugInfo, label); - parts.push({ - text: `[${pred}: ${formatValue(value)}]`, - ranges, - }); - }); - - return ( -
- {hasAnyDebug && ( - - ℹ - - )} - {!hasAnyDebug && } - onHover(operationRanges)} - onMouseLeave={onLeave} - > - {parts.map((part, idx) => - typeof part === "string" ? ( - {part} - ) : ( - - ), - )} - -
- ); -} - -// Component for rendering a block -function BlockRenderer({ - blockId, - block, - isEntry, - onHover, - onLeave, - showTooltip, - pinTooltip, - hideTooltip, -}: { - blockId: string; - block: Ir.Block; - isEntry: boolean; - onHover: (ranges: SourceRange[]) => void; - onLeave: () => void; - showTooltip: (e: React.MouseEvent, content: string) => void; - pinTooltip: (e: React.MouseEvent, content: string) => void; - hideTooltip: () => void; -}) { - return ( -
-
- {blockId}: - {isEntry && entry} -
-
- {block.phis.map((phi, idx) => ( - - ))} - {block.instructions.map((instruction, idx) => ( - - ))} - -
-
- ); -} - -// Component for rendering a function -function FunctionRenderer({ - name, - func, - onHover, - onLeave, - showTooltip, - pinTooltip, - hideTooltip, -}: { - name: string; - func: Ir.Function; - onHover: (ranges: SourceRange[]) => void; - onLeave: () => void; - showTooltip: (e: React.MouseEvent, content: string) => void; - pinTooltip: (e: React.MouseEvent, content: string) => void; - hideTooltip: () => void; -}) { - // Topological sort of blocks - const sortedBlocks = useMemo(() => { - const result: [string, Ir.Block][] = []; - const visited = new Set(); - const tempMarked = new Set(); - - const visit = (blockId: string) => { - if (tempMarked.has(blockId)) return; // Cycle detection - if (visited.has(blockId)) return; - - tempMarked.add(blockId); - - const block = func.blocks.get(blockId); - if (!block) return; - - // Visit successors first (reverse post-order) - const term = block.terminator; - if (term.kind === "jump") { - visit(term.target); - } else if (term.kind === "branch") { - visit(term.trueTarget); - visit(term.falseTarget); - } else if (term.kind === "call") { - visit(term.continuation); - } - - tempMarked.delete(blockId); - visited.add(blockId); - result.unshift([blockId, block]); - }; - - visit(func.entry); - - // Add any remaining unreachable blocks - for (const [blockId, block] of func.blocks) { - if (!visited.has(blockId)) { - result.push([blockId, block]); - } - } - - return result; - }, [func]); - - return ( -
-
-

{name}:

-
- {sortedBlocks.map(([blockId, block]) => ( - - ))} -
- ); -} - -export function IrView({ ir, onOpcodeHover }: IrViewProps) { - const { - tooltip, - setTooltip, - showTooltip, - pinTooltip, - hideTooltip, - closeTooltip, - } = useEthdebugTooltip(); - - const handleHover = (ranges: SourceRange[]) => { - onOpcodeHover?.(ranges); - }; - - const handleLeave = () => { - onOpcodeHover?.([]); - }; - - // Calculate stats for all functions - const mainBlocks = ir.main.blocks.size; - const createBlocks = ir.create?.blocks.size || 0; - - // Count user-defined functions - const userFunctionCount = ir.functions?.size || 0; - let userFunctionBlocks = 0; - if (ir.functions) { - for (const func of ir.functions.values()) { - userFunctionBlocks += func.blocks.size; - } - } - - return ( -
-
-

IR

-
- {userFunctionCount > 0 && ( - - Functions: {userFunctionCount} ({userFunctionBlocks} blocks) - - )} - {ir.create && Create: {createBlocks} blocks} - Main: {mainBlocks} blocks -
-
-
- {ir.functions && ir.functions.size > 0 && ( - <> -
User Functions:
- {Array.from(ir.functions.entries()).map(([name, func]) => ( - - ))} - - )} - {ir.create && ( - <> -
Constructor:
- - - )} -
Main (Runtime):
- -
- -
- ); -} diff --git a/packages/playground/src/visualization/debugUtils.ts b/packages/playground/src/visualization/debugUtils.ts deleted file mode 100644 index 8b10cd25ac..0000000000 --- a/packages/playground/src/visualization/debugUtils.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { Evm } from "@ethdebug/bugc"; - -export interface SourceRange { - offset: number; - length: number; -} - -/** - * Minimal types from ethdebug/format needed for extracting source ranges - */ -type CodeContext = { - code: { - range: { - offset: number; - length: number; - }; - }; -}; - -type GatherContext = { - gather: Context[]; -}; - -type PickContext = { - pick: Context[]; -}; - -type FrameContext = { - frame: { - context: Context; - }; -}; - -type Context = CodeContext | GatherContext | PickContext | FrameContext; - -/** - * Extract source ranges from a debug context, handling nondeterminism - * in "pick" contexts as per ethdebug format specification. - * - * When multiple alternatives exist (via "pick"), returns all valid - * source ranges so they can be displayed simultaneously. - * - * Returns an array where: - * - First element is the "primary" location (shown in one color) - * - Remaining elements are "alternative" locations (shown in another color) - */ -export function extractSourceRange( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - context: any | undefined, -): SourceRange[] { - if (!context) { - return []; - } - - // Handle "code" context directly - if ("code" in context && context.code?.range) { - return [ - { - offset: context.code.range.offset, - length: context.code.range.length, - }, - ]; - } - - // Handle "gather" context (multiple simultaneous contexts) - if ("gather" in context && Array.isArray(context.gather)) { - const allRanges: SourceRange[] = []; - for (const subContext of context.gather) { - const ranges = extractSourceRange(subContext); - allRanges.push(...ranges); - } - if (allRanges.length > 0) { - return allRanges; - } - } - - // Handle "pick" context (alternative contexts - collect ALL valid ones) - if ("pick" in context && Array.isArray(context.pick)) { - const allRanges: SourceRange[] = []; - for (const alternative of context.pick) { - const ranges = extractSourceRange(alternative); - allRanges.push(...ranges); - } - if (allRanges.length > 0) { - return allRanges; - } - } - - // Handle "frame" context (compilation stage context) - if ("frame" in context && context.frame?.context) { - return extractSourceRange(context.frame.context); - } - - return []; -} - -/** - * Build a map from bytecode position to instruction with debug info - */ -export function buildInstructionMap( - instructions: Evm.Instruction[], -): Map { - const map = new Map(); - let position = 0; - - for (const instruction of instructions) { - map.set(position, instruction); - - // Move position forward by 1 (opcode) + immediates length - position += 1 + (instruction.immediates?.length || 0); - } - - return map; -} diff --git a/packages/playground/src/visualization/formatBytecode.ts b/packages/playground/src/visualization/formatBytecode.ts deleted file mode 100644 index 05c766fc48..0000000000 --- a/packages/playground/src/visualization/formatBytecode.ts +++ /dev/null @@ -1,173 +0,0 @@ -// formatBytecode - disassembles hex bytecode string - -// EVM opcode names -const OPCODES: Record = { - 0x00: "STOP", - 0x01: "ADD", - 0x02: "MUL", - 0x03: "SUB", - 0x04: "DIV", - 0x05: "SDIV", - 0x06: "MOD", - 0x07: "SMOD", - 0x08: "ADDMOD", - 0x09: "MULMOD", - 0x0a: "EXP", - 0x0b: "SIGNEXTEND", - 0x10: "LT", - 0x11: "GT", - 0x12: "SLT", - 0x13: "SGT", - 0x14: "EQ", - 0x15: "ISZERO", - 0x16: "AND", - 0x17: "OR", - 0x18: "XOR", - 0x19: "NOT", - 0x1a: "BYTE", - 0x1b: "SHL", - 0x1c: "SHR", - 0x1d: "SAR", - 0x20: "KECCAK256", - 0x30: "ADDRESS", - 0x31: "BALANCE", - 0x32: "ORIGIN", - 0x33: "CALLER", - 0x34: "CALLVALUE", - 0x35: "CALLDATALOAD", - 0x36: "CALLDATASIZE", - 0x37: "CALLDATACOPY", - 0x38: "CODESIZE", - 0x39: "CODECOPY", - 0x3a: "GASPRICE", - 0x3b: "EXTCODESIZE", - 0x3c: "EXTCODECOPY", - 0x3d: "RETURNDATASIZE", - 0x3e: "RETURNDATACOPY", - 0x3f: "EXTCODEHASH", - 0x40: "BLOCKHASH", - 0x41: "COINBASE", - 0x42: "TIMESTAMP", - 0x43: "NUMBER", - 0x44: "DIFFICULTY", - 0x45: "GASLIMIT", - 0x46: "CHAINID", - 0x47: "SELFBALANCE", - 0x48: "BASEFEE", - 0x50: "POP", - 0x51: "MLOAD", - 0x52: "MSTORE", - 0x53: "MSTORE8", - 0x54: "SLOAD", - 0x55: "SSTORE", - 0x56: "JUMP", - 0x57: "JUMPI", - 0x58: "PC", - 0x59: "MSIZE", - 0x5a: "GAS", - 0x5b: "JUMPDEST", - 0x5f: "PUSH0", - 0x60: "PUSH1", - 0x61: "PUSH2", - 0x62: "PUSH3", - 0x63: "PUSH4", - 0x64: "PUSH5", - 0x65: "PUSH6", - 0x66: "PUSH7", - 0x67: "PUSH8", - 0x68: "PUSH9", - 0x69: "PUSH10", - 0x6a: "PUSH11", - 0x6b: "PUSH12", - 0x6c: "PUSH13", - 0x6d: "PUSH14", - 0x6e: "PUSH15", - 0x6f: "PUSH16", - 0x70: "PUSH17", - 0x71: "PUSH18", - 0x72: "PUSH19", - 0x73: "PUSH20", - 0x74: "PUSH21", - 0x75: "PUSH22", - 0x76: "PUSH23", - 0x77: "PUSH24", - 0x78: "PUSH25", - 0x79: "PUSH26", - 0x7a: "PUSH27", - 0x7b: "PUSH28", - 0x7c: "PUSH29", - 0x7d: "PUSH30", - 0x7e: "PUSH31", - 0x7f: "PUSH32", - 0x80: "DUP1", - 0x81: "DUP2", - 0x82: "DUP3", - 0x83: "DUP4", - 0x84: "DUP5", - 0x85: "DUP6", - 0x86: "DUP7", - 0x87: "DUP8", - 0x88: "DUP9", - 0x89: "DUP10", - 0x8a: "DUP11", - 0x8b: "DUP12", - 0x8c: "DUP13", - 0x8d: "DUP14", - 0x8e: "DUP15", - 0x8f: "DUP16", - 0x90: "SWAP1", - 0x91: "SWAP2", - 0x92: "SWAP3", - 0x93: "SWAP4", - 0x94: "SWAP5", - 0x95: "SWAP6", - 0x96: "SWAP7", - 0x97: "SWAP8", - 0x98: "SWAP9", - 0x99: "SWAP10", - 0x9a: "SWAP11", - 0x9b: "SWAP12", - 0x9c: "SWAP13", - 0x9d: "SWAP14", - 0x9e: "SWAP15", - 0x9f: "SWAP16", - 0xa0: "LOG0", - 0xa1: "LOG1", - 0xa2: "LOG2", - 0xa3: "LOG3", - 0xa4: "LOG4", - 0xf0: "CREATE", - 0xf1: "CALL", - 0xf2: "CALLCODE", - 0xf3: "RETURN", - 0xf4: "DELEGATECALL", - 0xf5: "CREATE2", - 0xfa: "STATICCALL", - 0xfd: "REVERT", - 0xfe: "INVALID", - 0xff: "SELFDESTRUCT", -}; - -export function formatBytecode(hex: string): string { - const lines: string[] = []; - let offset = 0; - - while (offset < hex.length) { - const pc = offset / 2; - const opcode = parseInt(hex.substr(offset, 2), 16); - const opName = OPCODES[opcode] || `UNKNOWN(0x${hex.substr(offset, 2)})`; - - // Handle PUSH instructions - if (opcode >= 0x60 && opcode <= 0x7f) { - const pushSize = opcode - 0x5f; - const value = hex.substr(offset + 2, pushSize * 2); - lines.push(`${pc.toString().padStart(4, "0")} ${opName} 0x${value}`); - offset += 2 + pushSize * 2; - } else { - lines.push(`${pc.toString().padStart(4, "0")} ${opName}`); - offset += 2; - } - } - - return lines.join("\n"); -} diff --git a/packages/playground/src/visualization/irDebugUtils.ts b/packages/playground/src/visualization/irDebugUtils.ts deleted file mode 100644 index 48492c96ff..0000000000 --- a/packages/playground/src/visualization/irDebugUtils.ts +++ /dev/null @@ -1,217 +0,0 @@ -import type { Ir } from "@ethdebug/bugc"; -import { extractSourceRange, type SourceRange } from "./debugUtils"; - -export interface MultiLevelDebugInfo { - operation?: Ir.Instruction.Debug | Ir.Block.Debug; - operands: { label: string; debug?: Ir.Instruction.Debug | Ir.Block.Debug }[]; -} - -/** - * Extract all debug contexts from an IR instruction - * (operation-level and all operand-level) - */ -export function extractInstructionDebug( - instruction: Ir.Instruction, -): MultiLevelDebugInfo { - const operands: { - label: string; - debug?: Ir.Instruction.Debug | Ir.Block.Debug; - }[] = []; - - switch (instruction.kind) { - case "read": - if (instruction.slot) { - operands.push({ label: "slot", debug: instruction.slotDebug }); - } - if (instruction.offset) { - operands.push({ label: "offset", debug: instruction.offsetDebug }); - } - if (instruction.length) { - operands.push({ label: "length", debug: instruction.lengthDebug }); - } - break; - - case "write": - if (instruction.slot) { - operands.push({ label: "slot", debug: instruction.slotDebug }); - } - if (instruction.offset) { - operands.push({ label: "offset", debug: instruction.offsetDebug }); - } - if (instruction.length) { - operands.push({ label: "length", debug: instruction.lengthDebug }); - } - operands.push({ label: "value", debug: instruction.valueDebug }); - break; - - case "compute_offset": - operands.push({ label: "base", debug: instruction.baseDebug }); - if ("index" in instruction && instruction.index) { - operands.push({ label: "index", debug: instruction.indexDebug }); - } - if ("offset" in instruction && instruction.offset) { - operands.push({ label: "offset", debug: instruction.offsetDebug }); - } - break; - - case "compute_slot": - operands.push({ label: "base", debug: instruction.baseDebug }); - if ("key" in instruction && instruction.key) { - operands.push({ label: "key", debug: instruction.keyDebug }); - } - break; - - case "const": - operands.push({ label: "value", debug: instruction.valueDebug }); - break; - - case "allocate": - operands.push({ label: "size", debug: instruction.sizeDebug }); - break; - - case "binary": - operands.push({ label: "left", debug: instruction.leftDebug }); - operands.push({ label: "right", debug: instruction.rightDebug }); - break; - - case "unary": - operands.push({ label: "operand", debug: instruction.operandDebug }); - break; - - case "hash": - operands.push({ label: "value", debug: instruction.valueDebug }); - break; - - case "cast": - operands.push({ label: "value", debug: instruction.valueDebug }); - break; - - case "length": - operands.push({ label: "object", debug: instruction.objectDebug }); - break; - } - - return { - operation: instruction.operationDebug, - operands, - }; -} - -/** - * Extract all debug contexts from a terminator - */ -export function extractTerminatorDebug( - terminator: Ir.Block.Terminator, -): MultiLevelDebugInfo { - const operands: { - label: string; - debug?: Ir.Instruction.Debug | Ir.Block.Debug; - }[] = []; - - switch (terminator.kind) { - case "branch": - operands.push({ label: "condition", debug: terminator.conditionDebug }); - break; - - case "return": - if (terminator.value) { - operands.push({ label: "value", debug: terminator.valueDebug }); - } - break; - - case "call": - if (terminator.argumentsDebug) { - terminator.arguments.forEach((_, index) => { - operands.push({ - label: `arg[${index}]`, - debug: terminator.argumentsDebug?.[index], - }); - }); - } - break; - } - - return { - operation: terminator.operationDebug, - operands, - }; -} - -/** - * Extract debug contexts from a phi node - */ -export function extractPhiDebug(phi: Ir.Block.Phi): MultiLevelDebugInfo { - const operands: { - label: string; - debug?: Ir.Instruction.Debug | Ir.Block.Debug; - }[] = []; - - if (phi.sourcesDebug) { - for (const pred of phi.sources.keys()) { - const debug = phi.sourcesDebug.get(pred); - operands.push({ label: `from ${pred}`, debug }); - } - } - - return { - operation: phi.operationDebug, - operands, - }; -} - -/** - * Format multi-level debug info as hierarchical JSON - */ -export function formatMultiLevelDebug(info: MultiLevelDebugInfo): string { - const result: Record = {}; - - if (info.operation?.context) { - result.operation = info.operation.context; - } - - const operandsWithDebug = info.operands.filter((op) => op.debug?.context); - if (operandsWithDebug.length > 0) { - result.operands = Object.fromEntries( - operandsWithDebug.map((op) => [op.label, op.debug!.context]), - ); - } - - return JSON.stringify(result, null, 2); -} - -/** - * Extract all source ranges from multi-level debug info - */ -export function extractAllSourceRanges( - info: MultiLevelDebugInfo, -): SourceRange[] { - const ranges: SourceRange[] = []; - - // Operation debug - if (info.operation?.context) { - ranges.push(...extractSourceRange(info.operation.context)); - } - - // Operand debug - for (const operand of info.operands) { - if (operand.debug?.context) { - ranges.push(...extractSourceRange(operand.debug.context)); - } - } - - return ranges; -} - -/** - * Extract source ranges from a specific operand by label - */ -export function extractOperandSourceRanges( - info: MultiLevelDebugInfo, - label: string, -): SourceRange[] { - const operand = info.operands.find((op) => op.label === label); - if (!operand?.debug?.context) { - return []; - } - return extractSourceRange(operand.debug.context); -} diff --git a/packages/playground/tsconfig.json b/packages/playground/tsconfig.json index 6052572044..2decbdcf7e 100644 --- a/packages/playground/tsconfig.json +++ b/packages/playground/tsconfig.json @@ -22,5 +22,9 @@ } }, "include": ["src/**/*"], - "references": [{ "path": "./tsconfig.node.json" }, { "path": "../bugc" }] + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "../bugc" }, + { "path": "../bugc-react" } + ] } diff --git a/yarn.lock b/yarn.lock index e2fccc2781..0dcb7197f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5567,9 +5567,9 @@ "@types/d3-zoom" "*" "@types/dagre@^0.7.52": - version "0.7.53" - resolved "https://registry.yarnpkg.com/@types/dagre/-/dagre-0.7.53.tgz#4dab441bf31b6fb08af0b3e2a3f5ab0c0217a701" - integrity sha512-f4gkWqzPZvYmKhOsDnhq/R8mO4UMcKdxZo+i5SCkOU1wvGeHJeUXGIHeE9pnwGyPMDof1Vx5ZQo4nxpeg2TTVQ== + version "0.7.54" + resolved "https://registry.yarnpkg.com/@types/dagre/-/dagre-0.7.54.tgz#4f3be2a3f89938b0483013739362a477ec978a84" + integrity sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ== "@types/debug@^4.0.0": version "4.1.12" @@ -17238,11 +17238,6 @@ vfile@^6.0.0, vfile@^6.0.1: unist-util-stringify-position "^4.0.0" vfile-message "^4.0.0" -vis-network@^9.1.9: - version "9.1.13" - resolved "https://registry.yarnpkg.com/vis-network/-/vis-network-9.1.13.tgz#3cb785ab0a45489556414d9f503a7fadc50ca90b" - integrity sha512-HLeHd5KZS92qzO1kC59qMh1/FWAZxMUEwUWBwDMoj6RKj/Ajkrgy/heEYo0Zc8SZNQ2J+u6omvK2+a28GX1QuQ== - vite-node@3.2.4: version "3.2.4" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-3.2.4.tgz#f3676d94c4af1e76898c162c92728bca65f7bb07"