From c71efdbc51a5be8e02ef149b175eddcb78146a9e Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 15 Jan 2026 16:20:14 -0500 Subject: [PATCH] Add @ethdebug/bug-playground package to monorepo Migrate the BUG language playground from the separate bugc repository. This is a Vite-based React app for interactive BUG code compilation and visualization. Changes: - Copy packages/playground with React components for editor and visualization (AST, IR, CFG, Bytecode views) - Change dev server port from 3000 to 3001 to avoid collision with Docusaurus - Update example paths to reference packages/bugc/examples/ - Add playground watch to bin/start - Exclude playground from vitest coverage (no tests) - Fix lint error in CfgView.tsx (case block braces) - Mark as private package (not published) Run with: yarn workspace @ethdebug/bug-playground start --- bin/start | 3 +- packages/playground/.eslintrc.cjs | 22 + packages/playground/.gitignore | 1 + packages/playground/README.md | 56 ++ packages/playground/index.html | 13 + packages/playground/package.json | 39 + packages/playground/src/App.tsx | 9 + .../src/compiler/CompilerOutput.css | 70 ++ .../src/compiler/CompilerOutput.tsx | 75 ++ .../playground/src/compiler/ErrorView.css | 51 ++ .../playground/src/compiler/ErrorView.tsx | 28 + packages/playground/src/compiler/compile.ts | 84 ++ packages/playground/src/compiler/types.ts | 25 + .../playground/src/compiler/useCompiler.ts | 35 + packages/playground/src/editor/Editor.tsx | 119 +++ packages/playground/src/editor/bugLanguage.ts | 192 ++++ packages/playground/src/index.css | 94 ++ packages/playground/src/main.tsx | 10 + .../playground/src/playground/Playground.css | 83 ++ .../playground/src/playground/Playground.tsx | 94 ++ .../playground/src/playground/examples.ts | 118 +++ .../playground/src/visualization/AstView.css | 15 + .../playground/src/visualization/AstView.tsx | 24 + .../src/visualization/BytecodeView.css | 111 +++ .../src/visualization/BytecodeView.tsx | 173 ++++ .../playground/src/visualization/CfgView.css | 195 ++++ .../playground/src/visualization/CfgView.tsx | 457 ++++++++++ .../src/visualization/EthdebugTooltip.css | 71 ++ .../src/visualization/EthdebugTooltip.tsx | 127 +++ .../playground/src/visualization/IrView.css | 140 +++ .../playground/src/visualization/IrView.tsx | 834 ++++++++++++++++++ .../src/visualization/debugUtils.ts | 114 +++ .../src/visualization/formatBytecode.ts | 173 ++++ .../src/visualization/irDebugUtils.ts | 217 +++++ packages/playground/src/vite-env.d.ts | 10 + packages/playground/tsconfig.json | 26 + packages/playground/tsconfig.node.json | 10 + packages/playground/vite.config.ts | 25 + vitest.config.ts | 1 + yarn.lock | 478 +++++++++- 40 files changed, 4396 insertions(+), 26 deletions(-) create mode 100644 packages/playground/.eslintrc.cjs create mode 100644 packages/playground/.gitignore create mode 100644 packages/playground/README.md create mode 100644 packages/playground/index.html create mode 100644 packages/playground/package.json create mode 100644 packages/playground/src/App.tsx create mode 100644 packages/playground/src/compiler/CompilerOutput.css create mode 100644 packages/playground/src/compiler/CompilerOutput.tsx create mode 100644 packages/playground/src/compiler/ErrorView.css create mode 100644 packages/playground/src/compiler/ErrorView.tsx create mode 100644 packages/playground/src/compiler/compile.ts create mode 100644 packages/playground/src/compiler/types.ts create mode 100644 packages/playground/src/compiler/useCompiler.ts create mode 100644 packages/playground/src/editor/Editor.tsx create mode 100644 packages/playground/src/editor/bugLanguage.ts create mode 100644 packages/playground/src/index.css create mode 100644 packages/playground/src/main.tsx create mode 100644 packages/playground/src/playground/Playground.css create mode 100644 packages/playground/src/playground/Playground.tsx create mode 100644 packages/playground/src/playground/examples.ts create mode 100644 packages/playground/src/visualization/AstView.css create mode 100644 packages/playground/src/visualization/AstView.tsx create mode 100644 packages/playground/src/visualization/BytecodeView.css create mode 100644 packages/playground/src/visualization/BytecodeView.tsx create mode 100644 packages/playground/src/visualization/CfgView.css create mode 100644 packages/playground/src/visualization/CfgView.tsx create mode 100644 packages/playground/src/visualization/EthdebugTooltip.css create mode 100644 packages/playground/src/visualization/EthdebugTooltip.tsx create mode 100644 packages/playground/src/visualization/IrView.css create mode 100644 packages/playground/src/visualization/IrView.tsx create mode 100644 packages/playground/src/visualization/debugUtils.ts create mode 100644 packages/playground/src/visualization/formatBytecode.ts create mode 100644 packages/playground/src/visualization/irDebugUtils.ts create mode 100644 packages/playground/src/vite-env.d.ts create mode 100644 packages/playground/tsconfig.json create mode 100644 packages/playground/tsconfig.node.json create mode 100644 packages/playground/vite.config.ts 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 ( +
+
{astJson}
+
+ ); +} 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 +
+
+ +
+
+

Hex

+
{runtimeHex}
+
+ +
+

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"