diff --git a/bin/start b/bin/start
index e9f2c0a4..17c20fb6 100755
--- a/bin/start
+++ b/bin/start
@@ -10,10 +10,11 @@ else
fi
# Run the commands with concurrently
-concurrently --names=format,pointers,bugc,web,tests \
+concurrently --names=format,pointers,bugc,playground,web,tests \
"cd ./packages/format && yarn watch" \
"cd ./packages/pointers && yarn watch" \
"cd ./packages/bugc && yarn watch" \
+ "cd ./packages/playground && yarn watch" \
"cd ./packages/web && yarn start $DOCUSAURUS_NO_OPEN" \
"sleep 5 && yarn test --ui --watch --coverage $VITEST_NO_OPEN"
diff --git a/packages/playground/.eslintrc.cjs b/packages/playground/.eslintrc.cjs
new file mode 100644
index 00000000..62b6946b
--- /dev/null
+++ b/packages/playground/.eslintrc.cjs
@@ -0,0 +1,22 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:react-hooks/recommended",
+ ],
+ ignorePatterns: ["dist", ".eslintrc.cjs"],
+ parser: "@typescript-eslint/parser",
+ plugins: ["react-refresh"],
+ rules: {
+ "react-refresh/only-export-components": [
+ "warn",
+ { allowConstantExport: true },
+ ],
+ "@typescript-eslint/no-explicit-any": "error",
+ "@typescript-eslint/explicit-function-return-type": "off",
+ "@typescript-eslint/explicit-module-boundary-types": "off",
+ "no-console": "off",
+ },
+};
diff --git a/packages/playground/.gitignore b/packages/playground/.gitignore
new file mode 100644
index 00000000..fb00af59
--- /dev/null
+++ b/packages/playground/.gitignore
@@ -0,0 +1 @@
+app.bundle.js
diff --git a/packages/playground/README.md b/packages/playground/README.md
new file mode 100644
index 00000000..22be49cd
--- /dev/null
+++ b/packages/playground/README.md
@@ -0,0 +1,56 @@
+# @ethdebug/bug-playground
+
+A web-based playground for the BUG language, built with React, TypeScript, and Vite.
+
+## Features
+
+- **Monaco Editor**: Full-featured code editor with BUG syntax highlighting
+- **Live Compilation**: See AST, IR (optimized and unoptimized), and bytecode output
+- **Optimization Levels**: Choose from 4 optimization levels (0-3)
+- **Example Programs**: Automatically loaded from the `examples/` directory
+- **Error Highlighting**: Clear error messages and warnings from the compiler
+
+## Development
+
+Install dependencies:
+
+```bash
+yarn install
+```
+
+Start the development server:
+
+```bash
+yarn dev
+```
+
+Build for production:
+
+```bash
+yarn build
+```
+
+## Scripts
+
+- `yarn start` - Start development server
+- `yarn build` - Build for production
+- `yarn preview` - Preview production build
+- `yarn typecheck` - Run TypeScript type checking
+- `yarn lint` - Run ESLint
+
+## Architecture
+
+The playground is organized by domain/logical concerns:
+
+- `playground/` - Main playground UI and example programs
+- `editor/` - Monaco editor integration and BUG language definition
+- `compiler/` - Compiler integration and output visualization
+- `visualization/` - AST, IR, and bytecode visualizations
+
+## Technologies
+
+- React 18
+- TypeScript 5
+- Vite 5
+- Monaco Editor
+- @ethdebug/bugc (BUG Compiler)
diff --git a/packages/playground/index.html b/packages/playground/index.html
new file mode 100644
index 00000000..ea0fa95c
--- /dev/null
+++ b/packages/playground/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ BUG Playground
+
+
+
+
+
+
diff --git a/packages/playground/package.json b/packages/playground/package.json
new file mode 100644
index 00000000..ffaf5f88
--- /dev/null
+++ b/packages/playground/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "@ethdebug/bug-playground",
+ "version": "0.1.0-0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "start": "vite",
+ "build": "tsc && vite build",
+ "watch": "vite build --watch",
+ "preview": "vite preview",
+ "typecheck": "tsc --noEmit",
+ "lint": "eslint . --ext .ts,.tsx",
+ "format": "prettier --write \"src/**/*.{ts,tsx}\"",
+ "format:check": "prettier --check \"src/**/*.{ts,tsx}\""
+ },
+ "dependencies": {
+ "@ethdebug/bugc": "^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"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.43",
+ "@types/react-dom": "^18.2.17",
+ "@typescript-eslint/eslint-plugin": "^6.14.0",
+ "@typescript-eslint/parser": "^6.14.0",
+ "@vitejs/plugin-react": "^4.2.1",
+ "eslint": "^8.55.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.5",
+ "typescript": "^5.2.2",
+ "vite": "^5.0.8"
+ }
+}
diff --git a/packages/playground/src/App.tsx b/packages/playground/src/App.tsx
new file mode 100644
index 00000000..c5740bfd
--- /dev/null
+++ b/packages/playground/src/App.tsx
@@ -0,0 +1,9 @@
+import { Playground } from "./playground/Playground";
+
+export function App() {
+ return (
+
+ );
+}
diff --git a/packages/playground/src/compiler/CompilerOutput.css b/packages/playground/src/compiler/CompilerOutput.css
new file mode 100644
index 00000000..4a022c31
--- /dev/null
+++ b/packages/playground/src/compiler/CompilerOutput.css
@@ -0,0 +1,70 @@
+.compiler-output {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background-color: #1e1e1e;
+}
+
+.output-tabs {
+ display: flex;
+ background-color: #2d2d30;
+ border-bottom: 1px solid #3e3e42;
+ overflow-x: auto;
+}
+
+.output-tab {
+ padding: 0.5rem 1rem;
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ color: #969696;
+ font-size: 0.875rem;
+ cursor: pointer;
+ white-space: nowrap;
+ transition:
+ color 0.2s,
+ border-color 0.2s;
+}
+
+.output-tab:hover:not(:disabled) {
+ color: #cccccc;
+}
+
+.output-tab.active {
+ color: #ffffff;
+ border-bottom-color: #0e639c;
+}
+
+.output-tab:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.output-content {
+ flex: 1;
+ overflow: auto;
+ padding: 1rem;
+}
+
+.output-warnings {
+ background-color: #332b00;
+ border-top: 1px solid #665500;
+ padding: 1rem;
+ color: #ffcc00;
+}
+
+.output-warnings h3 {
+ margin: 0 0 0.5rem 0;
+ font-size: 0.875rem;
+ font-weight: 600;
+}
+
+.output-warnings ul {
+ margin: 0;
+ padding-left: 1.5rem;
+}
+
+.output-warnings li {
+ font-size: 0.813rem;
+ line-height: 1.5;
+}
diff --git a/packages/playground/src/compiler/CompilerOutput.tsx b/packages/playground/src/compiler/CompilerOutput.tsx
new file mode 100644
index 00000000..865765bd
--- /dev/null
+++ b/packages/playground/src/compiler/CompilerOutput.tsx
@@ -0,0 +1,75 @@
+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 { ErrorView } from "./ErrorView";
+import type { SourceRange } from "../visualization/debugUtils";
+import "./CompilerOutput.css";
+
+interface CompilerOutputProps {
+ result: CompileResult;
+ onOpcodeHover?: (ranges: SourceRange[]) => void;
+}
+
+type TabType = "ast" | "ir" | "cfg" | "bytecode" | "error";
+
+export function CompilerOutput({ result, onOpcodeHover }: CompilerOutputProps) {
+ const [activeTab, setActiveTab] = useState(
+ result.success ? "ast" : "error",
+ );
+
+ if (!result.success) {
+ return ;
+ }
+
+ const tabs: { id: TabType; label: string; disabled?: boolean }[] = [
+ { id: "ast", label: "AST" },
+ { id: "ir", label: "IR" },
+ { id: "cfg", label: "CFG" },
+ { id: "bytecode", label: "Bytecode" },
+ ];
+
+ return (
+
+
+ {tabs.map((tab) => (
+
+ ))}
+
+
+
+ {activeTab === "ast" &&
}
+ {activeTab === "ir" && (
+
+ )}
+ {activeTab === "cfg" &&
}
+ {activeTab === "bytecode" && (
+
+ )}
+
+
+ {result.warnings.length > 0 && (
+
+
Warnings:
+
+ {result.warnings.map((warning, i) => (
+ - {warning}
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/packages/playground/src/compiler/ErrorView.css b/packages/playground/src/compiler/ErrorView.css
new file mode 100644
index 00000000..9c17c180
--- /dev/null
+++ b/packages/playground/src/compiler/ErrorView.css
@@ -0,0 +1,51 @@
+.error-view {
+ padding: 1rem;
+ height: 100%;
+ overflow: auto;
+}
+
+.error-content {
+ background-color: #5a1d1d;
+ border: 1px solid #f48771;
+ border-radius: 4px;
+ padding: 1rem;
+ margin-bottom: 1rem;
+}
+
+.error-content h3 {
+ color: #f48771;
+ margin: 0 0 0.5rem 0;
+ font-size: 1rem;
+}
+
+.error-message {
+ color: #ffcccc;
+ margin: 0;
+ font-family: "Consolas", "Monaco", "Courier New", monospace;
+ font-size: 0.875rem;
+ line-height: 1.5;
+ white-space: pre-wrap;
+}
+
+.error-warnings {
+ background-color: #332b00;
+ border: 1px solid #665500;
+ border-radius: 4px;
+ padding: 1rem;
+ color: #ffcc00;
+}
+
+.error-warnings h4 {
+ margin: 0 0 0.5rem 0;
+ font-size: 0.875rem;
+}
+
+.error-warnings ul {
+ margin: 0;
+ padding-left: 1.5rem;
+}
+
+.error-warnings li {
+ font-size: 0.813rem;
+ line-height: 1.5;
+}
diff --git a/packages/playground/src/compiler/ErrorView.tsx b/packages/playground/src/compiler/ErrorView.tsx
new file mode 100644
index 00000000..c2091356
--- /dev/null
+++ b/packages/playground/src/compiler/ErrorView.tsx
@@ -0,0 +1,28 @@
+import "./ErrorView.css";
+
+interface ErrorViewProps {
+ error: string;
+ warnings?: string[];
+}
+
+export function ErrorView({ error, warnings }: ErrorViewProps) {
+ return (
+
+
+
Compilation Error
+
{error}
+
+
+ {warnings && warnings.length > 0 && (
+
+
Warnings:
+
+ {warnings.map((warning, i) => (
+ - {warning}
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/packages/playground/src/compiler/compile.ts b/packages/playground/src/compiler/compile.ts
new file mode 100644
index 00000000..b692cc89
--- /dev/null
+++ b/packages/playground/src/compiler/compile.ts
@@ -0,0 +1,84 @@
+import { compile as bugCompile, type BugError, Severity } from "@ethdebug/bugc";
+import type { CompileResult } from "./types";
+
+export async function compile(
+ code: string,
+ optimizationLevel: number,
+): Promise {
+ console.debug("compiling %o", bugCompile);
+ // First, get the AST
+ const astResult = await bugCompile({ to: "ast", source: code });
+ console.debug("astResult %o", astResult);
+
+ if (!astResult.success) {
+ const errors = astResult.messages[Severity.Error] || [];
+ const warnings = astResult.messages[Severity.Warning] || [];
+ return {
+ success: false,
+ error: errors[0]?.message || "Parse failed",
+ warnings: warnings.map((w: BugError) => w.message),
+ };
+ }
+
+ const ast = astResult.value.ast;
+
+ // Get IR at selected optimization level
+ const irResult = await bugCompile({
+ to: "ir",
+ source: code,
+ optimizer: { level: optimizationLevel as 0 | 1 | 2 | 3 },
+ });
+
+ if (!irResult.success) {
+ const errors = irResult.messages[Severity.Error] || [];
+ const warnings = irResult.messages[Severity.Warning] || [];
+ return {
+ success: false,
+ error: errors[0]?.message || "IR generation failed",
+ ast,
+ warnings: warnings.map((w: BugError) => w.message),
+ };
+ }
+
+ const ir = irResult.value.ir;
+
+ // Generate bytecode with optimization
+ const bytecodeResult = await bugCompile({
+ to: "bytecode",
+ source: code,
+ optimizer: { level: optimizationLevel as 0 | 1 | 2 | 3 },
+ });
+
+ if (!bytecodeResult.success) {
+ const errors = bytecodeResult.messages[Severity.Error] || [];
+ const warnings = bytecodeResult.messages[Severity.Warning] || [];
+ return {
+ success: false,
+ error: errors[0]?.message || "Bytecode generation failed",
+ ast,
+ warnings: warnings.map((w: BugError) => w.message),
+ };
+ }
+
+ const bytecode = {
+ runtime: bytecodeResult.value.bytecode.runtime,
+ create: bytecodeResult.value.bytecode.create,
+ runtimeInstructions: bytecodeResult.value.bytecode.runtimeInstructions,
+ createInstructions: bytecodeResult.value.bytecode.createInstructions,
+ };
+
+ // Collect all warnings
+ const allWarnings = [
+ ...(astResult.messages[Severity.Warning] || []),
+ ...(irResult.messages[Severity.Warning] || []),
+ ...(bytecodeResult.messages[Severity.Warning] || []),
+ ].map((w: BugError) => w.message);
+
+ return {
+ success: true,
+ ast,
+ ir,
+ bytecode,
+ warnings: [...new Set(allWarnings)], // Remove duplicates
+ };
+}
diff --git a/packages/playground/src/compiler/types.ts b/packages/playground/src/compiler/types.ts
new file mode 100644
index 00000000..9222fbd7
--- /dev/null
+++ b/packages/playground/src/compiler/types.ts
@@ -0,0 +1,25 @@
+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;
diff --git a/packages/playground/src/compiler/useCompiler.ts b/packages/playground/src/compiler/useCompiler.ts
new file mode 100644
index 00000000..79ed3bac
--- /dev/null
+++ b/packages/playground/src/compiler/useCompiler.ts
@@ -0,0 +1,35 @@
+import { useState, useCallback } from "react";
+import { compile } from "./compile";
+import type { CompileResult } from "./types";
+
+export function useCompiler() {
+ const [compileResult, setCompileResult] = useState(
+ null,
+ );
+ const [isCompiling, setIsCompiling] = useState(false);
+
+ const doCompile = useCallback(
+ async (code: string, optimizationLevel: number) => {
+ setIsCompiling(true);
+ try {
+ const result = await compile(code, optimizationLevel);
+ setCompileResult(result);
+ } catch (error) {
+ setCompileResult({
+ success: false,
+ error:
+ error instanceof Error ? error.message : "Unknown error occurred",
+ });
+ } finally {
+ setIsCompiling(false);
+ }
+ },
+ [],
+ );
+
+ return {
+ compileResult,
+ isCompiling,
+ compile: doCompile,
+ };
+}
diff --git a/packages/playground/src/editor/Editor.tsx b/packages/playground/src/editor/Editor.tsx
new file mode 100644
index 00000000..48bd3622
--- /dev/null
+++ b/packages/playground/src/editor/Editor.tsx
@@ -0,0 +1,119 @@
+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";
+
+export interface SourceRange {
+ offset: number;
+ length: number;
+}
+
+interface EditorProps {
+ value: string;
+ onChange: (value: string) => void;
+ language?: string;
+ highlightedRanges?: SourceRange[];
+}
+
+export function Editor({
+ value,
+ onChange,
+ language = "bug",
+ highlightedRanges = [],
+}: EditorProps) {
+ const editorRef = useRef(
+ null,
+ );
+ const decorationsRef = useRef([]);
+
+ useEffect(() => {
+ registerBugLanguage();
+ }, []);
+
+ useEffect(() => {
+ const editor = editorRef.current;
+ if (!editor) {
+ return;
+ }
+
+ const model = editor.getModel();
+ if (!model) {
+ return;
+ }
+
+ // Clear previous decorations
+ decorationsRef.current = editor.deltaDecorations(
+ decorationsRef.current,
+ [],
+ );
+
+ // Add new decorations for all highlighted ranges
+ if (highlightedRanges.length > 0) {
+ const decorations = highlightedRanges.map((range, index) => {
+ const startPosition = model.getPositionAt(range.offset);
+ const endPosition = model.getPositionAt(range.offset + range.length);
+
+ // First range is "primary", rest are "alternative"
+ const isPrimary = index === 0;
+ const className = isPrimary
+ ? "opcode-hover-highlight"
+ : "opcode-hover-highlight-alternative";
+ const inlineClassName = isPrimary
+ ? "opcode-hover-highlight-inline"
+ : "opcode-hover-highlight-alternative-inline";
+
+ return {
+ range: {
+ startLineNumber: startPosition.lineNumber,
+ startColumn: startPosition.column,
+ endLineNumber: endPosition.lineNumber,
+ endColumn: endPosition.column,
+ },
+ options: {
+ className,
+ isWholeLine: false,
+ inlineClassName,
+ },
+ };
+ });
+
+ decorationsRef.current = editor.deltaDecorations([], decorations);
+
+ // Scroll to the first (primary) highlighted range
+ const firstRange = highlightedRanges[0];
+ const startPosition = model.getPositionAt(firstRange.offset);
+ const endPosition = model.getPositionAt(
+ firstRange.offset + firstRange.length,
+ );
+ editor.revealRangeInCenter({
+ startLineNumber: startPosition.lineNumber,
+ startColumn: startPosition.column,
+ endLineNumber: endPosition.lineNumber,
+ endColumn: endPosition.column,
+ });
+ }
+ }, [highlightedRanges]);
+
+ const handleEditorDidMount: OnMount = (editor) => {
+ editorRef.current = editor;
+ };
+
+ return (
+ onChange(value || "")}
+ onMount={handleEditorDidMount}
+ options={{
+ minimap: { enabled: false },
+ fontSize: 14,
+ lineNumbers: "on",
+ scrollBeyondLastLine: false,
+ automaticLayout: true,
+ tabSize: 2,
+ }}
+ />
+ );
+}
diff --git a/packages/playground/src/editor/bugLanguage.ts b/packages/playground/src/editor/bugLanguage.ts
new file mode 100644
index 00000000..a93fe34d
--- /dev/null
+++ b/packages/playground/src/editor/bugLanguage.ts
@@ -0,0 +1,192 @@
+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/index.css b/packages/playground/src/index.css
new file mode 100644
index 00000000..7d787a49
--- /dev/null
+++ b/packages/playground/src/index.css
@@ -0,0 +1,94 @@
+:root {
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
+ line-height: 1.5;
+ font-weight: 400;
+
+ color-scheme: light dark;
+ color: rgba(255, 255, 255, 0.87);
+ background-color: #242424;
+
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ display: flex;
+ place-items: center;
+ min-width: 320px;
+ min-height: 100vh;
+}
+
+#root {
+ width: 100%;
+ height: 100vh;
+ margin: 0 auto;
+}
+
+.app {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+}
+
+h1 {
+ font-size: 3.2em;
+ line-height: 1.1;
+}
+
+button {
+ border-radius: 8px;
+ border: 1px solid transparent;
+ padding: 0.6em 1.2em;
+ font-size: 1em;
+ font-weight: 500;
+ font-family: inherit;
+ background-color: #1a1a1a;
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+
+button:hover {
+ border-color: #646cff;
+}
+
+button:focus,
+button:focus-visible {
+ outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+ :root {
+ color: #213547;
+ background-color: #ffffff;
+ }
+ button {
+ background-color: #f9f9f9;
+ }
+}
+
+/* Monaco Editor custom decorations */
+/* Primary location (teal) - first source location */
+.opcode-hover-highlight {
+ background-color: rgba(78, 201, 176, 0.2);
+}
+
+.opcode-hover-highlight-inline {
+ background-color: rgba(78, 201, 176, 0.3);
+}
+
+/* Alternative locations (orange) - deduplicated sources */
+.opcode-hover-highlight-alternative {
+ background-color: rgba(255, 165, 0, 0.2);
+}
+
+.opcode-hover-highlight-alternative-inline {
+ background-color: rgba(255, 165, 0, 0.3);
+}
diff --git a/packages/playground/src/main.tsx b/packages/playground/src/main.tsx
new file mode 100644
index 00000000..5d6b5dee
--- /dev/null
+++ b/packages/playground/src/main.tsx
@@ -0,0 +1,10 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { App } from "./App";
+import "./index.css";
+
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+ ,
+);
diff --git a/packages/playground/src/playground/Playground.css b/packages/playground/src/playground/Playground.css
new file mode 100644
index 00000000..1fa43b53
--- /dev/null
+++ b/packages/playground/src/playground/Playground.css
@@ -0,0 +1,83 @@
+.playground {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background-color: #1e1e1e;
+}
+
+.playground-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 1rem 2rem;
+ background-color: #2d2d30;
+ border-bottom: 1px solid #3e3e42;
+}
+
+.playground-header h1 {
+ font-size: 1.5rem;
+ margin: 0;
+ color: #cccccc;
+}
+
+.playground-controls {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.example-select,
+.optimization-control select {
+ background-color: #3c3c3c;
+ color: #cccccc;
+ border: 1px solid #3e3e42;
+ padding: 0.5rem;
+ border-radius: 4px;
+ font-size: 0.875rem;
+}
+
+.optimization-control {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ color: #cccccc;
+ font-size: 0.875rem;
+}
+
+.compile-button {
+ background-color: #0e639c;
+ color: white;
+ border: none;
+ padding: 0.5rem 1rem;
+ border-radius: 4px;
+ font-size: 0.875rem;
+ cursor: pointer;
+ transition: background-color 0.2s;
+}
+
+.compile-button:hover:not(:disabled) {
+ background-color: #1177bb;
+}
+
+.compile-button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.playground-content {
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+}
+
+.playground-editor {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ border-right: 1px solid #3e3e42;
+}
+
+.playground-output {
+ flex: 1;
+ overflow: auto;
+}
diff --git a/packages/playground/src/playground/Playground.tsx b/packages/playground/src/playground/Playground.tsx
new file mode 100644
index 00000000..3a5c5734
--- /dev/null
+++ b/packages/playground/src/playground/Playground.tsx
@@ -0,0 +1,94 @@
+import { useState, useCallback, useEffect } from "react";
+import { Editor, type SourceRange } from "../editor/Editor";
+import { CompilerOutput } from "../compiler/CompilerOutput";
+import { useCompiler } from "../compiler/useCompiler";
+import { examples } from "./examples";
+import "./Playground.css";
+
+export function Playground() {
+ const [code, setCode] = useState(examples[0].code);
+ const [selectedExample, setSelectedExample] = useState(examples[0].name);
+ const [optimizationLevel, setOptimizationLevel] = useState(3);
+ const [highlightedRanges, setHighlightedRanges] = useState([]);
+
+ const { compileResult, isCompiling, compile } = useCompiler();
+
+ const handleExampleChange = useCallback((exampleName: string) => {
+ const example = examples.find((e) => e.name === exampleName);
+ if (example) {
+ setSelectedExample(exampleName);
+ setCode(example.code);
+ }
+ }, []);
+
+ const handleCompile = useCallback(() => {
+ compile(code, optimizationLevel);
+ }, [code, optimizationLevel, compile]);
+
+ // Compile on initial load
+ useEffect(() => {
+ compile(code, optimizationLevel);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+ BUG Playground
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {compileResult && (
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/playground/src/playground/examples.ts b/packages/playground/src/playground/examples.ts
new file mode 100644
index 00000000..54abb74c
--- /dev/null
+++ b/packages/playground/src/playground/examples.ts
@@ -0,0 +1,118 @@
+export interface Example {
+ name: string;
+ displayName: string;
+ category: "basic" | "intermediate" | "advanced" | "optimizations";
+ code: string;
+}
+
+// Import all .bug files from the bugc examples directory
+// Vite will inline the file contents at build time
+const exampleFiles = import.meta.glob("../../../bugc/examples/**/*.bug", {
+ query: "?raw",
+ import: "default",
+ eager: true,
+}) as Record;
+
+// Map the actual example files to the Example interface
+// Organized by category, showing only working examples
+export const examples: Example[] = [
+ // Basic examples
+ {
+ name: "minimal",
+ displayName: "Minimal",
+ category: "basic",
+ code: exampleFiles["../../../bugc/examples/basic/minimal.bug"] || "",
+ },
+ {
+ name: "conditionals",
+ displayName: "Conditionals",
+ category: "basic",
+ code: exampleFiles["../../../bugc/examples/basic/conditionals.bug"],
+ },
+ {
+ name: "functions",
+ displayName: "Functions",
+ category: "basic",
+ code: exampleFiles["../../../bugc/examples/basic/functions.bug"],
+ },
+ {
+ name: "array-length",
+ displayName: "Array Length",
+ category: "basic",
+ code: exampleFiles["../../../bugc/examples/basic/array-length.bug"],
+ },
+
+ // Intermediate examples
+ {
+ name: "owner-counter",
+ displayName: "Owner Counter",
+ category: "intermediate",
+ code: exampleFiles["../../../bugc/examples/intermediate/owner-counter.bug"],
+ },
+ {
+ name: "arrays",
+ displayName: "Arrays and Loops",
+ category: "intermediate",
+ code: exampleFiles["../../../bugc/examples/intermediate/arrays.bug"],
+ },
+ {
+ name: "mappings",
+ displayName: "Mappings",
+ category: "intermediate",
+ code: exampleFiles["../../../bugc/examples/intermediate/mappings.bug"],
+ },
+ {
+ name: "scopes",
+ displayName: "Variable Scopes",
+ category: "intermediate",
+ code: exampleFiles["../../../bugc/examples/intermediate/scopes.bug"],
+ },
+ {
+ name: "slices",
+ displayName: "Byte Slices",
+ category: "intermediate",
+ code: exampleFiles["../../../bugc/examples/intermediate/slices.bug"],
+ },
+ {
+ name: "calldata",
+ displayName: "Calldata Access",
+ category: "intermediate",
+ code: exampleFiles["../../../bugc/examples/intermediate/calldata.bug"],
+ },
+
+ // Advanced examples
+ {
+ name: "nested-mappings",
+ displayName: "Nested Mappings",
+ category: "advanced",
+ code: exampleFiles["../../../bugc/examples/advanced/nested-mappings.bug"],
+ },
+ {
+ name: "nested-arrays",
+ displayName: "Nested Arrays",
+ category: "advanced",
+ code: exampleFiles["../../../bugc/examples/advanced/nested-arrays.bug"],
+ },
+ {
+ name: "nested-structs",
+ displayName: "Nested Structs",
+ category: "advanced",
+ code: exampleFiles["../../../bugc/examples/advanced/nested-structs.bug"],
+ },
+
+ // Optimization demos
+ {
+ name: "cse",
+ displayName: "CSE Demo",
+ category: "optimizations",
+ code: exampleFiles["../../../bugc/examples/optimizations/cse.bug"],
+ },
+ {
+ name: "constant-folding",
+ displayName: "Constant Folding",
+ category: "optimizations",
+ code: exampleFiles[
+ "../../../bugc/examples/optimizations/constant-folding.bug"
+ ],
+ },
+];
diff --git a/packages/playground/src/visualization/AstView.css b/packages/playground/src/visualization/AstView.css
new file mode 100644
index 00000000..3536b032
--- /dev/null
+++ b/packages/playground/src/visualization/AstView.css
@@ -0,0 +1,15 @@
+.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
new file mode 100644
index 00000000..7263e0c5
--- /dev/null
+++ b/packages/playground/src/visualization/AstView.tsx
@@ -0,0 +1,24 @@
+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 (
+
+ );
+}
diff --git a/packages/playground/src/visualization/BytecodeView.css b/packages/playground/src/visualization/BytecodeView.css
new file mode 100644
index 00000000..bb352dbd
--- /dev/null
+++ b/packages/playground/src/visualization/BytecodeView.css
@@ -0,0 +1,111 @@
+.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
new file mode 100644
index 00000000..85b2e10c
--- /dev/null
+++ b/packages/playground/src/visualization/BytecodeView.tsx
@@ -0,0 +1,173 @@
+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
+
+
+
+
+
+
+
+
Instructions
+
+
+
+
+ );
+}
diff --git a/packages/playground/src/visualization/CfgView.css b/packages/playground/src/visualization/CfgView.css
new file mode 100644
index 00000000..20ce254b
--- /dev/null
+++ b/packages/playground/src/visualization/CfgView.css
@@ -0,0 +1,195 @@
+.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
new file mode 100644
index 00000000..cdb869f0
--- /dev/null
+++ b/packages/playground/src/visualization/CfgView.tsx
@@ -0,0 +1,457 @@
+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
new file mode 100644
index 00000000..4c903eee
--- /dev/null
+++ b/packages/playground/src/visualization/EthdebugTooltip.css
@@ -0,0 +1,71 @@
+.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
new file mode 100644
index 00000000..c9d9aab5
--- /dev/null
+++ b/packages/playground/src/visualization/EthdebugTooltip.tsx
@@ -0,0 +1,127 @@
+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
new file mode 100644
index 00000000..91eff222
--- /dev/null
+++ b/packages/playground/src/visualization/IrView.css
@@ -0,0 +1,140 @@
+.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
new file mode 100644
index 00000000..6fad402b
--- /dev/null
+++ b/packages/playground/src/visualization/IrView.tsx
@@ -0,0 +1,834 @@
+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
new file mode 100644
index 00000000..8b10cd25
--- /dev/null
+++ b/packages/playground/src/visualization/debugUtils.ts
@@ -0,0 +1,114 @@
+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
new file mode 100644
index 00000000..05c766fc
--- /dev/null
+++ b/packages/playground/src/visualization/formatBytecode.ts
@@ -0,0 +1,173 @@
+// 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
new file mode 100644
index 00000000..48492c96
--- /dev/null
+++ b/packages/playground/src/visualization/irDebugUtils.ts
@@ -0,0 +1,217 @@
+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/src/vite-env.d.ts b/packages/playground/src/vite-env.d.ts
new file mode 100644
index 00000000..99e26006
--- /dev/null
+++ b/packages/playground/src/vite-env.d.ts
@@ -0,0 +1,10 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_APP_TITLE: string;
+ // more env variables...
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/packages/playground/tsconfig.json b/packages/playground/tsconfig.json
new file mode 100644
index 00000000..60525720
--- /dev/null
+++ b/packages/playground/tsconfig.json
@@ -0,0 +1,26 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Path mapping */
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src/**/*"],
+ "references": [{ "path": "./tsconfig.node.json" }, { "path": "../bugc" }]
+}
diff --git a/packages/playground/tsconfig.node.json b/packages/playground/tsconfig.node.json
new file mode 100644
index 00000000..42872c59
--- /dev/null
+++ b/packages/playground/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/packages/playground/vite.config.ts b/packages/playground/vite.config.ts
new file mode 100644
index 00000000..5d770b40
--- /dev/null
+++ b/packages/playground/vite.config.ts
@@ -0,0 +1,25 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import { resolve } from "path";
+
+export default defineConfig({
+ plugins: [react()],
+ resolve: {
+ alias: {
+ "@": resolve(__dirname, "./src"),
+ },
+ },
+ optimizeDeps: {
+ include: ["@ethdebug/bugc"],
+ },
+ server: {
+ port: 3001,
+ },
+ build: {
+ outDir: "dist",
+ sourcemap: true,
+ commonjsOptions: {
+ transformMixedEsModules: true,
+ },
+ },
+});
diff --git a/vitest.config.ts b/vitest.config.ts
index 7e413b7b..10e92040 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -12,6 +12,7 @@ export default defineConfig({
"**/*.test.ts",
"**/*.d.ts",
"packages/web/**",
+ "packages/playground/**",
"**/vitest.config.ts",
"**/vitest.setup.ts",
"**/jest.config.ts",
diff --git a/yarn.lock b/yarn.lock
index ed34f2ea..13395320 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -274,7 +274,7 @@
json5 "^2.2.3"
semver "^6.3.1"
-"@babel/core@^7.25.9", "@babel/core@^7.28.6":
+"@babel/core@^7.25.9", "@babel/core@^7.28.0", "@babel/core@^7.28.6":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.6.tgz#531bf883a1126e53501ba46eb3bb414047af507f"
integrity sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==
@@ -736,18 +736,18 @@
js-tokens "^4.0.0"
picocolors "^1.0.0"
-"@babel/parser@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85"
- integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==
-
-"@babel/parser@^7.25.4", "@babel/parser@^7.28.6":
+"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.25.4", "@babel/parser@^7.28.6":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.6.tgz#f01a8885b7fa1e56dd8a155130226cd698ef13fd"
integrity sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==
dependencies:
"@babel/types" "^7.28.6"
+"@babel/parser@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.7.tgz#9a5226f92f0c5c8ead550b750f5608e766c8ce85"
+ integrity sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==
+
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.7.tgz#fd059fd27b184ea2b4c7e646868a9a381bbc3055"
@@ -1648,6 +1648,20 @@
dependencies:
"@babel/plugin-transform-react-jsx" "^7.27.1"
+"@babel/plugin-transform-react-jsx-self@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz#af678d8506acf52c577cac73ff7fe6615c85fc92"
+ integrity sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.27.1"
+
+"@babel/plugin-transform-react-jsx-source@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz#dcfe2c24094bb757bf73960374e7c55e434f19f0"
+ integrity sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.27.1"
+
"@babel/plugin-transform-react-jsx@^7.22.15", "@babel/plugin-transform-react-jsx@^7.22.5":
version "7.23.4"
resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz"
@@ -2127,7 +2141,7 @@
dependencies:
regenerator-runtime "^0.14.0"
-"@babel/runtime@^7.25.9":
+"@babel/runtime@^7.18.9", "@babel/runtime@^7.25.9":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.6.tgz#d267a43cb1836dc4d182cce93ae75ba954ef6d2b"
integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==
@@ -2179,6 +2193,14 @@
"@babel/types" "^7.28.6"
debug "^4.3.1"
+"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.25.4", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2", "@babel/types@^7.28.5", "@babel/types@^7.28.6":
+ version "7.28.6"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df"
+ integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==
+ dependencies:
+ "@babel/helper-string-parser" "^7.27.1"
+ "@babel/helper-validator-identifier" "^7.28.5"
+
"@babel/types@^7.21.3", "@babel/types@^7.24.7":
version "7.24.7"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.7.tgz#6027fe12bc1aa724cd32ab113fb7f1988f1f66f2"
@@ -2197,14 +2219,6 @@
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
-"@babel/types@^7.25.4", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.5", "@babel/types@^7.28.6":
- version "7.28.6"
- resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.6.tgz#c3e9377f1b155005bcc4c46020e7e394e13089df"
- integrity sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==
- dependencies:
- "@babel/helper-string-parser" "^7.27.1"
- "@babel/helper-validator-identifier" "^7.28.5"
-
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -4011,7 +4025,7 @@
dependencies:
state-local "^1.0.6"
-"@monaco-editor/react@^4.7.0":
+"@monaco-editor/react@^4.6.0", "@monaco-editor/react@^4.7.0":
version "4.7.0"
resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.7.0.tgz#35a1ec01bfe729f38bfc025df7b7bac145602a60"
integrity sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==
@@ -4598,6 +4612,11 @@
resolved "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz"
integrity sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==
+"@rolldown/pluginutils@1.0.0-beta.27":
+ version "1.0.0-beta.27"
+ resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz#47d2bf4cef6d470b22f5831b420f8964e0bf755f"
+ integrity sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==
+
"@rollup/rollup-android-arm-eabi@4.55.1":
version "4.55.1"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz#76e0fef6533b3ce313f969879e61e8f21f0eeb28"
@@ -5158,6 +5177,39 @@
dependencies:
"@types/estree" "*"
+"@types/babel__core@^7.20.5":
+ version "7.20.5"
+ resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
+ integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==
+ dependencies:
+ "@babel/parser" "^7.20.7"
+ "@babel/types" "^7.20.7"
+ "@types/babel__generator" "*"
+ "@types/babel__template" "*"
+ "@types/babel__traverse" "*"
+
+"@types/babel__generator@*":
+ version "7.27.0"
+ resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.27.0.tgz#b5819294c51179957afaec341442f9341e4108a9"
+ integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@types/babel__template@*":
+ version "7.4.4"
+ resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f"
+ integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@types/babel__traverse@*":
+ version "7.28.0"
+ resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.28.0.tgz#07d713d6cce0d265c9849db0cbe62d3f61f36f74"
+ integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==
+ dependencies:
+ "@babel/types" "^7.28.2"
+
"@types/bn.js@^5.1.0":
version "5.1.5"
resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.5.tgz#2e0dacdcce2c0f16b905d20ff87aedbc6f7b4bf0"
@@ -5203,6 +5255,221 @@
dependencies:
"@types/node" "*"
+"@types/d3-array@*":
+ version "3.2.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.2.tgz#e02151464d02d4a1b44646d0fcdb93faf88fde8c"
+ integrity sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==
+
+"@types/d3-axis@*":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795"
+ integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-brush@*":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c"
+ integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-chord@*":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d"
+ integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==
+
+"@types/d3-color@*":
+ version "3.1.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2"
+ integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
+
+"@types/d3-contour@*":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231"
+ integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==
+ dependencies:
+ "@types/d3-array" "*"
+ "@types/geojson" "*"
+
+"@types/d3-delaunay@*":
+ version "6.0.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1"
+ integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==
+
+"@types/d3-dispatch@*":
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz#ef004d8a128046cfce434d17182f834e44ef95b2"
+ integrity sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==
+
+"@types/d3-drag@*":
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02"
+ integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-dsv@*":
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17"
+ integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==
+
+"@types/d3-ease@*":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b"
+ integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
+
+"@types/d3-fetch@*":
+ version "3.0.7"
+ resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980"
+ integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==
+ dependencies:
+ "@types/d3-dsv" "*"
+
+"@types/d3-force@*":
+ version "3.0.10"
+ resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.10.tgz#6dc8fc6e1f35704f3b057090beeeb7ac674bff1a"
+ integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==
+
+"@types/d3-format@*":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90"
+ integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==
+
+"@types/d3-geo@*":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440"
+ integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==
+ dependencies:
+ "@types/geojson" "*"
+
+"@types/d3-hierarchy@*":
+ version "3.1.7"
+ resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b"
+ integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==
+
+"@types/d3-interpolate@*":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c"
+ integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
+ dependencies:
+ "@types/d3-color" "*"
+
+"@types/d3-path@*":
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.1.tgz#f632b380c3aca1dba8e34aa049bcd6a4af23df8a"
+ integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==
+
+"@types/d3-polygon@*":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c"
+ integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==
+
+"@types/d3-quadtree@*":
+ version "3.0.6"
+ resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f"
+ integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==
+
+"@types/d3-random@*":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb"
+ integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==
+
+"@types/d3-scale-chromatic@*":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz#dc6d4f9a98376f18ea50bad6c39537f1b5463c39"
+ integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==
+
+"@types/d3-scale@*":
+ version "4.0.9"
+ resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.9.tgz#57a2f707242e6fe1de81ad7bfcccaaf606179afb"
+ integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==
+ dependencies:
+ "@types/d3-time" "*"
+
+"@types/d3-selection@*":
+ version "3.0.11"
+ resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.11.tgz#bd7a45fc0a8c3167a631675e61bc2ca2b058d4a3"
+ integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==
+
+"@types/d3-shape@*":
+ version "3.1.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.8.tgz#d1516cc508753be06852cd06758e3bb54a22b0e3"
+ integrity sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==
+ dependencies:
+ "@types/d3-path" "*"
+
+"@types/d3-time-format@*":
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2"
+ integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==
+
+"@types/d3-time@*":
+ version "3.0.4"
+ resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.4.tgz#8472feecd639691450dd8000eb33edd444e1323f"
+ integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==
+
+"@types/d3-timer@*":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70"
+ integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
+
+"@types/d3-transition@*":
+ version "3.0.9"
+ resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.9.tgz#1136bc57e9ddb3c390dccc9b5ff3b7d2b8d94706"
+ integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-zoom@*":
+ version "3.0.8"
+ resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b"
+ integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==
+ dependencies:
+ "@types/d3-interpolate" "*"
+ "@types/d3-selection" "*"
+
+"@types/d3@^7.4.0":
+ version "7.4.3"
+ resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2"
+ integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==
+ dependencies:
+ "@types/d3-array" "*"
+ "@types/d3-axis" "*"
+ "@types/d3-brush" "*"
+ "@types/d3-chord" "*"
+ "@types/d3-color" "*"
+ "@types/d3-contour" "*"
+ "@types/d3-delaunay" "*"
+ "@types/d3-dispatch" "*"
+ "@types/d3-drag" "*"
+ "@types/d3-dsv" "*"
+ "@types/d3-ease" "*"
+ "@types/d3-fetch" "*"
+ "@types/d3-force" "*"
+ "@types/d3-format" "*"
+ "@types/d3-geo" "*"
+ "@types/d3-hierarchy" "*"
+ "@types/d3-interpolate" "*"
+ "@types/d3-path" "*"
+ "@types/d3-polygon" "*"
+ "@types/d3-quadtree" "*"
+ "@types/d3-random" "*"
+ "@types/d3-scale" "*"
+ "@types/d3-scale-chromatic" "*"
+ "@types/d3-selection" "*"
+ "@types/d3-shape" "*"
+ "@types/d3-time" "*"
+ "@types/d3-time-format" "*"
+ "@types/d3-timer" "*"
+ "@types/d3-transition" "*"
+ "@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==
+
"@types/debug@^4.0.0":
version "4.1.12"
resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz"
@@ -5288,6 +5555,11 @@
"@types/qs" "*"
"@types/serve-static" "^1"
+"@types/geojson@*":
+ version "7946.0.16"
+ resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.16.tgz#8ebe53d69efada7044454e3305c19017d97ced2a"
+ integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==
+
"@types/gtag.js@^0.0.12":
version "0.0.12"
resolved "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz"
@@ -5449,6 +5721,11 @@
resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz"
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
+"@types/react-dom@^18.2.17":
+ version "18.3.7"
+ resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.7.tgz#b89ddf2cd83b4feafcc4e2ea41afdfb95a0d194f"
+ integrity sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==
+
"@types/react-router-config@*", "@types/react-router-config@^5.0.7":
version "5.0.11"
resolved "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.11.tgz"
@@ -5484,6 +5761,19 @@
"@types/scheduler" "*"
csstype "^3.0.2"
+"@types/react@^18.2.43":
+ version "18.3.27"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.27.tgz#74a3b590ea183983dc65a474dc17553ae1415c34"
+ integrity sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==
+ dependencies:
+ "@types/prop-types" "*"
+ csstype "^3.2.2"
+
+"@types/resize-observer-browser@^0.1.7":
+ version "0.1.11"
+ resolved "https://registry.yarnpkg.com/@types/resize-observer-browser/-/resize-observer-browser-0.1.11.tgz#d3c98d788489d8376b7beac23863b1eebdd3c13c"
+ integrity sha512-cNw5iH8JkMkb3QkCoe7DaZiawbDQEUX8t7iuQaRTyLOyQCR2h+ibBD4GJt7p5yhUHrlOeL7ZtbxNHeipqNsBzQ==
+
"@types/retry@0.12.2":
version "0.12.2"
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.2.tgz#ed279a64fa438bb69f2480eda44937912bb7480a"
@@ -5593,7 +5883,7 @@
dependencies:
"@types/yargs-parser" "*"
-"@typescript-eslint/eslint-plugin@^6.0.0":
+"@typescript-eslint/eslint-plugin@^6.0.0", "@typescript-eslint/eslint-plugin@^6.14.0":
version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3"
integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==
@@ -5624,7 +5914,7 @@
natural-compare "^1.4.0"
ts-api-utils "^2.4.0"
-"@typescript-eslint/parser@^6.0.0":
+"@typescript-eslint/parser@^6.0.0", "@typescript-eslint/parser@^6.14.0":
version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b"
integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==
@@ -5790,6 +6080,18 @@
resolved "https://registry.yarnpkg.com/@vercel/oidc/-/oidc-3.1.0.tgz#066caee449b84079f33c7445fc862464fe10ec32"
integrity sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==
+"@vitejs/plugin-react@^4.2.1":
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz#647af4e7bb75ad3add578e762ad984b90f4a24b9"
+ integrity sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==
+ dependencies:
+ "@babel/core" "^7.28.0"
+ "@babel/plugin-transform-react-jsx-self" "^7.27.1"
+ "@babel/plugin-transform-react-jsx-source" "^7.27.1"
+ "@rolldown/pluginutils" "1.0.0-beta.27"
+ "@types/babel__core" "^7.20.5"
+ react-refresh "^0.17.0"
+
"@vitest/coverage-v8@^2.1.8":
version "2.1.9"
resolved "https://registry.yarnpkg.com/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz#060bebfe3705c1023bdc220e17fdea4bd9e2b24d"
@@ -7312,6 +7614,11 @@ ci-info@^4.0.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.3.1.tgz#355ad571920810b5623e11d40232f443f16f1daa"
integrity sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==
+classcat@^5.0.3:
+ version "5.0.5"
+ resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.5.tgz#8c209f359a93ac302404a10161b501eba9c09c77"
+ integrity sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==
+
clean-css@^5.2.2, clean-css@^5.3.3, clean-css@~5.3.2:
version "5.3.3"
resolved "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz"
@@ -8027,6 +8334,81 @@ csstype@^3.0.2:
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
+csstype@^3.2.2:
+ version "3.2.3"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
+ integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
+
+"d3-color@1 - 3":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2"
+ integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+"d3-dispatch@1 - 3":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e"
+ integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
+
+"d3-drag@2 - 3", d3-drag@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba"
+ integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-selection "3"
+
+"d3-ease@1 - 3":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
+ integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+"d3-interpolate@1 - 3":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
+ integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+ dependencies:
+ d3-color "1 - 3"
+
+"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
+ integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
+
+"d3-timer@1 - 3":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0"
+ integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
+"d3-transition@2 - 3":
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f"
+ integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
+ dependencies:
+ d3-color "1 - 3"
+ d3-dispatch "1 - 3"
+ d3-ease "1 - 3"
+ d3-interpolate "1 - 3"
+ d3-timer "1 - 3"
+
+d3-zoom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3"
+ integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-drag "2 - 3"
+ d3-interpolate "1 - 3"
+ d3-selection "2 - 3"
+ d3-transition "2 - 3"
+
+dagre@^0.8.5:
+ version "0.8.5"
+ resolved "https://registry.yarnpkg.com/dagre/-/dagre-0.8.5.tgz#ba30b0055dac12b6c1fcc247817442777d06afee"
+ integrity sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==
+ dependencies:
+ graphlib "^2.1.8"
+ lodash "^4.17.15"
+
dargs@^7.0.0:
version "7.0.0"
resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz"
@@ -8705,6 +9087,16 @@ escape-string-regexp@^5.0.0:
resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz"
integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
+eslint-plugin-react-hooks@^4.6.0:
+ version "4.6.2"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596"
+ integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==
+
+eslint-plugin-react-refresh@^0.4.5:
+ version "0.4.26"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz#2bcdd109ea9fb4e0b56bb1b5146cf8841b21b626"
+ integrity sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==
+
eslint-scope@5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz"
@@ -8731,7 +9123,7 @@ eslint-visitor-keys@^4.2.1:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1"
integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
-eslint@^8.0.0, eslint@^8.57.1:
+eslint@^8.0.0, eslint@^8.55.0, eslint@^8.57.1:
version "8.57.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9"
integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
@@ -9677,6 +10069,13 @@ graphemer@^1.4.0:
resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6"
integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
+graphlib@^2.1.8:
+ version "2.1.8"
+ resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
+ integrity sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==
+ dependencies:
+ lodash "^4.17.15"
+
gray-matter@^4.0.3:
version "4.0.3"
resolved "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz"
@@ -11263,7 +11662,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz"
integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==
-lodash@^4.17.14, lodash@^4.17.20, lodash@^4.17.21:
+lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -14345,7 +14744,7 @@ rc@1.2.8:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
-react-dom@^18.3.1:
+react-dom@^18.2.0, react-dom@^18.3.1:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
@@ -14358,6 +14757,20 @@ react-fast-compare@^3.2.0:
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz"
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
+react-flow-renderer@^10.3.17:
+ version "10.3.17"
+ resolved "https://registry.yarnpkg.com/react-flow-renderer/-/react-flow-renderer-10.3.17.tgz#06d6ecef5559ba5d3e64d2c8dcb74c43071d62b1"
+ integrity sha512-bywiqVErlh5kCDqw3x0an5Ur3mT9j9CwJsDwmhmz4i1IgYM1a0SPqqEhClvjX+s5pU4nHjmVaGXWK96pwsiGcQ==
+ dependencies:
+ "@babel/runtime" "^7.18.9"
+ "@types/d3" "^7.4.0"
+ "@types/resize-observer-browser" "^0.1.7"
+ classcat "^5.0.3"
+ d3-drag "^3.0.0"
+ d3-selection "^3.0.0"
+ d3-zoom "^3.0.0"
+ zustand "^3.7.2"
+
"react-helmet-async@npm:@slorber/react-helmet-async@1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz#11fbc6094605cf60aa04a28c17e0aab894b4ecff"
@@ -14420,6 +14833,11 @@ react-monaco-editor@^0.59.0:
resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.59.0.tgz#a3cdef4a47fd0cb899f412c9d66b365c51a76096"
integrity sha512-SggqfZCdUauNk7GI0388bk5n25zYsQ1ai1i+VhxAgwbCH+MTGl7L1fBNTJ6V+oXeUApf+bpzikprHJEZm9J/zA==
+react-refresh@^0.17.0:
+ version "0.17.0"
+ resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.17.0.tgz#b7e579c3657f23d04eccbe4ad2e58a8ed51e7e53"
+ integrity sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==
+
react-router-config@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/react-router-config/-/react-router-config-5.1.1.tgz"
@@ -14455,7 +14873,7 @@ react-router@5.3.4, react-router@^5.3.4:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
-react@^18.3.1:
+react@^18.2.0, react@^18.3.1:
version "18.3.1"
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
@@ -16290,7 +16708,7 @@ typedarray@^0.0.6:
resolved "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz"
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
-typescript@^5.0.0, typescript@^5.9.3:
+typescript@^5.0.0, typescript@^5.2.2, typescript@^5.9.3:
version "5.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
@@ -16634,6 +17052,11 @@ 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@2.1.9:
version "2.1.9"
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.9.tgz#549710f76a643f1c39ef34bdb5493a944e4f895f"
@@ -16656,7 +17079,7 @@ vite-node@3.2.4:
pathe "^2.0.3"
vite "^5.0.0 || ^6.0.0 || ^7.0.0-0"
-vite@^5.0.0:
+vite@^5.0.0, vite@^5.0.8:
version "5.4.21"
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.21.tgz#84a4f7c5d860b071676d39ba513c0d598fdc7027"
integrity sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==
@@ -17241,6 +17664,11 @@ zod@^4.1.8:
resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.5.tgz#aeb269a6f9fc259b1212c348c7c5432aaa474d2a"
integrity sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==
+zustand@^3.7.2:
+ version "3.7.2"
+ resolved "https://registry.yarnpkg.com/zustand/-/zustand-3.7.2.tgz#7b44c4f4a5bfd7a8296a3957b13e1c346f42514d"
+ integrity sha512-PIJDIZKtokhof+9+60cpockVOq05sJzHCriyvaLBmEJixseQ1a5Kdov6fWZfWOu5SK9c+FhH1jU0tntLxRJYMA==
+
zwitch@^2.0.0, zwitch@^2.0.4:
version "2.0.4"
resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz"