diff --git a/.eslintignore b/.eslintignore
deleted file mode 100644
index a57f5c729..000000000
--- a/.eslintignore
+++ /dev/null
@@ -1,13 +0,0 @@
-node_modules/
-dist/
-coverage/
-*.d.ts
-*.config.js
-*.config.ts
-
-# Auto-generated files
-packages/format/src/schemas/yamls.ts
-
-# Docusaurus
-packages/web/.docusaurus/
-packages/web/build/
diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index 76fdd6032..000000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,49 +0,0 @@
-{
- "parser": "@typescript-eslint/parser",
- "plugins": ["@typescript-eslint"],
- "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
- "rules": {
- "@typescript-eslint/no-explicit-any": "warn",
- "@typescript-eslint/no-unused-vars": [
- "error",
- { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }
- ],
- "@typescript-eslint/no-namespace": "off",
- "@typescript-eslint/no-empty-object-type": "off",
- "no-console": "warn",
- "require-yield": "off",
- "no-restricted-imports": [
- "error",
- {
- "patterns": [
- {
- "group": ["../types/*/index*", "../types/*/index"],
- "message": "Use #types/* alias instead of relative imports"
- },
- {
- "group": [
- "../materials/*",
- "../data/*",
- "../pointer/*",
- "../program/*",
- "../type/*"
- ],
- "message": "Use #types/* alias instead of relative imports"
- },
- {
- "group": ["../../test/*", "../../../test/*"],
- "message": "Use #test/* alias instead of relative imports"
- },
- {
- "group": ["../describe*"],
- "message": "Use #describe alias instead of relative imports"
- }
- ]
- }
- ]
- },
- "env": {
- "node": true,
- "es2022": true
- }
-}
diff --git a/bin/start b/bin/start
index 17c20fb61..ffd64a77b 100755
--- a/bin/start
+++ b/bin/start
@@ -10,10 +10,12 @@ else
fi
# Run the commands with concurrently
-concurrently --names=format,pointers,bugc,playground,web,tests \
+concurrently --names=format,pointers,evm,bugc,programs-react,playground,web,tests \
"cd ./packages/format && yarn watch" \
"cd ./packages/pointers && yarn watch" \
+ "cd ./packages/evm && yarn watch" \
"cd ./packages/bugc && yarn watch" \
+ "cd ./packages/programs-react && 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/eslint.config.js b/eslint.config.js
new file mode 100644
index 000000000..a80e6d847
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,79 @@
+import eslint from "@eslint/js";
+import tseslint from "typescript-eslint";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+
+export default tseslint.config(
+ eslint.configs.recommended,
+ ...tseslint.configs.recommended,
+ {
+ files: ["**/*.ts", "**/*.tsx"],
+ plugins: {
+ "react-hooks": reactHooks,
+ },
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ ...globals.es2022,
+ },
+ parserOptions: {
+ projectService: true,
+ },
+ },
+ rules: {
+ "@typescript-eslint/no-explicit-any": "warn",
+ "@typescript-eslint/no-unused-vars": [
+ "error",
+ { varsIgnorePattern: "^_", argsIgnorePattern: "^_" },
+ ],
+ "@typescript-eslint/no-namespace": "off",
+ "@typescript-eslint/no-empty-object-type": "off",
+ "no-console": "warn",
+ "require-yield": "off",
+ "react-hooks/rules-of-hooks": "error",
+ "react-hooks/exhaustive-deps": "warn",
+ "no-restricted-imports": [
+ "error",
+ {
+ patterns: [
+ {
+ group: ["../types/*/index*", "../types/*/index"],
+ message: "Use #types/* alias instead of relative imports",
+ },
+ {
+ group: [
+ "../materials/*",
+ "../data/*",
+ "../pointer/*",
+ "../program/*",
+ "../type/*",
+ ],
+ message: "Use #types/* alias instead of relative imports",
+ },
+ {
+ group: ["../../test/*", "../../../test/*"],
+ message: "Use #test/* alias instead of relative imports",
+ },
+ {
+ group: ["../describe*"],
+ message: "Use #describe alias instead of relative imports",
+ },
+ ],
+ },
+ ],
+ },
+ },
+ {
+ ignores: [
+ "node_modules/",
+ "**/dist/",
+ "coverage/",
+ "**/*.d.ts",
+ "**/*.config.js",
+ "**/*.config.ts",
+ "packages/format/src/schemas/yamls.ts",
+ "packages/web/.docusaurus/",
+ "packages/web/build/",
+ ],
+ },
+);
diff --git a/package.json b/package.json
index c58ffd699..1d299a18f 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"packages/*"
],
"scripts": {
- "build": "tsc --build packages/format packages/pointers packages/bugc",
+ "build": "tsc --build packages/format packages/pointers packages/evm packages/bugc packages/programs-react packages/pointers-react",
"bundle": "tsx ./bin/bundle-schema.ts",
"test": "vitest",
"test:coverage": "vitest run --coverage",
@@ -15,21 +15,25 @@
"prepare": "husky",
"format": "prettier --write .",
"format:check": "prettier --check .",
- "lint": "eslint . --ext .ts,.tsx",
- "lint:fix": "eslint . --ext .ts,.tsx --fix"
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix"
},
"devDependencies": {
+ "@eslint/js": "^9.39.2",
"@typescript-eslint/eslint-plugin": "^8.21.0",
"@typescript-eslint/parser": "^8.21.0",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"concurrently": "^8.2.2",
- "eslint": "^8.57.1",
+ "eslint": "^9.0.0",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "globals": "^17.0.0",
"husky": "^9.1.7",
"lerna": "^8.2.4",
"lint-staged": "^15.4.1",
"prettier": "^3.4.2",
"tsx": "^4.21.0",
+ "typescript-eslint": "^8.53.0",
"vitest": "^3.2.4"
},
"lint-staged": {
diff --git a/packages/bugc-react/package.json b/packages/bugc-react/package.json
new file mode 100644
index 000000000..e3e448fa8
--- /dev/null
+++ b/packages/bugc-react/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "@ethdebug/bugc-react",
+ "version": "0.1.0-0",
+ "description": "React components for visualizing BUG compiler output",
+ "type": "module",
+ "main": "dist/src/index.js",
+ "types": "dist/src/index.d.ts",
+ "license": "MIT",
+ "imports": {
+ "#components/*": {
+ "types": "./src/components/*.tsx",
+ "default": "./dist/src/components/*.js"
+ },
+ "#hooks/*": {
+ "types": "./src/hooks/*.ts",
+ "default": "./dist/src/hooks/*.js"
+ },
+ "#utils/*": {
+ "types": "./src/utils/*.ts",
+ "default": "./dist/src/utils/*.js"
+ },
+ "#types": {
+ "types": "./src/types.ts",
+ "default": "./dist/src/types.js"
+ }
+ },
+ "scripts": {
+ "prepare": "tsc",
+ "build": "tsc",
+ "watch": "tsc --watch --preserveWatchOutput",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "@ethdebug/bugc": "^0.1.0-0"
+ },
+ "devDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@testing-library/react": "^16.0.0",
+ "@types/react": "^18.2.43",
+ "@types/react-dom": "^18.2.17",
+ "jsdom": "^26.0.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "typescript": "^5.0.0",
+ "vitest": "^3.2.4"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ },
+ "optionalDependencies": {
+ "@monaco-editor/react": "^4.7.0",
+ "dagre": "^0.8.5",
+ "react-flow-renderer": "^10.3.17"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/bugc-react/src/components/AstView.css b/packages/bugc-react/src/components/AstView.css
new file mode 100644
index 000000000..453b38ba2
--- /dev/null
+++ b/packages/bugc-react/src/components/AstView.css
@@ -0,0 +1,21 @@
+/**
+ * Styles for AstView component.
+ */
+
+@import "./variables.css";
+
+.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: var(--bugc-text-code);
+ white-space: pre;
+ overflow: auto;
+}
diff --git a/packages/bugc-react/src/components/AstView.tsx b/packages/bugc-react/src/components/AstView.tsx
new file mode 100644
index 000000000..b7744567e
--- /dev/null
+++ b/packages/bugc-react/src/components/AstView.tsx
@@ -0,0 +1,45 @@
+/**
+ * AstView component for displaying the AST of a BUG program.
+ */
+
+import React from "react";
+import type { Ast } from "@ethdebug/bugc";
+
+/**
+ * Props for AstView component.
+ */
+export interface AstViewProps {
+ /** The AST to display */
+ ast: Ast.Program;
+}
+
+/**
+ * Displays a BUG program's Abstract Syntax Tree as formatted JSON.
+ *
+ * Automatically excludes parent references to avoid circular structures.
+ *
+ * @param props - AST to display
+ * @returns AstView element
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export function AstView({ ast }: AstViewProps): JSX.Element {
+ // Format AST as JSON, excluding parent references to avoid circular structure
+ const astJson = JSON.stringify(
+ ast,
+ (key, value) => {
+ if (key === "parent") return undefined;
+ return value;
+ },
+ 2,
+ );
+
+ return (
+
+ );
+}
diff --git a/packages/bugc-react/src/components/BytecodeView.css b/packages/bugc-react/src/components/BytecodeView.css
new file mode 100644
index 000000000..b66318e6e
--- /dev/null
+++ b/packages/bugc-react/src/components/BytecodeView.css
@@ -0,0 +1,115 @@
+/**
+ * Styles for BytecodeView component.
+ */
+
+@import "./variables.css";
+
+.bytecode-view {
+ height: 100%;
+ overflow: auto;
+}
+
+.bytecode-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+ background-color: var(--bugc-bg-secondary);
+ border-bottom: 1px solid var(--bugc-border-primary);
+ position: sticky;
+ top: 0;
+ z-index: 1;
+}
+
+.bytecode-header h3 {
+ margin: 0;
+ font-size: 1rem;
+ color: var(--bugc-text-primary);
+}
+
+.bytecode-stats {
+ display: flex;
+ gap: 1rem;
+ font-size: 0.813rem;
+ color: var(--bugc-text-secondary);
+}
+
+.bytecode-content {
+ padding: 1rem;
+}
+
+.bytecode-section {
+ margin-bottom: 2rem;
+}
+
+.bytecode-section h4 {
+ margin: 0 0 0.5rem 0;
+ font-size: 0.875rem;
+ color: var(--bugc-text-primary);
+}
+
+.bytecode-hex,
+.bytecode-disassembly {
+ margin: 0;
+ padding: 1rem;
+ background-color: var(--bugc-bg-code);
+ border: 1px solid var(--bugc-border-primary);
+ border-radius: 4px;
+ font-family: "Consolas", "Monaco", "Courier New", monospace;
+ font-size: 0.813rem;
+ line-height: 1.5;
+ color: var(--bugc-text-code);
+ 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 var(--bugc-border-primary);
+}
+
+.bytecode-disassembly-interactive {
+ padding: 1rem;
+ background-color: var(--bugc-bg-code);
+ border: 1px solid var(--bugc-border-primary);
+ 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: var(--bugc-bg-hover);
+}
+
+.opcode-line .pc {
+ color: var(--bugc-syntax-address);
+ min-width: 3rem;
+ text-align: right;
+}
+
+.opcode-line .opcode {
+ color: var(--bugc-syntax-opcode);
+ min-width: 6rem;
+ font-weight: 500;
+}
+
+.opcode-line .immediates {
+ color: var(--bugc-syntax-number);
+}
diff --git a/packages/bugc-react/src/components/BytecodeView.tsx b/packages/bugc-react/src/components/BytecodeView.tsx
new file mode 100644
index 000000000..441b0f5e0
--- /dev/null
+++ b/packages/bugc-react/src/components/BytecodeView.tsx
@@ -0,0 +1,204 @@
+/**
+ * BytecodeView component for displaying compiled EVM bytecode.
+ */
+
+import React from "react";
+import type { Evm } from "@ethdebug/bugc";
+import type { BytecodeOutput, SourceRange } from "#types";
+import { extractSourceRange } from "#utils/debugUtils";
+import { useEthdebugTooltip } from "#hooks/useEthdebugTooltip";
+import { EthdebugTooltip } from "./EthdebugTooltip.js";
+
+/**
+ * Props for BytecodeView component.
+ */
+export interface BytecodeViewProps {
+ /** Compiled bytecode output */
+ bytecode: BytecodeOutput;
+ /** Callback when hovering over an opcode with source ranges */
+ onOpcodeHover?: (ranges: SourceRange[]) => void;
+}
+
+interface InstructionsViewProps {
+ instructions: Evm.Instruction[];
+ onOpcodeHover?: (ranges: SourceRange[]) => void;
+}
+
+function InstructionsView({
+ instructions,
+ onOpcodeHover,
+}: InstructionsViewProps): JSX.Element {
+ 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("")}
+
+ )}
+
+ );
+ })}
+
+
+ );
+}
+
+/**
+ * Displays compiled EVM bytecode with interactive disassembly.
+ *
+ * Shows both hex representation and disassembled instructions.
+ * Supports hovering over opcodes to highlight source ranges.
+ *
+ * @param props - Bytecode output and callbacks
+ * @returns BytecodeView element
+ *
+ * @example
+ * ```tsx
+ * setHighlightedRanges(ranges)}
+ * />
+ * ```
+ */
+export function BytecodeView({
+ bytecode,
+ onOpcodeHover,
+}: BytecodeViewProps): JSX.Element {
+ const runtimeHex = Array.from(bytecode.runtime)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("");
+
+ const constructorHex = bytecode.create
+ ? Array.from(bytecode.create)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("")
+ : null;
+
+ return (
+
+ {bytecode.create && (
+ <>
+
+
Constructor Bytecode
+
+ Size: {bytecode.create.length / 2} bytes
+
+
+
+
+
+
Hex
+
{constructorHex}
+
+
+
+
Instructions
+ {bytecode.createInstructions && (
+
+ )}
+
+
+
+
+ >
+ )}
+
+
+
{bytecode.create ? "Runtime Bytecode" : "EVM Bytecode"}
+
+ Size: {bytecode.runtime.length / 2} bytes
+
+
+
+
+
+
+
+
Instructions
+
+
+
+
+ );
+}
diff --git a/packages/bugc-react/src/components/CfgView.css b/packages/bugc-react/src/components/CfgView.css
new file mode 100644
index 000000000..16e51dba0
--- /dev/null
+++ b/packages/bugc-react/src/components/CfgView.css
@@ -0,0 +1,206 @@
+/**
+ * Styles for CfgView component.
+ */
+
+@import "./variables.css";
+
+.cfg-view {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.cfg-header {
+ padding: 1rem;
+ border-bottom: 1px solid var(--bugc-border-primary);
+ background: var(--bugc-bg-secondary);
+}
+
+.cfg-header h3 {
+ margin: 0;
+ font-size: 1.1rem;
+ color: var(--bugc-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(--bugc-border-primary);
+ padding: 1rem;
+ overflow-y: auto;
+ background: var(--bugc-bg-secondary);
+ color: var(--bugc-text-primary);
+}
+
+.cfg-sidebar h4 {
+ margin: 0 0 1rem 0;
+ font-size: 1rem;
+ color: var(--bugc-text-primary);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.cfg-sidebar h5 {
+ margin: 0.5rem 0;
+ font-size: 0.9rem;
+ color: var(--bugc-text-secondary);
+}
+
+.cfg-sidebar-close {
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ cursor: pointer;
+ color: var(--bugc-text-secondary);
+ 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: var(--bugc-bg-hover);
+ color: var(--bugc-text-primary);
+}
+
+/* Custom node styles */
+.cfg-node {
+ background: var(--bugc-cfg-node-bg);
+ border: 2px solid var(--bugc-cfg-node-border);
+ border-radius: 8px;
+ padding: 10px 15px;
+ min-width: 120px;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.cfg-node.entry {
+ border-color: var(--bugc-cfg-entry-border);
+ background: var(--bugc-cfg-entry-bg);
+}
+
+.cfg-node.selected {
+ border-width: 3px;
+ box-shadow: 0 0 0 2px rgba(9, 105, 218, 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;
+}
+
+.cfg-view .entry-badge {
+ background: var(--bugc-accent-green);
+ color: white;
+ font-size: 10px;
+ padding: 2px 6px;
+ border-radius: 4px;
+ text-transform: uppercase;
+}
+
+.cfg-node-stats {
+ font-size: 12px;
+ color: var(--bugc-text-secondary);
+}
+
+/* Instruction display */
+.block-instructions {
+ margin-top: 1rem;
+}
+
+.instruction-list {
+ background: var(--bugc-bg-primary);
+ border: 1px solid var(--bugc-border-primary);
+ 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: var(--bugc-text-primary);
+}
+
+.cfg-view .instruction {
+ margin: 0.25rem 0;
+ padding: 0.125rem 0;
+}
+
+.cfg-view .instruction.terminator {
+ margin-top: 0.5rem;
+ padding-top: 0.5rem;
+ border-top: 1px dashed var(--bugc-border-primary);
+ color: var(--bugc-accent-red);
+ 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: var(--bugc-cfg-node-border);
+ border: 2px solid var(--bugc-bg-primary);
+}
+
+.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;
+}
+
+/* React Flow background in light/dark mode */
+.cfg-view .react-flow__background {
+ background-color: var(--bugc-bg-primary);
+}
diff --git a/packages/bugc-react/src/components/CfgView.tsx b/packages/bugc-react/src/components/CfgView.tsx
new file mode 100644
index 000000000..dba9b592a
--- /dev/null
+++ b/packages/bugc-react/src/components/CfgView.tsx
@@ -0,0 +1,562 @@
+/**
+ * CfgView component for displaying control flow graphs.
+ *
+ * Note: This component requires optional peer dependencies:
+ * - react-flow-renderer
+ * - dagre
+ */
+
+import React, { useMemo, useCallback, useState, useEffect } from "react";
+import type { Ir } from "@ethdebug/bugc";
+
+/**
+ * Props for CfgView component.
+ */
+export interface CfgViewProps {
+ /** The IR module to visualize */
+ ir: Ir.Module;
+ /** Show comparison view (not implemented) */
+ showComparison?: boolean;
+ /** IR for comparison (not implemented) */
+ comparisonIr?: Ir.Module;
+}
+
+interface BlockNodeData {
+ label: string;
+ block: Ir.Block;
+ isEntry: boolean;
+ instructionCount: number;
+ functionName?: string;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+let rfModule: any;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+let dagreModule: any;
+let dependenciesLoaded = false;
+
+async function loadDependencies(): Promise {
+ if (dependenciesLoaded) {
+ return true;
+ }
+
+ try {
+ rfModule = await import("react-flow-renderer");
+ dagreModule = await import("dagre");
+ dependenciesLoaded = true;
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+function BlockNodeComponent(props: {
+ data: BlockNodeData;
+ selected: boolean;
+}): JSX.Element {
+ const { data, selected } = props;
+
+ if (!rfModule) {
+ return Loading...
;
+ }
+
+ const { Handle, Position } = rfModule;
+
+ return (
+
+
+
+
+
+ {data.functionName}::{data.label}
+
+ {data.isEntry && entry }
+
+
+ {data.instructionCount} instruction
+ {data.instructionCount !== 1 ? "s" : ""}
+
+
+
+
+ );
+}
+
+function CfgViewContent({ ir }: CfgViewProps): JSX.Element {
+ const [selectedNode, setSelectedNode] = useState(null);
+
+ if (!rfModule || !dagreModule) {
+ return (
+
+
+
Control Flow Graph
+
+
+
Loading dependencies...
+
+
+ );
+ }
+
+ const {
+ default: ReactFlow,
+ Controls,
+ Background,
+ useNodesState,
+ useEdgesState,
+ useReactFlow,
+ MarkerType,
+ } = rfModule;
+
+ const dagre = dagreModule;
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const reactFlow = useReactFlow();
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const { initialNodes, initialEdges } = useMemo(() => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const nodes: any[] = [];
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const edges: any[] = [];
+
+ const processFunction = (func: Ir.Function, funcName: string) => {
+ const blockEntries = Array.from(func.blocks.entries());
+
+ blockEntries.forEach(([blockId, block]) => {
+ const nodeId = `${funcName}:${blockId}`;
+
+ nodes.push({
+ id: nodeId,
+ type: "block",
+ position: { x: 0, y: 0 },
+ data: {
+ label: blockId,
+ block,
+ isEntry: blockId === func.entry,
+ instructionCount: block.instructions.length + 1,
+ functionName: funcName,
+ },
+ });
+ });
+
+ 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") {
+ 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",
+ },
+ });
+ }
+ });
+ };
+
+ 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");
+
+ 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);
+
+ const layoutedNodes = nodes.map((node) => {
+ const nodeWithPosition = dagreGraph.node(node.id);
+ return {
+ ...node,
+ position: {
+ x: nodeWithPosition.x - 100,
+ y: nodeWithPosition.y - 40,
+ },
+ };
+ });
+
+ return { initialNodes: layoutedNodes, initialEdges: edges };
+ }, [ir, MarkerType, dagre]);
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [nodesState, setNodes, onNodesChange] = useNodesState(initialNodes);
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const [edgesState, setEdges, onEdgesChange] = useEdgesState(initialEdges);
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ useEffect(() => {
+ setNodes(initialNodes);
+ setEdges(initialEdges);
+ setTimeout(() => {
+ reactFlow?.fitView?.({ padding: 0.2, minZoom: 0.1, maxZoom: 2 });
+ }, 50);
+ }, [initialNodes, initialEdges, setNodes, setEdges, reactFlow]);
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const onNodeClick = useCallback(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (_event: React.MouseEvent, node: any) => {
+ setSelectedNode(node.id);
+ },
+ [],
+ );
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const selectedBlock = useMemo(() => {
+ if (!selectedNode) return null;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const node = nodesState.find((n: any) => n.id === selectedNode);
+ return node?.data.block ?? null;
+ }, [selectedNode, nodesState]);
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const selectedBlockName = useMemo(() => {
+ if (!selectedNode || selectedNode.includes("-label")) return null;
+ return selectedNode.replace(":", "::");
+ }, [selectedNode]);
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const formatInstruction = useCallback((inst: Ir.Instruction): 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 (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 as Ir.Instruction.Env;
+ 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})`;
+ }
+ return `unknown compute_slot`;
+ }
+ default: {
+ const unknownInst = inst as { dest?: string; kind?: string };
+ return `${unknownInst.dest || "?"} = ${unknownInst.kind || "unknown"}(...)`;
+ }
+ }
+ }, []);
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ 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`;
+ }
+ }, []);
+
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const nodeTypes = useMemo(() => ({ block: BlockNodeComponent }), []);
+
+ return (
+
+
+
Control Flow Graph
+
+
+
+
+
+
+
+
+ {selectedBlock && selectedBlockName && (
+
+
+ Block {selectedBlockName}
+ setSelectedNode(null)}
+ aria-label="Close sidebar"
+ >
+ ×
+
+
+
+
Instructions:
+
+ {selectedBlock.instructions.map(
+ (inst: Ir.Instruction, i: number) => (
+
+ {formatInstruction(inst)}
+
+ ),
+ )}
+
+ {formatTerminator(selectedBlock.terminator)}
+
+
+
+
+ )}
+
+
+ );
+}
+
+/**
+ * Displays a control flow graph visualization of the IR.
+ *
+ * Requires optional peer dependencies: react-flow-renderer, dagre
+ *
+ * @param props - IR module and options
+ * @returns CfgView element
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export function CfgView(props: CfgViewProps): JSX.Element {
+ const [loaded, setLoaded] = useState(dependenciesLoaded);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!loaded) {
+ loadDependencies()
+ .then((success) => {
+ if (success) {
+ setLoaded(true);
+ } else {
+ setError(
+ "CfgView requires react-flow-renderer and dagre packages. " +
+ "Please install them: npm install react-flow-renderer dagre",
+ );
+ }
+ })
+ .catch(() => {
+ setError("Failed to load CfgView dependencies");
+ });
+ }
+ }, [loaded]);
+
+ if (error) {
+ return (
+
+
+
Control Flow Graph
+
+
+
+ );
+ }
+
+ if (!loaded || !rfModule) {
+ return (
+
+
+
Control Flow Graph
+
+
+
+ );
+ }
+
+ const { ReactFlowProvider } = rfModule;
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/bugc-react/src/components/Editor.tsx b/packages/bugc-react/src/components/Editor.tsx
new file mode 100644
index 000000000..47f5c7872
--- /dev/null
+++ b/packages/bugc-react/src/components/Editor.tsx
@@ -0,0 +1,242 @@
+/**
+ * Monaco editor component for editing BUG source code.
+ *
+ * Note: This component requires optional peer dependency:
+ * - @monaco-editor/react
+ */
+
+import React, { useEffect, useRef, useState } from "react";
+import { registerBugLanguage } from "#utils/bugLanguage";
+
+/**
+ * Represents a range in the source by offset and length.
+ */
+export interface EditorSourceRange {
+ /** Starting byte offset */
+ offset: number;
+ /** Length in bytes */
+ length: number;
+}
+
+/**
+ * Props for Editor component.
+ */
+export interface EditorProps {
+ /** Current source code value */
+ value: string;
+ /** Callback when source code changes */
+ onChange: (value: string) => void;
+ /** Language mode (default: "bug") */
+ language?: string;
+ /** Ranges to highlight in the editor */
+ highlightedRanges?: EditorSourceRange[];
+ /** Theme mode (default: auto-detect from document) */
+ theme?: "light" | "dark" | "auto";
+ /** Editor height (default: "100%") */
+ height?: string;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+let monacoEditorModule: any;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+let monacoModule: any;
+let monacoLoaded = false;
+
+async function loadMonaco(): Promise {
+ if (monacoLoaded) {
+ return true;
+ }
+
+ try {
+ monacoEditorModule = await import("@monaco-editor/react");
+ monacoModule = await import("monaco-editor");
+ registerBugLanguage(monacoModule);
+ monacoLoaded = true;
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * Monaco editor component for editing BUG source code.
+ *
+ * Supports syntax highlighting, source range highlighting, and auto-theme
+ * detection.
+ *
+ * Requires optional peer dependency: @monaco-editor/react
+ *
+ * @param props - Editor configuration
+ * @returns Editor element
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export function Editor({
+ value,
+ onChange,
+ language = "bug",
+ highlightedRanges = [],
+ theme = "auto",
+ height = "100%",
+}: EditorProps): JSX.Element {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const editorRef = useRef(null);
+ const decorationsRef = useRef([]);
+ const [loaded, setLoaded] = useState(monacoLoaded);
+ const [error, setError] = useState(null);
+
+ // Detect theme from document
+ const [detectedTheme, setDetectedTheme] = useState<"light" | "dark">("dark");
+ useEffect(() => {
+ if (theme === "auto") {
+ const detectTheme = () => {
+ const dataTheme = document.documentElement.getAttribute("data-theme");
+ setDetectedTheme(dataTheme === "dark" ? "dark" : "light");
+ };
+ detectTheme();
+
+ // Watch for theme changes
+ const observer = new MutationObserver(detectTheme);
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["data-theme"],
+ });
+ return () => observer.disconnect();
+ }
+ }, [theme]);
+
+ const effectiveTheme = theme === "auto" ? detectedTheme : theme;
+ const monacoTheme = effectiveTheme === "dark" ? "vs-dark" : "vs";
+
+ // Load Monaco
+ useEffect(() => {
+ if (!loaded) {
+ loadMonaco()
+ .then((success) => {
+ if (success) {
+ setLoaded(true);
+ } else {
+ setError(
+ "Editor requires @monaco-editor/react package. " +
+ "Please install it: npm install @monaco-editor/react",
+ );
+ }
+ })
+ .catch(() => {
+ setError("Failed to load Monaco editor");
+ });
+ }
+ }, [loaded]);
+
+ // Handle decorations for highlighted ranges
+ 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]);
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const handleEditorDidMount = (editor: any) => {
+ editorRef.current = editor;
+ };
+
+ if (error) {
+ return (
+
+ {error}
+
+ );
+ }
+
+ if (!loaded || !monacoEditorModule) {
+ return Loading editor...
;
+ }
+
+ const MonacoEditor = monacoEditorModule.default;
+
+ return (
+ onChange(val || "")}
+ onMount={handleEditorDidMount}
+ options={{
+ minimap: { enabled: false },
+ fontSize: 14,
+ lineNumbers: "on",
+ scrollBeyondLastLine: false,
+ automaticLayout: true,
+ tabSize: 2,
+ }}
+ />
+ );
+}
diff --git a/packages/bugc-react/src/components/EthdebugTooltip.css b/packages/bugc-react/src/components/EthdebugTooltip.css
new file mode 100644
index 000000000..d758d9d57
--- /dev/null
+++ b/packages/bugc-react/src/components/EthdebugTooltip.css
@@ -0,0 +1,76 @@
+/**
+ * Styles for EthdebugTooltip component.
+ */
+
+@import "./variables.css";
+
+.ethdebug-tooltip {
+ position: fixed;
+ z-index: 1000;
+ background-color: var(--bugc-tooltip-bg);
+ border: 1px solid var(--bugc-tooltip-border);
+ border-radius: 4px;
+ padding: 0.5rem;
+ max-width: 600px;
+ max-height: 400px;
+ overflow: auto;
+ pointer-events: none;
+ box-shadow: 0 4px 12px var(--bugc-tooltip-shadow);
+}
+
+.ethdebug-tooltip.pinned {
+ pointer-events: auto;
+ border-color: var(--bugc-tooltip-pinned-border);
+ box-shadow: 0 4px 16px var(--bugc-tooltip-pinned-shadow);
+}
+
+.ethdebug-tooltip pre {
+ margin: 0;
+ font-family: "Courier New", monospace;
+ font-size: 0.75rem;
+ color: var(--bugc-text-code);
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.debug-info-icon {
+ color: var(--bugc-accent-blue);
+ 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: var(--bugc-accent-blue-bg);
+}
+
+.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: var(--bugc-text-code);
+ 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: var(--bugc-bg-hover);
+ color: var(--bugc-text-primary);
+}
diff --git a/packages/bugc-react/src/components/EthdebugTooltip.tsx b/packages/bugc-react/src/components/EthdebugTooltip.tsx
new file mode 100644
index 000000000..38b7d00a8
--- /dev/null
+++ b/packages/bugc-react/src/components/EthdebugTooltip.tsx
@@ -0,0 +1,150 @@
+/**
+ * Tooltip component for displaying ethdebug debug information.
+ */
+
+import React, { useRef, useEffect } from "react";
+import type { TooltipData } from "#types";
+
+/**
+ * Props for EthdebugTooltip component.
+ */
+export interface EthdebugTooltipProps {
+ /** Tooltip data to display, or null to hide */
+ tooltip: TooltipData | null;
+ /** Callback to update tooltip position */
+ onUpdate?: (tooltip: TooltipData) => void;
+ /** Callback when tooltip is closed */
+ onClose?: () => void;
+}
+
+/**
+ * Displays debug information in a floating tooltip.
+ *
+ * Automatically adjusts position to stay within viewport bounds.
+ * When pinned, the tooltip stays in place and can be closed manually.
+ *
+ * @param props - Tooltip data and callbacks
+ * @returns Tooltip element or null
+ *
+ * @example
+ * ```tsx
+ * const { tooltip, setTooltip, closeTooltip } = useEthdebugTooltip();
+ *
+ *
+ * ```
+ */
+export function EthdebugTooltip({
+ tooltip,
+ onUpdate,
+ onClose,
+}: EthdebugTooltipProps): JSX.Element | null {
+ 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}
+
+ );
+}
+
+/**
+ * Props for DebugInfoIcon component.
+ */
+export interface DebugInfoIconProps {
+ /** Callback when icon is hovered */
+ onMouseEnter: (e: React.MouseEvent) => void;
+ /** Callback when hover ends */
+ onMouseLeave: () => void;
+ /** Callback when icon is clicked (for pinning) */
+ onClick: (e: React.MouseEvent) => void;
+}
+
+/**
+ * Small icon indicating debug information is available.
+ *
+ * @param props - Event handlers
+ * @returns Icon element
+ */
+export function DebugInfoIcon({
+ onMouseEnter,
+ onMouseLeave,
+ onClick,
+}: DebugInfoIconProps): JSX.Element {
+ return (
+
+ ℹ
+
+ );
+}
+
+/**
+ * Spacer element to maintain alignment when no debug icon is shown.
+ */
+export function DebugInfoSpacer(): JSX.Element {
+ return ;
+}
diff --git a/packages/bugc-react/src/components/IrView.css b/packages/bugc-react/src/components/IrView.css
new file mode 100644
index 000000000..e449ed737
--- /dev/null
+++ b/packages/bugc-react/src/components/IrView.css
@@ -0,0 +1,147 @@
+/**
+ * Styles for IrView component.
+ */
+
+@import "./variables.css";
+
+.ir-view {
+ height: 100%;
+ overflow: auto;
+}
+
+.ir-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+ background-color: var(--bugc-bg-secondary);
+ border-bottom: 1px solid var(--bugc-border-primary);
+ position: sticky;
+ top: 0;
+ z-index: 1;
+}
+
+.ir-header h3 {
+ margin: 0;
+ font-size: 1rem;
+ color: var(--bugc-text-primary);
+}
+
+.ir-stats {
+ display: flex;
+ gap: 1rem;
+ font-size: 0.813rem;
+ color: var(--bugc-text-secondary);
+}
+
+.ir-content {
+ padding: 1rem;
+ font-family: "Consolas", "Monaco", "Courier New", monospace;
+ font-size: 0.875rem;
+ line-height: 1.6;
+ color: var(--bugc-text-primary);
+}
+
+.section-label {
+ color: var(--bugc-syntax-comment);
+ 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: var(--bugc-syntax-function);
+ 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: var(--bugc-syntax-type);
+}
+
+.entry-badge {
+ background-color: var(--bugc-accent-green-bg);
+ color: var(--bugc-accent-green);
+ 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: var(--bugc-bg-hover);
+}
+
+.instruction-operation,
+.terminator-operation,
+.phi-operation {
+ flex: 1;
+}
+
+.ir-terminator {
+ color: var(--bugc-syntax-terminator);
+ font-weight: 500;
+}
+
+.ir-phi {
+ color: var(--bugc-syntax-phi);
+ font-style: italic;
+}
+
+.hoverable-part {
+ display: inline;
+ transition: background-color 0.15s ease;
+}
+
+.hoverable-part.has-debug {
+ cursor: pointer;
+ border-bottom: 1px dotted var(--bugc-accent-blue);
+ border-bottom-color: rgba(86, 156, 214, 0.4);
+}
+
+.hoverable-part.has-debug:hover {
+ background-color: var(--bugc-accent-blue-bg);
+ border-bottom-color: var(--bugc-accent-blue);
+}
+
+.debug-info-icon.inline {
+ display: inline;
+ margin-left: 0.25rem;
+ font-size: 0.75rem;
+ vertical-align: super;
+}
diff --git a/packages/bugc-react/src/components/IrView.tsx b/packages/bugc-react/src/components/IrView.tsx
new file mode 100644
index 000000000..fd38f1e1b
--- /dev/null
+++ b/packages/bugc-react/src/components/IrView.tsx
@@ -0,0 +1,842 @@
+/**
+ * IrView component for displaying BUG compiler IR.
+ */
+
+import React, { useMemo } from "react";
+import { Ir } from "@ethdebug/bugc";
+import type { SourceRange } from "#types";
+import {
+ extractInstructionDebug,
+ extractTerminatorDebug,
+ extractPhiDebug,
+ formatMultiLevelDebug,
+ extractAllSourceRanges,
+ extractOperandSourceRanges,
+} from "#utils/irDebugUtils";
+import { useEthdebugTooltip } from "#hooks/useEthdebugTooltip";
+import { EthdebugTooltip } from "./EthdebugTooltip.js";
+
+/**
+ * Props for IrView component.
+ */
+export interface IrViewProps {
+ /** The IR module to display */
+ ir: Ir.Module;
+ /** Callback when hovering over IR elements with source ranges */
+ onOpcodeHover?: (ranges: SourceRange[]) => void;
+}
+
+interface HoverablePartData {
+ text: string;
+ ranges: SourceRange[];
+ className?: string;
+}
+
+function HoverablePart({
+ part,
+ onHover,
+ onLeave,
+ onDebugIconHover,
+ showDebugIcon,
+}: {
+ part: HoverablePartData;
+ onHover: (ranges: SourceRange[]) => void;
+ onLeave: () => void;
+ onDebugIconHover?: (e: React.MouseEvent) => void;
+ showDebugIcon?: boolean;
+}): JSX.Element {
+ return (
+ 0 ? "has-debug" : ""}`}
+ onMouseEnter={() => onHover(part.ranges)}
+ onMouseLeave={onLeave}
+ >
+ {part.text}
+ {showDebugIcon && part.ranges.length > 0 && onDebugIconHover && (
+
+ ℹ
+
+ )}
+
+ );
+}
+
+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 "?";
+ }
+}
+
+function formatType(type: Ir.Type): string {
+ return type.kind;
+}
+
+function formatDest(dest: string, type?: Ir.Type): string {
+ const prefix = dest.startsWith("t") ? "%" : "^";
+ const formattedDest = `${prefix}${dest}`;
+ return type ? `${formattedDest}: ${formatType(type)}` : formattedDest;
+}
+
+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;
+}): JSX.Element {
+ const debugInfo = extractInstructionDebug(instruction);
+ const operationRanges = debugInfo.operation?.context
+ ? extractAllSourceRanges({ operation: debugInfo.operation, operands: [] })
+ : [];
+
+ const parts: (HoverablePartData | 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 (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}
+ ) : (
+
+ ),
+ )}
+
+
+ );
+}
+
+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;
+}): JSX.Element {
+ const debugInfo = extractTerminatorDebug(terminator);
+ const operationRanges = debugInfo.operation?.context
+ ? extractAllSourceRanges({ operation: debugInfo.operation, operands: [] })
+ : [];
+
+ const parts: (HoverablePartData | 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}
+ ) : (
+
+ ),
+ )}
+
+
+ );
+}
+
+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;
+}): JSX.Element {
+ 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: (HoverablePartData | 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}
+ ) : (
+
+ ),
+ )}
+
+
+ );
+}
+
+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;
+}): JSX.Element {
+ return (
+
+
+ {blockId}:
+ {isEntry && entry }
+
+
+ {block.phis.map((phi, idx) => (
+
+ ))}
+ {block.instructions.map((instruction, idx) => (
+
+ ))}
+
+
+
+ );
+}
+
+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;
+}): JSX.Element {
+ 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;
+ if (visited.has(blockId)) return;
+
+ tempMarked.add(blockId);
+
+ const block = func.blocks.get(blockId);
+ if (!block) return;
+
+ 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);
+
+ for (const [blockId, block] of func.blocks) {
+ if (!visited.has(blockId)) {
+ result.push([blockId, block]);
+ }
+ }
+
+ return result;
+ }, [func]);
+
+ return (
+
+
+
{name}:
+
+ {sortedBlocks.map(([blockId, block]) => (
+
+ ))}
+
+ );
+}
+
+/**
+ * Displays the IR (Intermediate Representation) from a BUG compilation.
+ *
+ * Shows functions, blocks, instructions, and terminators with interactive
+ * source highlighting.
+ *
+ * @param props - IR module and callbacks
+ * @returns IrView element
+ *
+ * @example
+ * ```tsx
+ * setHighlightedRanges(ranges)}
+ * />
+ * ```
+ */
+export function IrView({ ir, onOpcodeHover }: IrViewProps): JSX.Element {
+ const {
+ tooltip,
+ setTooltip,
+ showTooltip,
+ pinTooltip,
+ hideTooltip,
+ closeTooltip,
+ } = useEthdebugTooltip();
+
+ const handleHover = (ranges: SourceRange[]) => {
+ onOpcodeHover?.(ranges);
+ };
+
+ const handleLeave = () => {
+ onOpcodeHover?.([]);
+ };
+
+ const mainBlocks = ir.main.blocks.size;
+ const createBlocks = ir.create?.blocks.size || 0;
+ 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/bugc-react/src/components/index.ts b/packages/bugc-react/src/components/index.ts
new file mode 100644
index 000000000..1eb6ca4ff
--- /dev/null
+++ b/packages/bugc-react/src/components/index.ts
@@ -0,0 +1,21 @@
+/**
+ * Component exports for @ethdebug/bugc-react.
+ */
+
+export {
+ EthdebugTooltip,
+ DebugInfoIcon,
+ DebugInfoSpacer,
+ type EthdebugTooltipProps,
+ type DebugInfoIconProps,
+} from "./EthdebugTooltip.js";
+
+export { BytecodeView, type BytecodeViewProps } from "./BytecodeView.js";
+
+export { AstView, type AstViewProps } from "./AstView.js";
+
+export { IrView, type IrViewProps } from "./IrView.js";
+
+export { CfgView, type CfgViewProps } from "./CfgView.js";
+
+export { Editor, type EditorProps, type EditorSourceRange } from "./Editor.js";
diff --git a/packages/bugc-react/src/components/variables.css b/packages/bugc-react/src/components/variables.css
new file mode 100644
index 000000000..490510c66
--- /dev/null
+++ b/packages/bugc-react/src/components/variables.css
@@ -0,0 +1,108 @@
+/**
+ * CSS custom properties for @ethdebug/bugc-react components.
+ *
+ * These variables provide theme-aware styling that integrates with
+ * Docusaurus/Infima theming.
+ */
+
+:root {
+ /* Background colors */
+ --bugc-bg-primary: #ffffff;
+ --bugc-bg-secondary: #f5f6f7;
+ --bugc-bg-code: #f5f6f7;
+ --bugc-bg-hover: rgba(0, 0, 0, 0.05);
+
+ /* Border colors */
+ --bugc-border-primary: #dadde1;
+ --bugc-border-secondary: #eaecef;
+
+ /* Text colors */
+ --bugc-text-primary: #1c1e21;
+ --bugc-text-secondary: #606770;
+ --bugc-text-muted: #898989;
+ --bugc-text-code: #1c1e21;
+
+ /* Syntax highlighting - light theme */
+ --bugc-syntax-keyword: #0550ae;
+ --bugc-syntax-type: #6f42c1;
+ --bugc-syntax-function: #8250df;
+ --bugc-syntax-string: #0a3069;
+ --bugc-syntax-number: #0550ae;
+ --bugc-syntax-comment: #6e7781;
+ --bugc-syntax-opcode: #116329;
+ --bugc-syntax-address: #6e7781;
+ --bugc-syntax-terminator: #cf222e;
+ --bugc-syntax-phi: #0550ae;
+
+ /* Accent colors */
+ --bugc-accent-blue: #0969da;
+ --bugc-accent-blue-bg: rgba(9, 105, 218, 0.1);
+ --bugc-accent-green: #1a7f37;
+ --bugc-accent-green-bg: rgba(26, 127, 55, 0.1);
+ --bugc-accent-red: #cf222e;
+ --bugc-accent-purple: #8250df;
+
+ /* Tooltip */
+ --bugc-tooltip-bg: #ffffff;
+ --bugc-tooltip-border: #dadde1;
+ --bugc-tooltip-shadow: rgba(0, 0, 0, 0.15);
+ --bugc-tooltip-pinned-border: #0969da;
+ --bugc-tooltip-pinned-shadow: rgba(9, 105, 218, 0.2);
+
+ /* CFG specific */
+ --bugc-cfg-node-bg: #ffffff;
+ --bugc-cfg-node-border: #0969da;
+ --bugc-cfg-entry-border: #1a7f37;
+ --bugc-cfg-entry-bg: #dafbe1;
+}
+
+[data-theme="dark"] {
+ /* Background colors */
+ --bugc-bg-primary: #1e1e1e;
+ --bugc-bg-secondary: #2d2d30;
+ --bugc-bg-code: #2d2d30;
+ --bugc-bg-hover: rgba(255, 255, 255, 0.05);
+
+ /* Border colors */
+ --bugc-border-primary: #3e3e42;
+ --bugc-border-secondary: #454545;
+
+ /* Text colors */
+ --bugc-text-primary: #cccccc;
+ --bugc-text-secondary: #969696;
+ --bugc-text-muted: #858585;
+ --bugc-text-code: #d4d4d4;
+
+ /* Syntax highlighting - dark theme */
+ --bugc-syntax-keyword: #569cd6;
+ --bugc-syntax-type: #4ec9b0;
+ --bugc-syntax-function: #dcdcaa;
+ --bugc-syntax-string: #ce9178;
+ --bugc-syntax-number: #b5cea8;
+ --bugc-syntax-comment: #6a9955;
+ --bugc-syntax-opcode: #4ec9b0;
+ --bugc-syntax-address: #858585;
+ --bugc-syntax-terminator: #c586c0;
+ --bugc-syntax-phi: #9cdcfe;
+
+ /* Accent colors */
+ --bugc-accent-blue: #569cd6;
+ --bugc-accent-blue-bg: rgba(86, 156, 214, 0.15);
+ --bugc-accent-green: #4ec9b0;
+ --bugc-accent-green-bg: rgba(78, 201, 176, 0.2);
+ --bugc-accent-red: #f14c4c;
+ --bugc-accent-purple: #c586c0;
+
+ /* Tooltip */
+ --bugc-tooltip-bg: #1e1e1e;
+ --bugc-tooltip-border: #3e3e42;
+ --bugc-tooltip-shadow: rgba(0, 0, 0, 0.4);
+ --bugc-tooltip-pinned-border: #569cd6;
+ --bugc-tooltip-pinned-shadow: rgba(86, 156, 214, 0.3);
+
+ /* CFG specific */
+ --bugc-cfg-node-bg: #2d2d30;
+ --bugc-cfg-node-border: #569cd6;
+ --bugc-cfg-entry-border: #4ec9b0;
+ --bugc-cfg-entry-bg: rgba(78, 201, 176, 0.15);
+}
diff --git a/packages/bugc-react/src/hooks/index.ts b/packages/bugc-react/src/hooks/index.ts
new file mode 100644
index 000000000..e0c7d142d
--- /dev/null
+++ b/packages/bugc-react/src/hooks/index.ts
@@ -0,0 +1,5 @@
+/**
+ * Hook exports for @ethdebug/bugc-react.
+ */
+
+export { useEthdebugTooltip } from "./useEthdebugTooltip.js";
diff --git a/packages/bugc-react/src/hooks/useEthdebugTooltip.ts b/packages/bugc-react/src/hooks/useEthdebugTooltip.ts
new file mode 100644
index 000000000..336b1a688
--- /dev/null
+++ b/packages/bugc-react/src/hooks/useEthdebugTooltip.ts
@@ -0,0 +1,109 @@
+/**
+ * Hook for managing ethdebug tooltip state.
+ */
+
+import React, { useState, useCallback } from "react";
+import type { TooltipData } from "#types";
+
+/**
+ * State and handlers returned by useEthdebugTooltip.
+ */
+export interface UseEthdebugTooltipResult {
+ /** Current tooltip data, if any */
+ tooltip: TooltipData | null;
+ /** Set tooltip data directly */
+ setTooltip: React.Dispatch>;
+ /** Show tooltip at element position */
+ showTooltip: (e: React.MouseEvent, content: string) => void;
+ /** Show and pin tooltip at element position */
+ pinTooltip: (e: React.MouseEvent, content: string) => void;
+ /** Hide tooltip if not pinned */
+ hideTooltip: () => void;
+ /** Close tooltip regardless of pinned state */
+ closeTooltip: () => void;
+}
+
+/**
+ * Hook for managing tooltip display state.
+ *
+ * Provides state and handlers for showing, hiding, pinning, and unpinning
+ * tooltips in debug information displays.
+ *
+ * @returns Tooltip state and control functions
+ *
+ * @example
+ * ```tsx
+ * function MyComponent() {
+ * const { tooltip, setTooltip, showTooltip, hideTooltip, closeTooltip } =
+ * useEthdebugTooltip();
+ *
+ * return (
+ *
+ * showTooltip(e, "Debug info here")}
+ * onMouseLeave={hideTooltip}
+ * onClick={(e) => pinTooltip(e, "Debug info here")}
+ * >
+ * Hover me
+ *
+ *
+ *
+ * );
+ * }
+ * ```
+ */
+export function useEthdebugTooltip(): UseEthdebugTooltipResult {
+ const [tooltip, setTooltip] = useState(null);
+
+ const showTooltip = useCallback(
+ (e: React.MouseEvent, content: string) => {
+ const rect = e.currentTarget.getBoundingClientRect();
+ setTooltip({
+ content,
+ x: rect.left,
+ y: rect.bottom,
+ pinned: false,
+ });
+ },
+ [],
+ );
+
+ const pinTooltip = useCallback(
+ (e: React.MouseEvent, content: string) => {
+ const rect = e.currentTarget.getBoundingClientRect();
+ setTooltip({
+ content,
+ x: rect.left,
+ y: rect.bottom,
+ pinned: true,
+ });
+ },
+ [],
+ );
+
+ const hideTooltip = useCallback(() => {
+ setTooltip((current) => {
+ if (current?.pinned) {
+ return current;
+ }
+ return null;
+ });
+ }, []);
+
+ const closeTooltip = useCallback(() => {
+ setTooltip(null);
+ }, []);
+
+ return {
+ tooltip,
+ setTooltip,
+ showTooltip,
+ pinTooltip,
+ hideTooltip,
+ closeTooltip,
+ };
+}
diff --git a/packages/bugc-react/src/index.ts b/packages/bugc-react/src/index.ts
new file mode 100644
index 000000000..b4151f575
--- /dev/null
+++ b/packages/bugc-react/src/index.ts
@@ -0,0 +1,73 @@
+/**
+ * @ethdebug/bugc-react
+ *
+ * React components for visualizing BUG compiler output.
+ *
+ * @packageDocumentation
+ */
+
+// Types
+export type {
+ SourceRange,
+ BytecodeOutput,
+ SuccessfulCompileResult,
+ FailedCompileResult,
+ CompileResult,
+ TooltipData,
+ TooltipState,
+} from "./types.js";
+
+// Hooks
+export { useEthdebugTooltip } from "#hooks/useEthdebugTooltip";
+export type { UseEthdebugTooltipResult } from "#hooks/useEthdebugTooltip";
+
+// Components
+export {
+ EthdebugTooltip,
+ DebugInfoIcon,
+ DebugInfoSpacer,
+ type EthdebugTooltipProps,
+ type DebugInfoIconProps,
+} from "#components/EthdebugTooltip";
+
+export { BytecodeView, type BytecodeViewProps } from "#components/BytecodeView";
+
+export { AstView, type AstViewProps } from "#components/AstView";
+
+export { IrView, type IrViewProps } from "#components/IrView";
+
+export { CfgView, type CfgViewProps } from "#components/CfgView";
+
+export {
+ Editor,
+ type EditorProps,
+ type EditorSourceRange,
+} from "#components/Editor";
+
+// Utilities
+export {
+ // Debug utilities
+ extractSourceRange,
+ formatDebugContext,
+ hasSourceRange,
+ // IR debug utilities
+ extractInstructionDebug,
+ extractTerminatorDebug,
+ extractPhiDebug,
+ formatMultiLevelDebug,
+ extractAllSourceRanges,
+ extractOperandSourceRanges,
+ type MultiLevelDebugInfo,
+ // Bytecode utilities
+ formatBytecode,
+ getOpcodeName,
+ OPCODES,
+ // BUG language utilities
+ bugKeywords,
+ bugTypeKeywords,
+ bugOperators,
+ bugLanguageId,
+ bugMonarchTokensProvider,
+ bugLanguageConfiguration,
+ registerBugLanguage,
+} from "#utils/index";
diff --git a/packages/bugc-react/src/types.ts b/packages/bugc-react/src/types.ts
new file mode 100644
index 000000000..939524075
--- /dev/null
+++ b/packages/bugc-react/src/types.ts
@@ -0,0 +1,87 @@
+/**
+ * Type definitions for @ethdebug/bugc-react components.
+ */
+
+import type { Ast, Ir, Evm } from "@ethdebug/bugc";
+
+/**
+ * Represents a source range in the original code.
+ */
+export interface SourceRange {
+ /** Starting byte offset */
+ offset: number;
+ /** Length in bytes */
+ length: number;
+}
+
+/**
+ * Output from bytecode compilation.
+ */
+export interface BytecodeOutput {
+ /** Runtime bytecode */
+ runtime: Uint8Array;
+ /** Creation bytecode (optional) */
+ create?: Uint8Array;
+ /** Disassembled runtime instructions */
+ runtimeInstructions: Evm.Instruction[];
+ /** Disassembled creation instructions (optional) */
+ createInstructions?: Evm.Instruction[];
+}
+
+/**
+ * Successful compilation result.
+ */
+export interface SuccessfulCompileResult {
+ success: true;
+ ast: Ast.Program;
+ ir: Ir.Module;
+ bytecode: BytecodeOutput;
+ warnings: string[];
+}
+
+/**
+ * Failed compilation result.
+ */
+export interface FailedCompileResult {
+ success: false;
+ error: string;
+ ast?: Ast.Program;
+ warnings?: string[];
+}
+
+/**
+ * Union of compilation results.
+ */
+export type CompileResult = SuccessfulCompileResult | FailedCompileResult;
+
+/**
+ * Tooltip data for EthdebugTooltip component.
+ */
+export interface TooltipData {
+ /** Content to display */
+ content: string;
+ /** X position */
+ x: number;
+ /** Y position */
+ y: number;
+ /** Whether the tooltip is pinned */
+ pinned?: boolean;
+}
+
+/**
+ * State returned by useEthdebugTooltip hook.
+ */
+export interface TooltipState {
+ /** Current tooltip data, if any */
+ tooltip: TooltipData | null;
+ /** Show tooltip at position */
+ showTooltip: (content: string, x: number, y: number) => void;
+ /** Hide the tooltip */
+ hideTooltip: () => void;
+ /** Pin the tooltip in place */
+ pinTooltip: () => void;
+ /** Unpin the tooltip */
+ unpinTooltip: () => void;
+ /** Whether the tooltip is currently pinned */
+ isPinned: boolean;
+}
diff --git a/packages/bugc-react/src/utils/bugLanguage.ts b/packages/bugc-react/src/utils/bugLanguage.ts
new file mode 100644
index 000000000..90114f379
--- /dev/null
+++ b/packages/bugc-react/src/utils/bugLanguage.ts
@@ -0,0 +1,240 @@
+/**
+ * Monaco editor language configuration for the BUG language.
+ *
+ * This module provides language definition and syntax highlighting for BUG
+ * source code in Monaco editor instances.
+ */
+
+/**
+ * BUG language keywords.
+ */
+export const keywords = [
+ "contract",
+ "function",
+ "let",
+ "if",
+ "else",
+ "while",
+ "return",
+ "storage",
+ "memory",
+ "calldata",
+ "emit",
+ "event",
+ "mapping",
+ "struct",
+ "public",
+ "private",
+ "internal",
+ "external",
+ "view",
+ "pure",
+ "payable",
+ "constant",
+ "immutable",
+];
+
+/**
+ * BUG language type keywords.
+ */
+export const typeKeywords = [
+ "uint256",
+ "uint128",
+ "uint64",
+ "uint32",
+ "uint16",
+ "uint8",
+ "int256",
+ "int128",
+ "int64",
+ "int32",
+ "int16",
+ "int8",
+ "bool",
+ "address",
+ "bytes32",
+ "bytes",
+ "string",
+];
+
+/**
+ * BUG language operators.
+ */
+export const operators = [
+ "=",
+ ">",
+ "<",
+ "!",
+ "~",
+ "?",
+ ":",
+ "==",
+ "<=",
+ ">=",
+ "!=",
+ "&&",
+ "||",
+ "++",
+ "--",
+ "+",
+ "-",
+ "*",
+ "/",
+ "&",
+ "|",
+ "^",
+ "%",
+ "<<",
+ ">>",
+ "+=",
+ "-=",
+ "*=",
+ "/=",
+ "&=",
+ "|=",
+ "^=",
+ "%=",
+ "<<=",
+ ">>=",
+];
+
+/**
+ * Monaco language ID for BUG.
+ */
+export const languageId = "bug";
+
+/**
+ * Monaco Monarch tokenizer configuration for BUG syntax highlighting.
+ */
+export const monarchTokensProvider = {
+ keywords,
+ typeKeywords,
+ operators,
+
+ symbols: /[=>](?!@symbols)/, "@brackets"],
+ [
+ /@symbols/,
+ {
+ cases: {
+ "@operators": "operator",
+ "@default": "",
+ },
+ },
+ ],
+
+ // Numbers
+ [/\d*\.\d+([eE][-+]?\d+)?/, "number.float"],
+ [/0[xX][0-9a-fA-F]+/, "number.hex"],
+ [/\d+/, "number"],
+
+ // Delimiter: after number because of .\d floats
+ [/[;,.]/, "delimiter"],
+
+ // Strings
+ [/"([^"\\]|\\.)*$/, "string.invalid"],
+ [/"/, { token: "string.quote", bracket: "@open", next: "@string" }],
+
+ // Characters
+ [/'[^\\']'/, "string"],
+ [/(')(@escapes)(')/, ["string", "string.escape", "string"]],
+ [/'/, "string.invalid"],
+ ],
+
+ comment: [
+ [/[^/*]+/, "comment"],
+ [/\/\*/, "comment", "@push"],
+ ["\\*/", "comment", "@pop"],
+ [/[/*]/, "comment"],
+ ],
+
+ string: [
+ [/[^\\"]+/, "string"],
+ [/@escapes/, "string.escape"],
+ [/\\./, "string.escape.invalid"],
+ [/"/, { token: "string.quote", bracket: "@close", next: "@pop" }],
+ ],
+
+ whitespace: [
+ [/[ \t\r\n]+/, "white"],
+ [/\/\*/, "comment", "@comment"],
+ [/\/\/.*$/, "comment"],
+ ],
+ },
+};
+
+/**
+ * Monaco language configuration for BUG.
+ */
+export const languageConfiguration = {
+ comments: {
+ lineComment: "//",
+ blockComment: ["/*", "*/"] as [string, string],
+ },
+ brackets: [
+ ["{", "}"],
+ ["[", "]"],
+ ["(", ")"],
+ ] as [string, string][],
+ autoClosingPairs: [
+ { open: "{", close: "}" },
+ { open: "[", close: "]" },
+ { open: "(", close: ")" },
+ { open: '"', close: '"' },
+ { open: "'", close: "'" },
+ ],
+ surroundingPairs: [
+ { open: "{", close: "}" },
+ { open: "[", close: "]" },
+ { open: "(", close: ")" },
+ { open: '"', close: '"' },
+ { open: "'", close: "'" },
+ ],
+};
+
+/**
+ * Register the BUG language with a Monaco editor instance.
+ *
+ * @param monaco - The Monaco editor module
+ */
+export function registerBugLanguage(
+ monaco: typeof import("monaco-editor"),
+): void {
+ // Register the language
+ monaco.languages.register({ id: languageId });
+
+ // Set the language configuration
+ monaco.languages.setLanguageConfiguration(
+ languageId,
+ languageConfiguration as import("monaco-editor").languages.LanguageConfiguration,
+ );
+
+ // Set the tokenizer
+ monaco.languages.setMonarchTokensProvider(
+ languageId,
+ monarchTokensProvider as import("monaco-editor").languages.IMonarchLanguage,
+ );
+}
diff --git a/packages/bugc-react/src/utils/debugUtils.ts b/packages/bugc-react/src/utils/debugUtils.ts
new file mode 100644
index 000000000..bf7485558
--- /dev/null
+++ b/packages/bugc-react/src/utils/debugUtils.ts
@@ -0,0 +1,95 @@
+/**
+ * Utilities for extracting debug information from ethdebug format contexts.
+ */
+
+import type { SourceRange } from "#types";
+
+/**
+ * Extract source ranges from an ethdebug format context object.
+ *
+ * Handles various context types including:
+ * - code: Direct source range
+ * - gather: Multiple code regions
+ * - pick: Conditional selection
+ * - frame: Nested context
+ *
+ * @param context - The ethdebug context object
+ * @returns Array of source ranges
+ */
+export function extractSourceRange(
+ context: unknown | undefined,
+): SourceRange[] {
+ if (!context || typeof context !== "object") {
+ return [];
+ }
+
+ const ctx = context as Record;
+
+ // Handle "code" context - direct source mapping
+ if (ctx.code && typeof ctx.code === "object") {
+ const code = ctx.code as Record;
+ if (code.range && typeof code.range === "object") {
+ const range = code.range as Record;
+ if (
+ typeof range.offset === "number" &&
+ typeof range.length === "number"
+ ) {
+ return [{ offset: range.offset, length: range.length }];
+ }
+ }
+ }
+
+ // Handle "gather" context - multiple code regions
+ if (ctx.gather && Array.isArray(ctx.gather)) {
+ const ranges: SourceRange[] = [];
+ for (const item of ctx.gather) {
+ ranges.push(...extractSourceRange(item));
+ }
+ return ranges;
+ }
+
+ // Handle "pick" context - select one from options
+ if (ctx.pick && Array.isArray(ctx.pick)) {
+ // For now, just return all possible ranges
+ const ranges: SourceRange[] = [];
+ for (const item of ctx.pick) {
+ ranges.push(...extractSourceRange(item));
+ }
+ return ranges;
+ }
+
+ // Handle "frame" context - nested context
+ if (ctx.frame && typeof ctx.frame === "object") {
+ return extractSourceRange(ctx.frame);
+ }
+
+ // Try to extract from nested context property
+ if (ctx.context) {
+ return extractSourceRange(ctx.context);
+ }
+
+ return [];
+}
+
+/**
+ * Format an ethdebug context object as a readable JSON string.
+ *
+ * @param context - The context object to format
+ * @returns Formatted JSON string
+ */
+export function formatDebugContext(context: unknown): string {
+ if (!context) {
+ return "";
+ }
+ return JSON.stringify(context, null, 2);
+}
+
+/**
+ * Check if a context contains source range information.
+ *
+ * @param context - The context to check
+ * @returns True if the context contains source ranges
+ */
+export function hasSourceRange(context: unknown): boolean {
+ return extractSourceRange(context).length > 0;
+}
diff --git a/packages/bugc-react/src/utils/formatBytecode.ts b/packages/bugc-react/src/utils/formatBytecode.ts
new file mode 100644
index 000000000..c955d9765
--- /dev/null
+++ b/packages/bugc-react/src/utils/formatBytecode.ts
@@ -0,0 +1,193 @@
+/**
+ * Bytecode formatting and disassembly utilities.
+ */
+
+/**
+ * EVM opcode names by byte value.
+ */
+export 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",
+};
+
+/**
+ * Format bytecode hex string as disassembled instructions.
+ *
+ * @param hex - Bytecode as hex string (without 0x prefix)
+ * @returns Formatted disassembly with addresses and mnemonics
+ */
+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");
+}
+
+/**
+ * Get the opcode name for a given byte value.
+ *
+ * @param opcode - Opcode byte value
+ * @returns Opcode mnemonic or "UNKNOWN"
+ */
+export function getOpcodeName(opcode: number): string {
+ return OPCODES[opcode] || "UNKNOWN";
+}
diff --git a/packages/bugc-react/src/utils/index.ts b/packages/bugc-react/src/utils/index.ts
new file mode 100644
index 000000000..335e2e091
--- /dev/null
+++ b/packages/bugc-react/src/utils/index.ts
@@ -0,0 +1,31 @@
+/**
+ * Utility exports for @ethdebug/bugc-react.
+ */
+
+export {
+ extractSourceRange,
+ formatDebugContext,
+ hasSourceRange,
+} from "./debugUtils.js";
+
+export {
+ extractInstructionDebug,
+ extractTerminatorDebug,
+ extractPhiDebug,
+ formatMultiLevelDebug,
+ extractAllSourceRanges,
+ extractOperandSourceRanges,
+ type MultiLevelDebugInfo,
+} from "./irDebugUtils.js";
+
+export { formatBytecode, getOpcodeName, OPCODES } from "./formatBytecode.js";
+
+export {
+ keywords as bugKeywords,
+ typeKeywords as bugTypeKeywords,
+ operators as bugOperators,
+ languageId as bugLanguageId,
+ monarchTokensProvider as bugMonarchTokensProvider,
+ languageConfiguration as bugLanguageConfiguration,
+ registerBugLanguage,
+} from "./bugLanguage.js";
diff --git a/packages/bugc-react/src/utils/irDebugUtils.ts b/packages/bugc-react/src/utils/irDebugUtils.ts
new file mode 100644
index 000000000..11bdcc767
--- /dev/null
+++ b/packages/bugc-react/src/utils/irDebugUtils.ts
@@ -0,0 +1,245 @@
+/**
+ * Utilities for extracting debug information from IR instructions.
+ */
+
+import type { Ir } from "@ethdebug/bugc";
+import type { SourceRange } from "#types";
+import { extractSourceRange } from "./debugUtils.js";
+
+/**
+ * Multi-level debug information extracted from IR elements.
+ */
+export interface MultiLevelDebugInfo {
+ /** Debug info for the operation itself */
+ operation?: Ir.Instruction.Debug | Ir.Block.Debug;
+ /** Debug info for each operand */
+ operands: { label: string; debug?: Ir.Instruction.Debug | Ir.Block.Debug }[];
+}
+
+/**
+ * Extract all debug contexts from an IR instruction.
+ *
+ * @param instruction - The IR instruction
+ * @returns Multi-level debug info with operation and operand contexts
+ */
+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.
+ *
+ * @param terminator - The block terminator
+ * @returns Multi-level debug info
+ */
+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.
+ *
+ * @param phi - The phi instruction
+ * @returns Multi-level debug info
+ */
+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.
+ *
+ * @param info - The debug info to format
+ * @returns Formatted JSON string
+ */
+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.
+ *
+ * @param info - The debug info
+ * @returns Array of source ranges from all levels
+ */
+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.
+ *
+ * @param info - The debug info
+ * @param label - The operand label to look for
+ * @returns Source ranges for the specified operand
+ */
+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/bugc-react/tsconfig.json b/packages/bugc-react/tsconfig.json
new file mode 100644
index 000000000..7dd5776e5
--- /dev/null
+++ b/packages/bugc-react/tsconfig.json
@@ -0,0 +1,19 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "./",
+ "outDir": "./dist/",
+ "baseUrl": "./",
+ "jsx": "react-jsx",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "paths": {
+ "#components/*": ["./src/components/*"],
+ "#hooks/*": ["./src/hooks/*"],
+ "#utils/*": ["./src/utils/*"],
+ "#types": ["./src/types"]
+ }
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"],
+ "references": [{ "path": "../bugc" }]
+}
diff --git a/packages/bugc/package.json b/packages/bugc/package.json
index 39be792dd..d8aa9cfdc 100644
--- a/packages/bugc/package.json
+++ b/packages/bugc/package.json
@@ -85,8 +85,6 @@
"@hyperjump/json-schema": "^1.11.0",
"@types/node": "^20.0.0",
"@types/parsimmon": "^1.10.9",
- "@typescript-eslint/eslint-plugin": "^6.0.0",
- "@typescript-eslint/parser": "^6.0.0",
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "^2.1.8",
"eslint": "^8.0.0",
diff --git a/packages/evm/package.json b/packages/evm/package.json
new file mode 100644
index 000000000..02c5c8336
--- /dev/null
+++ b/packages/evm/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@ethdebug/evm",
+ "version": "0.1.0-0",
+ "description": "EVM execution and state access for ethdebug/format",
+ "type": "module",
+ "main": "dist/src/index.js",
+ "types": "dist/src/index.d.ts",
+ "license": "MIT",
+ "imports": {
+ "#executor": {
+ "types": "./src/executor.ts",
+ "default": "./dist/src/executor.js"
+ },
+ "#machine": {
+ "types": "./src/machine.ts",
+ "default": "./dist/src/machine.js"
+ },
+ "#trace": {
+ "types": "./src/trace.ts",
+ "default": "./dist/src/trace.js"
+ }
+ },
+ "scripts": {
+ "prepare": "tsc",
+ "build": "tsc",
+ "watch": "tsc --watch --preserveWatchOutput",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "@ethereumjs/common": "^10.0.0",
+ "@ethereumjs/evm": "^10.0.0",
+ "@ethereumjs/statemanager": "^10.0.0",
+ "@ethereumjs/util": "^10.0.0",
+ "ethereum-cryptography": "^3.2.0"
+ },
+ "devDependencies": {
+ "@ethdebug/pointers": "^0.1.0-0",
+ "@types/node": "^20.0.0",
+ "typescript": "^5.0.0",
+ "vitest": "^3.2.4"
+ },
+ "peerDependencies": {
+ "@ethdebug/pointers": "^0.1.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@ethdebug/pointers": {
+ "optional": true
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/evm/src/executor.ts b/packages/evm/src/executor.ts
new file mode 100644
index 000000000..17a293075
--- /dev/null
+++ b/packages/evm/src/executor.ts
@@ -0,0 +1,304 @@
+/**
+ * EVM Executor
+ *
+ * Provides in-process EVM execution using @ethereumjs/evm.
+ * Supports contract deployment, execution, and storage access.
+ */
+
+import { EVM } from "@ethereumjs/evm";
+import { SimpleStateManager } from "@ethereumjs/statemanager";
+import { Common, Mainnet } from "@ethereumjs/common";
+import { Address, Account } from "@ethereumjs/util";
+import { hexToBytes, bytesToHex } from "ethereum-cryptography/utils";
+
+import type { TraceStep, TraceHandler } from "#trace";
+
+/**
+ * Options for executing a contract call.
+ */
+export interface ExecutionOptions {
+ /** ETH value to send with the call */
+ value?: bigint;
+ /** Calldata as hex string (without 0x prefix) */
+ data?: string;
+ /** Transaction origin address */
+ origin?: Address;
+ /** Caller address */
+ caller?: Address;
+ /** Gas limit for execution */
+ gasLimit?: bigint;
+}
+
+/**
+ * Result of contract execution.
+ */
+export interface ExecutionResult {
+ /** Whether execution completed without error */
+ success: boolean;
+ /** Gas consumed by execution */
+ gasUsed: bigint;
+ /** Return data from the call */
+ returnValue: Uint8Array;
+ /** Event logs emitted during execution */
+ logs: unknown[];
+ /** Error if execution failed */
+ error?: unknown;
+}
+
+interface ExecResult {
+ exceptionError?: unknown;
+ executionGasUsed?: bigint;
+ returnValue?: Uint8Array;
+ logs?: unknown[];
+}
+
+interface ResultWithExec extends ExecResult {
+ execResult?: ExecResult;
+}
+
+/**
+ * EVM executor for running bytecode in an isolated environment.
+ *
+ * Wraps @ethereumjs/evm to provide a simple interface for:
+ * - Deploying contracts
+ * - Executing contract calls
+ * - Reading/writing storage
+ * - Capturing execution traces
+ */
+export class Executor {
+ private evm: EVM;
+ private stateManager: SimpleStateManager;
+ private contractAddress: Address;
+ private deployerAddress: Address;
+
+ constructor() {
+ const common = new Common({
+ chain: Mainnet,
+ hardfork: "shanghai",
+ });
+ this.stateManager = new SimpleStateManager();
+ this.evm = new EVM({
+ common,
+ stateManager: this.stateManager,
+ });
+
+ // Use a fixed contract address for testing
+ this.contractAddress = new Address(
+ hexToBytes("1234567890123456789012345678901234567890"),
+ );
+
+ // Use a fixed deployer address
+ this.deployerAddress = new Address(
+ hexToBytes("0000000000000000000000000000000000000001"),
+ );
+ }
+
+ /**
+ * Get the deployer address used for deployment.
+ */
+ getDeployerAddress(): Address {
+ return this.deployerAddress;
+ }
+
+ /**
+ * Get the current contract address.
+ */
+ getContractAddress(): Address {
+ return this.contractAddress;
+ }
+
+ /**
+ * Deploy bytecode and set the contract address.
+ *
+ * @param bytecode - Contract creation bytecode as hex string
+ */
+ async deploy(bytecode: string): Promise {
+ const code = hexToBytes(bytecode);
+
+ // Initialize deployer account with 1 ETH
+ const deployerAccount = new Account(0n, BigInt(10) ** BigInt(18));
+ await this.stateManager.putAccount(this.deployerAddress, deployerAccount);
+
+ // Initialize contract account before execution
+ const contractAccount = new Account(0n, 0n);
+ await this.stateManager.putAccount(this.contractAddress, contractAccount);
+
+ // Use runCall with undefined 'to' to simulate CREATE
+ const result = await this.evm.runCall({
+ caller: this.deployerAddress,
+ origin: this.deployerAddress,
+ to: undefined,
+ data: code,
+ gasLimit: 10_000_000n,
+ value: 0n,
+ });
+
+ const error = result.execResult?.exceptionError;
+
+ if (error) {
+ throw new Error(`Deployment failed: ${JSON.stringify(error)}`);
+ }
+
+ // Update contract address to the created one
+ const createdAddress = result.createdAddress;
+ if (createdAddress) {
+ this.contractAddress = createdAddress;
+ }
+ }
+
+ /**
+ * Execute a call to the deployed contract.
+ *
+ * @param options - Execution options (value, data, gas, etc.)
+ * @param traceHandler - Optional handler for execution trace steps
+ */
+ async execute(
+ options: ExecutionOptions = {},
+ traceHandler?: TraceHandler,
+ ): Promise {
+ const runCallOpts = {
+ to: this.contractAddress,
+ caller: options.caller ?? this.deployerAddress,
+ origin: options.origin ?? this.deployerAddress,
+ data: options.data ? hexToBytes(options.data) : new Uint8Array(),
+ value: options.value ?? 0n,
+ gasLimit: options.gasLimit ?? 10_000_000n,
+ };
+
+ if (traceHandler) {
+ this.evm.events.on(
+ "step",
+ (step: { pc: number; opcode: { name: string }; stack: bigint[] }) => {
+ const traceStep: TraceStep = {
+ pc: step.pc,
+ opcode: step.opcode.name,
+ stack: [...step.stack],
+ };
+ traceHandler(traceStep);
+ },
+ );
+ }
+
+ const result = await this.evm.runCall(runCallOpts);
+
+ if (traceHandler) {
+ this.evm.events.removeAllListeners("step");
+ }
+
+ const rawResult = result as ResultWithExec;
+ const execResult = (rawResult.execResult || rawResult) as ExecResult;
+
+ return {
+ success: execResult.exceptionError === undefined,
+ gasUsed: execResult.executionGasUsed || 0n,
+ returnValue: execResult.returnValue || new Uint8Array(),
+ logs: execResult.logs || [],
+ error: execResult.exceptionError,
+ };
+ }
+
+ /**
+ * Execute bytecode directly without deployment.
+ *
+ * @param bytecode - Bytecode to execute as hex string
+ * @param options - Execution options
+ */
+ async executeCode(
+ bytecode: string,
+ options: ExecutionOptions = {},
+ ): Promise {
+ const code = hexToBytes(bytecode);
+
+ // Create a temporary account with the code
+ const tempAddress = new Address(
+ hexToBytes("9999999999999999999999999999999999999999"),
+ );
+ await this.stateManager.putCode(tempAddress, code);
+ await this.stateManager.putAccount(tempAddress, new Account(0n, 0n));
+
+ const runCodeOpts = {
+ code,
+ data: options.data ? hexToBytes(options.data) : new Uint8Array(),
+ gasLimit: options.gasLimit ?? 10_000_000n,
+ value: options.value ?? 0n,
+ origin: options.origin ?? new Address(Buffer.alloc(20)),
+ caller: options.caller ?? new Address(Buffer.alloc(20)),
+ address: tempAddress,
+ };
+
+ const result = await this.evm.runCode(runCodeOpts);
+
+ const rawResult = result as ResultWithExec;
+ const execResult = (rawResult.execResult || rawResult) as ExecResult;
+
+ return {
+ success: execResult.exceptionError === undefined,
+ gasUsed: execResult.executionGasUsed || 0n,
+ returnValue: execResult.returnValue || new Uint8Array(),
+ logs: execResult.logs || [],
+ error: execResult.exceptionError,
+ };
+ }
+
+ /**
+ * Get storage value at a specific slot.
+ *
+ * @param slot - Storage slot as bigint
+ * @returns Storage value as bigint
+ */
+ async getStorage(slot: bigint): Promise {
+ const slotBuffer = Buffer.alloc(32);
+ const hex = slot.toString(16).padStart(64, "0");
+ slotBuffer.write(hex, "hex");
+
+ const value = await this.stateManager.getStorage(
+ this.contractAddress,
+ slotBuffer,
+ );
+
+ if (value.length === 0) return 0n;
+ return BigInt("0x" + bytesToHex(value));
+ }
+
+ /**
+ * Set storage value at a specific slot.
+ *
+ * @param slot - Storage slot as bigint
+ * @param value - Value to store as bigint
+ */
+ async setStorage(slot: bigint, value: bigint): Promise {
+ const slotBuffer = Buffer.alloc(32);
+ slotBuffer.writeBigUInt64BE(slot, 24);
+
+ const valueBuffer = Buffer.alloc(32);
+ const hex = value.toString(16).padStart(64, "0");
+ valueBuffer.write(hex, "hex");
+
+ await this.stateManager.putStorage(
+ this.contractAddress,
+ slotBuffer,
+ valueBuffer,
+ );
+ }
+
+ /**
+ * Get the deployed bytecode at the contract address.
+ */
+ async getCode(): Promise {
+ return this.stateManager.getCode(this.contractAddress);
+ }
+
+ /**
+ * Reset the EVM state to a fresh instance.
+ */
+ async reset(): Promise {
+ this.stateManager = new SimpleStateManager();
+ this.evm = new EVM({
+ common: new Common({
+ chain: Mainnet,
+ hardfork: "shanghai",
+ }),
+ stateManager: this.stateManager,
+ });
+ }
+}
diff --git a/packages/evm/src/index.ts b/packages/evm/src/index.ts
new file mode 100644
index 000000000..1784e5521
--- /dev/null
+++ b/packages/evm/src/index.ts
@@ -0,0 +1,37 @@
+/**
+ * @ethdebug/evm
+ *
+ * EVM execution and state access for ethdebug/format.
+ *
+ * This package provides:
+ * - An EVM executor for running bytecode in isolation
+ * - A Machine.State adapter for pointer evaluation
+ * - Execution trace capture utilities
+ *
+ * @example
+ * ```typescript
+ * import { Executor, createMachineState } from "@ethdebug/evm";
+ * import { dereference } from "@ethdebug/pointers";
+ *
+ * // Create executor and deploy contract
+ * const executor = new Executor();
+ * await executor.deploy(bytecode);
+ * await executor.execute();
+ *
+ * // Create machine state for pointer evaluation
+ * const state = createMachineState(executor);
+ * const cursor = await dereference(pointer, { state });
+ * ```
+ */
+
+// Executor
+export { Executor } from "#executor";
+export type { ExecutionOptions, ExecutionResult } from "#executor";
+
+// Machine state adapter
+export { createMachineState } from "#machine";
+export type { MachineStateOptions } from "#machine";
+
+// Trace types
+export { createTraceCollector } from "#trace";
+export type { TraceStep, TraceHandler, Trace } from "#trace";
diff --git a/packages/evm/src/machine.ts b/packages/evm/src/machine.ts
new file mode 100644
index 000000000..b96aa99ad
--- /dev/null
+++ b/packages/evm/src/machine.ts
@@ -0,0 +1,105 @@
+/**
+ * Machine.State Adapter
+ *
+ * Implements the @ethdebug/pointers Machine.State interface
+ * to enable pointer evaluation against EVM executor state.
+ */
+
+import type { Machine } from "@ethdebug/pointers";
+import { Data } from "@ethdebug/pointers";
+import type { Executor } from "#executor";
+
+/**
+ * Options for creating a Machine.State adapter.
+ */
+export interface MachineStateOptions {
+ /** Program counter for the current state */
+ programCounter?: bigint;
+ /** Opcode at the current program counter */
+ opcode?: string;
+ /** Trace index (step number) */
+ traceIndex?: bigint;
+}
+
+/**
+ * Create a Machine.State from an Executor.
+ *
+ * This adapter allows using @ethdebug/pointers dereference()
+ * to evaluate pointers against an EVM executor's storage state.
+ *
+ * Note: This creates an "end-state" adapter where only storage
+ * is fully implemented. Stack, memory, etc. return empty/zero
+ * values since we only have post-execution state access.
+ *
+ * @param executor - The EVM executor to read state from
+ * @param options - Optional state context (PC, opcode, trace index)
+ * @returns A Machine.State suitable for pointer dereferencing
+ */
+export function createMachineState(
+ executor: Executor,
+ options: MachineStateOptions = {},
+): Machine.State {
+ const { programCounter = 0n, opcode = "STOP", traceIndex = 0n } = options;
+
+ return {
+ // Trace context
+ traceIndex: Promise.resolve(traceIndex),
+ programCounter: Promise.resolve(programCounter),
+ opcode: Promise.resolve(opcode),
+
+ // Stack - not available in end-state
+ stack: {
+ length: Promise.resolve(0n),
+ peek: async (): Promise => Data.zero(),
+ },
+
+ // Memory - not available in end-state
+ memory: {
+ length: Promise.resolve(0n),
+ read: async (): Promise => Data.zero(),
+ },
+
+ // Storage - fully implemented via executor
+ storage: {
+ async read({ slot, slice }): Promise {
+ const slotValue = slot.asUint();
+ const value = await executor.getStorage(slotValue);
+ const data = Data.fromUint(value);
+
+ if (slice) {
+ const padded = data.padUntilAtLeast(32);
+ const sliced = new Uint8Array(padded).slice(
+ Number(slice.offset),
+ Number(slice.offset + slice.length),
+ );
+ return Data.fromBytes(sliced);
+ }
+
+ return data.padUntilAtLeast(32);
+ },
+ },
+
+ // Calldata - not available in end-state
+ calldata: {
+ length: Promise.resolve(0n),
+ read: async (): Promise => Data.zero(),
+ },
+
+ // Returndata - not available in end-state
+ returndata: {
+ length: Promise.resolve(0n),
+ read: async (): Promise => Data.zero(),
+ },
+
+ // Code - not available in end-state
+ code: {
+ length: Promise.resolve(0n),
+ read: async (): Promise => Data.zero(),
+ },
+
+ // Transient storage - not available in end-state
+ transient: {
+ read: async (): Promise => Data.zero(),
+ },
+ };
+}
diff --git a/packages/evm/src/trace.ts b/packages/evm/src/trace.ts
new file mode 100644
index 000000000..2f9e3e389
--- /dev/null
+++ b/packages/evm/src/trace.ts
@@ -0,0 +1,52 @@
+/**
+ * EVM Execution Trace Types
+ *
+ * Types for capturing and representing EVM execution traces.
+ */
+
+/**
+ * A single step in an execution trace.
+ */
+export interface TraceStep {
+ /** Program counter */
+ pc: number;
+ /** Opcode name (e.g., "PUSH1", "SLOAD") */
+ opcode: string;
+ /** Stack state at this step */
+ stack: bigint[];
+ /** Memory state at this step (optional) */
+ memory?: Uint8Array;
+ /** Gas remaining (optional) */
+ gasRemaining?: bigint;
+}
+
+/**
+ * Handler function for trace steps during execution.
+ */
+export type TraceHandler = (step: TraceStep) => void;
+
+/**
+ * A complete execution trace.
+ */
+export interface Trace {
+ /** All steps in the trace */
+ steps: TraceStep[];
+}
+
+/**
+ * Create a trace handler that collects steps into a Trace object.
+ *
+ * @returns A tuple of [handler, getTrace] where handler collects steps
+ * and getTrace returns the collected trace.
+ */
+export function createTraceCollector(): [TraceHandler, () => Trace] {
+ const steps: TraceStep[] = [];
+
+ const handler: TraceHandler = (step) => {
+ steps.push(step);
+ };
+
+ const getTrace = (): Trace => ({ steps: [...steps] });
+
+ return [handler, getTrace];
+}
diff --git a/packages/evm/tsconfig.json b/packages/evm/tsconfig.json
new file mode 100644
index 000000000..18545ef8c
--- /dev/null
+++ b/packages/evm/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "./",
+ "outDir": "./dist/",
+ "baseUrl": "./",
+ "paths": {
+ "#executor": ["./src/executor"],
+ "#machine": ["./src/machine"],
+ "#trace": ["./src/trace"]
+ }
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"],
+ "references": [{ "path": "../pointers" }]
+}
diff --git a/packages/evm/vitest.config.ts b/packages/evm/vitest.config.ts
new file mode 100644
index 000000000..c88a255c3
--- /dev/null
+++ b/packages/evm/vitest.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: "node",
+ coverage: {
+ provider: "v8",
+ reporter: ["text", "json", "html"],
+ exclude: ["node_modules/", "dist/", "**/*.test.ts"],
+ },
+ },
+});
diff --git a/packages/playground/.eslintrc.cjs b/packages/playground/.eslintrc.cjs
deleted file mode 100644
index 62b6946be..000000000
--- a/packages/playground/.eslintrc.cjs
+++ /dev/null
@@ -1,22 +0,0 @@
-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/package.json b/packages/playground/package.json
index ffaf5f884..5eef47706 100644
--- a/packages/playground/package.json
+++ b/packages/playground/package.json
@@ -27,8 +27,6 @@
"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",
diff --git a/packages/pointers-react/package.json b/packages/pointers-react/package.json
new file mode 100644
index 000000000..86d5ba896
--- /dev/null
+++ b/packages/pointers-react/package.json
@@ -0,0 +1,51 @@
+{
+ "name": "@ethdebug/pointers-react",
+ "version": "0.1.0-0",
+ "description": "React components for visualizing ethdebug/format pointer resolution",
+ "type": "module",
+ "main": "dist/src/index.js",
+ "types": "dist/src/index.d.ts",
+ "license": "MIT",
+ "imports": {
+ "#components/*": {
+ "types": "./src/components/*.tsx",
+ "default": "./dist/src/components/*.js"
+ },
+ "#hooks/*": {
+ "types": "./src/hooks/*.ts",
+ "default": "./dist/src/hooks/*.js"
+ },
+ "#utils/*": {
+ "types": "./src/utils/*.ts",
+ "default": "./dist/src/utils/*.js"
+ }
+ },
+ "scripts": {
+ "prepare": "tsc",
+ "build": "tsc",
+ "watch": "tsc --watch --preserveWatchOutput",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "@ethdebug/format": "^0.1.0-0",
+ "@ethdebug/pointers": "^0.1.0-0"
+ },
+ "devDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@testing-library/react": "^16.0.0",
+ "@types/react": "^18.2.43",
+ "@types/react-dom": "^18.2.17",
+ "jsdom": "^26.0.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "typescript": "^5.0.0",
+ "vitest": "^3.2.4"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/pointers-react/src/components/PointerResolverContext.tsx b/packages/pointers-react/src/components/PointerResolverContext.tsx
new file mode 100644
index 000000000..9da54840e
--- /dev/null
+++ b/packages/pointers-react/src/components/PointerResolverContext.tsx
@@ -0,0 +1,63 @@
+/**
+ * React context for pointer resolution state management.
+ */
+
+import React, { createContext, useContext } from "react";
+import type { Pointer } from "@ethdebug/format";
+import {
+ usePointerResolution,
+ type PointerResolutionState,
+ type UsePointerResolutionOptions,
+} from "#hooks/usePointerResolution";
+import type { MockStateSpec } from "#utils/mockState";
+
+const PointerResolverContext = createContext<
+ PointerResolutionState | undefined
+>(undefined);
+
+/**
+ * Hook to access the PointerResolver context.
+ *
+ * @returns The current pointer resolution state
+ * @throws If used outside of a PointerResolverProvider
+ */
+export function usePointerResolverContext(): PointerResolutionState {
+ const context = useContext(PointerResolverContext);
+ if (context === undefined) {
+ throw new Error(
+ "usePointerResolverContext must be used within a PointerResolverProvider",
+ );
+ }
+ return context;
+}
+
+/**
+ * Props for PointerResolverProvider.
+ */
+export interface PointerResolverProviderProps extends UsePointerResolutionOptions {
+ children: React.ReactNode;
+}
+
+/**
+ * Provides pointer resolution context to child components.
+ *
+ * @param props - Provider configuration and children
+ * @returns Context provider wrapping children
+ */
+export function PointerResolverProvider({
+ children,
+ ...options
+}: PointerResolverProviderProps): JSX.Element {
+ const state = usePointerResolution(options);
+
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Re-export types for convenience.
+ */
+export type { Pointer, MockStateSpec, PointerResolutionState };
diff --git a/packages/pointers-react/src/components/RegionMap.css b/packages/pointers-react/src/components/RegionMap.css
new file mode 100644
index 000000000..0a68a7982
--- /dev/null
+++ b/packages/pointers-react/src/components/RegionMap.css
@@ -0,0 +1,117 @@
+/**
+ * Styles for RegionMap component.
+ */
+
+.region-map {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.region-map-empty {
+ padding: 1.5rem;
+ text-align: center;
+}
+
+.region-map-empty-text {
+ font-size: 0.875rem;
+ color: var(--pointers-text-muted);
+}
+
+.region-map-entry {
+ border: 1px solid var(--pointers-border-secondary);
+ border-radius: 6px;
+ overflow: hidden;
+}
+
+.region-map-entry.selected {
+ border-color: var(--pointers-border-highlight);
+}
+
+.region-map-name {
+ padding: 0.5rem 0.75rem;
+ font-size: 0.8125rem;
+ font-weight: 600;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ background: var(--pointers-bg-secondary);
+ color: var(--pointers-text-primary);
+}
+
+.region-map-regions {
+ display: flex;
+ flex-direction: column;
+}
+
+.region-map-region {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 0.75rem;
+ border-top: 1px solid var(--pointers-border-secondary);
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.region-map-region:hover {
+ background: var(--pointers-bg-hover);
+}
+
+.region-map-region:focus {
+ outline: none;
+ background: var(--pointers-bg-highlight);
+}
+
+.region-location-badge {
+ display: inline-block;
+ padding: 0.125rem 0.375rem;
+ font-size: 0.6875rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ border-radius: 3px;
+ white-space: nowrap;
+}
+
+/* Region type colors */
+.region-storage .region-location-badge {
+ color: var(--pointers-region-storage);
+ background: var(--pointers-region-storage-bg);
+}
+
+.region-memory .region-location-badge {
+ color: var(--pointers-region-memory);
+ background: var(--pointers-region-memory-bg);
+}
+
+.region-stack .region-location-badge {
+ color: var(--pointers-region-stack);
+ background: var(--pointers-region-stack-bg);
+}
+
+.region-calldata .region-location-badge {
+ color: var(--pointers-region-calldata);
+ background: var(--pointers-region-calldata-bg);
+}
+
+.region-returndata .region-location-badge {
+ color: var(--pointers-region-returndata);
+ background: var(--pointers-region-returndata-bg);
+}
+
+.region-code .region-location-badge {
+ color: var(--pointers-region-code);
+ background: var(--pointers-region-code-bg);
+}
+
+.region-transient .region-location-badge {
+ color: var(--pointers-region-transient);
+ background: var(--pointers-region-transient-bg);
+}
+
+.region-details {
+ font-size: 0.75rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ color: var(--pointers-text-secondary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
diff --git a/packages/pointers-react/src/components/RegionMap.tsx b/packages/pointers-react/src/components/RegionMap.tsx
new file mode 100644
index 000000000..a59039493
--- /dev/null
+++ b/packages/pointers-react/src/components/RegionMap.tsx
@@ -0,0 +1,130 @@
+/**
+ * Component for displaying a map of named regions.
+ */
+
+import React from "react";
+import type { Cursor } from "@ethdebug/pointers";
+import { formatData } from "#utils/mockState";
+
+export interface RegionMapProps {
+ /** Named regions to display */
+ regions: Record;
+ /** Currently selected region name */
+ selectedName?: string;
+ /** Callback when a region is clicked */
+ onRegionClick?: (name: string, region: Cursor.Region) => void;
+}
+
+/**
+ * Get the location type from a region.
+ */
+function getLocationType(region: Cursor.Region): string {
+ if ("location" in region && typeof region.location === "string") {
+ return region.location;
+ }
+ return "unknown";
+}
+
+/**
+ * Get CSS class for a location type.
+ */
+function getLocationClass(location: string): string {
+ const locationClasses: Record = {
+ storage: "region-storage",
+ memory: "region-memory",
+ stack: "region-stack",
+ calldata: "region-calldata",
+ returndata: "region-returndata",
+ code: "region-code",
+ transient: "region-transient",
+ };
+ return locationClasses[location] || "region-unknown";
+}
+
+/**
+ * Format a region for display.
+ */
+function formatRegion(region: Cursor.Region): string {
+ const location = getLocationType(region);
+ const parts: string[] = [location];
+
+ if ("slot" in region && region.slot !== undefined) {
+ const slotHex = formatData(region.slot);
+ parts.push(`slot: ${slotHex}`);
+ }
+
+ if ("offset" in region && region.offset !== undefined) {
+ const offsetHex = formatData(region.offset);
+ parts.push(`offset: ${offsetHex}`);
+ }
+
+ if ("length" in region && region.length !== undefined) {
+ const lengthHex = formatData(region.length);
+ parts.push(`length: ${lengthHex}`);
+ }
+
+ return parts.join(", ");
+}
+
+/**
+ * Component to display a map of named regions.
+ */
+export function RegionMap({
+ regions,
+ selectedName,
+ onRegionClick,
+}: RegionMapProps): JSX.Element {
+ const names = Object.keys(regions).sort();
+
+ if (names.length === 0) {
+ return (
+
+ No named regions
+
+ );
+ }
+
+ return (
+
+ {names.map((name) => {
+ const regionList = regions[name];
+ const isSelected = name === selectedName;
+
+ return (
+
+
{name}
+
+ {regionList.map((region, index) => {
+ const location = getLocationType(region);
+ const locationClass = getLocationClass(location);
+
+ return (
+
onRegionClick?.(name, region)}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" || e.key === " ") {
+ onRegionClick?.(name, region);
+ }
+ }}
+ >
+ {location}
+
+ {formatRegion(region)}
+
+
+ );
+ })}
+
+
+ );
+ })}
+
+ );
+}
diff --git a/packages/pointers-react/src/components/RegionOutput.css b/packages/pointers-react/src/components/RegionOutput.css
new file mode 100644
index 000000000..c9112a4b0
--- /dev/null
+++ b/packages/pointers-react/src/components/RegionOutput.css
@@ -0,0 +1,149 @@
+/**
+ * Styles for RegionOutput component.
+ */
+
+.region-output {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.region-output-empty {
+ padding: 1.5rem;
+ text-align: center;
+}
+
+.region-output-empty-text {
+ font-size: 0.875rem;
+ color: var(--pointers-text-muted);
+}
+
+.region-output-summary {
+ font-size: 0.75rem;
+ color: var(--pointers-text-secondary);
+ padding: 0 0.25rem;
+}
+
+.region-output-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.region-output-item {
+ border: 1px solid var(--pointers-border-secondary);
+ border-radius: 6px;
+ padding: 0.75rem;
+ background: var(--pointers-bg-secondary);
+}
+
+.region-output-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.region-output-index {
+ font-size: 0.6875rem;
+ font-weight: 600;
+ color: var(--pointers-text-muted);
+ font-family: var(--ifm-font-family-monospace, monospace);
+}
+
+.region-name-badge {
+ display: inline-block;
+ padding: 0.125rem 0.375rem;
+ font-size: 0.6875rem;
+ font-weight: 500;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ border-radius: 3px;
+ background: var(--pointers-accent-blue-bg);
+ color: var(--pointers-accent-blue);
+}
+
+.region-output-details {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ margin-bottom: 0.5rem;
+}
+
+.region-output-field {
+ display: flex;
+ align-items: baseline;
+ gap: 0.25rem;
+}
+
+.region-field-label {
+ font-size: 0.6875rem;
+ color: var(--pointers-text-muted);
+}
+
+.region-field-value {
+ font-size: 0.75rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ color: var(--pointers-syntax-number);
+ background: transparent;
+ padding: 0;
+}
+
+.region-output-value {
+ display: flex;
+ align-items: baseline;
+ gap: 0.5rem;
+ padding: 0.5rem;
+ background: var(--pointers-data-bg);
+ border: 1px solid var(--pointers-data-border);
+ border-radius: 4px;
+}
+
+.region-value-label {
+ font-size: 0.6875rem;
+ font-weight: 600;
+ color: var(--pointers-text-secondary);
+ white-space: nowrap;
+}
+
+.region-value-data {
+ font-size: 0.8125rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ color: var(--pointers-text-code);
+ word-break: break-all;
+ background: transparent;
+ padding: 0;
+}
+
+.region-value-data.no-value {
+ color: var(--pointers-text-muted);
+ font-style: italic;
+}
+
+/* Region type borders */
+.region-output-item.region-storage {
+ border-left: 3px solid var(--pointers-region-storage);
+}
+
+.region-output-item.region-memory {
+ border-left: 3px solid var(--pointers-region-memory);
+}
+
+.region-output-item.region-stack {
+ border-left: 3px solid var(--pointers-region-stack);
+}
+
+.region-output-item.region-calldata {
+ border-left: 3px solid var(--pointers-region-calldata);
+}
+
+.region-output-item.region-returndata {
+ border-left: 3px solid var(--pointers-region-returndata);
+}
+
+.region-output-item.region-code {
+ border-left: 3px solid var(--pointers-region-code);
+}
+
+.region-output-item.region-transient {
+ border-left: 3px solid var(--pointers-region-transient);
+}
diff --git a/packages/pointers-react/src/components/RegionOutput.tsx b/packages/pointers-react/src/components/RegionOutput.tsx
new file mode 100644
index 000000000..60204d78e
--- /dev/null
+++ b/packages/pointers-react/src/components/RegionOutput.tsx
@@ -0,0 +1,149 @@
+/**
+ * Component for displaying resolved regions and their values.
+ */
+
+import React from "react";
+import type { Cursor, Data } from "@ethdebug/pointers";
+import { formatData, formatDataShort } from "#utils/mockState";
+
+export interface RegionOutputProps {
+ /** Resolved regions to display */
+ regions: Cursor.Region[];
+ /** Values read from each region */
+ values: Map;
+ /** Whether to show full hex values or shortened */
+ showFullValues?: boolean;
+}
+
+/**
+ * Get the location type from a region.
+ */
+function getLocationType(region: Cursor.Region): string {
+ if ("location" in region && typeof region.location === "string") {
+ return region.location;
+ }
+ return "unknown";
+}
+
+/**
+ * Get CSS class for a location type.
+ */
+function getLocationClass(location: string): string {
+ const locationClasses: Record = {
+ storage: "region-storage",
+ memory: "region-memory",
+ stack: "region-stack",
+ calldata: "region-calldata",
+ returndata: "region-returndata",
+ code: "region-code",
+ transient: "region-transient",
+ };
+ return locationClasses[location] || "region-unknown";
+}
+
+/**
+ * Component to display a single region with its value.
+ */
+function RegionItem({
+ region,
+ value,
+ showFullValues,
+ index,
+}: {
+ region: Cursor.Region;
+ value: Data | undefined;
+ showFullValues: boolean;
+ index: number;
+}): JSX.Element {
+ const location = getLocationType(region);
+ const locationClass = getLocationClass(location);
+ const name = "name" in region ? (region.name as string) : undefined;
+
+ // Format the value
+ const valueDisplay = value
+ ? showFullValues
+ ? formatData(value)
+ : formatDataShort(value, 16)
+ : "(no value)";
+
+ return (
+
+
+ #{index + 1}
+ {location}
+ {name && {name} }
+
+
+ {"slot" in region && region.slot !== undefined && (
+
+ slot:
+
+ {formatData(region.slot)}
+
+
+ )}
+ {"offset" in region && region.offset !== undefined && (
+
+ offset:
+
+ {formatData(region.offset)}
+
+
+ )}
+ {"length" in region && region.length !== undefined && (
+
+ length:
+
+ {formatData(region.length)}
+
+
+ )}
+
+
+ value:
+
+ {valueDisplay}
+
+
+
+ );
+}
+
+/**
+ * Component to display all resolved regions and their values.
+ */
+export function RegionOutput({
+ regions,
+ values,
+ showFullValues = false,
+}: RegionOutputProps): JSX.Element {
+ if (regions.length === 0) {
+ return (
+
+ No regions resolved
+
+ );
+ }
+
+ return (
+
+
+ {regions.length} region{regions.length !== 1 ? "s" : ""} resolved
+
+
+ {regions.map((region, index) => (
+
+ ))}
+
+
+ );
+}
diff --git a/packages/pointers-react/src/components/ResolutionVisualizer.css b/packages/pointers-react/src/components/ResolutionVisualizer.css
new file mode 100644
index 000000000..2a8ea49d3
--- /dev/null
+++ b/packages/pointers-react/src/components/ResolutionVisualizer.css
@@ -0,0 +1,251 @@
+/**
+ * Styles for ResolutionVisualizer component.
+ */
+
+.resolution-visualizer {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ font-family: var(--ifm-font-family-base, system-ui, sans-serif);
+}
+
+.resolution-visualizer-inputs {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+}
+
+@media (max-width: 768px) {
+ .resolution-visualizer-inputs {
+ grid-template-columns: 1fr;
+ }
+}
+
+.resolution-visualizer-panel {
+ border: 1px solid var(--pointers-border-primary);
+ border-radius: 6px;
+ background: var(--pointers-bg-primary);
+ padding: 1rem;
+}
+
+.panel-title {
+ margin: 0 0 0.75rem 0;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--pointers-text-primary);
+}
+
+/* Pointer Input */
+.pointer-input {
+ width: 100%;
+ min-height: 150px;
+ padding: 0.75rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ border: 1px solid var(--pointers-border-secondary);
+ border-radius: 4px;
+ background: var(--pointers-bg-code);
+ color: var(--pointers-text-code);
+ resize: vertical;
+}
+
+.pointer-input:focus {
+ outline: none;
+ border-color: var(--pointers-border-highlight);
+}
+
+.pointer-parse-error {
+ margin-top: 0.5rem;
+ padding: 0.5rem;
+ font-size: 0.75rem;
+ color: var(--pointers-accent-red);
+ background: rgba(207, 34, 46, 0.1);
+ border-radius: 4px;
+}
+
+/* State Editor */
+.state-editor {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.state-editor-section {
+ border: 1px solid var(--pointers-border-secondary);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.state-editor-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem 0.75rem;
+ background: var(--pointers-bg-secondary);
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--pointers-text-secondary);
+}
+
+.state-editor-add-btn {
+ padding: 0.125rem 0.5rem;
+ font-size: 0.6875rem;
+ border: 1px solid var(--pointers-border-primary);
+ border-radius: 3px;
+ background: var(--pointers-bg-primary);
+ color: var(--pointers-text-secondary);
+ cursor: pointer;
+}
+
+.state-editor-add-btn:hover {
+ background: var(--pointers-bg-hover);
+ color: var(--pointers-text-primary);
+}
+
+.state-editor-empty {
+ padding: 0.75rem;
+ font-size: 0.75rem;
+ color: var(--pointers-text-muted);
+ text-align: center;
+}
+
+.state-editor-entries {
+ padding: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.375rem;
+}
+
+.state-editor-entry {
+ display: flex;
+ align-items: center;
+ gap: 0.375rem;
+}
+
+.state-editor-index {
+ font-size: 0.6875rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ color: var(--pointers-text-muted);
+ width: 2rem;
+}
+
+.state-editor-input {
+ flex: 1;
+ padding: 0.375rem 0.5rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ font-size: 0.75rem;
+ border: 1px solid var(--pointers-border-secondary);
+ border-radius: 3px;
+ background: var(--pointers-bg-code);
+ color: var(--pointers-text-code);
+}
+
+.state-editor-input.slot {
+ flex: 0.4;
+}
+
+.state-editor-input.value {
+ flex: 0.6;
+}
+
+.state-editor-input:focus {
+ outline: none;
+ border-color: var(--pointers-border-highlight);
+}
+
+.state-editor-arrow {
+ color: var(--pointers-text-muted);
+ font-family: var(--ifm-font-family-monospace, monospace);
+}
+
+.state-editor-remove-btn {
+ padding: 0.125rem 0.375rem;
+ font-size: 0.875rem;
+ line-height: 1;
+ border: none;
+ background: transparent;
+ color: var(--pointers-text-muted);
+ cursor: pointer;
+}
+
+.state-editor-remove-btn:hover {
+ color: var(--pointers-accent-red);
+}
+
+.state-editor-textarea {
+ width: 100%;
+ padding: 0.5rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ font-size: 0.75rem;
+ border: none;
+ background: var(--pointers-bg-code);
+ color: var(--pointers-text-code);
+ resize: vertical;
+}
+
+.state-editor-textarea:focus {
+ outline: none;
+}
+
+/* Controls */
+.resolution-visualizer-controls {
+ display: flex;
+ justify-content: center;
+ padding: 0.5rem 0;
+}
+
+.resolve-button {
+ padding: 0.625rem 1.5rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ border: none;
+ border-radius: 6px;
+ background: var(--pointers-accent-blue);
+ color: white;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.resolve-button:hover:not(:disabled) {
+ background: var(--ifm-color-primary-dark, #0860ca);
+}
+
+.resolve-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Output */
+.resolution-visualizer-output {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.resolution-error {
+ padding: 1rem;
+ border: 1px solid var(--pointers-accent-red);
+ border-radius: 6px;
+ background: rgba(207, 34, 46, 0.05);
+}
+
+.resolution-error h4 {
+ margin: 0 0 0.5rem 0;
+ font-size: 0.875rem;
+ color: var(--pointers-accent-red);
+}
+
+.resolution-error pre {
+ margin: 0;
+ padding: 0.5rem;
+ font-size: 0.75rem;
+ background: var(--pointers-bg-code);
+ border-radius: 4px;
+ overflow-x: auto;
+}
+
+.regions-panel,
+.output-panel {
+ background: var(--pointers-bg-primary);
+}
diff --git a/packages/pointers-react/src/components/ResolutionVisualizer.tsx b/packages/pointers-react/src/components/ResolutionVisualizer.tsx
new file mode 100644
index 000000000..043433cb7
--- /dev/null
+++ b/packages/pointers-react/src/components/ResolutionVisualizer.tsx
@@ -0,0 +1,327 @@
+/**
+ * Main component for visualizing pointer resolution.
+ */
+
+import React, { useState, useCallback } from "react";
+import type { Pointer } from "@ethdebug/format";
+import { usePointerResolverContext } from "./PointerResolverContext.js";
+import { RegionMap } from "./RegionMap.js";
+import { RegionOutput } from "./RegionOutput.js";
+
+export interface ResolutionVisualizerProps {
+ /** Whether to show the pointer JSON input */
+ showPointerInput?: boolean;
+ /** Whether to show the state editor */
+ showStateEditor?: boolean;
+ /** Whether to show the resolve button controls */
+ showControls?: boolean;
+ /** Whether to show full hex values */
+ showFullValues?: boolean;
+}
+
+/**
+ * Component for editing a storage state map.
+ */
+function StorageEditor({
+ storage,
+ onChange,
+}: {
+ storage: Record;
+ onChange: (storage: Record) => void;
+}): JSX.Element {
+ const entries = Object.entries(storage);
+
+ const handleSlotChange = (oldSlot: string, newSlot: string) => {
+ const newStorage = { ...storage };
+ const value = newStorage[oldSlot];
+ delete newStorage[oldSlot];
+ if (newSlot) {
+ newStorage[newSlot] = value;
+ }
+ onChange(newStorage);
+ };
+
+ const handleValueChange = (slot: string, value: string) => {
+ onChange({ ...storage, [slot]: value });
+ };
+
+ const handleAdd = () => {
+ const newSlot = `0x${entries.length.toString(16).padStart(2, "0")}`;
+ onChange({ ...storage, [newSlot]: "0x00" });
+ };
+
+ const handleRemove = (slot: string) => {
+ const newStorage = { ...storage };
+ delete newStorage[slot];
+ onChange(newStorage);
+ };
+
+ return (
+
+
+ Storage
+
+ + Add
+
+
+ {entries.length === 0 ? (
+
No storage entries
+ ) : (
+
+ )}
+
+ );
+}
+
+/**
+ * Component for editing stack entries.
+ */
+function StackEditor({
+ stack,
+ onChange,
+}: {
+ stack: Array;
+ onChange: (stack: Array) => void;
+}): JSX.Element {
+ const handleEntryChange = (index: number, value: string) => {
+ const newStack = [...stack];
+ newStack[index] = value;
+ onChange(newStack);
+ };
+
+ const handleAdd = () => {
+ onChange([...stack, "0x00"]);
+ };
+
+ const handleRemove = (index: number) => {
+ const newStack = [...stack];
+ newStack.splice(index, 1);
+ onChange(newStack);
+ };
+
+ return (
+
+
+ Stack (top first)
+
+ + Add
+
+
+ {stack.length === 0 ? (
+
Empty stack
+ ) : (
+
+ {stack.map((entry, index) => (
+
+ [{index}]
+ handleEntryChange(index, e.target.value)}
+ placeholder="value (hex)"
+ />
+ handleRemove(index)}
+ type="button"
+ aria-label="Remove entry"
+ >
+ ×
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+/**
+ * Component for editing memory contents.
+ */
+function MemoryEditor({
+ memory,
+ onChange,
+}: {
+ memory: string | undefined;
+ onChange: (memory: string) => void;
+}): JSX.Element {
+ return (
+
+ );
+}
+
+/**
+ * Main visualization component for pointer resolution.
+ */
+export function ResolutionVisualizer({
+ showPointerInput = true,
+ showStateEditor = true,
+ showControls = true,
+ showFullValues = false,
+}: ResolutionVisualizerProps): JSX.Element {
+ const {
+ pointer,
+ stateSpec,
+ isResolving,
+ result,
+ error,
+ setPointer,
+ updateStateSpec,
+ resolve,
+ } = usePointerResolverContext();
+
+ const [pointerJson, setPointerJson] = useState(
+ pointer ? JSON.stringify(pointer, null, 2) : "",
+ );
+ const [parseError, setParseError] = useState(null);
+
+ const handlePointerChange = useCallback(
+ (json: string) => {
+ setPointerJson(json);
+ setParseError(null);
+
+ if (!json.trim()) {
+ setPointer(null);
+ return;
+ }
+
+ try {
+ const parsed = JSON.parse(json) as Pointer;
+ setPointer(parsed);
+ } catch (err) {
+ setParseError(err instanceof Error ? err.message : "Invalid JSON");
+ }
+ },
+ [setPointer],
+ );
+
+ return (
+
+
+ {showPointerInput && (
+
+ )}
+
+ {showStateEditor && (
+
+
Machine State
+
+ updateStateSpec("storage", storage)}
+ />
+ updateStateSpec("stack", stack)}
+ />
+ updateStateSpec("memory", memory)}
+ />
+
+
+ )}
+
+
+ {showControls && (
+
+ resolve()}
+ disabled={isResolving || !pointer}
+ type="button"
+ >
+ {isResolving ? "Resolving..." : "Resolve Pointer"}
+
+
+ )}
+
+
+ {error && (
+
+
Resolution Error
+
{error.message}
+
+ )}
+
+ {result && (
+ <>
+
+
Named Regions
+
+
+
+
+
Resolved Output
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/packages/pointers-react/src/components/variables.css b/packages/pointers-react/src/components/variables.css
new file mode 100644
index 000000000..54ff98f01
--- /dev/null
+++ b/packages/pointers-react/src/components/variables.css
@@ -0,0 +1,116 @@
+/**
+ * CSS custom properties for @ethdebug/pointers-react components.
+ *
+ * These variables provide theme-aware styling that integrates with
+ * Docusaurus/Infima theming.
+ */
+
+:root {
+ /* Background colors */
+ --pointers-bg-primary: #ffffff;
+ --pointers-bg-secondary: #f5f6f7;
+ --pointers-bg-code: #f5f6f7;
+ --pointers-bg-hover: rgba(0, 0, 0, 0.05);
+ --pointers-bg-highlight: rgba(9, 105, 218, 0.1);
+
+ /* Border colors */
+ --pointers-border-primary: #dadde1;
+ --pointers-border-secondary: #eaecef;
+ --pointers-border-highlight: #0969da;
+
+ /* Text colors */
+ --pointers-text-primary: #1c1e21;
+ --pointers-text-secondary: #606770;
+ --pointers-text-muted: #898989;
+ --pointers-text-code: #1c1e21;
+
+ /* Syntax highlighting - light theme */
+ --pointers-syntax-keyword: #0550ae;
+ --pointers-syntax-type: #6f42c1;
+ --pointers-syntax-string: #0a3069;
+ --pointers-syntax-number: #0550ae;
+ --pointers-syntax-property: #8250df;
+
+ /* Region type colors */
+ --pointers-region-storage: #1a7f37;
+ --pointers-region-storage-bg: rgba(26, 127, 55, 0.1);
+ --pointers-region-memory: #0969da;
+ --pointers-region-memory-bg: rgba(9, 105, 218, 0.1);
+ --pointers-region-stack: #8250df;
+ --pointers-region-stack-bg: rgba(130, 80, 223, 0.1);
+ --pointers-region-calldata: #bf8700;
+ --pointers-region-calldata-bg: rgba(191, 135, 0, 0.1);
+ --pointers-region-returndata: #cf222e;
+ --pointers-region-returndata-bg: rgba(207, 34, 46, 0.1);
+ --pointers-region-code: #6e7781;
+ --pointers-region-code-bg: rgba(110, 119, 129, 0.1);
+ --pointers-region-transient: #953800;
+ --pointers-region-transient-bg: rgba(149, 56, 0, 0.1);
+
+ /* Accent colors */
+ --pointers-accent-blue: #0969da;
+ --pointers-accent-blue-bg: rgba(9, 105, 218, 0.1);
+ --pointers-accent-green: #1a7f37;
+ --pointers-accent-green-bg: rgba(26, 127, 55, 0.1);
+ --pointers-accent-red: #cf222e;
+ --pointers-accent-purple: #8250df;
+
+ /* Data display */
+ --pointers-data-bg: #f6f8fa;
+ --pointers-data-border: #d0d7de;
+}
+
+[data-theme="dark"] {
+ /* Background colors */
+ --pointers-bg-primary: #1e1e1e;
+ --pointers-bg-secondary: #2d2d30;
+ --pointers-bg-code: #2d2d30;
+ --pointers-bg-hover: rgba(255, 255, 255, 0.05);
+ --pointers-bg-highlight: rgba(86, 156, 214, 0.15);
+
+ /* Border colors */
+ --pointers-border-primary: #3e3e42;
+ --pointers-border-secondary: #454545;
+ --pointers-border-highlight: #569cd6;
+
+ /* Text colors */
+ --pointers-text-primary: #cccccc;
+ --pointers-text-secondary: #969696;
+ --pointers-text-muted: #858585;
+ --pointers-text-code: #d4d4d4;
+
+ /* Syntax highlighting - dark theme */
+ --pointers-syntax-keyword: #569cd6;
+ --pointers-syntax-type: #4ec9b0;
+ --pointers-syntax-string: #ce9178;
+ --pointers-syntax-number: #b5cea8;
+ --pointers-syntax-property: #9cdcfe;
+
+ /* Region type colors */
+ --pointers-region-storage: #4ec9b0;
+ --pointers-region-storage-bg: rgba(78, 201, 176, 0.15);
+ --pointers-region-memory: #569cd6;
+ --pointers-region-memory-bg: rgba(86, 156, 214, 0.15);
+ --pointers-region-stack: #c586c0;
+ --pointers-region-stack-bg: rgba(197, 134, 192, 0.15);
+ --pointers-region-calldata: #dcdcaa;
+ --pointers-region-calldata-bg: rgba(220, 220, 170, 0.15);
+ --pointers-region-returndata: #f14c4c;
+ --pointers-region-returndata-bg: rgba(241, 76, 76, 0.15);
+ --pointers-region-code: #858585;
+ --pointers-region-code-bg: rgba(133, 133, 133, 0.15);
+ --pointers-region-transient: #ce9178;
+ --pointers-region-transient-bg: rgba(206, 145, 120, 0.15);
+
+ /* Accent colors */
+ --pointers-accent-blue: #569cd6;
+ --pointers-accent-blue-bg: rgba(86, 156, 214, 0.15);
+ --pointers-accent-green: #4ec9b0;
+ --pointers-accent-green-bg: rgba(78, 201, 176, 0.2);
+ --pointers-accent-red: #f14c4c;
+ --pointers-accent-purple: #c586c0;
+
+ /* Data display */
+ --pointers-data-bg: #252526;
+ --pointers-data-border: #3e3e42;
+}
diff --git a/packages/pointers-react/src/hooks/usePointerResolution.ts b/packages/pointers-react/src/hooks/usePointerResolution.ts
new file mode 100644
index 000000000..58a47b405
--- /dev/null
+++ b/packages/pointers-react/src/hooks/usePointerResolution.ts
@@ -0,0 +1,179 @@
+/**
+ * Hook for pointer resolution with state management.
+ */
+
+import { useState, useCallback, useEffect } from "react";
+import type { Pointer } from "@ethdebug/format";
+import { dereference, type Cursor, Data } from "@ethdebug/pointers";
+import { createMockState, type MockStateSpec } from "#utils/mockState";
+
+/**
+ * Result of resolving a pointer against machine state.
+ */
+export interface ResolutionResult {
+ /** The resolved regions */
+ regions: Cursor.Region[];
+ /** Values read from each region */
+ values: Map;
+ /** Named region lookup */
+ namedRegions: Record;
+ /** Most recent region for each name */
+ lookup: Record;
+}
+
+/**
+ * State returned by usePointerResolution hook.
+ */
+export interface PointerResolutionState {
+ /** The current pointer being resolved */
+ pointer: Pointer | null;
+ /** The current machine state specification */
+ stateSpec: MockStateSpec;
+ /** Whether resolution is in progress */
+ isResolving: boolean;
+ /** The resolution result, if available */
+ result: ResolutionResult | null;
+ /** Any error that occurred during resolution */
+ error: Error | null;
+
+ /** Set a new pointer to resolve */
+ setPointer(pointer: Pointer | null): void;
+ /** Update the machine state specification */
+ setStateSpec(spec: MockStateSpec): void;
+ /** Update a single field of the state specification */
+ updateStateSpec(
+ key: K,
+ value: MockStateSpec[K],
+ ): void;
+ /** Trigger resolution (usually called automatically) */
+ resolve(): Promise;
+ /** Reset to initial state */
+ reset(): void;
+}
+
+/**
+ * Options for usePointerResolution hook.
+ */
+export interface UsePointerResolutionOptions {
+ /** Initial pointer to resolve */
+ initialPointer?: Pointer;
+ /** Initial machine state specification */
+ initialStateSpec?: MockStateSpec;
+ /** Whether to auto-resolve when pointer or state changes */
+ autoResolve?: boolean;
+}
+
+/**
+ * Hook for managing pointer resolution state.
+ *
+ * @param options - Configuration options
+ * @returns Pointer resolution state and controls
+ */
+export function usePointerResolution(
+ options: UsePointerResolutionOptions = {},
+): PointerResolutionState {
+ const {
+ initialPointer = null,
+ initialStateSpec = {},
+ autoResolve = true,
+ } = options;
+
+ const [pointer, setPointer] = useState(initialPointer);
+ const [stateSpec, setStateSpec] = useState(initialStateSpec);
+ const [isResolving, setIsResolving] = useState(false);
+ const [result, setResult] = useState(null);
+ const [error, setError] = useState(null);
+
+ const updateStateSpec = useCallback(
+ (key: K, value: MockStateSpec[K]) => {
+ setStateSpec((prev) => ({ ...prev, [key]: value }));
+ },
+ [],
+ );
+
+ const resolve = useCallback(async () => {
+ if (!pointer) {
+ setResult(null);
+ setError(null);
+ return;
+ }
+
+ setIsResolving(true);
+ setError(null);
+
+ try {
+ const state = createMockState(stateSpec);
+ const cursor = await dereference(pointer, { state });
+ const view = await cursor.view(state);
+
+ // Read values for each region
+ const values = new Map();
+ for (const region of view.regions) {
+ try {
+ const data = await view.read(region);
+ values.set(region, data);
+ } catch {
+ // Some regions may not be readable, skip them
+ }
+ }
+
+ // Build named regions map
+ const namedRegions: Record = {};
+ for (const region of view.regions) {
+ if ("name" in region && typeof region.name === "string") {
+ if (!namedRegions[region.name]) {
+ namedRegions[region.name] = [];
+ }
+ namedRegions[region.name].push(region);
+ }
+ }
+
+ // Build lookup (most recent by name)
+ const lookup: Record = {};
+ for (const [name, regions] of Object.entries(namedRegions)) {
+ if (regions.length > 0) {
+ lookup[name] = regions[regions.length - 1];
+ }
+ }
+
+ setResult({
+ regions: [...view.regions],
+ values,
+ namedRegions,
+ lookup,
+ });
+ } catch (err) {
+ setError(err instanceof Error ? err : new Error(String(err)));
+ setResult(null);
+ } finally {
+ setIsResolving(false);
+ }
+ }, [pointer, stateSpec]);
+
+ const reset = useCallback(() => {
+ setPointer(initialPointer);
+ setStateSpec(initialStateSpec);
+ setResult(null);
+ setError(null);
+ }, [initialPointer, initialStateSpec]);
+
+ // Auto-resolve when pointer or state changes
+ useEffect(() => {
+ if (autoResolve) {
+ resolve();
+ }
+ }, [autoResolve, resolve]);
+
+ return {
+ pointer,
+ stateSpec,
+ isResolving,
+ result,
+ error,
+ setPointer,
+ setStateSpec,
+ updateStateSpec,
+ resolve,
+ reset,
+ };
+}
diff --git a/packages/pointers-react/src/index.ts b/packages/pointers-react/src/index.ts
new file mode 100644
index 000000000..ec1e2c4bd
--- /dev/null
+++ b/packages/pointers-react/src/index.ts
@@ -0,0 +1,49 @@
+/**
+ * @ethdebug/pointers-react
+ *
+ * React components for visualizing ethdebug/format pointer resolution.
+ */
+
+// Context and Provider
+export {
+ PointerResolverProvider,
+ usePointerResolverContext,
+ type PointerResolverProviderProps,
+ type PointerResolutionState,
+} from "#components/PointerResolverContext";
+
+// Main visualization component
+export {
+ ResolutionVisualizer,
+ type ResolutionVisualizerProps,
+} from "#components/ResolutionVisualizer";
+
+// Sub-components
+export { RegionMap, type RegionMapProps } from "#components/RegionMap";
+
+export { RegionOutput, type RegionOutputProps } from "#components/RegionOutput";
+
+// Hooks
+export {
+ usePointerResolution,
+ type UsePointerResolutionOptions,
+ type ResolutionResult,
+} from "#hooks/usePointerResolution";
+
+// Utilities
+export {
+ createMockState,
+ formatData,
+ formatDataShort,
+ type MockStateSpec,
+} from "#utils/mockState";
+
+// Re-export types from dependencies for convenience
+export type { Pointer } from "@ethdebug/format";
+export type { Cursor, Machine, Data } from "@ethdebug/pointers";
+
+// CSS - consumers should import these stylesheets
+// import "@ethdebug/pointers-react/components/variables.css";
+// import "@ethdebug/pointers-react/components/ResolutionVisualizer.css";
+// import "@ethdebug/pointers-react/components/RegionMap.css";
+// import "@ethdebug/pointers-react/components/RegionOutput.css";
diff --git a/packages/pointers-react/src/utils/mockState.ts b/packages/pointers-react/src/utils/mockState.ts
new file mode 100644
index 000000000..d63f20a7b
--- /dev/null
+++ b/packages/pointers-react/src/utils/mockState.ts
@@ -0,0 +1,237 @@
+/**
+ * Utilities for creating mock Machine.State objects for visualization.
+ */
+
+import { type Machine, Data } from "@ethdebug/pointers";
+
+/**
+ * Specification for creating a mock Machine.State.
+ */
+export interface MockStateSpec {
+ /** Stack entries (from top to bottom). Can be hex strings or bigints. */
+ stack?: Array;
+ /** Memory contents as hex string (e.g., "0x1234...") */
+ memory?: string;
+ /** Storage slots: slot (hex) → value (hex) */
+ storage?: Record;
+ /** Calldata as hex string */
+ calldata?: string;
+ /** Return data as hex string */
+ returndata?: string;
+ /** Contract code as hex string */
+ code?: string;
+ /** Transient storage: slot (hex) → value (hex) */
+ transient?: Record;
+ /** Program counter */
+ programCounter?: bigint;
+ /** Current opcode name */
+ opcode?: string;
+ /** Trace index */
+ traceIndex?: bigint;
+}
+
+/**
+ * Create a mock Machine.State from a specification.
+ *
+ * This creates a fully functional Machine.State that can be used
+ * for pointer dereferencing in visualization contexts.
+ */
+export function createMockState(spec: MockStateSpec): Machine.State {
+ const stackEntries = (spec.stack || []).map((entry) =>
+ typeof entry === "string"
+ ? Data.fromHex(entry).padUntilAtLeast(32)
+ : Data.fromUint(entry).padUntilAtLeast(32),
+ );
+
+ const memoryData = spec.memory ? Data.fromHex(spec.memory) : Data.zero();
+
+ const storageMap = new Map();
+ for (const [slot, value] of Object.entries(spec.storage || {})) {
+ const normalizedSlot = Data.fromHex(slot).padUntilAtLeast(32).toHex();
+ storageMap.set(normalizedSlot, Data.fromHex(value).padUntilAtLeast(32));
+ }
+
+ const calldataData = spec.calldata
+ ? Data.fromHex(spec.calldata)
+ : Data.zero();
+ const returndataData = spec.returndata
+ ? Data.fromHex(spec.returndata)
+ : Data.zero();
+ const codeData = spec.code ? Data.fromHex(spec.code) : Data.zero();
+
+ const transientMap = new Map();
+ for (const [slot, value] of Object.entries(spec.transient || {})) {
+ const normalizedSlot = Data.fromHex(slot).padUntilAtLeast(32).toHex();
+ transientMap.set(normalizedSlot, Data.fromHex(value).padUntilAtLeast(32));
+ }
+
+ const stack: Machine.State.Stack = {
+ get length() {
+ return Promise.resolve(BigInt(stackEntries.length));
+ },
+ async peek({ depth, slice }) {
+ const index = Number(depth);
+ if (index >= stackEntries.length) {
+ throw new Error(`Stack underflow: depth ${depth} exceeds stack size`);
+ }
+ const entry = stackEntries[index];
+ if (!slice) {
+ return entry;
+ }
+ const { offset, length } = slice;
+ const startByte = 32 - Number(offset) - Number(length);
+ const endByte = startByte + Number(length);
+ return Data.fromBytes(entry.slice(startByte, endByte));
+ },
+ };
+
+ const memory: Machine.State.Bytes = {
+ get length() {
+ return Promise.resolve(BigInt(memoryData.length));
+ },
+ async read({ slice }) {
+ const { offset, length } = slice;
+ const start = Number(offset);
+ const end = start + Number(length);
+ if (end > memoryData.length) {
+ // Return zero-padded data for reads beyond memory
+ const result = new Uint8Array(Number(length));
+ const available = Math.max(0, memoryData.length - start);
+ if (available > 0 && start < memoryData.length) {
+ result.set(memoryData.slice(start, start + available), 0);
+ }
+ return Data.fromBytes(result);
+ }
+ return Data.fromBytes(memoryData.slice(start, end));
+ },
+ };
+
+ const storage: Machine.State.Words = {
+ async read({ slot, slice }) {
+ const normalizedSlot = slot.padUntilAtLeast(32).toHex();
+ const value =
+ storageMap.get(normalizedSlot) || Data.zero().padUntilAtLeast(32);
+ if (!slice) {
+ return value;
+ }
+ const { offset, length } = slice;
+ const startByte = 32 - Number(offset) - Number(length);
+ const endByte = startByte + Number(length);
+ return Data.fromBytes(value.slice(startByte, endByte));
+ },
+ };
+
+ const calldata: Machine.State.Bytes = {
+ get length() {
+ return Promise.resolve(BigInt(calldataData.length));
+ },
+ async read({ slice }) {
+ const { offset, length } = slice;
+ const start = Number(offset);
+ const end = start + Number(length);
+ if (end > calldataData.length) {
+ const result = new Uint8Array(Number(length));
+ const available = Math.max(0, calldataData.length - start);
+ if (available > 0 && start < calldataData.length) {
+ result.set(calldataData.slice(start, start + available), 0);
+ }
+ return Data.fromBytes(result);
+ }
+ return Data.fromBytes(calldataData.slice(start, end));
+ },
+ };
+
+ const returndata: Machine.State.Bytes = {
+ get length() {
+ return Promise.resolve(BigInt(returndataData.length));
+ },
+ async read({ slice }) {
+ const { offset, length } = slice;
+ const start = Number(offset);
+ const end = start + Number(length);
+ if (end > returndataData.length) {
+ const result = new Uint8Array(Number(length));
+ const available = Math.max(0, returndataData.length - start);
+ if (available > 0 && start < returndataData.length) {
+ result.set(returndataData.slice(start, start + available), 0);
+ }
+ return Data.fromBytes(result);
+ }
+ return Data.fromBytes(returndataData.slice(start, end));
+ },
+ };
+
+ const code: Machine.State.Bytes = {
+ get length() {
+ return Promise.resolve(BigInt(codeData.length));
+ },
+ async read({ slice }) {
+ const { offset, length } = slice;
+ const start = Number(offset);
+ const end = start + Number(length);
+ if (end > codeData.length) {
+ const result = new Uint8Array(Number(length));
+ const available = Math.max(0, codeData.length - start);
+ if (available > 0 && start < codeData.length) {
+ result.set(codeData.slice(start, start + available), 0);
+ }
+ return Data.fromBytes(result);
+ }
+ return Data.fromBytes(codeData.slice(start, end));
+ },
+ };
+
+ const transient: Machine.State.Words = {
+ async read({ slot, slice }) {
+ const normalizedSlot = slot.padUntilAtLeast(32).toHex();
+ const value =
+ transientMap.get(normalizedSlot) || Data.zero().padUntilAtLeast(32);
+ if (!slice) {
+ return value;
+ }
+ const { offset, length } = slice;
+ const startByte = 32 - Number(offset) - Number(length);
+ const endByte = startByte + Number(length);
+ return Data.fromBytes(value.slice(startByte, endByte));
+ },
+ };
+
+ return {
+ get traceIndex() {
+ return Promise.resolve(spec.traceIndex ?? 0n);
+ },
+ get programCounter() {
+ return Promise.resolve(spec.programCounter ?? 0n);
+ },
+ get opcode() {
+ return Promise.resolve(spec.opcode ?? "STOP");
+ },
+ stack,
+ memory,
+ storage,
+ calldata,
+ returndata,
+ code,
+ transient,
+ };
+}
+
+/**
+ * Format a Data value as a shortened hex string for display.
+ */
+export function formatDataShort(data: Data, maxLength = 10): string {
+ const hex = data.toHex();
+ if (hex.length <= maxLength + 2) {
+ return hex;
+ }
+ const prefixLength = Math.floor((maxLength - 2) / 2);
+ const suffixLength = maxLength - 2 - prefixLength;
+ return `${hex.slice(0, 2 + prefixLength)}...${hex.slice(-suffixLength)}`;
+}
+
+/**
+ * Format a Data value as a full hex string.
+ */
+export function formatData(data: Data): string {
+ return data.toHex();
+}
diff --git a/packages/pointers-react/tsconfig.json b/packages/pointers-react/tsconfig.json
new file mode 100644
index 000000000..80d8f4b3d
--- /dev/null
+++ b/packages/pointers-react/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "./",
+ "outDir": "./dist/",
+ "baseUrl": "./",
+ "jsx": "react-jsx",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "paths": {
+ "#components/*": ["./src/components/*"],
+ "#hooks/*": ["./src/hooks/*"],
+ "#utils/*": ["./src/utils/*"]
+ }
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"],
+ "references": [{ "path": "../format" }, { "path": "../pointers" }]
+}
diff --git a/packages/pointers/src/data.ts b/packages/pointers/src/data.ts
index 093c4c4d0..6dbe5229a 100644
--- a/packages/pointers/src/data.ts
+++ b/packages/pointers/src/data.ts
@@ -2,12 +2,9 @@ import { toHex } from "ethereum-cryptography/utils";
import type * as Util from "util";
-let util: typeof Util | undefined;
-try {
- util = await import("util");
-} catch {
- // util is not available in browser environments
-}
+// Symbol for custom inspect (Node.js util.inspect.custom)
+// We use a well-known symbol value to avoid top-level await issues
+const customInspectSymbol = Symbol.for("nodejs.util.inspect.custom");
export class Data extends Uint8Array {
static zero(): Data {
@@ -111,9 +108,7 @@ export class Data extends Uint8Array {
return `Data[${options.stylize(this.toHex(), "number")}]`;
}
- [util && "inspect" in util && typeof util.inspect === "object"
- ? util.inspect.custom
- : "_inspect"](
+ [customInspectSymbol](
depth: number,
options: Util.InspectOptionsStylized,
inspect: typeof Util.inspect,
diff --git a/packages/programs-react/package.json b/packages/programs-react/package.json
new file mode 100644
index 000000000..2d207f5b3
--- /dev/null
+++ b/packages/programs-react/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@ethdebug/programs-react",
+ "version": "0.1.0-0",
+ "description": "React components for visualizing ethdebug/format program annotations",
+ "type": "module",
+ "main": "dist/src/index.js",
+ "types": "dist/src/index.d.ts",
+ "license": "MIT",
+ "imports": {
+ "#components/*": {
+ "types": "./src/components/*.tsx",
+ "default": "./dist/src/components/*.js"
+ },
+ "#shiki/*": {
+ "types": "./src/shiki/*.ts",
+ "default": "./dist/src/shiki/*.js"
+ },
+ "#utils/*": {
+ "types": "./src/utils/*.ts",
+ "default": "./dist/src/utils/*.js"
+ }
+ },
+ "scripts": {
+ "prepare": "tsc",
+ "build": "tsc",
+ "watch": "tsc --watch --preserveWatchOutput",
+ "test": "vitest run"
+ },
+ "dependencies": {
+ "@ethdebug/format": "^0.1.0-0",
+ "@shikijs/langs": "^2.5.0",
+ "@shikijs/themes": "^2.5.0",
+ "shiki": "^2.5.0"
+ },
+ "devDependencies": {
+ "@testing-library/dom": "^10.0.0",
+ "@testing-library/react": "^16.0.0",
+ "@types/react": "^18.2.43",
+ "@types/react-dom": "^18.2.17",
+ "jsdom": "^26.0.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "typescript": "^5.0.0",
+ "vitest": "^3.2.4"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/web/src/theme/ProgramExample/HighlightedInstruction.tsx b/packages/programs-react/src/components/HighlightedInstruction.tsx
similarity index 58%
rename from packages/web/src/theme/ProgramExample/HighlightedInstruction.tsx
rename to packages/programs-react/src/components/HighlightedInstruction.tsx
index 8dcb4b476..86dddf447 100644
--- a/packages/web/src/theme/ProgramExample/HighlightedInstruction.tsx
+++ b/packages/programs-react/src/components/HighlightedInstruction.tsx
@@ -1,8 +1,17 @@
+/**
+ * Displays the currently highlighted instruction as JSON.
+ */
+
import React from "react";
-import { useProgramExampleContext } from "./ProgramExampleContext";
+import { useProgramExampleContext } from "./ProgramExampleContext.js";
-import { ShikiCodeBlock } from "@theme/ShikiCodeBlock";
+import { ShikiCodeBlock } from "#shiki/ShikiCodeBlock";
+/**
+ * Renders the currently highlighted instruction as formatted JSON.
+ *
+ * @returns JSON representation of the highlighted instruction
+ */
export function HighlightedInstruction(): JSX.Element {
const { highlightedInstruction } = useProgramExampleContext();
diff --git a/packages/programs-react/src/components/Opcodes.css b/packages/programs-react/src/components/Opcodes.css
new file mode 100644
index 000000000..7296ab1dd
--- /dev/null
+++ b/packages/programs-react/src/components/Opcodes.css
@@ -0,0 +1,37 @@
+dl.opcodes {
+ display: grid;
+ grid-template-columns: max-content max-content;
+ margin: 0;
+ padding: 0;
+ align-items: justify;
+}
+
+dl.opcodes dt {
+ grid-column-start: 1;
+ border-radius: var(--ifm-global-radius, 4px);
+ padding: 5px 10px;
+ margin: 0px;
+ border-bottom-left-radius: 0px;
+ border-bottom-right-radius: 0px;
+ cursor: pointer;
+}
+
+dl.opcodes dt,
+dl.opcodes dt + dd {
+ margin-top: 5px;
+ border-bottom: 1px solid var(--ifm-color-primary-light, #4dabf7);
+}
+
+dl.opcodes dd {
+ grid-column-start: 2;
+ margin: 0px;
+ padding: 5px 5px;
+}
+
+dl.opcodes dt.active {
+ background-color: var(--ifm-color-primary-lighter, #74c0fc);
+}
+
+dl.opcodes dt:not(.active):hover {
+ background-color: var(--ifm-hover-overlay, rgba(0, 0, 0, 0.05));
+}
diff --git a/packages/web/src/theme/ProgramExample/Opcodes.tsx b/packages/programs-react/src/components/Opcodes.tsx
similarity index 87%
rename from packages/web/src/theme/ProgramExample/Opcodes.tsx
rename to packages/programs-react/src/components/Opcodes.tsx
index 7e6debc37..3da00432d 100644
--- a/packages/web/src/theme/ProgramExample/Opcodes.tsx
+++ b/packages/programs-react/src/components/Opcodes.tsx
@@ -1,10 +1,20 @@
+/**
+ * Opcodes list component for displaying program instructions.
+ */
+
import React, { useEffect, useState } from "react";
-import { useProgramExampleContext } from "./ProgramExampleContext";
+import { useProgramExampleContext } from "./ProgramExampleContext.js";
import { Data, Program } from "@ethdebug/format";
-import "./Opcodes.css";
+// CSS is expected to be imported by the consuming application
+// import "./Opcodes.css";
+/**
+ * Displays a list of opcodes with interactive highlighting.
+ *
+ * @returns Opcodes list element
+ */
export function Opcodes(): JSX.Element {
const {
instructions,
@@ -44,6 +54,7 @@ export function Opcodes(): JSX.Element {
}
highlightInstruction(undefined);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeOffset, hoverOffset, highlightedInstruction, highlightMode]);
const handleClick = (offset: Data.Value) =>
@@ -54,7 +65,7 @@ export function Opcodes(): JSX.Element {
const handleMouseEnter = (offset: Data.Value) => setHoverOffset(offset);
// skipping the current hover offset check here and assuming that the mouse
// must leave the boundary of one offset before entering another
- const handleMouseLeave = (_offset: Data.Value) => setHoverOffset(undefined);
+ const handleMouseLeave = () => setHoverOffset(undefined);
const paddingLength = instructions.at(-1)!.offset.toString(16).length;
@@ -68,7 +79,7 @@ export function Opcodes(): JSX.Element {
paddingLength={paddingLength}
onClick={() => handleClick(instruction.offset)}
onMouseEnter={() => handleMouseEnter(instruction.offset)}
- onMouseLeave={() => handleMouseLeave(instruction.offset)}
+ onMouseLeave={handleMouseLeave}
/>
))}
diff --git a/packages/programs-react/src/components/ProgramExampleContext.test.tsx b/packages/programs-react/src/components/ProgramExampleContext.test.tsx
new file mode 100644
index 000000000..88ff691b3
--- /dev/null
+++ b/packages/programs-react/src/components/ProgramExampleContext.test.tsx
@@ -0,0 +1,155 @@
+/**
+ * Tests for ProgramExampleContext.
+ */
+
+import { describe, it, expect, vi } from "vitest";
+import { renderHook, act } from "@testing-library/react";
+import React from "react";
+import {
+ ProgramExampleContextProvider,
+ useProgramExampleContext,
+} from "./ProgramExampleContext.js";
+
+describe("ProgramExampleContext", () => {
+ const source = {
+ id: "test-source",
+ path: "/test/source.js",
+ language: "javascript",
+ contents: "let x = 1;",
+ };
+
+ const instructions = [
+ {
+ operation: { mnemonic: "PUSH1" as const, arguments: ["0x01"] },
+ context: { remark: "push value" },
+ },
+ {
+ operation: { mnemonic: "STOP" as const },
+ context: { remark: "stop" },
+ },
+ ];
+
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
+
+ {children}
+
+ );
+
+ it("provides sources from props", () => {
+ const { result } = renderHook(() => useProgramExampleContext(), {
+ wrapper,
+ });
+
+ expect(result.current.sources).toHaveLength(1);
+ expect(result.current.sources[0].id).toBe("test-source");
+ });
+
+ it("computes instruction offsets", () => {
+ const { result } = renderHook(() => useProgramExampleContext(), {
+ wrapper,
+ });
+
+ expect(result.current.instructions).toHaveLength(2);
+ expect(result.current.instructions[0].offset).toBe(0);
+ expect(result.current.instructions[1].offset).toBe(2); // After PUSH1 0x01
+ });
+
+ it("starts with no highlighted instruction", () => {
+ const { result } = renderHook(() => useProgramExampleContext(), {
+ wrapper,
+ });
+
+ expect(result.current.highlightedInstruction).toBeUndefined();
+ });
+
+ it("highlights instruction by offset", async () => {
+ const { result } = renderHook(() => useProgramExampleContext(), {
+ wrapper,
+ });
+
+ act(() => {
+ result.current.highlightInstruction(0);
+ });
+
+ // Wait for useEffect
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ expect(result.current.highlightedInstruction).toBeDefined();
+ expect(result.current.highlightedInstruction?.offset).toBe(0);
+ expect(result.current.highlightedInstruction?.context).toEqual({
+ remark: "push value",
+ });
+ });
+
+ it("clears highlight when offset is undefined", async () => {
+ const { result } = renderHook(() => useProgramExampleContext(), {
+ wrapper,
+ });
+
+ // First highlight an instruction
+ act(() => {
+ result.current.highlightInstruction(0);
+ });
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ expect(result.current.highlightedInstruction).toBeDefined();
+
+ // Then clear it
+ act(() => {
+ result.current.highlightInstruction(undefined);
+ });
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ expect(result.current.highlightedInstruction).toBeUndefined();
+ });
+
+ it("starts in simple highlight mode", () => {
+ const { result } = renderHook(() => useProgramExampleContext(), {
+ wrapper,
+ });
+
+ expect(result.current.highlightMode).toBe("simple");
+ });
+
+ it("switches to detailed mode", () => {
+ const { result } = renderHook(() => useProgramExampleContext(), {
+ wrapper,
+ });
+
+ act(() => {
+ result.current.showDetails();
+ });
+
+ expect(result.current.highlightMode).toBe("detailed");
+ });
+
+ it("switches back to simple mode", () => {
+ const { result } = renderHook(() => useProgramExampleContext(), {
+ wrapper,
+ });
+
+ act(() => {
+ result.current.showDetails();
+ });
+ expect(result.current.highlightMode).toBe("detailed");
+
+ act(() => {
+ result.current.hideDetails();
+ });
+ expect(result.current.highlightMode).toBe("simple");
+ });
+
+ it("throws when used outside provider", () => {
+ // Suppress React's console.error for expected error
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
+
+ expect(() => {
+ renderHook(() => useProgramExampleContext());
+ }).toThrow(/must be used within a ProgramExampleContextProvider/);
+
+ consoleSpy.mockRestore();
+ });
+});
diff --git a/packages/web/src/theme/ProgramExample/ProgramExampleContext.tsx b/packages/programs-react/src/components/ProgramExampleContext.tsx
similarity index 66%
rename from packages/web/src/theme/ProgramExample/ProgramExampleContext.tsx
rename to packages/programs-react/src/components/ProgramExampleContext.tsx
index d514cdea8..ff85ee020 100644
--- a/packages/web/src/theme/ProgramExample/ProgramExampleContext.tsx
+++ b/packages/programs-react/src/components/ProgramExampleContext.tsx
@@ -1,19 +1,33 @@
+/**
+ * React context for program example state management.
+ */
+
import React, { createContext, useContext, useState, useEffect } from "react";
import { Data, Materials, Program } from "@ethdebug/format";
-import { computeOffsets } from "./offsets";
-import { type DynamicInstruction, resolveDynamicInstruction } from "./dynamic";
-
+import { computeOffsets } from "#utils/offsets";
+import {
+ type DynamicInstruction,
+ resolveDynamicInstruction,
+} from "#utils/dynamic";
+
+/**
+ * State provided by the ProgramExample context.
+ */
export interface ProgramExampleState {
- // props
+ /** Source materials for the program */
sources: Materials.Source[];
+ /** Resolved program instructions */
instructions: Program.Instruction[];
-
- // stateful stuff
+ /** Currently highlighted instruction, if any */
highlightedInstruction: Program.Instruction | undefined;
+ /** Function to highlight an instruction by offset */
highlightInstruction(offset: Data.Value | undefined): void;
+ /** Current highlight mode */
highlightMode: "simple" | "detailed";
+ /** Switch to detailed highlight mode */
showDetails(): void;
+ /** Switch to simple highlight mode */
hideDetails(): void;
}
@@ -21,22 +35,40 @@ const ProgramExampleContext = createContext(
undefined,
);
-export function useProgramExampleContext() {
+/**
+ * Hook to access the ProgramExample context.
+ *
+ * @returns The current ProgramExample state
+ * @throws If used outside of a ProgramExampleContextProvider
+ */
+export function useProgramExampleContext(): ProgramExampleState {
const context = useContext(ProgramExampleContext);
if (context === undefined) {
throw new Error(
- "useProgramExampleContext must be used within a ProgramExampleContextProvider",
+ "useProgramExampleContext must be used within a " +
+ "ProgramExampleContextProvider",
);
}
return context;
}
+/**
+ * Props for ProgramExampleContextProvider.
+ */
export interface ProgramExampleProps {
+ /** Source materials */
sources: Materials.Source[];
+ /** Dynamic instructions (without offsets) */
instructions: Omit[];
}
+/**
+ * Provides program example context to child components.
+ *
+ * @param props - Sources, instructions, and children
+ * @returns Context provider wrapping children
+ */
export function ProgramExampleContextProvider({
children,
...props
@@ -81,6 +113,7 @@ export function ProgramExampleContextProvider({
}
setHighlightedInstruction(instruction);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, [highlightedOffset, setHighlightedInstruction]);
return (
diff --git a/packages/programs-react/src/components/SourceContents.css b/packages/programs-react/src/components/SourceContents.css
new file mode 100644
index 000000000..5bd07a844
--- /dev/null
+++ b/packages/programs-react/src/components/SourceContents.css
@@ -0,0 +1,14 @@
+.highlighted-code {
+ font-weight: bold;
+ background-color: var(--ifm-color-primary-lightest, #e7f5ff);
+}
+
+.highlighted-ambiguous-code {
+ font-weight: bold;
+ background-color: var(--ifm-color-warning-lightest, #fff9db);
+}
+
+.highlighted-variable-declaration {
+ text-decoration: underline;
+ text-decoration-style: wavy;
+}
diff --git a/packages/web/src/theme/ProgramExample/SourceContents.tsx b/packages/programs-react/src/components/SourceContents.tsx
similarity index 85%
rename from packages/web/src/theme/ProgramExample/SourceContents.tsx
rename to packages/programs-react/src/components/SourceContents.tsx
index 82429a604..793e67e67 100644
--- a/packages/web/src/theme/ProgramExample/SourceContents.tsx
+++ b/packages/programs-react/src/components/SourceContents.tsx
@@ -1,17 +1,28 @@
+/**
+ * Source contents viewer with instruction context highlighting.
+ */
+
import React from "react";
import {
ShikiCodeBlock,
- type Props as ShikiCodeBlockProps,
-} from "@theme/ShikiCodeBlock";
+ type ShikiCodeBlockProps,
+} from "#shiki/ShikiCodeBlock";
-import "./SourceContents.css";
+// CSS is expected to be imported by the consuming application
+// import "./SourceContents.css";
import type * as Shiki from "shiki/core";
-import { useProgramExampleContext } from "./ProgramExampleContext";
+import { useProgramExampleContext } from "./ProgramExampleContext.js";
import { Materials, Program } from "@ethdebug/format";
+/**
+ * Displays source contents with highlighting based on instruction context.
+ *
+ * @param props - Highlight options (language is overridden)
+ * @returns Highlighted source code element
+ */
export function SourceContents(
props: Omit,
): JSX.Element {
@@ -79,7 +90,8 @@ function decoratePickContext(
// contexts
if (!pick.every(Program.Context.isCode)) {
console.warn(
- "decoratePickContext encountered non-code contexts in pick array. These will be ignored.",
+ "decoratePickContext encountered non-code contexts in pick array. " +
+ "These will be ignored.",
);
return [];
}
diff --git a/packages/programs-react/src/components/TraceContext.tsx b/packages/programs-react/src/components/TraceContext.tsx
new file mode 100644
index 000000000..a5201bfd0
--- /dev/null
+++ b/packages/programs-react/src/components/TraceContext.tsx
@@ -0,0 +1,184 @@
+/**
+ * React context for execution trace state management.
+ */
+
+import React, {
+ createContext,
+ useContext,
+ useState,
+ useCallback,
+ useMemo,
+} from "react";
+import type { Program } from "@ethdebug/format";
+import {
+ type TraceStep,
+ extractVariablesFromInstruction,
+ buildPcToInstructionMap,
+} from "#utils/mockTrace";
+
+/**
+ * A variable with its resolved value.
+ */
+export interface ResolvedVariable {
+ /** Variable identifier (name) */
+ identifier?: string;
+ /** Variable type information */
+ type?: unknown;
+ /** Variable pointer for resolution */
+ pointer?: unknown;
+ /** Resolved value (if available) */
+ value?: string;
+ /** Error if resolution failed */
+ error?: string;
+}
+
+/**
+ * State provided by the Trace context.
+ */
+export interface TraceState {
+ /** The execution trace steps */
+ trace: TraceStep[];
+ /** The program with instruction metadata */
+ program: Program;
+ /** Current step index in the trace */
+ currentStepIndex: number;
+ /** Total number of steps */
+ totalSteps: number;
+ /** Current trace step */
+ currentStep: TraceStep | undefined;
+ /** Current instruction at the PC */
+ currentInstruction: Program.Instruction | undefined;
+ /** Variables in scope at current step */
+ currentVariables: ResolvedVariable[];
+ /** Whether we're at the first step */
+ isAtStart: boolean;
+ /** Whether we're at the last step */
+ isAtEnd: boolean;
+
+ /** Move to the next step */
+ stepForward(): void;
+ /** Move to the previous step */
+ stepBackward(): void;
+ /** Jump to a specific step */
+ jumpToStep(index: number): void;
+ /** Reset to the first step */
+ reset(): void;
+ /** Jump to the end */
+ jumpToEnd(): void;
+}
+
+const TraceContext = createContext(undefined);
+
+/**
+ * Hook to access the Trace context.
+ *
+ * @returns The current trace state
+ * @throws If used outside of a TraceProvider
+ */
+export function useTraceContext(): TraceState {
+ const context = useContext(TraceContext);
+ if (context === undefined) {
+ throw new Error("useTraceContext must be used within a TraceProvider");
+ }
+ return context;
+}
+
+/**
+ * Props for TraceProvider.
+ */
+export interface TraceProviderProps {
+ /** The execution trace */
+ trace: TraceStep[];
+ /** The program definition */
+ program: Program;
+ /** Initial step index (default: 0) */
+ initialStepIndex?: number;
+ /** Children to render */
+ children: React.ReactNode;
+}
+
+/**
+ * Provides trace context to child components.
+ */
+export function TraceProvider({
+ trace,
+ program,
+ initialStepIndex = 0,
+ children,
+}: TraceProviderProps): JSX.Element {
+ const [currentStepIndex, setCurrentStepIndex] = useState(
+ Math.min(initialStepIndex, trace.length - 1),
+ );
+
+ const pcToInstruction = useMemo(
+ () => buildPcToInstructionMap(program),
+ [program],
+ );
+
+ const currentStep = trace[currentStepIndex];
+ const currentInstruction = currentStep
+ ? pcToInstruction.get(currentStep.pc)
+ : undefined;
+
+ // Extract variables from current instruction
+ const currentVariables = useMemo(() => {
+ if (!currentInstruction) {
+ return [];
+ }
+
+ const vars = extractVariablesFromInstruction(currentInstruction);
+ return vars.map((v) => ({
+ identifier: v.identifier,
+ type: v.type,
+ pointer: v.pointer,
+ // Value resolution would require the full @ethdebug/pointers machinery
+ // For now we just show the variable metadata
+ value: undefined,
+ error: undefined,
+ }));
+ }, [currentInstruction]);
+
+ const stepForward = useCallback(() => {
+ setCurrentStepIndex((prev) => Math.min(prev + 1, trace.length - 1));
+ }, [trace.length]);
+
+ const stepBackward = useCallback(() => {
+ setCurrentStepIndex((prev) => Math.max(prev - 1, 0));
+ }, []);
+
+ const jumpToStep = useCallback(
+ (index: number) => {
+ setCurrentStepIndex(Math.max(0, Math.min(index, trace.length - 1)));
+ },
+ [trace.length],
+ );
+
+ const reset = useCallback(() => {
+ setCurrentStepIndex(0);
+ }, []);
+
+ const jumpToEnd = useCallback(() => {
+ setCurrentStepIndex(trace.length - 1);
+ }, [trace.length]);
+
+ const state: TraceState = {
+ trace,
+ program,
+ currentStepIndex,
+ totalSteps: trace.length,
+ currentStep,
+ currentInstruction,
+ currentVariables,
+ isAtStart: currentStepIndex === 0,
+ isAtEnd: currentStepIndex >= trace.length - 1,
+ stepForward,
+ stepBackward,
+ jumpToStep,
+ reset,
+ jumpToEnd,
+ };
+
+ return (
+ {children}
+ );
+}
diff --git a/packages/programs-react/src/components/TraceControls.css b/packages/programs-react/src/components/TraceControls.css
new file mode 100644
index 000000000..9dfcf5949
--- /dev/null
+++ b/packages/programs-react/src/components/TraceControls.css
@@ -0,0 +1,101 @@
+/**
+ * Styles for TraceControls component.
+ */
+
+.trace-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ padding: 0.75rem;
+ background: var(--programs-bg-secondary, #f5f6f7);
+ border-radius: 6px;
+}
+
+.trace-controls-navigation {
+ display: flex;
+ gap: 0.25rem;
+ justify-content: center;
+}
+
+.trace-control-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.25rem;
+ height: 2.25rem;
+ font-size: 1rem;
+ border: 1px solid var(--programs-border-primary, #dadde1);
+ border-radius: 4px;
+ background: var(--programs-bg-primary, #ffffff);
+ color: var(--programs-text-primary, #1c1e21);
+ cursor: pointer;
+ transition: all 0.15s;
+}
+
+.trace-control-btn:hover:not(:disabled) {
+ background: var(--programs-bg-hover, rgba(0, 0, 0, 0.05));
+ border-color: var(--programs-border-highlight, #0969da);
+}
+
+.trace-control-btn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+.trace-controls-info {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 1rem;
+ font-size: 0.8125rem;
+}
+
+.trace-step-counter {
+ color: var(--programs-text-secondary, #606770);
+}
+
+.trace-current-opcode code {
+ padding: 0.125rem 0.375rem;
+ font-size: 0.75rem;
+ background: var(--programs-accent-blue-bg, rgba(9, 105, 218, 0.1));
+ color: var(--programs-accent-blue, #0969da);
+ border-radius: 3px;
+}
+
+/* Progress bar */
+.trace-progress {
+ position: relative;
+ height: 8px;
+ background: var(--programs-bg-code, #f5f6f7);
+ border-radius: 4px;
+ cursor: pointer;
+ margin: 0.5rem 0;
+}
+
+.trace-progress:focus {
+ outline: 2px solid var(--programs-accent-blue, #0969da);
+ outline-offset: 2px;
+}
+
+.trace-progress-fill {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ background: var(--programs-accent-blue, #0969da);
+ border-radius: 4px;
+ transition: width 0.1s ease-out;
+}
+
+.trace-progress-handle {
+ position: absolute;
+ top: 50%;
+ width: 14px;
+ height: 14px;
+ background: var(--programs-accent-blue, #0969da);
+ border: 2px solid var(--programs-bg-primary, #ffffff);
+ border-radius: 50%;
+ transform: translate(-50%, -50%);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+ transition: left 0.1s ease-out;
+}
diff --git a/packages/programs-react/src/components/TraceControls.tsx b/packages/programs-react/src/components/TraceControls.tsx
new file mode 100644
index 000000000..cbda774c7
--- /dev/null
+++ b/packages/programs-react/src/components/TraceControls.tsx
@@ -0,0 +1,147 @@
+/**
+ * Navigation controls for stepping through an execution trace.
+ */
+
+import React from "react";
+import { useTraceContext } from "./TraceContext.js";
+
+// CSS is expected to be imported by the consuming application
+// import "./TraceControls.css";
+
+export interface TraceControlsProps {
+ /** Whether to show step count label */
+ showStepCount?: boolean;
+ /** Whether to show the opcode label */
+ showOpcode?: boolean;
+ /** Custom class name */
+ className?: string;
+}
+
+/**
+ * Navigation controls for trace stepping.
+ */
+export function TraceControls({
+ showStepCount = true,
+ showOpcode = true,
+ className = "",
+}: TraceControlsProps): JSX.Element {
+ const {
+ currentStepIndex,
+ totalSteps,
+ currentStep,
+ isAtStart,
+ isAtEnd,
+ stepBackward,
+ stepForward,
+ reset,
+ jumpToEnd,
+ } = useTraceContext();
+
+ return (
+
+
+
+ ⏮
+
+
+ ←
+
+
+ →
+
+
+ ⏭
+
+
+
+ {showStepCount && (
+
+
+ Step {currentStepIndex + 1} / {totalSteps}
+
+ {showOpcode && currentStep && (
+
+ {currentStep.opcode}
+
+ )}
+
+ )}
+
+ );
+}
+
+export interface TraceProgressProps {
+ /** Custom class name */
+ className?: string;
+}
+
+/**
+ * Progress bar showing current position in trace.
+ */
+export function TraceProgress({
+ className = "",
+}: TraceProgressProps): JSX.Element {
+ const { currentStepIndex, totalSteps, jumpToStep } = useTraceContext();
+
+ const progress = totalSteps > 1 ? currentStepIndex / (totalSteps - 1) : 0;
+
+ const handleClick = (e: React.MouseEvent) => {
+ const rect = e.currentTarget.getBoundingClientRect();
+ const x = e.clientX - rect.left;
+ const percent = x / rect.width;
+ const step = Math.round(percent * (totalSteps - 1));
+ jumpToStep(step);
+ };
+
+ return (
+ {
+ if (e.key === "ArrowRight") {
+ jumpToStep(Math.min(currentStepIndex + 1, totalSteps - 1));
+ } else if (e.key === "ArrowLeft") {
+ jumpToStep(Math.max(currentStepIndex - 1, 0));
+ }
+ }}
+ >
+
+
+
+ );
+}
diff --git a/packages/programs-react/src/components/VariableInspector.css b/packages/programs-react/src/components/VariableInspector.css
new file mode 100644
index 000000000..e4dd29473
--- /dev/null
+++ b/packages/programs-react/src/components/VariableInspector.css
@@ -0,0 +1,151 @@
+/**
+ * Styles for VariableInspector component.
+ */
+
+.variable-inspector {
+ display: flex;
+ flex-direction: column;
+}
+
+.variable-inspector-empty {
+ padding: 1rem;
+ text-align: center;
+}
+
+.variable-inspector-empty-text {
+ font-size: 0.875rem;
+ color: var(--programs-text-muted, #898989);
+}
+
+.variable-inspector-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.variable-item {
+ padding: 0.75rem;
+ background: var(--programs-bg-secondary, #f5f6f7);
+ border: 1px solid var(--programs-border-secondary, #eaecef);
+ border-radius: 6px;
+}
+
+.variable-item.has-error {
+ border-color: var(--programs-accent-red, #cf222e);
+ background: rgba(207, 34, 46, 0.05);
+}
+
+.variable-header {
+ display: flex;
+ align-items: baseline;
+ gap: 0.5rem;
+ margin-bottom: 0.375rem;
+}
+
+.variable-name {
+ font-family: var(--ifm-font-family-monospace, monospace);
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--programs-text-primary, #1c1e21);
+}
+
+.variable-type {
+ font-size: 0.6875rem;
+ padding: 0.125rem 0.375rem;
+ background: var(--programs-accent-purple-bg, rgba(130, 80, 223, 0.1));
+ color: var(--programs-accent-purple, #8250df);
+ border-radius: 3px;
+}
+
+.variable-value {
+ font-size: 0.8125rem;
+}
+
+.variable-resolved {
+ font-family: var(--ifm-font-family-monospace, monospace);
+ color: var(--programs-syntax-number, #0550ae);
+ word-break: break-all;
+}
+
+.variable-pending {
+ color: var(--programs-text-muted, #898989);
+ font-style: italic;
+}
+
+.variable-error {
+ color: var(--programs-accent-red, #cf222e);
+ font-size: 0.75rem;
+}
+
+.variable-pointer {
+ margin-top: 0.5rem;
+}
+
+.variable-pointer details {
+ font-size: 0.75rem;
+}
+
+.variable-pointer summary {
+ cursor: pointer;
+ color: var(--programs-text-secondary, #606770);
+}
+
+.variable-pointer-json {
+ margin-top: 0.25rem;
+ padding: 0.5rem;
+ font-size: 0.6875rem;
+ background: var(--programs-bg-code, #f5f6f7);
+ border-radius: 4px;
+ overflow-x: auto;
+}
+
+/* Stack Inspector */
+.stack-inspector {
+ display: flex;
+ flex-direction: column;
+}
+
+.stack-inspector-empty {
+ padding: 1rem;
+ text-align: center;
+}
+
+.stack-inspector-empty-text {
+ font-size: 0.875rem;
+ color: var(--programs-text-muted, #898989);
+}
+
+.stack-inspector-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.stack-entry {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.375rem 0.5rem;
+ background: var(--programs-bg-secondary, #f5f6f7);
+ border-radius: 4px;
+}
+
+.stack-index {
+ font-size: 0.6875rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ color: var(--programs-text-muted, #898989);
+ min-width: 1.5rem;
+}
+
+.stack-value {
+ font-size: 0.75rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ color: var(--programs-syntax-number, #0550ae);
+ word-break: break-all;
+}
+
+.stack-more {
+ font-size: 0.75rem;
+ color: var(--programs-text-muted, #898989);
+ font-style: italic;
+}
diff --git a/packages/programs-react/src/components/VariableInspector.tsx b/packages/programs-react/src/components/VariableInspector.tsx
new file mode 100644
index 000000000..c538fc2c9
--- /dev/null
+++ b/packages/programs-react/src/components/VariableInspector.tsx
@@ -0,0 +1,175 @@
+/**
+ * Component for inspecting variables in scope at the current trace step.
+ */
+
+import React from "react";
+import { useTraceContext, type ResolvedVariable } from "./TraceContext.js";
+
+// CSS is expected to be imported by the consuming application
+// import "./VariableInspector.css";
+
+export interface VariableInspectorProps {
+ /** Whether to show variable types */
+ showTypes?: boolean;
+ /** Whether to show pointers */
+ showPointers?: boolean;
+ /** Custom class name */
+ className?: string;
+}
+
+/**
+ * Displays variables in scope at the current execution step.
+ */
+export function VariableInspector({
+ showTypes = false,
+ showPointers = false,
+ className = "",
+}: VariableInspectorProps): JSX.Element {
+ const { currentVariables, currentInstruction } = useTraceContext();
+
+ if (!currentInstruction) {
+ return (
+
+
+ No instruction selected
+
+
+ );
+ }
+
+ if (currentVariables.length === 0) {
+ return (
+
+
+ No variables in scope
+
+
+ );
+ }
+
+ return (
+
+
+ {currentVariables.map((variable, index) => (
+
+ ))}
+
+
+ );
+}
+
+interface VariableItemProps {
+ variable: ResolvedVariable;
+ showType: boolean;
+ showPointer: boolean;
+}
+
+function VariableItem({
+ variable,
+ showType,
+ showPointer,
+}: VariableItemProps): JSX.Element {
+ const { identifier, type, pointer, value, error } = variable;
+
+ // Extract type name if available
+ const typeName =
+ type && typeof type === "object" && "kind" in type
+ ? (type as { kind: string }).kind
+ : type
+ ? JSON.stringify(type)
+ : undefined;
+
+ return (
+
+
+ {identifier || "(anonymous)"}
+ {typeName && showType && (
+ {typeName}
+ )}
+
+
+
+ {error ? (
+
+ Error: {error}
+
+ ) : value !== undefined ? (
+ {value}
+ ) : (
+ (value not resolved)
+ )}
+
+
+ {showPointer && !!pointer && (
+
+
+ Pointer
+
+ {JSON.stringify(pointer, null, 2)}
+
+
+
+ )}
+
+ );
+}
+
+export interface StackInspectorProps {
+ /** Maximum stack entries to show */
+ maxEntries?: number;
+ /** Custom class name */
+ className?: string;
+}
+
+/**
+ * Displays the EVM stack at the current trace step.
+ */
+export function StackInspector({
+ maxEntries = 10,
+ className = "",
+}: StackInspectorProps): JSX.Element {
+ const { currentStep } = useTraceContext();
+
+ if (!currentStep || !currentStep.stack || currentStep.stack.length === 0) {
+ return (
+
+ Empty stack
+
+ );
+ }
+
+ const stack = currentStep.stack;
+ const displayStack = stack.slice(0, maxEntries);
+ const hasMore = stack.length > maxEntries;
+
+ return (
+
+
+ {displayStack.map((entry, index) => (
+
+ [{index}]
+
+ {typeof entry === "bigint" ? `0x${entry.toString(16)}` : entry}
+
+
+ ))}
+ {hasMore && (
+
+ ... {stack.length - maxEntries} more entries
+
+ )}
+
+
+ );
+}
diff --git a/packages/programs-react/src/components/index.ts b/packages/programs-react/src/components/index.ts
new file mode 100644
index 000000000..886c22d62
--- /dev/null
+++ b/packages/programs-react/src/components/index.ts
@@ -0,0 +1,39 @@
+/**
+ * Component exports.
+ */
+
+export {
+ ProgramExampleContextProvider,
+ useProgramExampleContext,
+ type ProgramExampleState,
+ type ProgramExampleProps,
+} from "./ProgramExampleContext.js";
+
+export { Opcodes } from "./Opcodes.js";
+
+export { SourceContents } from "./SourceContents.js";
+
+export { HighlightedInstruction } from "./HighlightedInstruction.js";
+
+// Trace components
+export {
+ TraceProvider,
+ useTraceContext,
+ type TraceState,
+ type TraceProviderProps,
+ type ResolvedVariable,
+} from "./TraceContext.js";
+
+export {
+ TraceControls,
+ TraceProgress,
+ type TraceControlsProps,
+ type TraceProgressProps,
+} from "./TraceControls.js";
+
+export {
+ VariableInspector,
+ StackInspector,
+ type VariableInspectorProps,
+ type StackInspectorProps,
+} from "./VariableInspector.js";
diff --git a/packages/programs-react/src/index.ts b/packages/programs-react/src/index.ts
new file mode 100644
index 000000000..aa32365c2
--- /dev/null
+++ b/packages/programs-react/src/index.ts
@@ -0,0 +1,68 @@
+/**
+ * @ethdebug/programs-react
+ *
+ * React components for visualizing ethdebug program annotations.
+ */
+
+// Components
+export {
+ ProgramExampleContextProvider,
+ useProgramExampleContext,
+ type ProgramExampleState,
+ type ProgramExampleProps,
+} from "#components/ProgramExampleContext";
+
+export { Opcodes } from "#components/Opcodes";
+
+export { SourceContents } from "#components/SourceContents";
+
+export { HighlightedInstruction } from "#components/HighlightedInstruction";
+
+// Trace components
+export {
+ TraceProvider,
+ useTraceContext,
+ TraceControls,
+ TraceProgress,
+ VariableInspector,
+ StackInspector,
+ type TraceState,
+ type TraceProviderProps,
+ type ResolvedVariable,
+ type TraceControlsProps,
+ type TraceProgressProps,
+ type VariableInspectorProps,
+ type StackInspectorProps,
+} from "#components/index";
+
+// Shiki utilities
+export {
+ useHighlighter,
+ ShikiCodeBlock,
+ type Highlighter,
+ type HighlightOptions,
+ type ShikiCodeBlockProps,
+} from "#shiki/index";
+
+// Utility functions
+export {
+ computeOffsets,
+ resolveDynamicInstruction,
+ createMockTrace,
+ findInstructionAtPc,
+ extractVariablesFromInstruction,
+ buildPcToInstructionMap,
+ type DynamicInstruction,
+ type DynamicContext,
+ type ContextThunk,
+ type FindSourceRangeOptions,
+ type ResolverOptions,
+ type TraceStep,
+ type MockTraceSpec,
+} from "#utils/index";
+
+// CSS - consumers should import these stylesheets
+// import "@ethdebug/programs-react/components/Opcodes.css";
+// import "@ethdebug/programs-react/components/SourceContents.css";
+// import "@ethdebug/programs-react/components/TraceControls.css";
+// import "@ethdebug/programs-react/components/VariableInspector.css";
diff --git a/packages/programs-react/src/shiki/ShikiCodeBlock.tsx b/packages/programs-react/src/shiki/ShikiCodeBlock.tsx
new file mode 100644
index 000000000..ebee07643
--- /dev/null
+++ b/packages/programs-react/src/shiki/ShikiCodeBlock.tsx
@@ -0,0 +1,38 @@
+/**
+ * Simple code block component using Shiki syntax highlighting.
+ */
+
+import React from "react";
+import { type HighlightOptions, useHighlighter } from "./useHighlighter.js";
+
+/**
+ * Props for ShikiCodeBlock component.
+ */
+export interface ShikiCodeBlockProps extends HighlightOptions {
+ code: string;
+ className?: string;
+}
+
+/**
+ * Renders a code block with syntax highlighting using Shiki.
+ *
+ * @param props - Code and highlight options
+ * @returns Highlighted code block element
+ */
+export function ShikiCodeBlock({
+ code,
+ className,
+ ...highlightOptions
+}: ShikiCodeBlockProps): JSX.Element {
+ const highlighter = useHighlighter();
+
+ if (!highlighter) {
+ return <>Loading...>;
+ }
+
+ const html = highlighter.highlight(code, highlightOptions);
+
+ return (
+
+ );
+}
diff --git a/packages/programs-react/src/shiki/index.ts b/packages/programs-react/src/shiki/index.ts
new file mode 100644
index 000000000..b4ed16f3b
--- /dev/null
+++ b/packages/programs-react/src/shiki/index.ts
@@ -0,0 +1,9 @@
+/**
+ * Shiki syntax highlighting exports.
+ */
+
+export { useHighlighter } from "./useHighlighter.js";
+export type { Highlighter, HighlightOptions } from "./useHighlighter.js";
+
+export { ShikiCodeBlock } from "./ShikiCodeBlock.js";
+export type { ShikiCodeBlockProps } from "./ShikiCodeBlock.js";
diff --git a/packages/web/src/theme/ShikiCodeBlock/useHighlighter.ts b/packages/programs-react/src/shiki/useHighlighter.ts
similarity index 72%
rename from packages/web/src/theme/ShikiCodeBlock/useHighlighter.ts
rename to packages/programs-react/src/shiki/useHighlighter.ts
index 96f091e61..82a2112ef 100644
--- a/packages/web/src/theme/ShikiCodeBlock/useHighlighter.ts
+++ b/packages/programs-react/src/shiki/useHighlighter.ts
@@ -1,18 +1,36 @@
+/**
+ * React hook for Shiki syntax highlighter.
+ */
+
import { useEffect, useState } from "react";
import * as Shiki from "shiki/core";
import { createOnigurumaEngine } from "shiki/engine/oniguruma";
+/**
+ * Highlighter interface for syntax highlighting.
+ */
export interface Highlighter {
highlight(text: string, options: HighlightOptions): string;
}
+/**
+ * Options for highlighting code.
+ */
export interface HighlightOptions {
language?: string;
decorations?: Shiki.DecorationItem[];
+ className?: string;
}
-export function useHighlighter() {
+/**
+ * React hook that provides a Shiki highlighter instance.
+ *
+ * The highlighter is created asynchronously on mount.
+ *
+ * @returns Highlighter instance or undefined while loading
+ */
+export function useHighlighter(): Highlighter | undefined {
const [highlighter, setHighlighter] = useState();
useEffect(() => {
diff --git a/packages/programs-react/src/utils/dynamic.test.ts b/packages/programs-react/src/utils/dynamic.test.ts
new file mode 100644
index 000000000..1347f8329
--- /dev/null
+++ b/packages/programs-react/src/utils/dynamic.test.ts
@@ -0,0 +1,163 @@
+/**
+ * Tests for dynamic instruction resolution.
+ */
+
+import { describe, it, expect } from "vitest";
+import {
+ resolveDynamicInstruction,
+ type DynamicInstruction,
+ type ContextThunk,
+} from "./dynamic.js";
+
+describe("resolveDynamicInstruction", () => {
+ const source = {
+ id: "test-source",
+ path: "/test/source.js",
+ language: "javascript",
+ contents: "let x = 1;\nlet y = 2;\nlet z = 3;",
+ };
+
+ it("passes through static context unchanged", () => {
+ const instruction: DynamicInstruction = {
+ offset: 0,
+ operation: { mnemonic: "PUSH1", arguments: ["0x01"] },
+ context: { remark: "static context" },
+ };
+
+ const result = resolveDynamicInstruction(instruction, {
+ sources: [source],
+ });
+
+ expect(result.context).toEqual({ remark: "static context" });
+ expect(result.offset).toBe(0);
+ expect(result.operation).toEqual({
+ mnemonic: "PUSH1",
+ arguments: ["0x01"],
+ });
+ });
+
+ it("resolves dynamic context thunk", () => {
+ const context: ContextThunk = ({ findSourceRange }) => {
+ const range = findSourceRange("let x");
+ return {
+ code: {
+ source: { id: source.id },
+ range: range?.range,
+ },
+ };
+ };
+ const instruction: DynamicInstruction = {
+ offset: 0,
+ operation: { mnemonic: "PUSH1", arguments: ["0x01"] },
+ context,
+ };
+
+ const result = resolveDynamicInstruction(instruction, {
+ sources: [source],
+ });
+
+ expect(result.context).toEqual({
+ code: {
+ source: { id: "test-source" },
+ range: {
+ offset: 0,
+ length: 5,
+ },
+ },
+ });
+ });
+
+ it("findSourceRange locates string in source", () => {
+ const context: ContextThunk = ({ findSourceRange }) => {
+ const range = findSourceRange("let y");
+ return { remark: `found at ${range?.range?.offset}` };
+ };
+ const instruction: DynamicInstruction = {
+ offset: 0,
+ operation: { mnemonic: "PUSH1", arguments: ["0x01"] },
+ context,
+ };
+
+ const result = resolveDynamicInstruction(instruction, {
+ sources: [source],
+ });
+
+ // "let y" starts at position 11 (after "let x = 1;\n")
+ expect(result.context).toEqual({ remark: "found at 11" });
+ });
+
+ it("findSourceRange with after option skips to position after query", () => {
+ const sourceWithRepeats = {
+ id: "repeats",
+ path: "/test/repeats.js",
+ language: "javascript",
+ contents: "let a = 1; let a = 2; let a = 3;",
+ };
+
+ const context: ContextThunk = ({ findSourceRange }) => {
+ // Find second occurrence of "let a" by searching after the first "= 1"
+ const range = findSourceRange("let a", { after: "= 1" });
+ return { remark: `found at ${range?.range?.offset}` };
+ };
+ const instruction: DynamicInstruction = {
+ offset: 0,
+ operation: { mnemonic: "PUSH1", arguments: ["0x01"] },
+ context,
+ };
+
+ const result = resolveDynamicInstruction(instruction, {
+ sources: [sourceWithRepeats],
+ });
+
+ // Second "let a" starts at position 11
+ expect(result.context).toEqual({ remark: "found at 11" });
+ });
+
+ it("throws when after query not found", () => {
+ const context: ContextThunk = ({ findSourceRange }) => {
+ findSourceRange("let x", { after: "nonexistent" });
+ return { remark: "should not reach here" };
+ };
+ const instruction: DynamicInstruction = {
+ offset: 0,
+ operation: { mnemonic: "PUSH1", arguments: ["0x01"] },
+ context,
+ };
+
+ expect(() =>
+ resolveDynamicInstruction(instruction, { sources: [source] }),
+ ).toThrow(/could not find string nonexistent/);
+ });
+
+ it("throws when query not found", () => {
+ const context: ContextThunk = ({ findSourceRange }) => {
+ findSourceRange("nonexistent");
+ return { remark: "should not reach here" };
+ };
+ const instruction: DynamicInstruction = {
+ offset: 0,
+ operation: { mnemonic: "PUSH1", arguments: ["0x01"] },
+ context,
+ };
+
+ expect(() =>
+ resolveDynamicInstruction(instruction, { sources: [source] }),
+ ).toThrow(/could not find string nonexistent/);
+ });
+
+ it("returns undefined range when no sources", () => {
+ const context: ContextThunk = ({ findSourceRange }) => {
+ const range = findSourceRange("let x");
+ return { remark: range ? "found" : "not found" };
+ };
+ const instruction: DynamicInstruction = {
+ offset: 0,
+ operation: { mnemonic: "PUSH1", arguments: ["0x01"] },
+ context,
+ };
+
+ const result = resolveDynamicInstruction(instruction, { sources: [] });
+
+ expect(result.context).toEqual({ remark: "not found" });
+ });
+});
diff --git a/packages/web/src/theme/ProgramExample/dynamic.ts b/packages/programs-react/src/utils/dynamic.ts
similarity index 74%
rename from packages/web/src/theme/ProgramExample/dynamic.ts
rename to packages/programs-react/src/utils/dynamic.ts
index e80dada73..285b7bb8e 100644
--- a/packages/web/src/theme/ProgramExample/dynamic.ts
+++ b/packages/programs-react/src/utils/dynamic.ts
@@ -1,12 +1,28 @@
+/**
+ * Dynamic instruction resolution.
+ *
+ * Allows defining instructions with context thunks that are resolved
+ * against source materials.
+ */
+
import { Program, Materials } from "@ethdebug/format";
+/**
+ * Instruction with dynamic context that can be resolved.
+ */
export type DynamicInstruction = Omit<
Program.Instruction,
"context" | "operation"
> & { operation: Program.Instruction.Operation } & { context: DynamicContext };
+/**
+ * Context that can be either static or a thunk that resolves against sources.
+ */
export type DynamicContext = Program.Context | ContextThunk;
+/**
+ * Function that resolves a dynamic context using source information.
+ */
export type ContextThunk = (props: {
findSourceRange(
query: string,
@@ -14,15 +30,28 @@ export type ContextThunk = (props: {
): Materials.SourceRange | undefined;
}) => Program.Context;
+/**
+ * Options for finding a source range.
+ */
export interface FindSourceRangeOptions {
source?: Materials.Reference;
after?: string;
}
+/**
+ * Options for resolving dynamic instructions.
+ */
export interface ResolverOptions {
sources: Materials.Source[];
}
+/**
+ * Resolve a dynamic instruction to a static instruction.
+ *
+ * @param dynamicInstruction - Instruction with potentially dynamic context
+ * @param options - Resolver options including source materials
+ * @returns Resolved static instruction
+ */
export function resolveDynamicInstruction(
dynamicInstruction: DynamicInstruction,
options: ResolverOptions,
diff --git a/packages/programs-react/src/utils/index.ts b/packages/programs-react/src/utils/index.ts
new file mode 100644
index 000000000..9e24ab67b
--- /dev/null
+++ b/packages/programs-react/src/utils/index.ts
@@ -0,0 +1,23 @@
+/**
+ * Utility exports.
+ */
+
+export { computeOffsets } from "./offsets.js";
+
+export {
+ resolveDynamicInstruction,
+ type DynamicInstruction,
+ type DynamicContext,
+ type ContextThunk,
+ type FindSourceRangeOptions,
+ type ResolverOptions,
+} from "./dynamic.js";
+
+export {
+ createMockTrace,
+ findInstructionAtPc,
+ extractVariablesFromInstruction,
+ buildPcToInstructionMap,
+ type TraceStep,
+ type MockTraceSpec,
+} from "./mockTrace.js";
diff --git a/packages/programs-react/src/utils/mockTrace.ts b/packages/programs-react/src/utils/mockTrace.ts
new file mode 100644
index 000000000..6adef9e05
--- /dev/null
+++ b/packages/programs-react/src/utils/mockTrace.ts
@@ -0,0 +1,119 @@
+/**
+ * Utilities for creating mock execution traces.
+ */
+
+import type { Program } from "@ethdebug/format";
+
+/**
+ * A single step in an execution trace.
+ */
+export interface TraceStep {
+ /** Program counter (byte offset in bytecode) */
+ pc: number;
+ /** Opcode mnemonic (e.g., "PUSH1", "SLOAD") */
+ opcode: string;
+ /** Stack entries (from top to bottom) as hex strings or bigints */
+ stack?: Array;
+ /** Memory contents as hex string */
+ memory?: string;
+ /** Storage state: slot (hex) → value (hex) */
+ storage?: Record;
+ /** Gas remaining */
+ gas?: bigint;
+ /** Return data from last call */
+ returndata?: string;
+}
+
+/**
+ * Specification for creating a mock trace.
+ */
+export interface MockTraceSpec {
+ /** Sequence of execution steps */
+ steps: TraceStep[];
+ /** Program definition with instructions */
+ program: Program;
+}
+
+/**
+ * Create a mock trace from a specification.
+ *
+ * This allows creating traces for demonstration without running real EVM.
+ */
+export function createMockTrace(spec: MockTraceSpec): TraceStep[] {
+ return spec.steps.map((step) => ({
+ ...step,
+ // Ensure stack has default value
+ stack: step.stack || [],
+ // Ensure storage has default value
+ storage: step.storage || {},
+ }));
+}
+
+/**
+ * Find the instruction at a given program counter.
+ */
+export function findInstructionAtPc(
+ program: Program,
+ pc: number,
+): Program.Instruction | undefined {
+ return program.instructions?.find((instr) => instr.offset === pc);
+}
+
+/**
+ * Extract variables that are in scope at a given instruction.
+ *
+ * This walks the context and extracts variables from Variables contexts.
+ */
+export function extractVariablesFromInstruction(
+ instruction: Program.Instruction,
+): Array<{ identifier?: string; type?: unknown; pointer?: unknown }> {
+ if (!instruction.context) {
+ return [];
+ }
+
+ return extractVariablesFromContext(instruction.context);
+}
+
+function extractVariablesFromContext(
+ context: Program.Context,
+): Array<{ identifier?: string; type?: unknown; pointer?: unknown }> {
+ // Variables context
+ if ("variables" in context && Array.isArray(context.variables)) {
+ return context.variables;
+ }
+
+ // Gather context (combines multiple contexts)
+ if ("gather" in context && Array.isArray(context.gather)) {
+ return context.gather.flatMap(extractVariablesFromContext);
+ }
+
+ // Pick context (picks from multiple contexts - take first with variables)
+ if ("pick" in context && Array.isArray(context.pick)) {
+ for (const subContext of context.pick) {
+ const vars = extractVariablesFromContext(subContext);
+ if (vars.length > 0) {
+ return vars;
+ }
+ }
+ }
+
+ return [];
+}
+
+/**
+ * Build a map of PC to instruction for quick lookup.
+ */
+export function buildPcToInstructionMap(
+ program: Program,
+): Map {
+ const map = new Map();
+ for (const instr of program.instructions || []) {
+ // offset can be number or hex string (Data.Value)
+ const offset =
+ typeof instr.offset === "string"
+ ? parseInt(instr.offset, 16)
+ : instr.offset;
+ map.set(offset, instr);
+ }
+ return map;
+}
diff --git a/packages/programs-react/src/utils/offsets.test.ts b/packages/programs-react/src/utils/offsets.test.ts
new file mode 100644
index 000000000..0f8377785
--- /dev/null
+++ b/packages/programs-react/src/utils/offsets.test.ts
@@ -0,0 +1,93 @@
+/**
+ * Tests for offset computation utility.
+ */
+
+import { describe, it, expect } from "vitest";
+import { computeOffsets } from "./offsets.js";
+
+describe("computeOffsets", () => {
+ it("computes offset 0 for first instruction", () => {
+ const instructions = [
+ { operation: { mnemonic: "PUSH1" as const, arguments: ["0x80"] } },
+ ];
+
+ const result = computeOffsets(instructions);
+
+ expect(result).toHaveLength(1);
+ expect(result[0].offset).toBe(0);
+ });
+
+ it("computes sequential offsets based on operation size", () => {
+ const instructions = [
+ // PUSH1 0x80 = 2 bytes (1 opcode + 1 byte argument)
+ { operation: { mnemonic: "PUSH1" as const, arguments: ["0x80"] } },
+ // PUSH1 0x40 = 2 bytes
+ { operation: { mnemonic: "PUSH1" as const, arguments: ["0x40"] } },
+ // MSTORE = 1 byte (no arguments)
+ { operation: { mnemonic: "MSTORE" as const } },
+ ];
+
+ const result = computeOffsets(instructions);
+
+ expect(result).toHaveLength(3);
+ expect(result[0].offset).toBe(0); // First instruction at 0
+ expect(result[1].offset).toBe(2); // After PUSH1 0x80 (2 bytes)
+ expect(result[2].offset).toBe(4); // After PUSH1 0x40 (2 bytes)
+ });
+
+ it("handles larger push operations correctly", () => {
+ const instructions = [
+ // PUSH32 = 33 bytes (1 opcode + 32 byte argument)
+ {
+ operation: {
+ mnemonic: "PUSH32" as const,
+ arguments: ["0x" + "ff".repeat(32)],
+ },
+ },
+ // STOP = 1 byte
+ { operation: { mnemonic: "STOP" as const } },
+ ];
+
+ const result = computeOffsets(instructions);
+
+ expect(result).toHaveLength(2);
+ expect(result[0].offset).toBe(0);
+ expect(result[1].offset).toBe(33); // 1 + 32
+ });
+
+ it("handles numeric arguments", () => {
+ const instructions = [
+ // Using numeric argument
+ { operation: { mnemonic: "PUSH1" as const, arguments: [128] } },
+ { operation: { mnemonic: "STOP" as const } },
+ ];
+
+ const result = computeOffsets(instructions);
+
+ expect(result).toHaveLength(2);
+ expect(result[0].offset).toBe(0);
+ expect(result[1].offset).toBe(2); // 1 opcode + 1 byte for 0x80
+ });
+
+ it("preserves original instruction properties", () => {
+ const instructions = [
+ {
+ operation: { mnemonic: "PUSH1" as const, arguments: ["0x80"] },
+ context: { remark: "test" },
+ },
+ ];
+
+ const result = computeOffsets(instructions);
+
+ expect(result[0]).toMatchObject({
+ offset: 0,
+ operation: { mnemonic: "PUSH1", arguments: ["0x80"] },
+ context: { remark: "test" },
+ });
+ });
+
+ it("returns empty array for empty input", () => {
+ const result = computeOffsets([]);
+ expect(result).toEqual([]);
+ });
+});
diff --git a/packages/web/src/theme/ProgramExample/offsets.ts b/packages/programs-react/src/utils/offsets.ts
similarity index 75%
rename from packages/web/src/theme/ProgramExample/offsets.ts
rename to packages/programs-react/src/utils/offsets.ts
index 9e60320f4..2b6c911ed 100644
--- a/packages/web/src/theme/ProgramExample/offsets.ts
+++ b/packages/programs-react/src/utils/offsets.ts
@@ -1,15 +1,30 @@
+/**
+ * Compute instruction offsets from operation sizes.
+ */
+
import { Data, Program } from "@ethdebug/format";
-// define base generic instruction since other parts of this module
-// allow dynamic contexts and such
+/**
+ * Base instruction type that can have offsets computed.
+ */
interface OffsetComputableInstruction {
operation: Program.Instruction.Operation;
}
+/**
+ * Instruction with computed offset.
+ */
type OffsetComputedInstruction = I & {
offset: Data.Value;
};
+/**
+ * Compute bytecode offsets for a sequence of instructions based on their
+ * operation sizes.
+ *
+ * @param instructions - Instructions without offsets
+ * @returns Instructions with computed offsets
+ */
export function computeOffsets(
instructions: I[],
): OffsetComputedInstruction[] {
diff --git a/packages/programs-react/tsconfig.json b/packages/programs-react/tsconfig.json
new file mode 100644
index 000000000..cdfb74de8
--- /dev/null
+++ b/packages/programs-react/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "rootDir": "./",
+ "outDir": "./dist/",
+ "baseUrl": "./",
+ "jsx": "react-jsx",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "paths": {
+ "#components/*": ["./src/components/*"],
+ "#shiki/*": ["./src/shiki/*"],
+ "#utils/*": ["./src/utils/*"]
+ }
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"],
+ "references": [{ "path": "../format" }]
+}
diff --git a/packages/programs-react/vitest.config.ts b/packages/programs-react/vitest.config.ts
new file mode 100644
index 000000000..ab56fbfcb
--- /dev/null
+++ b/packages/programs-react/vitest.config.ts
@@ -0,0 +1,13 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ globals: true,
+ environment: "jsdom",
+ coverage: {
+ provider: "v8",
+ reporter: ["text", "json", "html"],
+ exclude: ["node_modules/", "dist/", "**/*.test.ts", "**/*.test.tsx"],
+ },
+ },
+});
diff --git a/packages/web/docs/concepts/_category_.json b/packages/web/docs/concepts/_category_.json
new file mode 100644
index 000000000..1d3167d49
--- /dev/null
+++ b/packages/web/docs/concepts/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Concepts",
+ "position": 3
+}
diff --git a/packages/web/docs/concepts/data-locations.mdx b/packages/web/docs/concepts/data-locations.mdx
new file mode 100644
index 000000000..5a4701ffd
--- /dev/null
+++ b/packages/web/docs/concepts/data-locations.mdx
@@ -0,0 +1,158 @@
+---
+sidebar_position: 2
+---
+
+# Data locations
+
+The EVM stores data in several distinct locations, each with different
+characteristics. Understanding these locations is essential for working with
+ethdebug/format pointers.
+
+## Storage
+
+**Storage** is persistent data associated with a contract. It survives
+transaction boundaries and is the primary place contracts store their state.
+
+Key characteristics:
+
+- **Persistent** — Values remain until explicitly changed
+- **Slot-based** — Organized into 32-byte slots numbered from 0
+- **Expensive** — Reading and writing storage costs significant gas
+- **Contract-specific** — Each contract has its own storage space
+
+Storage is where you find:
+
+- Contract state variables
+- Mapping contents (at computed slot locations)
+- Dynamic array contents (at computed slot locations)
+
+In ethdebug/format, storage locations use `"location": "storage"`.
+
+## Memory
+
+**Memory** is temporary data that exists only during a single transaction's
+execution. It's cleared between calls.
+
+Key characteristics:
+
+- **Temporary** — Cleared after each external call returns
+- **Byte-addressable** — Accessed by byte offset, not slots
+- **Linear** — Grows as needed, starting from offset 0
+- **Cheaper than storage** — But costs gas to expand
+
+Memory is where you find:
+
+- Function arguments for external calls
+- Return data being prepared
+- Temporary variables and intermediate values
+- Dynamic data being constructed
+
+In ethdebug/format, memory locations use `"location": "memory"`.
+
+## Stack
+
+**The stack** is where the EVM performs computations. It holds operands and
+intermediate results.
+
+Key characteristics:
+
+- **256-bit words** — Each stack item is 32 bytes
+- **Limited depth** — Maximum 1024 items
+- **LIFO** — Last in, first out access pattern
+- **Ephemeral** — Contents change constantly during execution
+
+The stack is where you find:
+
+- Function arguments (for internal calls)
+- Local variables (in some cases)
+- Intermediate computation results
+- Return addresses
+
+In ethdebug/format, stack locations use `"location": "stack"`.
+
+## Calldata
+
+**Calldata** is the read-only input data sent to a contract when it's called.
+
+Key characteristics:
+
+- **Read-only** — Cannot be modified during execution
+- **Byte-addressable** — Accessed by byte offset
+- **Transaction-specific** — Contains the call's input parameters
+- **Cheap to read** — Cheaper than memory or storage reads
+
+Calldata is where you find:
+
+- Function selector (first 4 bytes)
+- ABI-encoded function arguments
+
+In ethdebug/format, calldata locations use `"location": "calldata"`.
+
+## Returndata
+
+**Returndata** is the output from the most recent external call.
+
+Key characteristics:
+
+- **Read-only** — Set by called contract, read by caller
+- **Replaced on each call** — Each external call overwrites previous
+ returndata
+- **Byte-addressable** — Accessed by byte offset
+
+Returndata is where you find:
+
+- Return values from external function calls
+- Revert reasons (when calls fail)
+
+In ethdebug/format, returndata locations use `"location": "returndata"`.
+
+## Code
+
+**Code** refers to the contract's bytecode itself. Sometimes data is embedded
+in the bytecode.
+
+Key characteristics:
+
+- **Immutable** — Cannot change after deployment
+- **Byte-addressable** — Accessed by byte offset
+
+Code is where you find:
+
+- Immutable variables (in some compiler implementations)
+- Embedded constants
+
+In ethdebug/format, code locations use `"location": "code"`.
+
+## Transient storage
+
+**Transient storage** (EIP-1153) is storage that persists within a transaction
+but is cleared afterward.
+
+Key characteristics:
+
+- **Transaction-scoped** — Persists across calls within a transaction
+- **Cleared after transaction** — Does not persist to the next transaction
+- **Slot-based** — Like storage, organized into 32-byte slots
+- **Cheaper than storage** — Lower gas costs for temporary data
+
+In ethdebug/format, transient storage locations use
+`"location": "transient"`.
+
+## Summary
+
+| Location | Persistence | Addressing | Primary use |
+| ---------- | ------------------ | -------------- | ------------------- |
+| Storage | Permanent | 32-byte slots | Contract state |
+| Memory | Single call | Byte offset | Temporary data |
+| Stack | Instruction-level | Position index | Computation |
+| Calldata | Single call | Byte offset | Input parameters |
+| Returndata | Until next call | Byte offset | Call results |
+| Code | Permanent | Byte offset | Bytecode/immutables |
+| Transient | Single transaction | 32-byte slots | Tx-scoped state |
+
+## Next steps
+
+- **[Pointers](/docs/pointers)** — Learn how ethdebug/format describes
+ locations in these data areas
+- **[Regions](/docs/pointers/regions)** — Understand how regions combine
+ location, offset, and length
diff --git a/packages/web/docs/concepts/index.mdx b/packages/web/docs/concepts/index.mdx
new file mode 100644
index 000000000..36a917533
--- /dev/null
+++ b/packages/web/docs/concepts/index.mdx
@@ -0,0 +1,88 @@
+---
+sidebar_position: 1
+sidebar_label: Overview
+---
+
+# Concepts
+
+This section introduces the core concepts behind ethdebug/format. Understanding
+these mental models will help you work with the format effectively, whether
+you're consuming it in a debugger or producing it from a compiler.
+
+## The main components
+
+ethdebug/format consists of three main kinds of information:
+
+### Types
+
+**Types** describe what kind of data you're looking at. They tell a debugger
+how to interpret raw bytes as meaningful values.
+
+For example, the same 32 bytes could be:
+
+- A `uint256` representing a token balance
+- An `address` padded to 32 bytes
+- Part of a `bytes` dynamic byte array
+- Two packed `uint128` values
+
+Type definitions give debuggers the information they need to decode bytes
+correctly.
+
+**[Learn more about types →](/docs/types)**
+
+### Pointers
+
+**Pointers** describe where data lives. They're recipes for finding bytes in
+EVM state.
+
+Simple pointers specify static locations:
+
+- "Storage slot 0"
+- "Memory offset 0x80"
+- "Stack position 2"
+
+Complex pointers describe dynamic locations:
+
+- "Storage slot `keccak256(key, baseSlot)`" for mapping values
+- "Memory at the offset stored in stack position 1" for dynamic references
+
+Pointers can include expressions that compute locations based on runtime state.
+
+**[Learn more about pointers →](/docs/pointers)**
+
+### Programs
+
+**Programs** describe runtime context. They tell a debugger what's happening
+at each point in execution.
+
+Programs answer questions like:
+
+- What source code corresponds to this bytecode instruction?
+- What variables are in scope right now?
+- What function are we in?
+
+This information enables source-level debugging of optimized bytecode.
+
+**[Learn more about programs →](/docs/programs)**
+
+## How they work together
+
+These components combine to enable rich debugging:
+
+1. A debugger reads **program** information to know which variables are in
+ scope at the current instruction
+2. Each variable has a **type** that describes its structure
+3. Each variable has a **pointer** that describes where to find its value
+4. The debugger resolves the pointer against current EVM state to get raw bytes
+5. The debugger decodes the bytes using the type definition
+6. The user sees meaningful variable values
+
+## Next steps
+
+- **[Data locations](/docs/concepts/data-locations)** — Understand where data
+ can live in the EVM
+- **[Types deep dive](/docs/types)** — Full documentation on type definitions
+- **[Pointers deep dive](/docs/pointers)** — Full documentation on pointer
+ definitions
+- **[Programs deep dive](/docs/programs)** — Full documentation on program
+ annotations
diff --git a/packages/web/docs/examples/_category_.json b/packages/web/docs/examples/_category_.json
new file mode 100644
index 000000000..c618bc181
--- /dev/null
+++ b/packages/web/docs/examples/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Examples",
+ "position": 8
+}
diff --git a/packages/web/docs/examples/bug-playground.mdx b/packages/web/docs/examples/bug-playground.mdx
new file mode 100644
index 000000000..53114cfb2
--- /dev/null
+++ b/packages/web/docs/examples/bug-playground.mdx
@@ -0,0 +1,40 @@
+---
+sidebar_position: 2
+---
+
+# BUG Playground
+
+BUG is a minimal smart contract language designed for demonstrating and testing
+the ethdebug format. It compiles to EVM bytecode and produces rich debug
+information.
+
+Use the interactive playground below to explore how BUG code compiles to
+intermediate representations and EVM bytecode.
+
+import { BugPlayground } from "@theme/BugcExample";
+
+
+
+## About BUG
+
+BUG is intentionally minimal—it provides just enough
+language features to demonstrate the debugging challenges that ethdebug
+addresses:
+
+- **Storage variables** with automatic slot allocation
+- **Functions** with parameters and return values
+- **Control flow** (if/else, while loops)
+- **Basic types** (uint256, bool, address, bytes32)
+- **Complex types** (arrays, mappings, structs)
+
+## Compilation Views
+
+The playground shows several stages of compilation:
+
+- **AST** - The abstract syntax tree after parsing
+- **IR** - The intermediate representation used for optimization
+- **CFG** - The control flow graph visualizing program structure
+- **Bytecode** - The final EVM bytecode with debug annotations
+
+Hover over instructions in the IR or Bytecode views to highlight the
+corresponding source code.
diff --git a/packages/web/docs/examples/index.mdx b/packages/web/docs/examples/index.mdx
new file mode 100644
index 000000000..69f8b70c6
--- /dev/null
+++ b/packages/web/docs/examples/index.mdx
@@ -0,0 +1,22 @@
+---
+sidebar_position: 1
+---
+
+# Examples
+
+Interactive examples demonstrating ethdebug/format concepts.
+
+## Available examples
+
+- [**BUG Playground**](/docs/examples/bug-playground) — Interactive compiler
+ playground for the BUG language, showing AST, IR, CFG, and bytecode views.
+
+:::note[More coming soon]
+Additional interactive examples are planned, including:
+
+- Storage variable layouts
+- Dynamic array handling
+- Mapping lookups
+- Pointer resolution
+- EVM trace inspection
+ :::
diff --git a/packages/web/docs/getting-started/_category_.json b/packages/web/docs/getting-started/_category_.json
new file mode 100644
index 000000000..877a378f7
--- /dev/null
+++ b/packages/web/docs/getting-started/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Getting Started",
+ "position": 2
+}
diff --git a/packages/web/docs/getting-started/for-compiler-authors.mdx b/packages/web/docs/getting-started/for-compiler-authors.mdx
new file mode 100644
index 000000000..d8f461bd6
--- /dev/null
+++ b/packages/web/docs/getting-started/for-compiler-authors.mdx
@@ -0,0 +1,137 @@
+---
+sidebar_position: 3
+---
+
+# For compiler authors
+
+You're building a compiler or toolchain that produces EVM bytecode. Here's how
+to emit ethdebug/format data so debuggers can provide rich debugging
+experiences for your users.
+
+## What you need to emit
+
+ethdebug/format defines several kinds of debug information:
+
+- **Type information** — Describe the types in your language so debuggers can
+ decode raw bytes into meaningful values
+- **Pointer information** — Describe where variables are stored at runtime,
+ including dynamic locations computed from other values
+- **Program information** — Describe what's in scope at each bytecode
+ instruction, mapping bytecode back to source code
+
+## Quick example: Describing a type
+
+Here's how to describe a simple `uint256`:
+
+```json
+{
+ "kind": "uint",
+ "bits": 256
+}
+```
+
+A storage mapping from addresses to balances:
+
+```json
+{
+ "kind": "mapping",
+ "key": { "kind": "address" },
+ "value": { "kind": "uint", "bits": 256 }
+}
+```
+
+## Quick example: Describing a storage variable
+
+A pointer tells debuggers where to find a variable's value. For a `uint256`
+at storage slot 0:
+
+```json
+{
+ "location": "storage",
+ "slot": "0x0",
+ "length": 32
+}
+```
+
+For a dynamic array where the length is at slot 2 and elements start at
+`keccak256(2)`:
+
+```json
+{
+ "collection": "list",
+ "count": {
+ "location": "storage",
+ "slot": "0x2",
+ "length": 32
+ },
+ "each": "i",
+ "from": {
+ "location": "storage",
+ "slot": {
+ "$sum": [{ "keccak256": ["0x0000...0002"] }, "$i"]
+ }
+ }
+}
+```
+
+## Integration approach
+
+Most compilers can add ethdebug/format support incrementally:
+
+1. **Start with types** — Emit type definitions for your language's data
+ structures. This is often the easiest starting point.
+2. **Add storage pointers** — Describe where storage variables live. Many
+ variables have static locations that are simple to emit.
+3. **Add memory/stack pointers** — Describe temporary values. These often
+ require tracking allocation during code generation.
+4. **Add program information** — Emit source mappings and scope information.
+ This typically requires the most compiler changes.
+
+## Go deeper
+
+
+
+
+
+
Compiler implementation guide
+
+
+ Detailed guidance on emitting ethdebug/format from your compiler.
+
+
+
+
+
+
+
+
Specification
+
+
+ Formal schema definitions for all ethdebug/format structures.
+
+
+
+
+
+
+### Explore by topic
+
+- **[Types](/docs/types)** — Full documentation on type representations
+- **[Pointers](/docs/pointers)** — Full documentation on pointer definitions
+- **[Programs](/docs/programs)** — Full documentation on program annotations
+- **[BUG Playground](/docs/examples/bug-playground)** — See a working
+ compiler that emits ethdebug/format
diff --git a/packages/web/docs/getting-started/for-debugger-authors.mdx b/packages/web/docs/getting-started/for-debugger-authors.mdx
new file mode 100644
index 000000000..78f87a434
--- /dev/null
+++ b/packages/web/docs/getting-started/for-debugger-authors.mdx
@@ -0,0 +1,121 @@
+---
+sidebar_position: 2
+---
+
+# For debugger authors
+
+You're building a debugger, transaction tracer, or analysis tool. Here's how
+ethdebug/format helps you understand smart contract execution.
+
+## What the format gives you
+
+With ethdebug/format data, your debugger can:
+
+- **Decode raw bytes into meaningful values** — Know that 32 bytes at a
+ storage slot represent a `uint256` balance or a `mapping(address => uint)`
+- **Find variable values at runtime** — Resolve pointers to locate data in
+ storage, memory, or the stack, even when locations are computed dynamically
+- **Map bytecode to source** — Show users which line of source code
+ corresponds to the current instruction
+- **Display variables in scope** — Know which variables exist at each point
+ in execution
+
+## Quick example: Reading a type
+
+Type information tells you how to interpret bytes. Here's what a simple
+`uint256` type looks like in ethdebug/format:
+
+```json
+{
+ "kind": "uint",
+ "bits": 256
+}
+```
+
+And a more complex struct:
+
+```json
+{
+ "kind": "struct",
+ "name": "Position",
+ "members": [
+ { "name": "x", "type": { "kind": "int", "bits": 256 } },
+ { "name": "y", "type": { "kind": "int", "bits": 256 } }
+ ]
+}
+```
+
+Your debugger reads these definitions and uses them to decode raw bytes from
+EVM state into values users can understand.
+
+## Quick example: Resolving a pointer
+
+Pointers describe where data lives. A simple storage variable pointer:
+
+```json
+{
+ "location": "storage",
+ "slot": "0x0"
+}
+```
+
+This says: "read from storage slot 0." But pointers can express complex,
+dynamic locations too — like array elements or mapping values whose locations
+depend on runtime state.
+
+## What you need to implement
+
+To consume ethdebug/format, your debugger needs:
+
+1. **Schema parsing** — Load and validate ethdebug/format JSON
+2. **Type decoding** — Convert bytes to values based on type definitions
+3. **Pointer resolution** — Evaluate pointer expressions against EVM state
+4. **Program interpretation** — Track context as execution progresses
+
+## Go deeper
+
+
+
+
+
+
Understand the concepts
+
+
+ Learn the mental models behind types, pointers, and programs.
+
+
+
+
+
+
+
+
Implementation guide
+
+
+ Walk through a reference implementation of pointer dereferencing.
+
+
+
+
+
+
+### Explore by topic
+
+- **[Types](/docs/types)** — How the format describes data structures
+- **[Pointers](/docs/pointers)** — How the format describes data locations
+- **[Programs](/docs/programs)** — How the format describes runtime context
+- **[Specification](/spec/overview)** — Formal schema definitions
diff --git a/packages/web/docs/getting-started/index.mdx b/packages/web/docs/getting-started/index.mdx
new file mode 100644
index 000000000..6899fc83c
--- /dev/null
+++ b/packages/web/docs/getting-started/index.mdx
@@ -0,0 +1,71 @@
+---
+sidebar_position: 1
+sidebar_label: Overview
+---
+
+# Getting started
+
+Welcome to ethdebug/format! This section helps you get started based on what
+you're building.
+
+## Choose your path
+
+
+
+
+
+
For debugger authors
+
+
+
+ You're building a debugger, transaction tracer, or other tool that
+ needs to understand smart contract execution.
+
+
+ Learn how to consume ethdebug/format data to decode
+ variables, resolve pointers, and map bytecode back to source.
+
+
+
+
+
+
+
+
+
For compiler authors
+
+
+
+ You're building a compiler or toolchain that produces EVM bytecode.
+
+
+ Learn how to emit ethdebug/format data alongside your
+ bytecode so debuggers can provide rich debugging experiences.
+
+
+
+
+
+
+
+## Other starting points
+
+- **[Concepts](/docs/concepts)** — Understand the mental models behind the
+ format
+- **[Examples](/docs/examples)** — See the format in action with interactive
+ demos
+- **[Specification](/spec/overview)** — Read the formal schema definitions
diff --git a/packages/web/docs/implementation-guides/_category_.json b/packages/web/docs/implementation-guides/_category_.json
new file mode 100644
index 000000000..3949327ea
--- /dev/null
+++ b/packages/web/docs/implementation-guides/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Implementation Guides",
+ "position": 7
+}
diff --git a/packages/web/docs/implementation-guides/compiler/_category_.json b/packages/web/docs/implementation-guides/compiler/_category_.json
new file mode 100644
index 000000000..5b1098d90
--- /dev/null
+++ b/packages/web/docs/implementation-guides/compiler/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Compiler Guides",
+ "position": 2
+}
diff --git a/packages/web/docs/implementation-guides/compiler/case-study-bug.mdx b/packages/web/docs/implementation-guides/compiler/case-study-bug.mdx
new file mode 100644
index 000000000..cb6bd433b
--- /dev/null
+++ b/packages/web/docs/implementation-guides/compiler/case-study-bug.mdx
@@ -0,0 +1,354 @@
+---
+sidebar_position: 2
+sidebar_label: "Case Study: BUG"
+---
+
+# Case Study: BUG Compiler
+
+BUG is a small experimental language designed to demonstrate ethdebug/format
+integration. This case study explains how BUG implements debug information
+generation, providing a reference for other compiler authors.
+
+:::tip[Try it yourself]
+See the [BUG Playground](/docs/examples/bug-playground) to experiment with
+BUG and see its debug output.
+:::
+
+## BUG Language Overview
+
+BUG is a minimal smart contract language with:
+
+- **Storage declarations** — Named variables with explicit slot positions
+- **Elementary types** — `uint256`, `bool`, `address`, etc.
+- **Composite types** — Arrays, mappings, structs
+- **Control flow** — `if`, `while`, expressions
+- **Two code sections** — `create` (constructor) and `code` (runtime)
+
+A simple BUG contract:
+
+```bug
+name Counter;
+
+storage {
+ [0] count: uint256;
+ [1] threshold: uint256;
+}
+
+create {
+ count = 0;
+ threshold = 100;
+}
+
+code {
+ count = count + 1;
+ if (count >= threshold) {
+ count = 0;
+ }
+}
+```
+
+## Compilation Pipeline
+
+BUG uses a multi-stage compilation pipeline:
+
+```
+Source → AST → IR → EVM Bytecode
+ ↓ ↓ ↓
+ Types Debug Program
+```
+
+1. **Parsing** — Source text to AST with source locations
+2. **Type checking** — Validates types and collects type information
+3. **IR generation** — Converts AST to intermediate representation
+4. **EVM code generation** — Produces final bytecode with debug annotations
+
+## Debug Information Strategy
+
+BUG generates ethdebug/format output alongside bytecode by:
+
+1. **Tracking source locations** through all compilation phases
+2. **Preserving type information** from the type checker
+3. **Computing storage layouts** during IR generation
+4. **Emitting program annotations** during code generation
+
+### Key Design Decisions
+
+**Type preservation**: BUG's IR types carry an "origin" field linking back to
+the source-level type. This allows generating rich ethdebug type information
+even after type erasure in the IR.
+
+**Storage analysis**: During IR generation, BUG analyzes storage access
+patterns to determine variable locations. This analysis traces through
+`compute_slot` instructions to reconstruct the storage layout.
+
+**Program builder**: A dedicated `ProgramBuilder` class accumulates
+instructions with their contexts during code generation, then serializes the
+complete program annotation.
+
+## Type Generation
+
+BUG converts its type system to ethdebug/format types:
+
+```typescript title="packages/bugc/src/irgen/debug/types.ts"
+export function convertBugType(bugType: BugType): Format.Type | undefined {
+ // Elementary types
+ if (BugType.isElementary(bugType)) {
+ return convertElementaryType(bugType);
+ }
+
+ // Array types
+ if (BugType.isArray(bugType)) {
+ const elementType = convertBugType(bugType.element);
+ return {
+ kind: "array",
+ contains: { type: elementType },
+ ...(bugType.size !== undefined && { length: bugType.size }),
+ };
+ }
+
+ // Mapping and struct types follow similar patterns...
+}
+```
+
+The type conversion handles:
+
+- **Elementary types** — Direct mapping (`uint256` → `{kind: "uint", bits: 256}`)
+- **Arrays** — Recursive conversion with optional length
+- **Mappings** — Key/value type conversion
+- **Structs** — Field-by-field conversion with names
+
+## Pointer Generation
+
+BUG generates pointers that describe how to locate variables at runtime.
+
+### Storage Variables
+
+For simple storage variables, BUG generates direct slot pointers:
+
+```json
+{
+ "location": "storage",
+ "slot": 0
+}
+```
+
+### Composite Types
+
+For structs, BUG generates group pointers with field offsets:
+
+```json
+{
+ "group": [
+ { "name": "field1", "location": "storage", "slot": 0 },
+ { "name": "field2", "location": "storage", "slot": 0, "offset": 16 }
+ ]
+}
+```
+
+### Dynamic Arrays
+
+For dynamic arrays, BUG generates pointers with keccak256 expressions:
+
+```json
+{
+ "group": [
+ { "name": "array-length", "location": "storage", "slot": 0 },
+ {
+ "list": {
+ "count": { "$read": "array-length" },
+ "each": "i",
+ "is": {
+ "name": "element",
+ "location": "storage",
+ "slot": { "$sum": [{ "$keccak256": [{ "$wordsized": 0 }] }, "i"] }
+ }
+ }
+ }
+ ]
+}
+```
+
+### Storage Analysis
+
+BUG includes a storage analysis pass that traces `compute_slot` instructions
+to reconstruct dynamic storage locations. This handles patterns like:
+
+```
+compute_slot(mapping, baseSlot, key) → keccak256(key, slot)
+compute_slot(array, baseSlot) → keccak256(slot)
+compute_slot(field, baseSlot, offset) → slot + offset
+```
+
+## Program Annotation
+
+BUG emits program annotations that map bytecode to source context.
+
+### Instruction Context
+
+Each bytecode instruction includes context with:
+
+- **Source range** — Where in source this instruction originates
+- **Variables** — Variables in scope at this point
+- **Remarks** — Human-readable annotations
+
+```json
+{
+ "offset": "0x1a",
+ "operation": { "mnemonic": "SLOAD" },
+ "context": {
+ "gather": [
+ {
+ "code": {
+ "source": { "id": "main" },
+ "range": { "offset": 120, "length": 5 }
+ }
+ },
+ {
+ "variables": [
+ {
+ "identifier": "count",
+ "type": { "kind": "uint", "bits": 256 },
+ "pointer": { "location": "storage", "slot": 0 }
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+### Program Builder
+
+The `ProgramBuilder` class manages instruction accumulation:
+
+```typescript title="Simplified pattern"
+class ProgramBuilder {
+ private instructions: Program.Instruction[] = [];
+
+ addInstruction(
+ offset: number,
+ operation: Program.Instruction.Operation,
+ context?: Program.Context,
+ ) {
+ this.instructions.push({ offset, operation, context });
+ }
+
+ build(): Program {
+ return {
+ instructions: this.instructions,
+ };
+ }
+}
+```
+
+## Variable Scoping
+
+BUG includes storage variables in the variables context for every instruction
+that accesses them. This allows debuggers to inspect storage values at any
+execution point.
+
+```typescript title="packages/bugc/src/irgen/debug/variables.ts"
+export function collectVariablesWithLocations(
+ state: State,
+ sourceId: string,
+): VariableInfo[] {
+ const variables: VariableInfo[] = [];
+
+ // Storage variables have fixed slots
+ for (const storageDecl of state.module.storageDeclarations) {
+ const bugType = state.types.get(storageDecl.id);
+ const pointer = generateStoragePointer(storageDecl.slot, bugType);
+
+ variables.push({
+ identifier: storageDecl.name,
+ type: convertBugType(bugType),
+ pointer,
+ declaration: storageDecl.loc
+ ? {
+ source: { id: sourceId },
+ range: storageDecl.loc,
+ }
+ : undefined,
+ });
+ }
+
+ return variables;
+}
+```
+
+## Testing Strategy
+
+BUG tests debug output in several ways:
+
+1. **Unit tests** — Test individual conversion functions
+2. **Integration tests** — Compile programs and verify output structure
+3. **Playground** — Visual verification of output (see BUG Playground)
+
+Example test pattern:
+
+```typescript
+test("generates correct storage pointer", () => {
+ const source = `
+ name Test;
+ storage { [0] value: uint256; }
+ code { value = 42; }
+ `;
+
+ const result = compile({ to: "bytecode", source });
+ const program = result.value.program;
+
+ // Find the SLOAD instruction
+ const sload = program.instructions.find(
+ (i) => i.operation?.mnemonic === "SLOAD",
+ );
+
+ expect(sload.context).toMatchObject({
+ variables: [
+ {
+ identifier: "value",
+ pointer: { location: "storage", slot: 0 },
+ },
+ ],
+ });
+});
+```
+
+## Lessons Learned
+
+### Start Simple
+
+BUG started with just storage variables and elementary types. Complex features
+(arrays, mappings, structs) were added incrementally after the basic
+infrastructure was working.
+
+### Preserve Information Early
+
+Type information is easier to preserve than reconstruct. BUG's IR types carry
+their source-level origin, which simplifies later conversion.
+
+### Test Visually
+
+The BUG Playground proved invaluable for debugging the debug output. Being able
+to see the generated annotations alongside bytecode helped catch issues that
+unit tests missed.
+
+### Handle Edge Cases Gracefully
+
+When storage analysis can't determine a location (e.g., computed slot from
+a non-constant), BUG still generates useful partial information rather than
+failing entirely.
+
+## Future Work
+
+Areas for improvement in BUG's debug support:
+
+- **Memory tracking** — Track memory allocations for local variable pointers
+- **Stack variables** — Generate stack pointers during code generation
+- **Richer contexts** — Add frame information for function calls
+- **Source maps** — More granular source location tracking
+
+## Resources
+
+- [BUG Playground](/docs/examples/bug-playground) — Interactive compiler demo
+- [BUG Source Code](https://github.com/ethdebug/format/tree/main/packages/bugc) — Implementation reference
+- [ethdebug/format Specification](/spec/overview) — Format reference
diff --git a/packages/web/docs/implementation-guides/compiler/index.mdx b/packages/web/docs/implementation-guides/compiler/index.mdx
new file mode 100644
index 000000000..782a93e29
--- /dev/null
+++ b/packages/web/docs/implementation-guides/compiler/index.mdx
@@ -0,0 +1,21 @@
+---
+sidebar_position: 1
+sidebar_label: Compiler Guides
+---
+
+# Compiler Implementation Guides
+
+Guides for implementing ethdebug/format support in compilers.
+
+:::note[Coming soon]
+This section is under construction. It will explain how compilers can emit
+ethdebug/format debug information.
+:::
+
+## Overview
+
+Compilers that want to support ethdebug/format need to:
+
+1. **Emit type information** — Describe the types used in the program
+2. **Emit pointer information** — Describe where variables are stored
+3. **Emit program information** — Describe runtime context at each instruction
diff --git a/packages/web/docs/implementation-guides/implementation-guides.mdx b/packages/web/docs/implementation-guides/implementation-guides.mdx
index 90f42493f..645412c1e 100644
--- a/packages/web/docs/implementation-guides/implementation-guides.mdx
+++ b/packages/web/docs/implementation-guides/implementation-guides.mdx
@@ -1,57 +1,54 @@
---
-sidebar_position: 4
+sidebar_position: 1
+sidebar_label: Overview
pagination_prev: null
pagination_next: null
---
# Implementation guides
-This section of these docs serves to provide resources that guide readers who
-are looking to implement one or more components of **ethdebug/format**.
-Because of the distinct concerns involved in implementing this format on the
-compilation side vs. the concerns involved on the debugging side, this page
-lists and categorizes the available guides into the appropriate heading.
+These guides help you implement ethdebug/format support in your project.
+Choose the section that matches what you're building.
## For debuggers
+Guides for consuming ethdebug/format data in debuggers, tracers, and analysis
+tools.
+
-**Guide: [Dereferencing pointers](/docs/implementation-guides/pointers)**
+**[Dereferencing pointers](/docs/implementation-guides/pointers)**
-This guide provides readers with a tour of the **@ethdebug/pointers**
-TypeScript reference implementation, showing example concrete logic for how a
-debugger might process **ethdebug/format** pointers.
+A detailed walkthrough of the **@ethdebug/pointers** TypeScript reference
+implementation, showing how to resolve pointers to concrete values.
-For an introduction to **ethdebug/format** pointers, please see
-the Pointer specification's [Overview](/spec/pointer/overview) and
-[Key concepts](/spec/pointer/concepts) pages.
+For background on pointer concepts, see
+the [Pointers documentation](/docs/pointers) and the
+[Pointer specification](/spec/pointer/overview).
**Other guides**
- _Guides for other aspects of debugger-side **ethdebug/format** implementation
- are planned and still need to be written._
+ _Additional debugger implementation guides are planned._
-
## For compilers
+Guides for emitting ethdebug/format data from compilers and toolchains.
+
- **No availble guides yet**
+ **[Compiler guides](/docs/implementation-guides/compiler)**
- _Guides for implementing **ethdebug/format** support inside a compiler are
- planned and still need to be written._
+ Guidance on integrating ethdebug/format output into your compiler, including
+ what to emit and when.
-:::tip[Work in progress]
-
-Sadly, things are looking a little scarce right now. Please stay tuned as work
-on this effort progresses.
+:::tip[Contributing]
-**Interested in helping out?** If you'd like to help with writing initial
-reference implementations for one or more schemas, please reach out in our
-[Matrix.chat](https://matrix.to/#/#ethdebug:matrix.org).
+Interested in helping improve these guides? Contributions are welcome!
+Reach out in our [Matrix.chat](https://matrix.to/#/#ethdebug:matrix.org)
+or open an issue on [GitHub](https://github.com/ethdebug/format).
:::
diff --git a/packages/web/docs/overview.mdx b/packages/web/docs/overview.mdx
index c7065468e..f7b471ee1 100644
--- a/packages/web/docs/overview.mdx
+++ b/packages/web/docs/overview.mdx
@@ -4,9 +4,74 @@ sidebar_position: 1
# Project overview
-The security of smart contracts hinges on the availability of robust debugging
-tools. As the compiler optimizes a contract, it may move instructions around or
-remove them thus weakening its relationship with the original source code. The
-debugger then faces the challenging task of reversing these transformations to
-enrich the often cryptic artifacts with contexts mapping back to the contract's
-source.
+**ethdebug/format** is an open specification for debugging information in
+EVM-based smart contracts. It provides a standard way for compilers to emit
+rich debug data that debuggers can use to help developers understand what
+their contracts are doing at runtime.
+
+## The problem
+
+When a compiler optimizes source code into EVM bytecode, it transforms the
+original program structure in ways that make debugging difficult. Instructions
+get reordered, variables get eliminated, and the connection between bytecode
+and source code becomes obscured. Without standardized debug information,
+every debugger must reverse-engineer these transformations — or worse, each
+compiler-debugger pair requires custom integration.
+
+## What this format provides
+
+The ethdebug/format specification defines schemas for:
+
+- **Types** — Describe the structure of data (integers, structs, arrays,
+ mappings) so debuggers can decode raw bytes into meaningful values
+- **Pointers** — Describe where data lives at runtime, including dynamic
+ locations that depend on execution state
+- **Programs** — Describe the runtime context at each bytecode instruction,
+ including which variables are in scope and what source code corresponds
+ to each operation
+
+## Get started
+
+
+
+
+
+
Building a debugger?
+
+
+ Learn how to consume ethdebug/format data to build better debugging
+ tools.
+
+
+
+
+
+
+
+
Building a compiler?
+
+
+ Learn how to emit ethdebug/format data from your compiler.
+
+
+
+
+
+
+Or explore the [concepts](/docs/concepts) to understand the format's design,
+browse [examples](/docs/examples) to see it in action, or dive into the
+[specification](/spec/overview) for formal definitions.
diff --git a/packages/web/docs/pointers/_category_.json b/packages/web/docs/pointers/_category_.json
new file mode 100644
index 000000000..7f83e9d7c
--- /dev/null
+++ b/packages/web/docs/pointers/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Pointers",
+ "position": 5
+}
diff --git a/packages/web/docs/pointers/collections.mdx b/packages/web/docs/pointers/collections.mdx
new file mode 100644
index 000000000..a363fdfdc
--- /dev/null
+++ b/packages/web/docs/pointers/collections.mdx
@@ -0,0 +1,350 @@
+---
+sidebar_position: 4
+---
+
+import { PointerPlayground, PointerExample } from "@theme/PointersExample";
+
+# Collections
+
+
+
+While [regions](./regions) describe single contiguous byte ranges, **collections**
+aggregate multiple pointers together. Collections handle cases where data
+structures span multiple locations or have dynamic configurations.
+
+## Why collections?
+
+Consider a Solidity struct with multiple fields, or a dynamic array whose
+length isn't known at compile time. These require more than pointing to a
+single memory location—they need to describe relationships between multiple
+pointers or generate pointers based on runtime values.
+
+Collections provide six patterns for composing pointers:
+
+| Collection | Purpose |
+| ------------- | -------------------------------------------- |
+| `group` | Combine multiple pointers into one |
+| `list` | Generate pointers for indexed sequences |
+| `conditional` | Choose between pointers based on a condition |
+| `scope` | Define variables for use in nested pointers |
+| `reference` | Invoke reusable pointer templates |
+| `templates` | Define inline templates for local reuse |
+
+Click **"▶ Try it"** on any example to load it into the Pointer Playground
+drawer at the bottom of the screen.
+
+## Group
+
+A **group** combines multiple pointers into a single composite pointer. Each
+pointer in the group can have a name for identification.
+
+
+
+Groups are useful for structs and other compound data types where multiple
+fields need to be accessed together.
+
+## List
+
+A **list** generates a sequence of pointers based on a count expression. This
+handles dynamic arrays and other indexed collections.
+
+
+
+The list evaluates `count` to determine how many pointers to generate, then
+for each index (bound to the variable named by `each`), it evaluates the `is`
+pointer template.
+
+### List with dynamic count
+
+When the count comes from storage:
+
+
+
+### List properties
+
+| Property | Description |
+| -------- | --------------------------------------------------- |
+| `count` | Expression evaluating to the number of items |
+| `each` | Variable name for the current index (starting at 0) |
+| `is` | Pointer template evaluated for each index |
+
+## Conditional
+
+A **conditional** selects between pointers based on whether an expression
+evaluates to a non-zero value.
+
+
+
+### Conditional properties
+
+| Property | Description |
+| -------- | ---------------------------------------- |
+| `if` | Expression to evaluate (non-zero = true) |
+| `then` | Pointer to use when condition is true |
+| `else` | Optional pointer when condition is false |
+
+## Scope
+
+A **scope** defines variables that can be used in a nested pointer. Variables
+are evaluated in order, so later variables can reference earlier ones.
+
+
+
+Scopes help break complex pointer definitions into readable steps. For examples
+combining scopes with keccak256 for storage slot computation, see the
+[expressions documentation](./expressions#computing-storage-slots-with-keccak256).
+
+### Scope properties
+
+| Property | Description |
+| -------- | --------------------------------------------- |
+| `define` | Object mapping variable names to expressions |
+| `in` | Pointer where defined variables are available |
+
+## Reference and Templates
+
+A **reference** invokes a named pointer template, while **templates** defines
+them inline. These work together for reusable pointer patterns.
+
+
+
+### Template definition
+
+Each template in the `templates` object has:
+
+| Property | Description |
+| -------- | -------------------------------- |
+| `expect` | Array of required variable names |
+| `for` | The pointer template body |
+
+### Reference properties
+
+| Property | Description |
+| ---------- | ---------------------------------------------------------- |
+| `template` | Name of the template to invoke |
+| `yields` | Optional object mapping template region names to new names |
+
+## Nesting collections
+
+Collections can be nested to build complex pointer structures. A group might
+contain lists, conditionals might wrap groups, and scopes can define variables
+used throughout nested collections.
+
+
+
+## Named regions in collections
+
+Pointers within collections can include a `name` property. Named regions are
+tracked during resolution and can be referenced using the `.slot`, `.offset`,
+and `.length` syntax in expressions.
+
+
+
+## Learn more
+
+- [Regions](./regions) for simple pointer definitions
+- [Expressions](./expressions) for dynamic value computation
+- [Pointer specification](/spec/pointer/collection) for formal definitions
+
+
diff --git a/packages/web/docs/pointers/expressions.mdx b/packages/web/docs/pointers/expressions.mdx
new file mode 100644
index 000000000..0f803bfbc
--- /dev/null
+++ b/packages/web/docs/pointers/expressions.mdx
@@ -0,0 +1,466 @@
+---
+sidebar_position: 3
+---
+
+import { PointerPlayground, PointerExample } from "@theme/PointersExample";
+
+# Expressions
+
+
+
+Static offsets work for simple variables, but most interesting data has
+locations that depend on runtime values. Expressions let pointers compute
+addresses dynamically.
+
+## Why expressions are needed
+
+Consider reading element `i` from a memory array. The element's location
+depends on:
+
+- Where the array starts (might come from the free memory pointer)
+- Which element we want (the index `i`)
+- How big each element is (32 bytes for `uint256`)
+
+A static pointer can't capture this. Expressions can:
+
+
+
+## Arithmetic expressions
+
+Basic math operations for computing addresses:
+
+### `$sum` — Addition
+
+Adds all values in an array:
+
+
+
+### `$difference` — Subtraction
+
+Subtracts the second value from the first (saturates at zero):
+
+
+
+### `$product` — Multiplication
+
+Multiplies all values in an array:
+
+
+
+### `$quotient` — Division
+
+Integer division of first value by second:
+
+
+
+### `$remainder` — Modulo
+
+Remainder after division:
+
+
+
+## Reading values
+
+### `$read` — Read from a named region
+
+Reads the bytes from a previously defined region:
+
+
+
+The `$read` expression retrieves the actual runtime value stored in the
+`array-length-slot` region—the array's length.
+
+## Region property lookups
+
+Reference properties of named regions with `.property` syntax:
+
+### `.offset` — Region's offset
+
+```json
+{ ".offset": "previous-element" }
+```
+
+Returns the offset of the named region.
+
+### `.length` — Region's length
+
+```json
+{ ".length": "previous-element" }
+```
+
+Returns the length of the named region.
+
+### `.slot` — Region's slot
+
+```json
+{ ".slot": "base-slot" }
+```
+
+Returns the slot number for storage/stack/transient regions.
+
+### Chaining lookups
+
+Compute the next element's position from the previous one:
+
+
+
+## Computing storage slots with `$keccak256`
+
+Solidity uses keccak256 hashing to compute storage locations for dynamic data.
+
+### Array element slots
+
+For a dynamic array at slot `n`, elements start at `keccak256(n)`:
+
+
+
+### Mapping value slots
+
+For a mapping at slot `n`, the value for key `k` is at `keccak256(k, n)`:
+
+
+
+### Nested mappings
+
+For `mapping(address => mapping(uint => uint))` at slot 2:
+
+
+
+This computes: `keccak256(inner_key, keccak256(outer_key, 2))`
+
+## Data manipulation
+
+### `$concat` — Concatenate bytes
+
+Joins byte sequences without padding:
+
+
+
+Useful for building hash inputs from multiple values.
+
+### `$sized` — Resize to N bytes
+
+Truncates or pads to exactly N bytes:
+
+
+
+Pads with zeros on the left; truncates from the left if too long.
+
+### `$wordsized` — Resize to word size
+
+Equivalent to `$sized32` on the EVM:
+
+
+
+## Variables in expressions
+
+Expressions can reference variables by name. These come from list pointer
+contexts:
+
+
+
+The variable `"i"` takes values from 0 to count-1, computing each element's
+slot.
+
+## Complete example: Dynamic array element
+
+Reading element `i` from `uint256[] storage arr` at slot 5:
+
+
+
+The pointer:
+
+1. Defines the array's base slot
+2. Computes the element's slot: `keccak256(5) + element_index`
+3. Returns that storage location
+
+## Learn more
+
+- [Regions documentation](./regions) for region structure
+- [Expression specification](/spec/pointer/expression) for the complete
+ expression language
+- [Implementation guide](/docs/implementation-guides/pointers/evaluating-expressions)
+ for building an expression evaluator
+
+
diff --git a/packages/web/docs/pointers/index.mdx b/packages/web/docs/pointers/index.mdx
new file mode 100644
index 000000000..208129e36
--- /dev/null
+++ b/packages/web/docs/pointers/index.mdx
@@ -0,0 +1,192 @@
+---
+sidebar_position: 1
+---
+
+# Pointers
+
+Pointers describe where data lives in the EVM. They're recipes that tell a
+debugger how to find bytes at runtime—recipes that can depend on the current
+machine state.
+
+## Why pointers are complex
+
+In high-level languages, a variable like `uint256 balance` seems simple. But
+at the EVM level, finding that value might require:
+
+1. Reading a storage slot whose address depends on a mapping key
+2. Following a memory pointer stored on the stack
+3. Computing an offset based on array indices
+
+A pointer in **ethdebug/format** captures all this complexity in a
+self-contained description.
+
+## Core concepts
+
+### Pointers are recursive
+
+A pointer is either:
+
+- A **region**: a single, continuous sequence of bytes
+- A **collection**: an aggregation of other pointers
+
+This recursive structure lets you build up complex allocations from simple
+pieces.
+
+### Regions locate bytes
+
+A region identifies a contiguous block of bytes in a specific data location:
+
+```json
+{
+ "location": "memory",
+ "offset": "0x40",
+ "length": 32
+}
+```
+
+This says: "32 bytes in memory starting at offset 0x40."
+
+Different locations use different addressing schemes:
+
+- **Memory, calldata, returndata, code**: byte offsets (`offset` + `length`)
+- **Storage, transient, stack**: slot-based (`slot`)
+
+### Collections group pointers
+
+When data spans multiple regions (like struct members), collections aggregate
+them:
+
+```json
+{
+ "group": [
+ {
+ "name": "balance",
+ "location": "memory",
+ "offset": "0x40",
+ "length": 32
+ },
+ {
+ "name": "owner",
+ "location": "memory",
+ "offset": "0x60",
+ "length": 32
+ }
+ ]
+}
+```
+
+Collection types include `group`, `list`, `conditional`, and more. See
+[collections](./collections) for the complete reference.
+
+### Expressions make pointers dynamic
+
+Real allocations rarely use constant offsets. Expressions let you compute
+addresses from runtime state—reading values from named regions, performing
+arithmetic, and computing keccak256 hashes for storage slot calculation.
+
+See [expressions](./expressions) for the full expression language.
+
+### Named regions enable composition
+
+Regions can have names that other parts of the pointer reference. This allows
+one region's address to be computed from another's properties, or its value to
+be read and used in expressions. See [expressions](./expressions) for the full
+expression language.
+
+## What's next
+
+
+
+
+
+
Regions
+
+
+
How to specify byte ranges in different data locations.
+
+
+
+
+
+
+
+
Expressions
+
+
+
Computing dynamic locations with arithmetic and hashing.
+
+
+
+
+
+
+
+
+
+
+
Collections
+
+
+
Grouping pointers for structs, arrays, and conditionals.
+
+
+
+
+
+
+
+
Implementation guide
+
+
+
Build a pointer resolver for your debugger.
+
+
+
+
+
+
+
+
+
+
+
Full specification
+
+
+
Complete JSON schemas and expression reference.
+
+
+
+
+
diff --git a/packages/web/docs/pointers/regions.mdx b/packages/web/docs/pointers/regions.mdx
new file mode 100644
index 000000000..693cb0afd
--- /dev/null
+++ b/packages/web/docs/pointers/regions.mdx
@@ -0,0 +1,231 @@
+---
+sidebar_position: 2
+---
+
+import { PointerPlayground, PointerExample } from "@theme/PointersExample";
+
+# Regions
+
+
+
+A region represents a contiguous block of bytes in a specific EVM data
+location. Regions are the leaves of the pointer tree—the actual byte ranges
+that hold data.
+
+## Addressing schemes
+
+The EVM uses two different models for organizing bytes, and regions reflect
+this:
+
+### Slice-based locations
+
+**Memory**, **calldata**, **returndata**, and **code** are byte-addressable.
+Regions in these locations use `offset` and `length`:
+
+
+
+- `offset`: byte position from the start (required)
+- `length`: number of bytes (optional; may be computed or implied by type)
+
+### Slot-based locations
+
+**Storage**, **transient storage**, and **stack** are organized in 32-byte
+slots. Regions use `slot`:
+
+
+
+For storage and transient storage, values that don't fill a full slot can
+specify sub-slot positioning:
+
+
+
+This addresses 20 bytes starting at byte 12 within slot 0—useful for packed
+storage.
+
+## Location-specific details
+
+### Memory
+
+Memory is a simple byte array that grows as needed:
+
+
+
+Memory addresses often come from the free memory pointer (stored at `0x40`).
+
+### Storage
+
+Storage persists between transactions. Slots are 32-byte words addressed by
+256-bit keys:
+
+
+
+Slot addresses can be literal numbers, hex strings, or computed expressions.
+
+### Stack
+
+The EVM stack holds up to 1024 words. Slot 0 is the top:
+
+
+
+Stack regions are typically read-only from a debugging perspective—you observe
+values but don't address sub-ranges.
+
+### Calldata
+
+Function arguments arrive in calldata, read-only and byte-addressable:
+
+
+
+The first 4 bytes are typically the function selector; arguments follow.
+
+### Returndata
+
+After a call, the returned data is accessible:
+
+
+
+### Code
+
+Contract bytecode can be read as data:
+
+
+
+This is used for immutable variables and other data embedded in bytecode.
+
+### Transient storage
+
+Transient storage (EIP-1153) persists only within a transaction:
+
+
+
+Uses the same slot-based addressing as regular storage.
+
+## Naming regions
+
+Any region can have a `name` that other parts of the pointer reference:
+
+
+
+Names enable:
+
+- Reading the region's value with `{ "$read": "token-balance" }`
+- Referencing properties with `{ ".slot": "token-balance" }`
+- Building self-documenting pointer structures
+
+## Dynamic addresses
+
+Region fields like `offset`, `slot`, and `length` can use expressions to
+compute values at runtime. This enables pointers for dynamic data like arrays
+and mappings.
+
+For the full expression language including arithmetic, `$keccak256`, and value
+reading, see [expressions](./expressions).
+
+## Learn more
+
+For complete schemas for each location type, see the
+[pointer region specification](/spec/pointer/region).
+
+
diff --git a/packages/web/docs/programs/_category_.json b/packages/web/docs/programs/_category_.json
new file mode 100644
index 000000000..d5d043dda
--- /dev/null
+++ b/packages/web/docs/programs/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Programs",
+ "position": 6
+}
diff --git a/packages/web/docs/programs/index.mdx b/packages/web/docs/programs/index.mdx
new file mode 100644
index 000000000..1f07e7edb
--- /dev/null
+++ b/packages/web/docs/programs/index.mdx
@@ -0,0 +1,187 @@
+---
+sidebar_position: 1
+---
+
+# Programs
+
+Programs describe the high-level context at each point in EVM bytecode
+execution. They're the bridge between raw machine instructions and the
+source code developers wrote.
+
+## What programs contain
+
+A program record corresponds to one block of executable bytecode—either a
+contract's runtime code (executed when called) or its creation code (executed
+during deployment).
+
+Each program contains:
+
+- **Contract metadata**: which contract this bytecode belongs to
+- **Instruction list**: one entry per bytecode instruction, in order
+
+## Instructions carry context
+
+The instruction list is the heart of a program. Each instruction record
+provides:
+
+- **Byte offset**: where this instruction appears in the bytecode (equivalent
+ to program counter on pre-EOF EVMs)
+- **Context information**: high-level details valid after this instruction
+ executes
+
+Context information may include:
+
+- **Source ranges**: which lines of source code this instruction relates to
+- **Variables**: what variables are in scope and where their values live
+- **Control flow hints**: whether this instruction is part of a function call,
+ return, or other high-level operation
+
+## How debuggers use programs
+
+When stepping through EVM execution, a debugger:
+
+1. Observes the current program counter
+2. Looks up the corresponding instruction in the program
+3. Uses the context to update its model of the high-level state
+4. Presents source code, variables, and call stacks to the developer
+
+Each instruction's context acts as a state transition—a compile-time guarantee
+about what's true after that instruction runs.
+
+## Example: Simple instruction
+
+```json
+{
+ "offset": 42,
+ "context": {
+ "code": {
+ "source": {
+ "id": 0,
+ "range": {
+ "start": { "line": 15, "column": 4 },
+ "end": { "line": 15, "column": 28 }
+ }
+ }
+ }
+ }
+}
+```
+
+This says: "The instruction at byte 42 corresponds to line 15, columns 4-28
+of source file 0."
+
+## Example: Instruction with variables
+
+```json
+{
+ "offset": 100,
+ "context": {
+ "code": {
+ "source": { "id": 0, "range": { ... } }
+ },
+ "variables": [
+ {
+ "name": "balance",
+ "type": { "kind": "uint", "bits": 256 },
+ "pointer": {
+ "location": "stack",
+ "slot": 0
+ }
+ }
+ ]
+ }
+}
+```
+
+After instruction 100, the variable `balance` is in scope and its value is at
+the top of the stack.
+
+## Context accumulates
+
+Not every instruction needs complete context. The format supports incremental
+updates:
+
+- **`"pick"`**: select from multiple possible contexts based on runtime
+ conditions
+- **`"gather"`**: combine contexts (like nested scopes)
+- **`"remark"`**: add metadata without changing variable scope
+
+This lets compilers emit compact debugging info that debuggers expand at
+runtime.
+
+## What's next
+
+
+
+
+
+
Instructions
+
+
+
How instruction records map bytecode to source.
+
+
+
+
+
+
+
+
Variables
+
+
+
Connecting identifiers to runtime locations.
+
+
+
+
+
+
+
+
+
+
+
Tracing execution
+
+
+
Using programs to inspect variables during execution.
+
+
+
+
+
+
+
+
Full specification
+
+
+
Complete JSON schemas for programs, instructions, and contexts.
+
+
+
+
+
diff --git a/packages/web/docs/programs/instructions.mdx b/packages/web/docs/programs/instructions.mdx
new file mode 100644
index 000000000..3a6e24e95
--- /dev/null
+++ b/packages/web/docs/programs/instructions.mdx
@@ -0,0 +1,182 @@
+---
+sidebar_position: 2
+---
+
+# Instructions
+
+Each instruction record in a program corresponds to one EVM opcode in the
+bytecode. Instructions carry the context information that debuggers need.
+
+## Structure
+
+An instruction has two required fields:
+
+```json
+{
+ "offset": 42,
+ "context": { ... }
+}
+```
+
+- **`offset`**: byte position in the bytecode (the program counter value when
+ this instruction executes)
+- **`context`**: high-level information valid after this instruction
+
+## Context types
+
+The `context` field can take several forms:
+
+### Code context
+
+Maps the instruction to source code:
+
+```json
+{
+ "offset": 42,
+ "context": {
+ "code": {
+ "source": {
+ "id": 0,
+ "range": {
+ "start": { "line": 10, "column": 4 },
+ "end": { "line": 10, "column": 20 }
+ }
+ }
+ }
+ }
+}
+```
+
+The `source` field references a source file by ID and specifies the exact
+character range.
+
+### Variables context
+
+Declares variables that are in scope:
+
+```json
+{
+ "offset": 100,
+ "context": {
+ "variables": [
+ {
+ "name": "amount",
+ "type": { "kind": "uint", "bits": 256 },
+ "pointer": {
+ "location": "stack",
+ "slot": 0
+ }
+ }
+ ]
+ }
+}
+```
+
+Each variable includes:
+
+- `name`: the identifier from source code
+- `type`: an ethdebug/format type reference
+- `pointer`: where to find the variable's value
+
+### Frame context
+
+Indicates call stack changes:
+
+```json
+{
+ "offset": 200,
+ "context": {
+ "frame": "step-in"
+ }
+}
+```
+
+Frame values include:
+
+- `"step-in"`: entering a function
+- `"step-out"`: returning from a function
+
+## Composing contexts
+
+### Gather
+
+Combine multiple contexts (like nested scopes):
+
+```json
+{
+ "offset": 150,
+ "context": {
+ "gather": [
+ {
+ "code": {
+ "source": { "id": 0, "range": { ... } }
+ }
+ },
+ {
+ "variables": [
+ { "name": "x", "type": { ... }, "pointer": { ... } }
+ ]
+ }
+ ]
+ }
+}
+```
+
+### Pick
+
+Choose between contexts based on runtime conditions:
+
+```json
+{
+ "offset": 175,
+ "context": {
+ "pick": [
+ {
+ "guard": { "$read": "condition-flag" },
+ "context": {
+ "variables": [
+ { "name": "result", "type": { ... }, "pointer": { ... } }
+ ]
+ }
+ },
+ {
+ "context": {
+ "variables": []
+ }
+ }
+ ]
+ }
+}
+```
+
+The debugger evaluates guards at runtime and uses the first matching context.
+A context without a guard acts as the default.
+
+### Remark
+
+Add metadata without affecting scope:
+
+```json
+{
+ "offset": 180,
+ "context": {
+ "remark": "loop iteration boundary"
+ }
+}
+```
+
+## Instruction ordering
+
+Instructions must be listed in bytecode order, matching the sequence of
+opcodes. The list is indexed by offset, so debuggers can quickly find the
+instruction for any program counter value.
+
+Not every byte offset needs an instruction—only positions where opcodes begin.
+Push data, for instance, doesn't get its own instruction entry.
+
+## Learn more
+
+For complete schemas, see:
+
+- [Instruction schema](/spec/program/instruction)
+- [Context schema](/spec/program/context)
diff --git a/packages/web/docs/programs/tracing.mdx b/packages/web/docs/programs/tracing.mdx
new file mode 100644
index 000000000..0be7eb8e9
--- /dev/null
+++ b/packages/web/docs/programs/tracing.mdx
@@ -0,0 +1,240 @@
+---
+sidebar_position: 4
+---
+
+import { TracePlayground, TraceExample } from "@theme/ProgramExample";
+
+# Tracing execution
+
+
+
+Tracing brings together programs, pointers, and types to show what's happening
+at each step of EVM execution. Click **"Try it"** on any example to open the
+Trace Playground, where you can compile and step through real BUG code.
+
+## What tracing provides
+
+With ethdebug/format data, a trace viewer can show:
+
+- **Current source location**: Which line of source code corresponds to the
+ current bytecode instruction
+- **Variables in scope**: What identifiers are valid and their current values
+- **Call context**: Function name, parameters, and return expectations
+- **Data inspection**: Drill into complex types (structs, arrays, mappings)
+
+## The tracing process
+
+At each instruction in a transaction trace:
+
+1. **Look up the program counter**: Find the instruction record for the current
+ PC
+2. **Read the context**: Get variables, source ranges, and other metadata
+3. **Resolve pointers**: For each variable, resolve its pointer to get the
+ current value
+4. **Decode values**: Use the type information to interpret raw bytes
+
+## Try it yourself
+
+Click **"Try it"** on any example below to load it into the Trace Playground.
+The drawer will open at the bottom of the screen where you can compile the
+code and step through the execution trace.
+
+### Simple counter increment
+
+This example shows a basic counter that increments a storage variable:
+
+
+
+### Storage with threshold check
+
+This example demonstrates conditional logic with storage variables:
+
+= threshold) {
+count = 0;
+}
+}`}
+/>
+
+### Multiple storage slots
+
+This example shows working with multiple storage locations:
+
+
+
+## Trace data structure
+
+A trace step captures the EVM state at a single point:
+
+```typescript
+interface TraceStep {
+ pc: number; // Program counter
+ opcode: string; // Mnemonic (SLOAD, ADD, etc.)
+ stack: bigint[]; // Stack contents (top first)
+ memory?: Uint8Array; // Memory contents
+ storage?: Record; // Changed slots
+}
+```
+
+Combined with the program annotation, this gives us complete visibility.
+
+## Mapping trace to program
+
+The program's instruction list maps each PC to its context:
+
+```json
+{
+ "offset": 18,
+ "operation": { "mnemonic": "SLOAD" },
+ "context": {
+ "gather": [
+ {
+ "code": {
+ "source": { "id": "main" },
+ "range": { "offset": 120, "length": 5 }
+ }
+ },
+ {
+ "variables": [
+ {
+ "identifier": "count",
+ "type": { "kind": "uint", "bits": 256 },
+ "pointer": { "location": "storage", "slot": 0 }
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+This tells us:
+
+- The SLOAD at PC 18 corresponds to source at offset 120
+- The variable `count` is in scope
+- We can resolve its value using the pointer
+
+## Variable resolution during tracing
+
+To show variable values, trace viewers:
+
+1. **Get the variable's pointer** from the program context
+2. **Create machine state** from the trace step (stack, storage, memory)
+3. **Resolve the pointer** to get concrete byte regions
+4. **Decode using the type** to get the display value
+
+## Building a trace viewer
+
+The key components for trace integration:
+
+### 1. Trace source
+
+Get transaction traces from:
+
+- JSON-RPC `debug_traceTransaction`
+- Local simulation (Ganache, Anvil, Hardhat)
+- Historical archive nodes
+
+### 2. Program loader
+
+Load compiled program data containing:
+
+- Instruction list with contexts
+- Source materials
+- Type definitions
+
+### 3. Pointer resolver
+
+Use `@ethdebug/pointers` to resolve variable locations:
+
+```typescript
+import { dereference } from "@ethdebug/pointers";
+
+// For each variable in scope
+const cursor = await dereference(variable.pointer, { state: machineState });
+const view = await cursor.view(machineState);
+const value = await view.read(view.regions[0]);
+```
+
+### 4. Type decoder
+
+Interpret raw bytes according to type:
+
+```typescript
+function decodeValue(bytes: Data, type: Type): string {
+ switch (type.kind) {
+ case "uint":
+ return bytes.asUint().toString();
+ case "bool":
+ return bytes.asUint() !== 0n ? "true" : "false";
+ case "address":
+ return "0x" + bytes.toHex().slice(-40);
+ // ... other types
+ }
+}
+```
+
+## Learn more
+
+- [Instructions documentation](./instructions) for understanding instruction
+ records
+- [Variables documentation](./variables) for variable structure and lifetime
+- [Pointers](../pointers) for resolving variable locations
+- [BUG Playground](/docs/examples/bug-playground) for more interactive examples
+
+
diff --git a/packages/web/docs/programs/variables.mdx b/packages/web/docs/programs/variables.mdx
new file mode 100644
index 000000000..a6d35c41c
--- /dev/null
+++ b/packages/web/docs/programs/variables.mdx
@@ -0,0 +1,148 @@
+---
+sidebar_position: 3
+---
+
+# Variables
+
+Variables connect source-level identifiers to runtime locations. They're the
+key to showing developers meaningful values instead of raw bytes.
+
+## Variable structure
+
+A variable declaration includes three parts:
+
+```json
+{
+ "name": "balance",
+ "type": { "kind": "uint", "bits": 256 },
+ "pointer": {
+ "location": "storage",
+ "slot": 0
+ }
+}
+```
+
+- **`name`**: the identifier from source code
+- **`type`**: how to interpret the bytes (an ethdebug/format type)
+- **`pointer`**: where to find the bytes (an ethdebug/format pointer)
+
+## Types tell debuggers how to decode
+
+The `type` field references the type system. A debugger uses this to:
+
+1. Know how many bytes to read
+2. Interpret those bytes correctly (signed vs unsigned, struct layout, etc.)
+3. Display the value in a readable format
+
+For complex types, the type definition guides the debugger through nested
+structures:
+
+```json
+{
+ "name": "user",
+ "type": {
+ "kind": "struct",
+ "contains": [
+ { "name": "id", "type": { "kind": "uint", "bits": 256 } },
+ { "name": "active", "type": { "kind": "bool" } }
+ ]
+ },
+ "pointer": { ... }
+}
+```
+
+## Pointers tell debuggers where to look
+
+The `pointer` field specifies the data's location. This can be simple:
+
+```json
+{
+ "pointer": {
+ "location": "stack",
+ "slot": 0
+ }
+}
+```
+
+Or complex, for data spread across multiple locations:
+
+```json
+{
+ "pointer": {
+ "group": [
+ { "name": "id", "location": "storage", "slot": 5 },
+ { "name": "active", "location": "storage", "slot": 5, "offset": 31 }
+ ]
+ }
+}
+```
+
+## Scope and lifetime
+
+Variables appear in context when they're valid. The instruction's context
+represents what's true _after_ that instruction executes.
+
+A variable might:
+
+- Become available when a function is entered
+- Change location as it moves from stack to memory
+- Go out of scope when a block ends
+
+The instruction list captures these transitions. As a debugger steps through
+execution, it accumulates and discards variables based on each instruction's
+context.
+
+## Example: Local variable lifecycle
+
+Consider this Solidity snippet:
+
+```solidity
+function transfer(address to, uint256 amount) {
+ uint256 balance = balances[msg.sender];
+ // ... use balance ...
+}
+```
+
+The compiled bytecode might have:
+
+1. **Instruction at offset 50**: `balance` comes into scope, stored on stack
+2. **Instructions 51-100**: `balance` remains in scope
+3. **Instruction at offset 101**: `balance` leaves scope (function returns)
+
+The program captures this by including `balance` in the variables list for
+instructions 50-100 and omitting it afterward.
+
+## Multiple variables
+
+An instruction's context can declare multiple variables:
+
+```json
+{
+ "offset": 75,
+ "context": {
+ "variables": [
+ {
+ "name": "sender",
+ "type": { "kind": "address" },
+ "pointer": { "location": "stack", "slot": 2 }
+ },
+ {
+ "name": "value",
+ "type": { "kind": "uint", "bits": 256 },
+ "pointer": { "location": "stack", "slot": 1 }
+ },
+ {
+ "name": "success",
+ "type": { "kind": "bool" },
+ "pointer": { "location": "stack", "slot": 0 }
+ }
+ ]
+ }
+}
+```
+
+## Learn more
+
+- [Types documentation](../types) for type representation details
+- [Pointers documentation](../pointers) for location specification
+- [Context schema](/spec/program/context/variables) for the full schema
diff --git a/packages/web/docs/reference/_category_.json b/packages/web/docs/reference/_category_.json
new file mode 100644
index 000000000..04b7db947
--- /dev/null
+++ b/packages/web/docs/reference/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Reference",
+ "position": 9
+}
diff --git a/packages/web/docs/known-challenges.mdx b/packages/web/docs/reference/challenges.mdx
similarity index 99%
rename from packages/web/docs/known-challenges.mdx
rename to packages/web/docs/reference/challenges.mdx
index c051dc4b5..f1d293453 100644
--- a/packages/web/docs/known-challenges.mdx
+++ b/packages/web/docs/reference/challenges.mdx
@@ -4,7 +4,7 @@ sidebar_position: 2
import TOCInline from "@theme/TOCInline";
-# Known challenges
+# Challenges
The fundamental challenge for an Ethereum debugging data format is that, on the
one hand, we want it to be able to handle the complexity of Solidity and other
diff --git a/packages/web/docs/reference/glossary.mdx b/packages/web/docs/reference/glossary.mdx
new file mode 100644
index 000000000..a17dab232
--- /dev/null
+++ b/packages/web/docs/reference/glossary.mdx
@@ -0,0 +1,224 @@
+---
+sidebar_position: 3
+---
+
+# Glossary
+
+This page defines key terms used throughout the ethdebug/format specification
+and documentation.
+
+## Core Concepts
+
+### Pointer
+
+A structured definition that describes how to locate data within EVM state.
+Pointers act as "recipes" that can be evaluated against machine state to
+produce concrete byte regions. Unlike static offsets, pointers can include
+dynamic computations (like keccak256 hashing for storage slots) that depend on
+runtime values.
+
+See: [Pointers documentation](/docs/pointers)
+
+### Region
+
+A contiguous range of bytes within a specific data location. A region
+specifies:
+
+- **Location**: Where the data lives (storage, memory, stack, etc.)
+- **Offset/Slot**: Where in that location the region starts
+- **Length**: How many bytes the region spans
+
+Regions are the concrete output of pointer resolution.
+
+See: [Regions documentation](/docs/pointers/regions)
+
+### Cursor
+
+The result of dereferencing a pointer. A cursor provides an interface for
+viewing pointer regions against different machine states. It can be thought of
+as a "resolved pointer" that knows how to extract data from the EVM.
+
+### Type
+
+A structured definition describing how to interpret bytes as meaningful data.
+Types define the semantic meaning of raw bytes, enabling debuggers to display
+values in human-readable form (e.g., showing `0x01` as `true` for a boolean).
+
+See: [Types documentation](/docs/types)
+
+### Program
+
+A representation of compiled code that maps bytecode instructions to their
+semantic context. Programs describe what code means at each instruction,
+including:
+
+- Source code ranges
+- Variables in scope
+- Frame information
+- Remarks and annotations
+
+See: [Programs documentation](/docs/programs)
+
+## EVM Data Locations
+
+### Storage
+
+Persistent, contract-specific data that survives between transactions.
+Organized as a key-value store where:
+
+- Keys are 32-byte **slots** (often computed via keccak256)
+- Values are 32-byte words
+
+Storage is the only data location that persists after execution ends.
+
+### Memory
+
+Temporary, byte-addressable scratch space available during execution. Memory:
+
+- Starts empty at the beginning of each call
+- Can be expanded dynamically (costs gas)
+- Is byte-addressed (unlike word-addressed storage)
+- Is cleared when the call ends
+
+### Stack
+
+The EVM's operand stack, used for computation. The stack:
+
+- Holds 256-bit (32-byte) values
+- Has a maximum depth of 1024 items
+- Is accessed from the top (LIFO)
+- Is ephemeral per call frame
+
+### Calldata
+
+Read-only input data provided to a transaction or call. Calldata:
+
+- Is byte-addressed
+- Cannot be modified during execution
+- Contains the function selector and encoded arguments
+
+### Returndata
+
+Data returned by the most recent external call. Returndata:
+
+- Is byte-addressed
+- Is overwritten by each subsequent call
+- Contains the return value or revert reason
+
+### Code
+
+The deployed bytecode of a contract. Code:
+
+- Is immutable after deployment
+- Can be read via CODECOPY/EXTCODECOPY
+- Contains both executable instructions and embedded data
+
+### Transient Storage
+
+Temporary storage that persists within a transaction but is cleared at the end.
+Introduced in EIP-1153, transient storage:
+
+- Has the same slot/word structure as persistent storage
+- Is cleared after each transaction
+- Is cheaper than persistent storage
+
+## Pointer Expressions
+
+### Expression
+
+A computation that produces a bytes value when evaluated against machine state.
+Expressions enable pointers to describe dynamic data locations. Common
+expression types include:
+
+- **Arithmetic**: `$sum`, `$difference`, `$product`, `$quotient`, `$remainder`
+- **Hashing**: `$keccak256` (for computing storage slots)
+- **Lookups**: `$offset`, `$length`, `$slot` (reading from defined regions)
+- **References**: `$this`, variables by name
+
+### Variable (in Pointers)
+
+A named binding that can be referenced within a pointer definition. Variables
+allow pointers to name and reuse intermediate computations:
+
+```json
+{
+ "define": { "name": "baseSlot", "location": "storage", "slot": 0 },
+ "in": {
+ "location": "storage",
+ "slot": { "$keccak256": [{ ".slot": "baseSlot" }] }
+ }
+}
+```
+
+## Program Context
+
+### Context
+
+Information associated with a bytecode instruction that describes its semantic
+meaning. Contexts can include:
+
+- **Code**: Source location
+- **Variables**: Variables entering scope
+- **Remark**: Human-readable annotation
+- **Frame**: Function/call frame identifier
+
+### Instruction
+
+A single bytecode operation at a specific program counter offset. Instructions
+combine:
+
+- **Offset**: Byte position in the bytecode
+- **Operation**: The opcode and its arguments
+- **Context**: Semantic information about what the instruction does
+
+### Variable (in Programs)
+
+A named value in the source program that can be inspected during debugging.
+Program variables have:
+
+- **Identifier**: The variable's name
+- **Declaration**: Where it was defined in source
+- **Type**: How to interpret the value
+- **Pointer**: Where to find the value at runtime
+
+## Execution Tracing
+
+### Trace
+
+A record of EVM execution showing the state at each step. Traces capture:
+
+- Program counter
+- Opcode executed
+- Stack contents
+- Memory changes
+- Storage modifications
+
+### Machine State
+
+A snapshot of all EVM state at a specific execution point. Machine state
+includes the current values of:
+
+- Stack
+- Memory
+- Storage
+- Calldata
+- Returndata
+- Program counter
+- Gas remaining
+
+## Materials
+
+### Source
+
+Original source code associated with a compiled program. Sources are referenced
+by ID and contain the raw text content.
+
+### Source Range
+
+A reference to a specific portion of source code, defined by:
+
+- **Source**: Which source file
+- **Range**: Offset and length within that file
+
+Source ranges enable mapping bytecode back to the original code that produced
+it.
diff --git a/packages/web/docs/goals.mdx b/packages/web/docs/reference/goals.mdx
similarity index 99%
rename from packages/web/docs/goals.mdx
rename to packages/web/docs/reference/goals.mdx
index 94c022037..721d1517c 100644
--- a/packages/web/docs/goals.mdx
+++ b/packages/web/docs/reference/goals.mdx
@@ -1,5 +1,5 @@
---
-sidebar_position: 2
+sidebar_position: 1
---
# Goals
diff --git a/packages/web/docs/sketches/_category_.json b/packages/web/docs/sketches/_category_.json
deleted file mode 100644
index f40aabfa9..000000000
--- a/packages/web/docs/sketches/_category_.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "label": "Prototype sketches",
- "position": 3,
- "link": {
- "type": "generated-index",
- "description": "Informally specified proposals to inform format design"
- }
-}
diff --git a/packages/web/docs/sketches/layout.mdx b/packages/web/docs/sketches/layout.mdx
deleted file mode 100644
index 9cf980945..000000000
--- a/packages/web/docs/sketches/layout.mdx
+++ /dev/null
@@ -1,496 +0,0 @@
----
-description: Initial format sketch
----
-
-# @haltman-at's allocation data draft
-
-## Status of this document
-
-This is an initial draft for review and comment. It does not have consensus and should only be cited as work in progress.
-
-## Goal of this document
-
-To present the skeleton of a format for describing layout of complex types or variables of those types (in storage or elsewhere) that is:
-
-1. Expressive enough to cover what Solidity and Vyper actually do,
-2. Simple enough to be usable, and
-3. Decently general, avoiding too much building in of Solidity and Vyper behaviors, and instead providing a way to specify those behaviors
-
-Hopefully this approximately does that! (Note that it may make assumptions based on the EVM, rather than Solidity and Vyper;
-e.g., in our discussion of endianness, we'll say that we don't need to support little-endian numbers, because the EVM makes them
-difficult; but note this is a property of the EVM, not any particular language.)
-
-This is something of a skeleton. One big problem that needs to be solved is to what extent this is applied to types vs to what
-extent it's applied to individual variables. For now this will basically assume it's applied to types. Of course, it is also necessary
-to describe the placement of individual variables, but hopefully with type layout information it's not necessary to individually describe
-their layout.
-
-So, for each type, we'll discuss what needs to be specified to specify the type itself, and then what needs to be specified to specify how it's laid out
-in each particular location.
-Also, we'll discuss how to specify locations of individual variables.
-
-What's written here might not be entirely compatible with what's in [prototype.mdx](./prototype.mdx). That will need to be hammered out.
-
-### Things this doesn't do
-
-There's one big thing that this doesn't attempt, which is arrays that are directly multidimensional; more generally it doesn't cover
-anything similar, like having arrays of structs where each struct takes up multiple words but they don't all start on word boundaries
-but rather are packed in as if it was all just primitive types. That seems to be too much complexity.
-
-There's some other weird possibilities I didn't consider, like arrays that go downward in storage instead of upward.
-
-## Specifying variable positions
-
-Of course, the overall location itself will need to be specified, which (for now) can be memory, calldata, code, the stack, or storage.
-(Coming soon: Transient storage?) For each location, further information is then needed to specify the position within the location.
-
-**Discussion**: Should position specifications include both start and end? Notionally, end is redundant if layout is specified in the
-type information. I'll just discuss start here. ("End" also potentially gets a bit messy when not everything runs the same way in
-storage.)
-
-**Discussion**: This document mentions "bytes" a lot. Should many of these mentions be "bits"? In many cases this would make no sense,
-but in some cases, it could conceptually be possible. The problem is that using bits instead of bytes is overall less convenient but
-doesn't gain much generality. But, it does gain us one important case (regarding how strings are stored in storage in Solidity),
-so we need it at least there. It seems inconsistent to use it only there and not more generally, though. So likely we should more
-often be using bits instead of bytes? Something for later.
-
-:::note
-@cameel comments here:
-
-> The string format does not really require this though. You can always look at
-> the last bit just as a part of the length field. I.e. the length is specified
-> as either 2N or 2N+1 and odd numbers indicate one format and even ones the
-> other.
-> :::
-
-### Positions in memory, calldata, or code
-
-These locations are byte-based, so here, positions can just be described as byte offsets.
-
-### Positions on the stack
-
-The stack is word-based. So positions can be described as stack slots (counted from the bottom), plus a byte within the slot
-(numbered from the little end?). Now this last part may seem unnecessary, as who would put two different variables in the
-same stack slot? Well, see below regarding internal function pointers; I think we may need this.
-
-### Positions in storage
-
-Note: This presumably will apply also to transient storage, although implementation there is yet to be seen.
-
-Sometimes multiple variables are packed into the same storage slot, so we need to specify both a storage slot and a byte within that slot (from the little end, probably).
-
-This leaves the question of specifying a storage slot -- is it sufficient to just give the slot address, or do we need to show how it was constructed? For
-top-level variables, the slot address should be enough. So if that's all we need, we don't need to say any more. But I'll cover the other case just to be sure.
-
-#### A note on endianness in storage
-
-Above speaks of the "start", but what's the "start" in storage for, e.g., an integer packed into the middle of a word? Is it the big end or the little end?
-
-Assuming any particular endianness in storage seems bad (in Solidity e.g. it's different for arrays vs bytestrings), so each type should have a storage endianness
-specified -- which does not need to agree with the endianness of its component types! It covers only the outermost layer.
-For something like an integer this is meaningless per se, but it is necessary to make sense of the "start" of that integer.
-
-:::note
-@cameel asks about this:
-
-> How do you define endianness for arrays?
-> :::
-
-#### Specifying complex storage slots (if necessary)
-
-A storage slot can be specified as one of the following objects:
-
-`{ slotType: "raw", offset: bigint }`
-
-`{ slotType: "offset", path: Slot, offset: bigint }`
-
-`{ slotType: "hashedoffset", path: Slot, offset: bigint }`
-
-:::note
-@cameel asks:
-
-> Do we need a distinction between relative and absolute locations? I.e. when
-> describing the nested layout or something like a struct you might want to
-> interpret locations as relative but then you might still want to have some
-> things interpreted as absolute (specifically the hashed locations).
-> :::
-
-```
-{
-slotType: "mapentry",
-path: Slot,
-mapType: "prefix" | "postfix" | "postfix-prehashed" | "prefix-prehashed"
-key:
-}
-```
-
-Here, prefix vs postfix means, does the key go before the map slot, or after? "Prehashed" means we hash the key separately and then hash the _result_
-together with the map slot (Vyper does this for certain types). The possibility "prefix-prehashed" isn't currently used anywhere but may as well include
-it form generality.
-
-Ideally the key might be represented as some sort of decoded value, but that seems out of scope, so let's just record the raw bytes of it, I figure.
-
-Possibly, for types that get padded before hashing, we could restrict the `key` field to be the bytes that actually represent the value, and
-correspondingly increase the set of `mapType`s to also include information about how the value is padded. Something to consider. See the section
-on specifying mappings for more discussion of this.
-
-Question: Allow offset on map entry? Don't really see a need for this.
-
-## Specifying basic types
-
-This might not need to be this complex. The suggestions in [prototype.mdx](./prototype.mdx) suggest group all these together as just primitive types
-with just `keyword`, `bitwidth`, and `alignment`. Maybe that's better? Although `alignment` should likely distinguish between zero-padding and sign-padding.
-
-### Integers
-
-Integers can be signed or unsigned and take up a specified number of bytes. No need for anything exotic here. We assume no integer type takes
-up more than a single word.
-
-`{ signed: boolean, bytes: number }`
-
-#### Specifying layout
-
-There are two things here that might need to be specified: endianness and padding. Note that since we assume no integer type takes up more than a single word,
-endianness is only a question for byte-based locations (memory, calldata, code). It's not a meaningful question for storage or the stack, as these are word-based. (However for storage layout
-information there should still be an endianness specified, even though it's technically meaningless, so that sense can be made of which end is the "start".)
-
-The EVM only really makes big-endian easy, so we probably don't need to specify endianness, and can just assume everything is big-endian. If anyone ever does
-little-endian for some reason, support for that can be added later. For now though we can ignore the distinction between bytes that are earlier and bytes that
-are more significant.
-
-That leaves padding. We can specify this as follows:
-
-`{ paddedBytes: number, paddingType: "zero" | "sign" | "right" }`
-
-:::note
-@cameel asks about this:
-
-> Does bytes include paddedBytes or not?
->
-> From the note below about "bytewidth of the unpadded type" I assume it does
-> not, but perhaps that should be said explicitly.
-> :::
-
-(Here `"zero"` means left-padded with zeroes, and `"right"` means right-padded with zeroes; `"sign"` means sign-padding.)
-
-Likely there should be some simpler way to indicate when no padding is used (`{paddingType: "none"}`?), but this will do.
-
-Note we don't include the bytewidth (or bitwidth) of the unpadded type, as that's in the type information rather than the layout information. But obviously it needs to be specified somewhere.
-
-### Fixed-point numbers
-
-These work like integers, except we also need to specify a denominator. Two possibilities:
-
-1. Add a `bigint` `denominator` field
-2. Add a `number` `base` field and a `number` `places` field
-
-Either should work.
-
-One could argue that we only need `places`, as only decimal fixed-point is implemented in any popular EVM language (Vyper), but binary fixed-point has
-also been discussed in the past, and there's little cost to being general here. If someone wants to do ternary fixed-point for some reason, sure, we can support that,
-that isn't costly to include.
-
-#### Specifying layout
-
-Same as for integers.
-
-### Short fixed-length bytestrings
-
-"Short" meaning "fits in a word and is treated as a primitive type". Probably this should be folded in with bytestrings more generally rather than treated
-separately, see below about that, but this is listed here in case we want to treat it separately.
-
-Not much to say here, just number of bytes.
-
-#### Specifying layout
-
-Same as above!
-
-### Booleans
-
-It's a boolean, nothing to say here.
-
-#### Specifying layout
-
-Same as above!
-
-### Addresses and other primitive types?
-
-Addresses are often treated as primitive? The idea of not separating out primitive types is starting to sound like a better idea. So maybe that's the thing to do, or maybe we can have the types above
-and then just have a bucket for other primitives, such as addresses.
-
-#### Specifying layout
-
-Same as above!
-
-#### A note on function pointers
-
-What about function pointers? Those are treated as a primitive type in Solidity!
-
-Well, external function pointers decompose into two parts, an address and a selector. So I think they should be treated as a complex type for our purposes here.
-Internal function pointers also decompose into two parts in non-IR Solidity.
-
-But, in IR Solidity, they don't decompose. Also, in non-IR Solidity, what do they decompose into? We might want some way to mark one of these miscellaneous primitive types
-as an internal function pointer, so that whatever's reading this format can know to treat them as that. (I don't see that we need this for external function pointers, since
-each _part_ of those is meaningful without this annotation.)
-
-:::note
-@cameel adds:
-
-> They decompose into two separate jump destinations: one into the creation
-> code, the other into the deployed code. But this is something that feels like
-> an implementation detail so not sure it has a place here.
-> :::
-
-## Specifying more complex types
-
-### Structs and tuples
-
-This can include things that may not necessarily be structs according to the language, but similarly contain a fixed number of parts and which aren't arrays.
-So, for instance, as suggested above, external function pointers could be handled here, as well as internal function pointers in non-IR Solidity (of course then the two
-components of that need to be handled some other way).
-
-Anyway, obviously, you have to specify the component types and their order.
-
-#### Specifying layout
-
-For byte-based locations: Each component needs to have its starting offset specified, but that's not enough. Each one also needs padding specified.
-You can also specify an overall length for the whole thing, which is useful for in storage specifying that it should take up a whole number of words;
-for storage this should be allowed in bytes or in words.
-
-Also, each component needs to have specified how it's stored. Based on how things are done in Solidity and Vyper, we can have several possibilities:
-
-1. It's stored inline. (This includes reference types in storage; they're not always "inline" per se
- but they're inline for our purposes.)
-2. It's stored as a pointer. In this case we'll need to specify the length of the pointer.
-3. It's stored as a relative pointer. Now, in Solidity, when relative pointers are used, they're not relative to
- the current location, they're relative to the start of the container they're inside. We can allow for both possibilities,
- probably (relative pointers aren't so exotic). And of course we need to know the length of the pointer.
-
-:::note
-@cameel adds:
-
-> In the future in Solidity also pointers to data stored in other locations
-> will be possible. Things like a storage struct nested inside a memory struct.
-> The concept of located types in the main spec already allows for that in full
-> generality.
-> :::
-
-For the stack: Overall this is similar? Structs don't live on the stack, but function pointers do. It'll be necessary here
-to use the ability to specify particular bytes within a stack slot. Alternatively, if we don't want to allow that,
-because we don't think splitting up internal function pointers is a good idea, we could allow separately specifying the padding
-in each stack slot (this is necessary to handle Solidity's external function pointers, assuming we're handling them under this).
-
-:::note
-@cameel adds:
-
-> In the future structs will be allowed to live anywhere.
-> :::
-
-For storage: We _could_ do something complicated, assuming that structs might get relocated in all sorts of weird ways,
-but this is probably not a good idea to start with. Instead we'll just assume that each struct either:
-
-1. always start on a word boundary and so is always laid out internally in the same way, so we can give the
- locations of the components relative to the start of the struct, or
-2. is no more than a single word in length and never crosses word boundaries, in which case we can give positions
- within the single word it's contained within (byte offsets relative to the start; endianness would have to be
- marked to make these meaningful).
-
-It'll probably be necessary to include an explicit tag to distinguish between these two cases. Note the second
-case is included to cover things that aren't actually structs but decompose into multiple parts.
-
-### Tagged unions
-
-These don't currently exist in Solidity or Vyper, but we should probably handle them? Pyramid had them (in that
-it was dynamically typed so everything was one).
-
-:::note
-@cameel notes:
-
-> They're planned in Solidity and may already exist in Fe. In Solidity they
-> will most likely be implemented in a form similar to Rust's enums with data.
-> Algebraic types in general will be possible in the future.
-> :::
-
-For the type, we say what it's a union of.
-
-#### Specifying layout
-
-So, we have to specify where to find the tag, and what to do in each case.
-
-For where to find the tag, we can give a start position and a length; note that for the reasons discussed below,
-we may want to allow the tag to be have start and length given in individual _bits_ rather than bytes.
-
-For each option, then, we can give a layout specification and a start point.
-
-### Union representations of non-union types
-
-So, this is a bit funky, but what if we allowed union representations of non-union types?
-
-That is, a type could indicate that in a particular location, it had a tagged union representation;
-as with tagged unions, it would be specified where to find the tag, and then there'd be an object for each case.
-But the object would specify a layout, not a type!
-
-This would allow handling Solidity storage strings. The last bit of the word would be the tag. In case 0,
-bits 1-31 are the length, and bits 32-255 are the contents. (So, we'd need to be able to specify individual
-bits here, not just bytes. Of course that's partly a concern for strings, not unions.) In case 1, bits 1-255 are
-the length, and we specify that the contents are at a hashed location. (Note that if we use the ideas below,
-we wouldn't actually specify the end of the contents, only the start.)
-
-Of course, doing this means that all _ordinary_ representations descriptions would need to have an additional
-field to specify that they're not a union. Or perhaps this information could go in a field outside the representation
-description, to avoid that?
-
-### Enumerations
-
-Maybe these are treated like primitive types? Maybe they're treated like tagged unions whose unioned types are all the unit type? In that case we'd need to be able
-to represent the unit type.
-
-:::note
-@cameel adds:
-
-> This _might_ need specifying the size in bytes. In older Solidity versions
-> enums took a variable number of bytes, depending on the number of members.
-> Now they're limited to 256 members so 1 byte
-> (https://github.com/ethereum/solidity/pull/10247). Other languages could be
-> doing it differently.
-> :::
-
-### Strings and bytestrings
-
-Type information: Is it a string or a bytestring? Is there a bound on its length? Is the bound an exact length it must be (as has been proposed for Solidity), or is it a cap (as in Vyper)?
-
-We probably don't need to bother with questions of string encodings, everything can be assumed to be UTF-8. Possibly we could have a separate type for ASCII-only strings,
-since some languages may want that as a separate type (Solidity has separate literals for with or without Unicode, though not separate types).
-We probably don't need Latin-1 strings or anything like that.
-
-#### Specifying layout
-
-For numbers, endianness was potentially a concern for byte-based locations. Here, it's not; instead it's potentially a concern for storage, since it's _not_ byte-based. Once again, though,
-the EVM makes big-endian easy and little-endian hard, so we'll just assume big-endian and not include an endianness specification.
-
-(On the other hand, Solidity does little-endian for arrays, so...?)
-
-For ones of fixed (not merely bounded) length, there's not much to specify. We're assuming big-endian, and the start is stored elsewhere. We may want
-to allow an offset in case the length is stored redundantly? Also, for storage specifically, we do have to notate whether the
-string is stored at the _actual_ specified start, or at a hashed location. So, `{ hashSlot: boolean }`.
-
-For ones of variable length, we have more work to do, as we have to specify where to find both the length and the contents.
-
-For storage, we can reasonably assume that strings have the two cases that structs do (possibly just the first but seems less clear we should assume that).
-(Actually, if we don't assume that, possibly we could fold primitive bytestrings into the fixed-length case here as well. There may be some situations that warrant
-distinguishing, but that could likely be handled by explicitly tagging the different types as different types, not representing them differently
-internally aside from the tag.)
-
-So, we can specify where to find the length, the length of the length (or that can be determined by giving the length a type?), and the start of the contents. For byte-based locations
-that suffices.
-
-However in storage, when we specify the offset, we also have to specify (for both the length and the contents separately!) whether the offset is relative
-to the current slot or to the hash of the current slot.
-
-You can also specify an overall length for the whole thing, which is useful for in storage specifying that it should take up a whole number of words;
-for storage this should be allowed in bytes or in words.
-
-Of course, Solidity famously does something more complicated with its strings, see union representation of non-union types for a possibility regarding handling that.
-
-### Mappings
-
-Have to specify key and value types, obviously.
-
-Mappings are weird and specific enough that it makes sense to build-in a lot of the behavior rather than attempting to be very general.
-
-#### Specifying layout
-
-We'll just assume all mappings use something like Solidity or Vyper's system. In this case, what needs to be specified for a given mapping is:
-
-1. Does the key go before the slot, or after?
-2. Is the key pre-hashed, like for strings in Vyper?
-3. Is the key padded at all, and if so how? I.e., to what width and with which padding type. (Notionally this padding information could go in the key type itself, adding a "key" location for this purpose. I am not assuming that
- all locations get the same type of padding because this has not always been true in all versions of Solidity.)
-
-Probably it is best to combine (1) and (2) into a `mapType` and keep (3) separate as a `paddingType`.
-
-### Arrays
-
-Note: This will exclude strings and bytestrings, handling them separately above, unlike [prototype.mdx](./prototype.mdx); another difference that will have to be figured out.
-
-We can split these into fixed-length and variable length (whether bounded or unbounded). And then you've got the base type.
-
-#### Specifying layout
-
-Oh boy, arrays. This is where it truly gets messy if we want to be general. Probably some generality will have to be
-axed here for the sake of simplicity.
-
-If the array is variable length, you need to specify the start of the length and of the contents;
-for fixed-length, only the latter (it may not be at the start as the length may be stored redundantly). You also need to specify the
-length of the length, or perhaps that can be handled by giving the length a type.
-
-In the case of storage, as is typical, this requires not only specifying an offset but also whether to hash the slot (this is separate
-for the length and for the contents!).
-
-Also, as with structs, you're going to have to specify whether the base type is stored inline, or whether it's a pointer, or whether it's
-a relative pointer and of what sort.
-
-You can once again also specify an overall length for the whole thing, which is useful for in storage specifying that it should take up a whole number of words;
-for storage this should be allowed in bytes or in words.
-
-What about padding of the elements? Well, that's the messy part... the stride pattern.
-
-See, we _could_ just specify padding for the base type (what it's padded to and with what padding type). But this wouldn't suffice to
-handle the case of how Solidity does arrays in storage! Maybe we can make this optional -- you can give a `paddedWith` and `paddingType`,
-_or_ you can use the more complicated stride pattern system.
-
-Note that for storage you will also need to specify an endianness, since storage is word-based rather than byte-based.
-Solidity does arrays little-endian! So we really do need this to be specified here. This could be specified for every
-location for consistency, but that seems unnecessary.
-
-Anyway, stride patterns. Here's a simple proposal for how a stride pattern might be represented.
-
-A stride pattern will be an array of objects, each of which is one of the following: `{ type: "element" }`, `{ type: "zero", length: number }`, or `{ type: "sign", length: number }`.
-
-A stride pattern is interpreted as follows: `"element"` means an element goes here, of its appropriate length (no padding). The `"zero"` type means this many bytes of zeroes.
-And (this isn't currently necessary, but) `"sign"` will mean this many bytes of sign-padding, where the thing it's sign-padding is determined from context
-(in big-endian contexts, it's the next thing; in little-endian contexts, the previous thing). The stride pattern is implicitly periodic; the number of `"element"` entries is not
-supposed to match that of the array, rather, when you get to the end of the stride pattern you go back to the start.
-
-In a byte-based location, this means what it sounds like. In storage, you have to read according to the endianness that was specified. Note it's assumed that no element
-that fits in a word will cross a word boundary, and that you won't use `"sign"` in places it doesn't make sense, that you won't have structs that are supposed to start
-on a word boundary start elsewhere, etc.
-
-In addition to the stride pattern, you can separately specify padding for the array as a whole (useful for making clear that it should take up a whole number of words).
-
-Solidity examples:
-
-- `uint256[]` -- it takes up the whole word, so the pattern is `[{ type: "element" }]`
-- `uint128[]` -- there's two of them, so `[{ type: "element" }, { type: "element" }]`
-- `uint96[]` -- there's two of them and then 64 bytes of padding, so `[{ type: "element" }, { type: "element" }, { type: "zero", length: 64 }]`
-- `uint96[3][]` -- a `uint96[3]` takes up two full words always, so just `[{ type: "element" }]` suffices; what goes on inside the `uint96[3]` can be handled inside there
-- `uint96[3]` -- the stride pattern is `[{ type: "element" }, { type: "element" }, { type: "zero", length: 64 }]` as above, but now we should _also_ specify that the array as
- a whole has an overall length of two words, so that in a `uint96[3][]`, there's no confusion about the fact that each one should start on a fresh word boundary.
- (Not that it would be legal to start it anywhere else, but it should still be explicitly specified, not left as error-recovery behavior.)
-
-#### Things probably not to include for now
-
-Probably don't attempt to handle arrays that are directly multidimensional (as opposed to
-multidimensional arrays just being ordinary arrays of arrays). Allowing this also raises possibility
-of a flag for row-major vs column-major order. Probably best to just exclude this for now.
-
-:::note
-@haltman-at notes in a comment (after writing this)
-
-> Oh, geez, I just realized there's something big I left out: How things are
-> pointed to on the stack. Actually, one could perhaps speak of cross-location
-> pointers in general, but as that doesn't exist mostly at the moment, probably
-> no sense in including that; it's premature.
->
-> But, I guess something that needs to be added is, for each type, for the
-> stack location, I talked about from/to but really we also need to say, does
-> this thing live directly on the stack or is it pointed to. And if it's
-> pointed to, we need to specify the pointer format -- do we just point to the
-> start, or do we have start/length? And then if it's start/length we need to
-> break down which part is the start and which part is the length... also, for
-> length, we likely want to be able to specify what the length is measured in
-> -- for instance it could potentially be `"bytes"` or `"words"` or `"items"`.
->
-> (Yes this should be added to the PR itself but I don't have a lot of time at
-> the moment)
-> :::
diff --git a/packages/web/docs/sketches/prototype.mdx b/packages/web/docs/sketches/prototype.mdx
deleted file mode 100644
index 789fb9059..000000000
--- a/packages/web/docs/sketches/prototype.mdx
+++ /dev/null
@@ -1,390 +0,0 @@
----
-description: Initial format sketch
----
-
-import TOCInline from "@theme/TOCInline";
-
-# @jtoman's format prototype
-
-
-
-## Status of this document
-
-This is an initial draft for review and comment. It does not have consensus and should only be cited as work in progress.
-
-## Scope of this Document
-
-This document proposes a "general" shape of the ultimate debugging format to be decided upon
-by the ethdebug working group. As such, it does not aim to be a complete formal specification
-of a JSON format that is expected to cover every single case. Rather, it aims to provide a basis
-from which a fully formal specification will be developed based on discussions generated around
-this document.
-
-Under this vague scope, it is worth noting some non-goals. This document describes a debug
-format for EVM bytecode; support for other VMs is at least initially out of scope. In addition,
-this proposal is necessarily geared towards the state of the Solidity/Vyper languages as they
-exist now. It cannot (and will not) account for any possible future changes to the source language
-(Solidity, Vyper, etc.); rather, any significant changes to source languages/target VMs
-that require fundamental extensions to this format should be developed as needed and gated
-with a versioning scheme.
-
-## Goals of this Format
-
-Before describing the format, it is useful to lay out the information that this initial proposal is
-attempting to provide to end-users of the format. Extensions to this format to support other use
-cases not explicitly identified here are expected.
-
-### Local Variable Information
-
-Currently decompilers and formal methods tools must use internally generated names to give names to
-the values stored on the stack. The debugging format should provide information about what stack slots correspond
-to which source level identifiers.
-
-### Type Information
-
-The EVM has one "type": a 256-bit word. Source languages that compile to the EVM have richer type information
-which can aid in debugging and fuzzing; for example, the counterexample generation used by the Certora prover
-could use type information to pretty print values according to their high-level type.
-
-### Jump Resolution
-
-The EVM allows jumping to arbitrary values on the stack (subject to the restriction that the destination has a corresponding JUMPDEST opcode).
-This makes construction of a static control flow graph challenging (albeit not impossible). The format should provide reasonable hints
-about possible targets of jump commands.
-
-### Internal Function Calls
-
-The EVM has no built-in concept of internal functions. Rather, Solidity internal function implementations are placed at some offset in the
-contract's bytecode, and callers jump to this predetermined location, passing arguments on the stack along with the return location (this is
-one possible complication when attempting to statically resolve jump destinations).
-
-Statically detecting these internal calls and informing an end-user can be surprisingly complicated.
-For example, the Solidity compiler will in some cases perform a "tail-call" optimization: for nested calls like `f(g(x))`
-the compiler will push the entry point of `f` as the return address for the call to `g`. The format should
-help explicitly identify the targets of internal function calls and what arguments are being passed on the stack.
-
-### Mapping key identification
-
-EVM languages commonly include non-enumerable mappings. As such, it is useful to be able to dynamically identify any mapping keys that may appear
-while analyzing a transaction trace or debugging.
-
-## The Format
-
-The format will be JSON so that it may be included in the standard input/output APIs that the Vyper and Solidity compilers support.
-
-### Top Level
-
-The "top-level" artifact of the debug format will be a JSON dictionary with (at least) the following fields:
-
-- `version`: A representation of the major/minor version of the format. The actual representation of this version (a string, an array, etc.) can be decided later.
-- `types`: An array describing the layout of user-defined types defined in contracts referenced during compilation (see below).
-- `bytecode`: Debug information about the bytecode output by the compiler.
-
-### Type Descriptions
-
-When describing user defined types in contracts or describing the types of values on the stack, the format
-will use `type descriptors` to describe the type in question. There is one type descriptor per type in the
-source language. Each descriptor is a JSON object with at least the following fields:
-
-- `id`: a unique numeric id. This may be referenced by type descriptors for aggregate types (arrays, structs, etc.)
-- `sort`: A string representing the sort of the type. Possible values include:
- - `"mapping"` for a dynamic mapping from a key type to a value type
- - `"primitive"` built in primitive type
- - `"array"` for a homogeneous dynamic array of bounded/unbounded size
- - `"static_array"` for homogeneous static arrays
- - `"struct"` for user defined aggregate struct types
- - `"enum"` user defined enumeration types
- - `"contract"` a refinement of an address primitive with information about the contract deployed at the address
- - `"alias"` a user defined alias for some type
- - `"located"` a reference to another type with a data location attached
-- `label`: a (not necessarily human-readable) string representation of the type. Expected to be used for debugging
-
-Depending on the value of `sort` the type descriptor will have additional fields.
-
-**Discussion** The types here do _not_ include events or errors. These can be described elsewhere in the format,
-and indeed, they will likely reference the types defined here. However, as events and errors are not currently
-first class in any language targeting the EVM that I'm aware of (i.e., you cannot declare a variable `x` to be of
-type `error Foo()`) they should be described elsewhere.
-
-**Notes**: some preference was expressed for `kind` over `sort`. In addition, it was suggested we use `pointer` or `reference` over `located`.
-
-#### Mappings
-
-The type descriptor for a mapping type has the following additional fields defined.
-
-- `keyType`: contains the `id` of the type that is the domain of the mapping.
-- `valueType`: contains the `id` of the type that is the codomain of the mapping.
-
-#### Primitives
-
-The type descriptor for a primitive has the following additional fields:
-
-- `keyword`: the source keyword for the type. Examples include `uint256`, `boolean` etc.
-- `bitwidth`: the maximum number of bits a value of this type may occupy
-- `alignment`: one of `high` / `low`, indicating if the bits occur in the most significant bits (`high`) or least significant bits (`low`) of 256-bit EVM word.
-
-**Discussion**: The bitwidth field is an initial attempt to come up with some language agnostic way to
-describe primitive types. It is expected that further fields may be added, or perhaps the Primitive sort
-should be split up into more specific units, like `Integral` and `Real` etc.
-
-#### Array
-
-The type descriptor for an array is further subdivided depending on whether the array
-is a bytes array or any other array. It has at least the following fields:
-
-- `arraySort`: either the string `"bytes"` or `"generic"` (names not final).
-- `bound`: a field indicating the statically known upper bound on the size of this array (for Vyper). If null the array is unbounded.
-
-If `arraySort` is `"bytes"` then the descriptor has the following field:
-
-- `keyword`: the keyword used to declare this type, to account for `string` vs `bytes`
-
-If the `arraySort` is `"generic"` then descriptor has the following field:
-
-- `elementType`: a numeric id that references the type of values held in each element of the array.
-
-**Discussion**: Here, as elsewhere, no attempt is made here in the type descriptors to describe the physical representation
-of the type. Short of some semi-turing complete DSL, there doesn't seem to be a compact way
-to describe declaratively the packed storage representation of strings in storage for example.
-
-#### Static Arrays
-
-The type descriptor for a static array has the following additional fields:
-
-- `size`: the static, pre-declared size of the fixed size array/list
-- `elementType`: a numeric id that references the type of values held in each element of the array.
-
-#### Struct
-
-This format assumes that all struct types are user defined types and thus have a declaration site.
-The type descriptor for a struct has the following addition fields:
-
-- `declaration`: A dictionary describing the definition site of the struct, see below.
-- `fields`: An ordered list of dictionaries describing the fields of the struct.
-- `name`: The name of the struct without the `struct` keyword and without contract qualifiers.
-
-The order of the elements in `fields` is significant, and should match the order that fields are declared in the source file.
-
-Each element of the `fields` array is a dictionary with the following fields:
-
-- `name`: the name of the field
-- `type`: the numeric id of the type held in this field
-
-#### Enums
-
-As with structs, this format assumes that all enumeration types are user defined. The descriptor for an enum contains the following fields:
-
-- `declaration`: A dictionary describing the definition site of the enum, see elow.
-- `name`: the name of the enum, without the `enum` keyword and without any contract qualifiers.
-- `members`: A list of members of the enum, as strings.
-
-The order of elements within `members` is significant, and should match the order that members of the enum are declared in the source file.
-
-#### Contracts
-
-The contract type refers to a primitive value that is known/expected to be an address of a contract deployed on the blockchain
-which implements the given type. It contains the following field:
-
-- `contractDeclaration`: The AST id of the declaration of the contract type.
-- `name`: A string holding the (fully qualified) name of the contract type.
-
-**Discussion** It is unclear to me whether this should actually be separate from primitives. I lean towards no, but it is presented this
-way to prompt discussion. Note that this format assumes that the declaration of the contract type is "visible" to the compiler
-during compilation and thus the declaration site is available for reference.
-
-#### Aliases
-
-As with enums and structs, this format assumes that all aliases are user defined, but this restriction could be relaxed by making the `definitionScope` field optional.
-An alias type descriptor has the following additional fields:
-
-- `aliasName`: The user provided name of the alias type, without qualifiers.
-- `definitionScope`: A dictionary describing the site of the definition, see below
-- `aliasedType`: The numeric id of the type for which this is an alias.
-
-**Discussion**: This could be extended with information such as "is this alias opaque" a la private types in OCaml.
-
-#### Located Types
-
-A "located" type is simply a type that is additionally qualified with a data location, that is, a refinement on some other type to restrict its location.
-A located type has the following fields defined:
-
-- `location`: A string describing EVM data locations. Possible values are `"memory"`, `"storage"`, `"calldata"`, `"returndata"`, `"code"`.
-- `type`: The numeric ID of the type with this location.
-
-It is expected that the type referenced in `type` is not itself a located type, as this would indicate a type like `uint[] calldata memory` which is not
-valid and is never expected to be.
-
-**Discussion**: The lack of a `stack` or `default` location is intentional, but can be added if needed. The choice to separate the location from rest of
-the type was to avoid multiple descriptors for a struct depending on where that struct is located. Under this design, there is a single definition for the
-shape of the struct, and the different data locations of that struct are handled by located type descriptors.
-
-#### Definition Scopes
-
-To provide information about where a user defined type was declared, the descriptors for those type include a `definitionScope` field.
-This field is a dictionary with the following fields:
-
-- `definitionScope`: A dictionary describing where the type is defined. It has at least the following fields
- - `sort`: a string, either `"file"` indicating a top-level declaration or `"contract"` indicating a type defined within a contract
-- `name`: The string representation of the type name. For struct types this is the name of the struct, and does _not_ include the `struct` keyword, and similarly for enums.
-
-The `definitionScope` dictionary has additional fields depending on the value of `sort`. If it is `"contract"`
-then it has the following field:
-
-- `definingContract`: A dictionary with the following fields:
- - `name`: the source name of the defining contract
- - `astId`: the numeric AST id of the declaration which holds this definition
-
-If the field is `"file"`, then it instead has:
-
-- `definingFile`: A dictionary with the following fields:
- - `name`: The path to the file (John: Fully resolved path? The path as understood by the compiler?)
-
-It is expected that the combination of `definitionScope` and `name` is unique within the `types` array
-(otherwise we would have multiple declarations in the same scope).
-
-#### Unresolved Questions
-
-What about generics? Do we want to try to describe their format before any implementation is ready?
-
-### Bytecode Debug Information
-
-The debug information for the bytecode is a dictionary of bytecode offsets to debug information. It is **not**
-required that every opcode in the bytecode has a corresponding entry in the debug dictionary. Implementers
-are encouraged, however, to have as much coverage as possible. Each entry in the debug information dictionary
-is itself a dictionary that (optionally) includes some of the following:
-
-- The source location(s) that "correspond" to the opcode
-- The AST ID(s) that "correspond" to the opcode
-- The layout of the stack, including type information and local variable names (if available)
-- Jump target information (if available/applicable)
-- Identification of mapping key information
-
-In the above "correspond" roughly means "what source code caused the generation of this opcode".
-
-Specifically the dictionary may have the following fields:
-
-- `source`: a list of source location specifiers. The format of these source location specifiers should be decided later. Every element should provide the location of the textual source code
- that contributed to the generation of this opcode.
-- `ast`: A list of AST ids for the "closest" AST node that contributed to the generation of this opcode.
-- `stack` A layout of the stack as understood by the compiler, represented as a list.
-- `jumps`: If present, provides hints about the location being jumped to by a jumping command (JUMP or JUMPI)
-- `mappings`: If present, contains information about how the opcode relates to mapping keys.
-
-#### Source Locations
-
-The choice of which source location should be attached to each opcode is likely an inexact science. However, implementers are encouraged to be as exact as possible: while it
-is technically correct to give the entirety of the contract file as the "source" of every opcode, this is not a useful result. Consumers of this information should also take care
-to assume that source code operations may map to (surprising) AST ids. For example, an optimizing compiler may tag a `PUSH` of a constant `16` with the AST id of the following expression
-`(5 + 11)`. An even more aggressive optimizing compiler could even tag the same push with the AST ids of the literals `5` and `11` in the following `(5 + x) + 11`.
-
-#### Stack Information
-
-Given internal function calls, the format will not (and cannot) represent the entire stack at every point during execution; a program can be reached at many different stack depths.
-However, it is expected that all compilers will have a view of some "prefix" of the stack at each program point analogous to an activation frame in low-level assembly code.
-The list contained in the `stack` field exposes this view; consumers can combine this information with the `jumps` information to build a complete representation of the stack.
-
-The list is ordered such that the first element provides information about the top of the stack, the second element is the next element below it, and so on. Each element is a dictionary
-with the following fields:
-
-- `type`: The type of the value stored in this stack slot. This is _not_ a reference to a type descriptor or an embedding of the type descriptor, see below.
-- `sourceName`: A nullable string representation of the identifier held in this stack slot. A value of null indicates that the value does not come from any single identifier.
-- `sourceId`: A nullable numerical AST id that holds the definition (John: declaration?) of the identifier held in this stack slot. A value of null indicates the value does not come from
- any single identifier.
-
-Note that due to `dup` commands, multiple stack locations may hold the same variable name. If a compiler knows that a stack slot that holds
-a variable will be later overwritten with a new value, it should mark the to be overwritten value with the "junk" type (see below).
-
-The `type` dictionary provides information about the value stored in the stack slot. The types used here are a superset of the types described by type descriptors.
-
-The `type` dictionary has the following field:
-
-- `sort`: A string indicating the sort of value stored in the stack slot, drawn from one of the following values:
- - `"junk"` indicates a value that is dead or about to be popped.
- - `"pc"` A refinement of the numeric type, indicating the slot holds a location which is a jump destination target
- - `"program"` The stack slot holds a value with a "program" type, i.e., one that can be expressed using type descriptors.
- - `"internal"` Indicates that the stack slot holds a value that is being used by the compiler but does not correspond to a user type.
-
-The dictionaries for `pc` and `junk` sorts do not have any additional information. The `internal` type is to be used for, e.g., "scratch" pointers that are used to
-marshal calldata buffers or hash storage keys. Compilers may insert their own information into the `internal` dictionary but this format remains intentionally agnostic
-on these contents. (John: every time a standard has allowed a "vendor specific" extension, it goes badly. Maybe we want to just say, consumers shouldn't look at this field)
-
-If the `sort` is `"program"` then the dictionary has the following field:
-
-- `typeId`: The numeric ID of the type held in this slot
-
-Additionally, the compiler may insert a field to provide additional information about the representation on the stack. This field, if present, has the name `representation` and holds a dictionary.
-This dictionary has the following optional fields:
-
-- `published`: A boolean field which, if present, indicates that this stack slot holds a pointer to some location in memory/storage. Further, if the field is true, then the object is "fully initialized" (the formal definition of
- fully initialized is to be decided on later)
-- `componentOf`: If the representation of a single value spans multiple stack slots, this field provides information about how the value is spread across the stack. It is a dictionary with the following fields:
- - `id`: an ID unique within each stack list. All stack slots with the same value of `id` are considered to represent the same logical value. It is allowed to re-use the same ID in different entries of the `stack` list.
- - `componentName`: The name of the component. The only known use case for this is the decomposition of calldata arrays, so there are two possible values `"ELEM_PTR"` and `"LENGTH"` indicating the stack slots hold the pointer to the calldata location of the array's elements or the logical length of the array respectively.
-
-#### Jumps
-
-For jumping commands, the `jumps` field provides information about the expected target of the jump, and information about the internal function stack.
-
-The value of the `jumps` field is a dictionary with the following (potentially optional) fields:
-
-- `targets`: if present, a list of known PCs to which this command may jump. For JUMPI, this does **not** include the fallthrough case, as this is readily computable. This list may be non-singleton due to,
- e.g., function pointers, but the compiler is able to restrict the potential callees.
-- `sort`: A string indicating the type of jump being performed. One of the following values:
- - `"return"`: Used for a jump out of an internal function
- - `"call"`: Used for a jump into an internal function
- - `"normal"`: Used for all other jumps
-
-**Discussion**: It may be useful to ask compilers to provide richer information about some jumps. For example, tagging a loop exit as a "break" or a backjump as a "continue". This may be redundant given sufficiently
-reliable source information however.
-
-As elsewhere, the dictionary may contain additional fields depending on the value in `sort`.
-
-If the value is `"call"`, then the dictionary contains the following fields:
-
-- `arguments`: A list describing the calling convention. As in the `stack` layout, the first element of this list describes the value on the top of the stack (**after** popping the jump destination). Each element is a
- dictionary described below.
-
-If the callee of the call is known, then the dictionary with sort `"call"` has the following field:
-
-- `callee`: a dictionary with the following fields:
- - `target`: a human readable string name for the function being called
- - `astId`: the AST id of the declaration site of the callee
-
-Note that if the function being called is `virtual` then the declaration site may not have any corresponding body.
-
-Each element of the `arguments` array is a dictionary with the following fields:
-
-- `sort`: `"program"` or `"return_address"`. `"program"` has the same interpretation as in the `type` dictionary above. `"return_address"` is a refinement of the `pc` type indicating this stack slot holds
- the return address of the call being performed.
-- `position`: The logical position of the **parameter** represented by this stack value. The ordering of parameters is defined by their program declaration order, where the first formal parameter to a function has position `0`,
- the next `1`, etc. As with the stack, a single logical argument can be spread across multiple stack slots. If multiple entries share the same `position` value, then those arguments
- should have a `representation` field that has a `componentOf` entry.
-
-**Note**
-Due to named arguments, the order given in the debug information may not match the order of parameters as they appear at a call-site. For example, given a declaration:
-
-```
-function myFunction(uint a, uint b) ...
-```
-
-and an invocation:
-
-```
-myFunction(b = 3, a = 4)
-```
-
-the stack location which contains the `4` argument value will be tagged with position `0`, as that is the position of parameter `a` in the declaration.
-
-If the value of `sort` is `"return"`, then the dictionary has the following field:
-
-- `returns`: A list of dictionaries with the same format as the `arguments` array of `call`, but without any `return_address` entries.
-
-**Discussion**: The above proposal doesn't really handle the case of "tail-calls" identified at the beginning of this document, where multiple return addresses can be pushed onto the stack. Is that something the debug format must explicitly model?
-
-#### Mapping key identification
-
-The value of this field (when present) is a dictionary with (some of) the following fields:
-
-- `isMappingHash`: A boolean that identifies whether the opcode is computing a hash for a mapping.
-- `isMappingPreHash`: For mappings that use two hashes, this boolean can identify whether the opcode is computing the first of the two hashes. Possibly this field should be combined with a previous one into some sort of enum?
-- `mappingHashFormat`: An enumeration; specifies the format of what gets hashed for the mapping. Formats could include "prefix" (for Solidity), "postfix" (for Vyper value types), and "postfix-prehashed" (for Vyper strings and bytestrings). Possibly "prefix" could be split further into "prefix-padded" (for Solidity value types) and "prefix-unpadded" (for Solidity strings and bytestrings). This could be expanded in the future if necessary. (Also, potentially `"prefix-padded"`, if split out, could be broken down even further, by padding type -- zero padding (left) vs sign-padding vs zero-padding (right)...)
diff --git a/packages/web/docs/types/_category_.json b/packages/web/docs/types/_category_.json
new file mode 100644
index 000000000..8b257445f
--- /dev/null
+++ b/packages/web/docs/types/_category_.json
@@ -0,0 +1,4 @@
+{
+ "label": "Types",
+ "position": 4
+}
diff --git a/packages/web/docs/types/composite.mdx b/packages/web/docs/types/composite.mdx
new file mode 100644
index 000000000..b46a1a3e3
--- /dev/null
+++ b/packages/web/docs/types/composite.mdx
@@ -0,0 +1,207 @@
+---
+sidebar_position: 3
+---
+
+# Composite types
+
+Composite types (also called complex types) contain other types. They use the
+`contains` field to express this composition.
+
+## Arrays
+
+Arrays represent ordered collections of a single element type.
+
+### Dynamic arrays
+
+```json
+{
+ "kind": "array",
+ "contains": {
+ "type": { "kind": "uint", "bits": 256 }
+ }
+}
+```
+
+This represents `uint256[]`—an array of any length.
+
+### Fixed-size arrays
+
+```json
+{
+ "kind": "array",
+ "count": 10,
+ "contains": {
+ "type": { "kind": "address" }
+ }
+}
+```
+
+This represents `address[10]`—exactly 10 addresses.
+
+## Structs
+
+Structs group named members of potentially different types:
+
+```json
+{
+ "kind": "struct",
+ "definition": {
+ "name": "User"
+ },
+ "contains": [
+ {
+ "name": "balance",
+ "type": { "kind": "uint", "bits": 256 }
+ },
+ {
+ "name": "owner",
+ "type": { "kind": "address" }
+ },
+ {
+ "name": "active",
+ "type": { "kind": "bool" }
+ }
+ ]
+}
+```
+
+The `contains` field is an ordered list. Each element includes:
+
+- `name`: The member's identifier
+- `type`: The member's type (inline or by reference)
+
+## Mappings
+
+Mappings represent key-value associations:
+
+```json
+{
+ "kind": "mapping",
+ "contains": {
+ "key": {
+ "type": { "kind": "address" }
+ },
+ "value": {
+ "type": { "kind": "uint", "bits": 256 }
+ }
+ }
+}
+```
+
+This represents `mapping(address => uint256)`.
+
+### Nested mappings
+
+Mappings can nest by using another mapping as the value type:
+
+```json
+{
+ "kind": "mapping",
+ "contains": {
+ "key": {
+ "type": { "kind": "address" }
+ },
+ "value": {
+ "type": {
+ "kind": "mapping",
+ "contains": {
+ "key": { "type": { "kind": "address" } },
+ "value": { "type": { "kind": "uint", "bits": 256 } }
+ }
+ }
+ }
+ }
+}
+```
+
+This represents `mapping(address => mapping(address => uint256))`, commonly
+used for ERC-20 allowances.
+
+## Tuples
+
+Tuples represent ordered, unnamed sequences:
+
+```json
+{
+ "kind": "tuple",
+ "contains": [
+ { "type": { "kind": "uint", "bits": 256 } },
+ { "type": { "kind": "address" } },
+ { "type": { "kind": "bool" } }
+ ]
+}
+```
+
+Unlike structs, tuple elements don't have names. Tuples commonly appear in
+function return types and event parameters.
+
+## Type aliases
+
+Aliases give names to other types:
+
+```json
+{
+ "kind": "alias",
+ "definition": {
+ "name": "TokenId"
+ },
+ "contains": {
+ "type": { "kind": "uint", "bits": 256 }
+ }
+}
+```
+
+This represents a user-defined type like Solidity's `type TokenId is uint256`.
+
+## Function types
+
+Function types represent callable references:
+
+```json
+{
+ "kind": "function",
+ "contains": {
+ "parameters": {
+ "type": {
+ "kind": "tuple",
+ "contains": [
+ { "type": { "kind": "address" } },
+ { "type": { "kind": "uint", "bits": 256 } }
+ ]
+ }
+ },
+ "returns": {
+ "type": { "kind": "bool" }
+ }
+ }
+}
+```
+
+## Using type references
+
+For deeply nested or repeated types, use references to avoid duplication:
+
+```json
+{
+ "kind": "struct",
+ "definition": { "name": "Order" },
+ "contains": [
+ {
+ "name": "maker",
+ "type": { "id": "user-type-id" }
+ },
+ {
+ "name": "taker",
+ "type": { "id": "user-type-id" }
+ }
+ ]
+}
+```
+
+Both `maker` and `taker` reference the same `User` type by ID rather than
+repeating the full definition.
+
+## Learn more
+
+For complete schema definitions, see the
+[complex types specification](/spec/category/complex-types).
diff --git a/packages/web/docs/types/elementary.mdx b/packages/web/docs/types/elementary.mdx
new file mode 100644
index 000000000..7bf14c0d3
--- /dev/null
+++ b/packages/web/docs/types/elementary.mdx
@@ -0,0 +1,154 @@
+---
+sidebar_position: 2
+---
+
+# Elementary types
+
+Elementary types are atomic—they don't contain other types. These form the
+building blocks that complex types compose.
+
+## Numeric types
+
+### Unsigned integers (`uint`)
+
+Unsigned integers range from 8 to 256 bits, in increments of 8:
+
+```json
+{ "kind": "uint", "bits": 256 }
+```
+
+```json
+{ "kind": "uint", "bits": 8 }
+```
+
+The `bits` field is required and must be a multiple of 8 between 8 and 256.
+
+### Signed integers (`int`)
+
+Signed integers use two's complement representation:
+
+```json
+{ "kind": "int", "bits": 256 }
+```
+
+Like `uint`, the `bits` field must be a multiple of 8 between 8 and 256.
+
+### Fixed-point numbers (`ufixed`, `fixed`)
+
+Fixed-point decimals specify both total bits and decimal places:
+
+```json
+{ "kind": "ufixed", "bits": 128, "places": 18 }
+```
+
+```json
+{ "kind": "fixed", "bits": 128, "places": 18 }
+```
+
+## Address type
+
+Addresses represent 20-byte Ethereum addresses:
+
+```json
+{ "kind": "address" }
+```
+
+In Solidity, `address payable` is a distinct type but uses the same
+representation—the distinction is semantic rather than structural.
+
+## Boolean type
+
+Booleans represent true/false values:
+
+```json
+{ "kind": "bool" }
+```
+
+In the EVM, booleans occupy a full 32-byte word where 0 is false and any
+non-zero value is true.
+
+## Byte types
+
+### Fixed-size bytes (`bytes`)
+
+Fixed-size byte arrays range from 1 to 32 bytes:
+
+```json
+{ "kind": "bytes", "size": 32 }
+```
+
+```json
+{ "kind": "bytes", "size": 4 }
+```
+
+The `size` field is required for fixed-size bytes.
+
+### Dynamic bytes (`bytes`)
+
+Dynamic byte arrays have no size limit:
+
+```json
+{ "kind": "bytes" }
+```
+
+When `size` is omitted, the type represents dynamically-sized bytes.
+
+## String type
+
+Strings represent UTF-8 encoded text:
+
+```json
+{ "kind": "string" }
+```
+
+Strings are dynamically sized. Languages that treat strings as character
+arrays may choose to represent them as `array` types instead.
+
+## Enum type
+
+Enums represent a fixed set of named values:
+
+```json
+{
+ "kind": "enum",
+ "definition": {
+ "name": "Status"
+ },
+ "values": ["Pending", "Active", "Completed"]
+}
+```
+
+The `values` field lists all possible values in order. The underlying
+representation is typically a `uint8` (or larger if needed).
+
+## User-defined types with definitions
+
+Some elementary types (particularly enums) are defined in source code. These
+include a `definition` field:
+
+```json
+{
+ "kind": "enum",
+ "definition": {
+ "name": "OrderStatus",
+ "source": {
+ "id": 42,
+ "range": {
+ "start": { "line": 10, "column": 0 },
+ "end": { "line": 15, "column": 1 }
+ }
+ }
+ },
+ "values": ["Created", "Filled", "Cancelled"]
+}
+```
+
+The `definition` field can include:
+
+- `name`: The identifier used in source code
+- `source`: A reference to where the type is defined
+
+## Learn more
+
+For complete schema definitions and all available fields, see the
+[elementary types specification](/spec/category/elementary-types).
diff --git a/packages/web/docs/types/index.mdx b/packages/web/docs/types/index.mdx
new file mode 100644
index 000000000..b6d725bff
--- /dev/null
+++ b/packages/web/docs/types/index.mdx
@@ -0,0 +1,185 @@
+---
+sidebar_position: 1
+---
+
+# Types
+
+Types in **ethdebug/format** describe the structure and interpretation of raw
+bytes. They tell a debugger how to decode values from EVM state into
+human-readable representations.
+
+## Why types matter
+
+When a debugger reads bytes from storage, memory, or the stack, those bytes
+are meaningless without context. A 32-byte value could be:
+
+- A `uint256` representing a token balance
+- An `address` padded to 32 bytes
+- Part of a `string` or `bytes` array
+- A storage slot containing packed struct members
+
+Type information bridges this gap. It tells the debugger exactly how to
+interpret the raw bytes and present them to developers in a meaningful way.
+
+## Core ideas
+
+### Types are organized by kind
+
+Every type representation is a JSON object with a `kind` field:
+
+```json
+{
+ "kind": "uint",
+ "bits": 256
+}
+```
+
+The `kind` field determines which schema applies. Common kinds include:
+
+- **Elementary types**: `uint`, `int`, `bool`, `address`, `bytes`, `string`,
+ `enum`, `ufixed`, `fixed`
+- **Complex types**: `array`, `struct`, `mapping`, `tuple`, `alias`, `function`
+
+### Elementary vs. complex
+
+Types fall into two classes:
+
+- **Elementary types** stand alone—they don't contain other types. Examples:
+ `uint256`, `address`, `bool`
+- **Complex types** compose other types. A `uint256[]` array contains a
+ `uint256`. A `mapping(address => uint256)` contains both `address` and
+ `uint256`.
+
+### Complex types use `contains`
+
+Complex types express their composition through a `contains` field. This field
+can take three forms:
+
+**Single type** (arrays, aliases):
+
+```json
+{
+ "kind": "array",
+ "contains": {
+ "type": { "kind": "uint", "bits": 256 }
+ }
+}
+```
+
+**Ordered list** (structs, tuples):
+
+```json
+{
+ "kind": "struct",
+ "contains": [
+ { "name": "balance", "type": { "kind": "uint", "bits": 256 } },
+ { "name": "owner", "type": { "kind": "address" } }
+ ]
+}
+```
+
+**Object mapping** (mappings):
+
+```json
+{
+ "kind": "mapping",
+ "contains": {
+ "key": { "type": { "kind": "address" } },
+ "value": { "type": { "kind": "uint", "bits": 256 } }
+ }
+}
+```
+
+### Type references avoid duplication
+
+Instead of repeating a full type definition everywhere it's used, you can
+reference types by ID:
+
+```json
+{
+ "kind": "array",
+ "contains": {
+ "type": { "id": "some-opaque-type-id" }
+ }
+}
+```
+
+This keeps debugging information compact and allows types to reference each
+other (useful for recursive structures).
+
+## What's next
+
+
+
+
+
+
Elementary types
+
+
+
Integers, addresses, booleans, and other atomic types.
+
+
+
+
+
+
+
+
Composite types
+
+
+
Arrays, structs, mappings, and types that contain other types.
+
+
+
+
+
+
+
+
+
+
+
Representation
+
+
+
How types map to bytes in storage vs. memory encoding contexts.
+
+
+
+
+
+
+
+
Full specification
+
+
+
Complete JSON schemas and detailed reference.
+
+
+
+
+
diff --git a/packages/web/docs/types/representation.mdx b/packages/web/docs/types/representation.mdx
new file mode 100644
index 000000000..0cbf71255
--- /dev/null
+++ b/packages/web/docs/types/representation.mdx
@@ -0,0 +1,190 @@
+---
+sidebar_position: 4
+---
+
+# Representation
+
+Types in ethdebug/format describe _what_ data is, but compilers must also
+encode _how_ that data is stored as bytes. The same logical type can have
+different byte representations depending on context.
+
+## Encoding contexts
+
+The EVM uses different encoding rules depending on where data lives:
+
+### Storage encoding
+
+Storage packs values tightly to minimize slot usage. Multiple small values
+share a single 32-byte slot:
+
+```
+Slot 0: |-------- uint128 a --------|-------- uint128 b --------|
+ 16 bytes 16 bytes
+```
+
+A `uint128` needs only 16 bytes, so two fit in one slot.
+
+### Memory and calldata encoding
+
+Memory and calldata use ABI encoding rules. Each value occupies a full 32-byte
+word, padded as needed:
+
+```
+Memory: |---------------- uint128 a (padded) ----------------|
+ 32 bytes (16 bytes value + 16 bytes zero padding)
+```
+
+### Why this matters for debugging
+
+A debugger reading a `uint128` needs to know:
+
+- In storage: read 16 bytes from the correct offset within a slot
+- In memory: read 32 bytes, then interpret only the relevant portion
+
+The pointer specifies _where_, the type specifies _what_, and encoding context
+determines _how_ to interpret the bytes.
+
+## Byte ordering
+
+The EVM is big-endian for most purposes:
+
+- **Integers** are stored with the most significant byte first
+- **Addresses** occupy 20 bytes, left-padded with zeros in 32-byte contexts
+- **Fixed-size bytes** (`bytes1` through `bytes32`) are right-padded
+
+For a `uint256` value of `0x1234`:
+
+```
+Storage/Memory: 0x0000...001234
+ ^ ^
+ MSB LSB (rightmost)
+```
+
+## Packed storage layout
+
+Solidity packs storage variables when possible. Consider this struct:
+
+```solidity
+struct Packed {
+ uint128 a; // 16 bytes
+ uint64 b; // 8 bytes
+ uint64 c; // 8 bytes
+ uint256 d; // 32 bytes (new slot)
+}
+```
+
+The layout in storage:
+
+```
+Slot 0: | c (8 bytes) | b (8 bytes) | a (16 bytes) |
+ offset 24 offset 16 offset 0
+
+Slot 1: | d (32 bytes) |
+ offset 0
+```
+
+Note that `c` is stored at a higher offset than `a` despite appearing later
+in the source. Solidity fills slots from low to high offsets, but struct
+fields are placed in declaration order within that constraint.
+
+## Representing layout in pointers
+
+Pointers capture this layout using offsets within slots:
+
+```json
+{
+ "group": [
+ {
+ "name": "a",
+ "location": "storage",
+ "slot": 0,
+ "offset": 0,
+ "length": 16
+ },
+ {
+ "name": "b",
+ "location": "storage",
+ "slot": 0,
+ "offset": 16,
+ "length": 8
+ },
+ {
+ "name": "c",
+ "location": "storage",
+ "slot": 0,
+ "offset": 24,
+ "length": 8
+ },
+ {
+ "name": "d",
+ "location": "storage",
+ "slot": 1,
+ "offset": 0,
+ "length": 32
+ }
+ ]
+}
+```
+
+The type describes the struct's logical shape; the pointer describes its
+physical layout.
+
+## Dynamic data representation
+
+Dynamic types (dynamic arrays, mappings, strings, bytes) store a fixed-size
+component at their declared slot, with actual data elsewhere:
+
+### Dynamic arrays
+
+The array's slot holds its length. Elements are stored starting at
+`keccak256(slot)`:
+
+```
+Slot 5: [length]
+Slot keccak256(5): [element 0]
+Slot keccak256(5)+1: [element 1]
+...
+```
+
+### Mappings
+
+Mapping slots are empty. Values are stored at `keccak256(key, slot)`:
+
+```
+Slot 3: (unused)
+Slot keccak256(addr, 3): [balances[addr]]
+```
+
+### Strings and bytes
+
+Short strings (≤31 bytes) store data and length in a single slot. Long strings
+store length in the base slot and data starting at `keccak256(slot)`.
+
+## Types don't encode layout
+
+A key design principle: ethdebug/format types describe logical structure, not
+physical layout. The same type definition works regardless of encoding context:
+
+```json
+{
+ "kind": "struct",
+ "contains": [
+ { "name": "a", "type": { "kind": "uint", "bits": 128 } },
+ { "name": "b", "type": { "kind": "uint", "bits": 64 } }
+ ]
+}
+```
+
+This struct definition is the same whether `a` and `b` are packed in storage
+or padded in memory. The pointer tells the debugger where each field actually
+lives.
+
+This separation lets compilers describe complex optimizations (reordering,
+packing, splitting across locations) without inventing new type constructs.
+
+## Learn more
+
+- [Regions documentation](../pointers/regions) for addressing within slots
+- [Expressions documentation](../pointers/expressions) for computing dynamic
+ locations
+- [Type specification](/spec/type) for formal type definitions
diff --git a/packages/web/docusaurus.config.ts b/packages/web/docusaurus.config.ts
index 3abec10e5..7218db98c 100644
--- a/packages/web/docusaurus.config.ts
+++ b/packages/web/docusaurus.config.ts
@@ -1,5 +1,6 @@
import { themes as prismThemes } from "prism-react-renderer";
import path from "path";
+import webpack from "webpack";
import type { Config } from "@docusaurus/types";
import type * as Preset from "@docusaurus/preset-classic";
import type { Configuration } from "webpack";
@@ -59,12 +60,17 @@ const config: Config = {
},
],
},
+ plugins: [
+ new webpack.ProvidePlugin({
+ Buffer: ["buffer", "Buffer"],
+ }),
+ ],
resolve: {
alias: {
react: path.resolve("../../node_modules/react"),
},
fallback: {
- buffer: false,
+ buffer: require.resolve("buffer/"),
util: false,
},
fullySpecified: false,
@@ -168,7 +174,7 @@ const config: Config = {
},
{
label: "Known challenges",
- to: "/docs/known-challenges",
+ to: "/docs/reference/challenges",
},
],
},
diff --git a/packages/web/package.json b/packages/web/package.json
index cdd5a76fe..8cbd3b74d 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -21,8 +21,13 @@
"@docusaurus/preset-classic": "^3.9.2",
"@docusaurus/tsconfig": "^3.9.2",
"@docusaurus/types": "^3.9.2",
+ "@ethdebug/bugc": "^0.1.0-0",
+ "@ethdebug/bugc-react": "^0.1.0-0",
+ "@ethdebug/evm": "^0.1.0-0",
"@ethdebug/format": "^0.1.0-0",
"@ethdebug/pointers": "^0.1.0-0",
+ "@ethdebug/pointers-react": "^0.1.0-0",
+ "@ethdebug/programs-react": "^0.1.0-0",
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
@@ -33,6 +38,7 @@
"@saucelabs/theme-github-codeblock": "^0.2.3",
"@shikijs/themes": "^2.5.0",
"ajv": "^8.17.1",
+ "buffer": "^6.0.3",
"clsx": "^1.2.1",
"docusaurus-json-schema-plugin": "^1.15.0",
"prism-react-renderer": "^2.4.1",
diff --git a/packages/web/src/theme/BugcExample/AstView.css b/packages/web/src/theme/BugcExample/AstView.css
new file mode 100644
index 000000000..453b38ba2
--- /dev/null
+++ b/packages/web/src/theme/BugcExample/AstView.css
@@ -0,0 +1,21 @@
+/**
+ * Styles for AstView component.
+ */
+
+@import "./variables.css";
+
+.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: var(--bugc-text-code);
+ white-space: pre;
+ overflow: auto;
+}
diff --git a/packages/web/src/theme/BugcExample/BugPlayground.css b/packages/web/src/theme/BugcExample/BugPlayground.css
new file mode 100644
index 000000000..97f405637
--- /dev/null
+++ b/packages/web/src/theme/BugcExample/BugPlayground.css
@@ -0,0 +1,193 @@
+/**
+ * BugPlayground styles for Docusaurus.
+ * Uses Docusaurus/Infima CSS variables for theming.
+ */
+
+.bug-playground {
+ display: flex;
+ flex-direction: column;
+ border: 1px solid var(--ifm-color-emphasis-300);
+ border-radius: var(--ifm-global-radius);
+ overflow: hidden;
+ background: var(--ifm-background-color);
+}
+
+.bug-playground-header {
+ display: flex;
+ justify-content: flex-end;
+ padding: 0.5rem 1rem;
+ background: var(--ifm-color-emphasis-100);
+ border-bottom: 1px solid var(--ifm-color-emphasis-300);
+}
+
+.bug-playground-controls {
+ display: flex;
+ gap: 1rem;
+ align-items: center;
+}
+
+.bug-playground-opt-control {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 0.875rem;
+}
+
+.bug-playground-opt-control select {
+ padding: 0.25rem 0.5rem;
+ border-radius: var(--ifm-global-radius);
+ border: 1px solid var(--ifm-color-emphasis-300);
+ background: var(--ifm-background-color);
+ color: var(--ifm-font-color-base);
+ font-size: 0.875rem;
+}
+
+.bug-playground-compile-btn {
+ padding: 0.375rem 1rem;
+ border-radius: var(--ifm-global-radius);
+ border: none;
+ background: var(--ifm-color-primary);
+ color: white;
+ font-weight: 600;
+ cursor: pointer;
+ transition: background 0.2s;
+}
+
+.bug-playground-compile-btn:hover:not(:disabled) {
+ background: var(--ifm-color-primary-dark);
+}
+
+.bug-playground-compile-btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.bug-playground-content {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+}
+
+.bug-playground-editor {
+ border-right: 1px solid var(--ifm-color-emphasis-300);
+ overflow: hidden;
+}
+
+.bug-playground-output {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.bug-playground-tabs {
+ display: flex;
+ border-bottom: 1px solid var(--ifm-color-emphasis-300);
+ background: var(--ifm-color-emphasis-100);
+}
+
+.bug-playground-tab {
+ padding: 0.5rem 1rem;
+ border: none;
+ background: transparent;
+ color: var(--ifm-font-color-secondary);
+ cursor: pointer;
+ font-size: 0.875rem;
+ font-weight: 500;
+ transition:
+ color 0.2s,
+ background 0.2s;
+}
+
+.bug-playground-tab:hover {
+ color: var(--ifm-font-color-base);
+ background: var(--ifm-color-emphasis-200);
+}
+
+.bug-playground-tab.active {
+ color: var(--ifm-color-primary);
+ background: var(--ifm-background-color);
+ border-bottom: 2px solid var(--ifm-color-primary);
+ margin-bottom: -1px;
+}
+
+.bug-playground-tab-content {
+ flex: 1;
+ overflow: auto;
+ padding: 1rem;
+}
+
+.bug-playground-error {
+ padding: 1rem;
+ background: var(--ifm-color-danger-contrast-background);
+ border: 1px solid var(--ifm-color-danger-dark);
+ border-radius: var(--ifm-global-radius);
+ margin: 1rem;
+}
+
+.bug-playground-error h4 {
+ color: var(--ifm-color-danger-dark);
+ margin: 0 0 0.5rem 0;
+}
+
+.bug-playground-error pre {
+ margin: 0;
+ white-space: pre-wrap;
+ font-size: 0.875rem;
+ color: var(--ifm-color-danger-darkest);
+}
+
+.bug-playground-warnings {
+ padding: 0.75rem 1rem;
+ background: var(--ifm-color-warning-contrast-background);
+ border-top: 1px solid var(--ifm-color-warning-dark);
+}
+
+.bug-playground-warnings h4 {
+ color: var(--ifm-color-warning-darkest);
+ margin: 0 0 0.5rem 0;
+ font-size: 0.875rem;
+}
+
+.bug-playground-warnings ul {
+ margin: 0;
+ padding-left: 1.25rem;
+}
+
+.bug-playground-warnings li {
+ font-size: 0.8125rem;
+ color: var(--ifm-color-warning-darkest);
+}
+
+/* Responsive layout for smaller screens */
+@media (max-width: 996px) {
+ .bug-playground-content {
+ grid-template-columns: 1fr;
+ grid-template-rows: 1fr 1fr;
+ }
+
+ .bug-playground-editor {
+ border-right: none;
+ border-bottom: 1px solid var(--ifm-color-emphasis-300);
+ }
+}
+
+/* Monaco Editor custom decorations for source highlighting */
+/* 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/web/src/theme/BugcExample/BugPlayground.tsx b/packages/web/src/theme/BugcExample/BugPlayground.tsx
new file mode 100644
index 000000000..f26427948
--- /dev/null
+++ b/packages/web/src/theme/BugcExample/BugPlayground.tsx
@@ -0,0 +1,318 @@
+/**
+ * BugPlayground component for the Docusaurus site.
+ *
+ * Provides an interactive BUG compiler playground with editor and output views.
+ */
+
+import React, { useState, useCallback, useEffect } from "react";
+import {
+ compile as bugCompile,
+ type BugError,
+ Severity,
+ type Ast,
+ type Ir,
+} from "@ethdebug/bugc";
+import {
+ Editor,
+ AstView,
+ IrView,
+ CfgView,
+ BytecodeView,
+ type SourceRange,
+ type BytecodeOutput,
+} from "@ethdebug/bugc-react";
+import "./BugPlayground.css";
+
+// Import CSS for bugc-react components
+import "./variables.css";
+import "./AstView.css";
+import "./BytecodeView.css";
+import "./CfgView.css";
+import "./EthdebugTooltip.css";
+import "./IrView.css";
+
+/**
+ * Result of a BUG compilation.
+ */
+export interface CompileResult {
+ success: boolean;
+ error?: string;
+ ast?: Ast.Contract;
+ ir?: Ir.Module;
+ bytecode?: BytecodeOutput;
+ warnings: string[];
+}
+
+/**
+ * Compile BUG source code.
+ */
+async function compile(
+ code: string,
+ optimizationLevel: number,
+): Promise {
+ // Get AST
+ const astResult = await bugCompile({ to: "ast", source: code });
+
+ 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
+ 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;
+
+ // Get bytecode
+ 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 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)],
+ };
+}
+
+type TabType = "ast" | "ir" | "cfg" | "bytecode";
+
+export interface BugPlaygroundProps {
+ /** Initial code to display in the editor */
+ initialCode?: string;
+ /** Default optimization level (0-3) */
+ defaultOptimizationLevel?: number;
+ /** Whether to show optimization level selector */
+ showOptimizationSelector?: boolean;
+ /** Height of the playground */
+ height?: string;
+}
+
+/**
+ * Interactive BUG compiler playground.
+ */
+export function BugPlayground({
+ initialCode = `name Counter;
+
+storage {
+ [0] count: uint256;
+ [1] threshold: uint256;
+}
+
+create {
+ count = 0;
+ threshold = 100;
+}
+
+code {
+ // Increment the counter
+ count = count + 1;
+
+ // Check threshold
+ if (count >= threshold) {
+ count = 0;
+ }
+}
+`,
+ defaultOptimizationLevel = 3,
+ showOptimizationSelector = true,
+ height = "600px",
+}: BugPlaygroundProps): JSX.Element {
+ const [code, setCode] = useState(initialCode);
+ const [optimizationLevel, setOptimizationLevel] = useState(
+ defaultOptimizationLevel,
+ );
+ const [compileResult, setCompileResult] = useState(
+ null,
+ );
+ const [isCompiling, setIsCompiling] = useState(false);
+ const [activeTab, setActiveTab] = useState("ast");
+ const [highlightedRanges, setHighlightedRanges] = useState([]);
+
+ // Handler for hovering over IR/Bytecode elements
+ const handleOpcodeHover = useCallback((ranges: SourceRange[]) => {
+ setHighlightedRanges(ranges);
+ }, []);
+
+ const handleCompile = useCallback(async () => {
+ setIsCompiling(true);
+ try {
+ const result = await compile(code, optimizationLevel);
+ setCompileResult(result);
+ if (!result.success) {
+ setActiveTab("ast"); // Show AST tab for errors
+ }
+ } catch (error) {
+ setCompileResult({
+ success: false,
+ error: error instanceof Error ? error.message : "Unknown error",
+ warnings: [],
+ });
+ } finally {
+ setIsCompiling(false);
+ }
+ }, [code, optimizationLevel]);
+
+ // Compile on mount
+ useEffect(() => {
+ handleCompile();
+ }, []);
+
+ const tabs: { id: TabType; label: string }[] = [
+ { id: "ast", label: "AST" },
+ { id: "ir", label: "IR" },
+ { id: "cfg", label: "CFG" },
+ { id: "bytecode", label: "Bytecode" },
+ ];
+
+ return (
+
+
+
+ {showOptimizationSelector && (
+
+ Optimization:
+ setOptimizationLevel(Number(e.target.value))}
+ >
+ 0 - None
+ 1 - Basic
+ 2 - Standard
+ 3 - Full
+
+
+ )}
+
+ {isCompiling ? "Compiling..." : "Compile"}
+
+
+
+
+
+
+
+
+
+
+ {compileResult && !compileResult.success && (
+
+
Compilation Error
+
{compileResult.error}
+
+ )}
+
+ {compileResult?.success && (
+ <>
+
+ {tabs.map((tab) => (
+ setActiveTab(tab.id)}
+ >
+ {tab.label}
+
+ ))}
+
+
+
+ {activeTab === "ast" && compileResult.ast && (
+
+ )}
+ {activeTab === "ir" && compileResult.ir && (
+
+ )}
+ {activeTab === "cfg" && compileResult.ir && (
+
+ )}
+ {activeTab === "bytecode" && compileResult.bytecode && (
+
+ )}
+
+ >
+ )}
+
+ {compileResult && compileResult.warnings.length > 0 && (
+
+
Warnings
+
+ {compileResult.warnings.map((warning, i) => (
+ {warning}
+ ))}
+
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/web/src/theme/BugcExample/BytecodeView.css b/packages/web/src/theme/BugcExample/BytecodeView.css
new file mode 100644
index 000000000..b66318e6e
--- /dev/null
+++ b/packages/web/src/theme/BugcExample/BytecodeView.css
@@ -0,0 +1,115 @@
+/**
+ * Styles for BytecodeView component.
+ */
+
+@import "./variables.css";
+
+.bytecode-view {
+ height: 100%;
+ overflow: auto;
+}
+
+.bytecode-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+ background-color: var(--bugc-bg-secondary);
+ border-bottom: 1px solid var(--bugc-border-primary);
+ position: sticky;
+ top: 0;
+ z-index: 1;
+}
+
+.bytecode-header h3 {
+ margin: 0;
+ font-size: 1rem;
+ color: var(--bugc-text-primary);
+}
+
+.bytecode-stats {
+ display: flex;
+ gap: 1rem;
+ font-size: 0.813rem;
+ color: var(--bugc-text-secondary);
+}
+
+.bytecode-content {
+ padding: 1rem;
+}
+
+.bytecode-section {
+ margin-bottom: 2rem;
+}
+
+.bytecode-section h4 {
+ margin: 0 0 0.5rem 0;
+ font-size: 0.875rem;
+ color: var(--bugc-text-primary);
+}
+
+.bytecode-hex,
+.bytecode-disassembly {
+ margin: 0;
+ padding: 1rem;
+ background-color: var(--bugc-bg-code);
+ border: 1px solid var(--bugc-border-primary);
+ border-radius: 4px;
+ font-family: "Consolas", "Monaco", "Courier New", monospace;
+ font-size: 0.813rem;
+ line-height: 1.5;
+ color: var(--bugc-text-code);
+ 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 var(--bugc-border-primary);
+}
+
+.bytecode-disassembly-interactive {
+ padding: 1rem;
+ background-color: var(--bugc-bg-code);
+ border: 1px solid var(--bugc-border-primary);
+ 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: var(--bugc-bg-hover);
+}
+
+.opcode-line .pc {
+ color: var(--bugc-syntax-address);
+ min-width: 3rem;
+ text-align: right;
+}
+
+.opcode-line .opcode {
+ color: var(--bugc-syntax-opcode);
+ min-width: 6rem;
+ font-weight: 500;
+}
+
+.opcode-line .immediates {
+ color: var(--bugc-syntax-number);
+}
diff --git a/packages/web/src/theme/BugcExample/CfgView.css b/packages/web/src/theme/BugcExample/CfgView.css
new file mode 100644
index 000000000..16e51dba0
--- /dev/null
+++ b/packages/web/src/theme/BugcExample/CfgView.css
@@ -0,0 +1,206 @@
+/**
+ * Styles for CfgView component.
+ */
+
+@import "./variables.css";
+
+.cfg-view {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.cfg-header {
+ padding: 1rem;
+ border-bottom: 1px solid var(--bugc-border-primary);
+ background: var(--bugc-bg-secondary);
+}
+
+.cfg-header h3 {
+ margin: 0;
+ font-size: 1.1rem;
+ color: var(--bugc-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(--bugc-border-primary);
+ padding: 1rem;
+ overflow-y: auto;
+ background: var(--bugc-bg-secondary);
+ color: var(--bugc-text-primary);
+}
+
+.cfg-sidebar h4 {
+ margin: 0 0 1rem 0;
+ font-size: 1rem;
+ color: var(--bugc-text-primary);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.cfg-sidebar h5 {
+ margin: 0.5rem 0;
+ font-size: 0.9rem;
+ color: var(--bugc-text-secondary);
+}
+
+.cfg-sidebar-close {
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ cursor: pointer;
+ color: var(--bugc-text-secondary);
+ 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: var(--bugc-bg-hover);
+ color: var(--bugc-text-primary);
+}
+
+/* Custom node styles */
+.cfg-node {
+ background: var(--bugc-cfg-node-bg);
+ border: 2px solid var(--bugc-cfg-node-border);
+ border-radius: 8px;
+ padding: 10px 15px;
+ min-width: 120px;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.cfg-node.entry {
+ border-color: var(--bugc-cfg-entry-border);
+ background: var(--bugc-cfg-entry-bg);
+}
+
+.cfg-node.selected {
+ border-width: 3px;
+ box-shadow: 0 0 0 2px rgba(9, 105, 218, 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;
+}
+
+.cfg-view .entry-badge {
+ background: var(--bugc-accent-green);
+ color: white;
+ font-size: 10px;
+ padding: 2px 6px;
+ border-radius: 4px;
+ text-transform: uppercase;
+}
+
+.cfg-node-stats {
+ font-size: 12px;
+ color: var(--bugc-text-secondary);
+}
+
+/* Instruction display */
+.block-instructions {
+ margin-top: 1rem;
+}
+
+.instruction-list {
+ background: var(--bugc-bg-primary);
+ border: 1px solid var(--bugc-border-primary);
+ 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: var(--bugc-text-primary);
+}
+
+.cfg-view .instruction {
+ margin: 0.25rem 0;
+ padding: 0.125rem 0;
+}
+
+.cfg-view .instruction.terminator {
+ margin-top: 0.5rem;
+ padding-top: 0.5rem;
+ border-top: 1px dashed var(--bugc-border-primary);
+ color: var(--bugc-accent-red);
+ 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: var(--bugc-cfg-node-border);
+ border: 2px solid var(--bugc-bg-primary);
+}
+
+.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;
+}
+
+/* React Flow background in light/dark mode */
+.cfg-view .react-flow__background {
+ background-color: var(--bugc-bg-primary);
+}
diff --git a/packages/web/src/theme/BugcExample/EthdebugTooltip.css b/packages/web/src/theme/BugcExample/EthdebugTooltip.css
new file mode 100644
index 000000000..d758d9d57
--- /dev/null
+++ b/packages/web/src/theme/BugcExample/EthdebugTooltip.css
@@ -0,0 +1,76 @@
+/**
+ * Styles for EthdebugTooltip component.
+ */
+
+@import "./variables.css";
+
+.ethdebug-tooltip {
+ position: fixed;
+ z-index: 1000;
+ background-color: var(--bugc-tooltip-bg);
+ border: 1px solid var(--bugc-tooltip-border);
+ border-radius: 4px;
+ padding: 0.5rem;
+ max-width: 600px;
+ max-height: 400px;
+ overflow: auto;
+ pointer-events: none;
+ box-shadow: 0 4px 12px var(--bugc-tooltip-shadow);
+}
+
+.ethdebug-tooltip.pinned {
+ pointer-events: auto;
+ border-color: var(--bugc-tooltip-pinned-border);
+ box-shadow: 0 4px 16px var(--bugc-tooltip-pinned-shadow);
+}
+
+.ethdebug-tooltip pre {
+ margin: 0;
+ font-family: "Courier New", monospace;
+ font-size: 0.75rem;
+ color: var(--bugc-text-code);
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.debug-info-icon {
+ color: var(--bugc-accent-blue);
+ 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: var(--bugc-accent-blue-bg);
+}
+
+.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: var(--bugc-text-code);
+ 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: var(--bugc-bg-hover);
+ color: var(--bugc-text-primary);
+}
diff --git a/packages/web/src/theme/BugcExample/IrView.css b/packages/web/src/theme/BugcExample/IrView.css
new file mode 100644
index 000000000..e449ed737
--- /dev/null
+++ b/packages/web/src/theme/BugcExample/IrView.css
@@ -0,0 +1,147 @@
+/**
+ * Styles for IrView component.
+ */
+
+@import "./variables.css";
+
+.ir-view {
+ height: 100%;
+ overflow: auto;
+}
+
+.ir-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 1rem;
+ background-color: var(--bugc-bg-secondary);
+ border-bottom: 1px solid var(--bugc-border-primary);
+ position: sticky;
+ top: 0;
+ z-index: 1;
+}
+
+.ir-header h3 {
+ margin: 0;
+ font-size: 1rem;
+ color: var(--bugc-text-primary);
+}
+
+.ir-stats {
+ display: flex;
+ gap: 1rem;
+ font-size: 0.813rem;
+ color: var(--bugc-text-secondary);
+}
+
+.ir-content {
+ padding: 1rem;
+ font-family: "Consolas", "Monaco", "Courier New", monospace;
+ font-size: 0.875rem;
+ line-height: 1.6;
+ color: var(--bugc-text-primary);
+}
+
+.section-label {
+ color: var(--bugc-syntax-comment);
+ 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: var(--bugc-syntax-function);
+ 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: var(--bugc-syntax-type);
+}
+
+.entry-badge {
+ background-color: var(--bugc-accent-green-bg);
+ color: var(--bugc-accent-green);
+ 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: var(--bugc-bg-hover);
+}
+
+.instruction-operation,
+.terminator-operation,
+.phi-operation {
+ flex: 1;
+}
+
+.ir-terminator {
+ color: var(--bugc-syntax-terminator);
+ font-weight: 500;
+}
+
+.ir-phi {
+ color: var(--bugc-syntax-phi);
+ font-style: italic;
+}
+
+.hoverable-part {
+ display: inline;
+ transition: background-color 0.15s ease;
+}
+
+.hoverable-part.has-debug {
+ cursor: pointer;
+ border-bottom: 1px dotted var(--bugc-accent-blue);
+ border-bottom-color: rgba(86, 156, 214, 0.4);
+}
+
+.hoverable-part.has-debug:hover {
+ background-color: var(--bugc-accent-blue-bg);
+ border-bottom-color: var(--bugc-accent-blue);
+}
+
+.debug-info-icon.inline {
+ display: inline;
+ margin-left: 0.25rem;
+ font-size: 0.75rem;
+ vertical-align: super;
+}
diff --git a/packages/web/src/theme/BugcExample/index.ts b/packages/web/src/theme/BugcExample/index.ts
new file mode 100644
index 000000000..b951609b2
--- /dev/null
+++ b/packages/web/src/theme/BugcExample/index.ts
@@ -0,0 +1,46 @@
+// Re-export from @ethdebug/bugc-react
+export {
+ // Components
+ Editor,
+ AstView,
+ IrView,
+ CfgView,
+ BytecodeView,
+ EthdebugTooltip,
+ DebugInfoIcon,
+
+ // Hooks
+ useEthdebugTooltip,
+
+ // Types
+ type SourceRange,
+ type BytecodeOutput,
+ type CompileResult,
+ type TooltipData,
+} from "@ethdebug/bugc-react";
+
+// Utilities
+export {
+ extractSourceRange,
+ formatDebugContext,
+ hasSourceRange,
+ extractInstructionDebug,
+ extractTerminatorDebug,
+ extractPhiDebug,
+ formatMultiLevelDebug,
+ extractAllSourceRanges,
+ extractOperandSourceRanges,
+ formatBytecode,
+ getOpcodeName,
+ OPCODES,
+ registerBugLanguage,
+ bugKeywords,
+ bugTypeKeywords,
+ bugOperators,
+ bugLanguageId,
+ bugMonarchTokensProvider,
+ bugLanguageConfiguration,
+} from "@ethdebug/bugc-react";
+
+// Local Docusaurus-specific components
+export * from "./BugPlayground";
diff --git a/packages/web/src/theme/BugcExample/variables.css b/packages/web/src/theme/BugcExample/variables.css
new file mode 100644
index 000000000..490510c66
--- /dev/null
+++ b/packages/web/src/theme/BugcExample/variables.css
@@ -0,0 +1,108 @@
+/**
+ * CSS custom properties for @ethdebug/bugc-react components.
+ *
+ * These variables provide theme-aware styling that integrates with
+ * Docusaurus/Infima theming.
+ */
+
+:root {
+ /* Background colors */
+ --bugc-bg-primary: #ffffff;
+ --bugc-bg-secondary: #f5f6f7;
+ --bugc-bg-code: #f5f6f7;
+ --bugc-bg-hover: rgba(0, 0, 0, 0.05);
+
+ /* Border colors */
+ --bugc-border-primary: #dadde1;
+ --bugc-border-secondary: #eaecef;
+
+ /* Text colors */
+ --bugc-text-primary: #1c1e21;
+ --bugc-text-secondary: #606770;
+ --bugc-text-muted: #898989;
+ --bugc-text-code: #1c1e21;
+
+ /* Syntax highlighting - light theme */
+ --bugc-syntax-keyword: #0550ae;
+ --bugc-syntax-type: #6f42c1;
+ --bugc-syntax-function: #8250df;
+ --bugc-syntax-string: #0a3069;
+ --bugc-syntax-number: #0550ae;
+ --bugc-syntax-comment: #6e7781;
+ --bugc-syntax-opcode: #116329;
+ --bugc-syntax-address: #6e7781;
+ --bugc-syntax-terminator: #cf222e;
+ --bugc-syntax-phi: #0550ae;
+
+ /* Accent colors */
+ --bugc-accent-blue: #0969da;
+ --bugc-accent-blue-bg: rgba(9, 105, 218, 0.1);
+ --bugc-accent-green: #1a7f37;
+ --bugc-accent-green-bg: rgba(26, 127, 55, 0.1);
+ --bugc-accent-red: #cf222e;
+ --bugc-accent-purple: #8250df;
+
+ /* Tooltip */
+ --bugc-tooltip-bg: #ffffff;
+ --bugc-tooltip-border: #dadde1;
+ --bugc-tooltip-shadow: rgba(0, 0, 0, 0.15);
+ --bugc-tooltip-pinned-border: #0969da;
+ --bugc-tooltip-pinned-shadow: rgba(9, 105, 218, 0.2);
+
+ /* CFG specific */
+ --bugc-cfg-node-bg: #ffffff;
+ --bugc-cfg-node-border: #0969da;
+ --bugc-cfg-entry-border: #1a7f37;
+ --bugc-cfg-entry-bg: #dafbe1;
+}
+
+[data-theme="dark"] {
+ /* Background colors */
+ --bugc-bg-primary: #1e1e1e;
+ --bugc-bg-secondary: #2d2d30;
+ --bugc-bg-code: #2d2d30;
+ --bugc-bg-hover: rgba(255, 255, 255, 0.05);
+
+ /* Border colors */
+ --bugc-border-primary: #3e3e42;
+ --bugc-border-secondary: #454545;
+
+ /* Text colors */
+ --bugc-text-primary: #cccccc;
+ --bugc-text-secondary: #969696;
+ --bugc-text-muted: #858585;
+ --bugc-text-code: #d4d4d4;
+
+ /* Syntax highlighting - dark theme */
+ --bugc-syntax-keyword: #569cd6;
+ --bugc-syntax-type: #4ec9b0;
+ --bugc-syntax-function: #dcdcaa;
+ --bugc-syntax-string: #ce9178;
+ --bugc-syntax-number: #b5cea8;
+ --bugc-syntax-comment: #6a9955;
+ --bugc-syntax-opcode: #4ec9b0;
+ --bugc-syntax-address: #858585;
+ --bugc-syntax-terminator: #c586c0;
+ --bugc-syntax-phi: #9cdcfe;
+
+ /* Accent colors */
+ --bugc-accent-blue: #569cd6;
+ --bugc-accent-blue-bg: rgba(86, 156, 214, 0.15);
+ --bugc-accent-green: #4ec9b0;
+ --bugc-accent-green-bg: rgba(78, 201, 176, 0.2);
+ --bugc-accent-red: #f14c4c;
+ --bugc-accent-purple: #c586c0;
+
+ /* Tooltip */
+ --bugc-tooltip-bg: #1e1e1e;
+ --bugc-tooltip-border: #3e3e42;
+ --bugc-tooltip-shadow: rgba(0, 0, 0, 0.4);
+ --bugc-tooltip-pinned-border: #569cd6;
+ --bugc-tooltip-pinned-shadow: rgba(86, 156, 214, 0.3);
+
+ /* CFG specific */
+ --bugc-cfg-node-bg: #2d2d30;
+ --bugc-cfg-node-border: #569cd6;
+ --bugc-cfg-entry-border: #4ec9b0;
+ --bugc-cfg-entry-bg: rgba(78, 201, 176, 0.15);
+}
diff --git a/packages/web/src/theme/Drawer/Drawer.css b/packages/web/src/theme/Drawer/Drawer.css
new file mode 100644
index 000000000..96f0aa8c7
--- /dev/null
+++ b/packages/web/src/theme/Drawer/Drawer.css
@@ -0,0 +1,162 @@
+/**
+ * Shared Drawer Styles
+ *
+ * Base styles for bottom-of-viewport pull-out panels.
+ * Used by pointer playground, trace playground, etc.
+ */
+
+.drawer {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ background: var(--ifm-background-color);
+ border-top: 1px solid var(--ifm-color-emphasis-300);
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
+ transition: transform 0.3s ease-out;
+}
+
+.drawer.closed {
+ transform: translateY(calc(100% - 44px));
+}
+
+.drawer.open {
+ transform: translateY(0);
+ height: var(--drawer-height, 45vh);
+ max-height: 85vh;
+ min-height: 200px;
+ display: flex;
+ flex-direction: column;
+}
+
+.drawer.resizing {
+ transition: none;
+ user-select: none;
+}
+
+/* Resize handle */
+.drawer-resize-handle {
+ position: absolute;
+ top: -6px;
+ left: 0;
+ right: 0;
+ height: 12px;
+ cursor: ns-resize;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10;
+}
+
+.drawer-resize-bar {
+ width: 40px;
+ height: 4px;
+ background: var(--ifm-color-emphasis-400);
+ border-radius: 2px;
+ transition: background 0.2s;
+}
+
+.drawer-resize-handle:hover .drawer-resize-bar,
+.drawer.resizing .drawer-resize-bar {
+ background: var(--ifm-color-primary);
+}
+
+/* Header */
+.drawer-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 16px;
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+ background: var(--ifm-background-surface-color);
+ flex-shrink: 0;
+}
+
+.drawer-toggle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 8px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--ifm-color-content);
+ border-radius: 4px;
+ transition: background 0.2s;
+}
+
+.drawer-toggle:hover {
+ background: var(--ifm-color-emphasis-100);
+}
+
+.drawer-toggle-icon {
+ font-size: 10px;
+ line-height: 1;
+}
+
+.drawer-toggle-text {
+ font-family: var(--ifm-font-family-base);
+}
+
+.drawer-header-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.drawer-close {
+ padding: 4px 8px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 16px;
+ color: var(--ifm-color-emphasis-600);
+ border-radius: 4px;
+ transition:
+ background 0.2s,
+ color 0.2s;
+ line-height: 1;
+}
+
+.drawer-close:hover {
+ background: var(--ifm-color-emphasis-100);
+ color: var(--ifm-color-content);
+}
+
+/* Content */
+.drawer-content {
+ flex: 1;
+ overflow: hidden;
+ background: var(--ifm-background-color);
+}
+
+/* Mobile: full screen */
+@media (max-width: 768px) {
+ .drawer.open {
+ height: 100vh !important;
+ max-height: 100vh;
+ top: 0;
+ border-top: none;
+ }
+
+ .drawer-resize-handle {
+ display: none;
+ }
+
+ .drawer-header {
+ padding: 12px 16px;
+ }
+}
+
+/* Dark mode */
+[data-theme="dark"] .drawer {
+ border-top-color: var(--ifm-color-emphasis-400);
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.4);
+}
+
+[data-theme="dark"] .drawer-header {
+ border-bottom-color: var(--ifm-color-emphasis-400);
+}
diff --git a/packages/web/src/theme/Drawer/Drawer.tsx b/packages/web/src/theme/Drawer/Drawer.tsx
new file mode 100644
index 000000000..2f6c53e5f
--- /dev/null
+++ b/packages/web/src/theme/Drawer/Drawer.tsx
@@ -0,0 +1,181 @@
+/**
+ * Shared Drawer component for bottom-of-viewport pull-out panels.
+ *
+ * Features:
+ * - Fixed at bottom of viewport
+ * - Resizable by dragging the top edge
+ * - Full-screen on mobile
+ * - Keyboard accessible (Escape to close)
+ */
+
+import React, {
+ useState,
+ useCallback,
+ useRef,
+ useEffect,
+ type ReactNode,
+} from "react";
+
+import "./Drawer.css";
+
+const MIN_HEIGHT = 200;
+const MAX_HEIGHT_RATIO = 0.85;
+const DEFAULT_HEIGHT_RATIO = 0.45;
+
+export interface DrawerProps {
+ /** Whether the drawer is open */
+ isOpen: boolean;
+ /** Callback when drawer should close */
+ onClose: () => void;
+ /** Callback to toggle open/closed */
+ onToggle: () => void;
+ /** Title shown in the header */
+ title: string;
+ /** Header actions (buttons, etc.) rendered after title */
+ headerActions?: ReactNode;
+ /** Main content of the drawer */
+ children: ReactNode;
+ /** Optional className for styling variants */
+ className?: string;
+}
+
+export function Drawer({
+ isOpen,
+ onClose,
+ onToggle,
+ title,
+ headerActions,
+ children,
+ className = "",
+}: DrawerProps): JSX.Element {
+ const [height, setHeight] = useState(() =>
+ typeof window !== "undefined"
+ ? Math.round(window.innerHeight * DEFAULT_HEIGHT_RATIO)
+ : 400,
+ );
+ const [isResizing, setIsResizing] = useState(false);
+ const drawerRef = useRef(null);
+
+ // Handle resize drag
+ const handleResizeStart = useCallback(
+ (e: React.MouseEvent | React.TouchEvent) => {
+ e.preventDefault();
+ setIsResizing(true);
+ },
+ [],
+ );
+
+ useEffect(() => {
+ if (!isResizing) return;
+
+ const handleMove = (e: MouseEvent | TouchEvent) => {
+ const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
+ const windowHeight = window.innerHeight;
+ const newHeight = windowHeight - clientY;
+ const maxHeight = windowHeight * MAX_HEIGHT_RATIO;
+
+ setHeight(Math.max(MIN_HEIGHT, Math.min(newHeight, maxHeight)));
+ };
+
+ const handleEnd = () => {
+ setIsResizing(false);
+ };
+
+ document.addEventListener("mousemove", handleMove);
+ document.addEventListener("mouseup", handleEnd);
+ document.addEventListener("touchmove", handleMove);
+ document.addEventListener("touchend", handleEnd);
+
+ return () => {
+ document.removeEventListener("mousemove", handleMove);
+ document.removeEventListener("mouseup", handleEnd);
+ document.removeEventListener("touchmove", handleMove);
+ document.removeEventListener("touchend", handleEnd);
+ };
+ }, [isResizing]);
+
+ // Handle escape key to close
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape" && isOpen) {
+ onClose();
+ }
+ };
+
+ document.addEventListener("keydown", handleKeyDown);
+ return () => document.removeEventListener("keydown", handleKeyDown);
+ }, [isOpen, onClose]);
+
+ const baseClass = `drawer ${className}`.trim();
+
+ // When closed, render a simple bar
+ if (!isOpen) {
+ return (
+
+ );
+ }
+
+ // When open, show full drawer
+ return (
+
+ {/* Resize handle */}
+
+
+
+
+ ▼
+ {title}
+
+
+
+ {headerActions}
+
+ ✕
+
+
+
+
+
{children}
+
+ );
+}
+
+export default Drawer;
diff --git a/packages/web/src/theme/Drawer/index.ts b/packages/web/src/theme/Drawer/index.ts
new file mode 100644
index 000000000..f17636b92
--- /dev/null
+++ b/packages/web/src/theme/Drawer/index.ts
@@ -0,0 +1 @@
+export { Drawer, type DrawerProps } from "./Drawer";
diff --git a/packages/web/src/theme/PointersExample/PointerDrawer.css b/packages/web/src/theme/PointersExample/PointerDrawer.css
new file mode 100644
index 000000000..5fe6ec161
--- /dev/null
+++ b/packages/web/src/theme/PointersExample/PointerDrawer.css
@@ -0,0 +1,191 @@
+/**
+ * Pointer Drawer Styles
+ *
+ * Simple pull-out drawer from the bottom of the viewport.
+ */
+
+.pointer-drawer {
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 100;
+ background: #1b1b1d; /* Solid fallback */
+ background: var(--ifm-background-color);
+ border-top: 1px solid var(--ifm-color-emphasis-300);
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.15);
+ transition: transform 0.3s ease-out;
+}
+
+html[data-theme="light"] .pointer-drawer {
+ background: #ffffff;
+}
+
+.pointer-drawer.closed {
+ transform: translateY(calc(100% - 44px));
+}
+
+.pointer-drawer.open {
+ transform: translateY(0);
+ height: var(--drawer-height, 40vh);
+ max-height: 85vh;
+ min-height: 200px;
+}
+
+.pointer-drawer.resizing {
+ transition: none;
+ user-select: none;
+}
+
+/* Resize handle */
+.pointer-drawer-resize-handle {
+ position: absolute;
+ top: -6px;
+ left: 0;
+ right: 0;
+ height: 12px;
+ cursor: ns-resize;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10;
+}
+
+.resize-handle-bar {
+ width: 40px;
+ height: 4px;
+ background: var(--ifm-color-emphasis-400);
+ border-radius: 2px;
+ transition: background 0.2s;
+}
+
+.pointer-drawer-resize-handle:hover .resize-handle-bar,
+.pointer-drawer.resizing .resize-handle-bar {
+ background: var(--ifm-color-primary);
+}
+
+/* Header */
+.pointer-drawer-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 8px 16px;
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+ background: var(--ifm-background-surface-color);
+}
+
+.pointer-drawer-toggle {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 8px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--ifm-color-content);
+ border-radius: 4px;
+ transition: background 0.2s;
+}
+
+.pointer-drawer-toggle:hover {
+ background: var(--ifm-color-emphasis-100);
+}
+
+.toggle-icon {
+ font-size: 10px;
+}
+
+.pointer-drawer-header-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.pointer-drawer-header-actions .resolve-button {
+ padding: 6px 16px;
+ background: var(--ifm-color-primary);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition:
+ background 0.2s,
+ opacity 0.2s;
+}
+
+.pointer-drawer-header-actions .resolve-button:hover:not(:disabled) {
+ background: var(--ifm-color-primary-dark);
+}
+
+.pointer-drawer-header-actions .resolve-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.pointer-drawer-close {
+ padding: 4px 8px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 16px;
+ color: var(--ifm-color-emphasis-600);
+ border-radius: 4px;
+ transition:
+ background 0.2s,
+ color 0.2s;
+}
+
+.pointer-drawer-close:hover {
+ background: var(--ifm-color-emphasis-100);
+ color: var(--ifm-color-content);
+}
+
+/* Content - simple scrolling container */
+.pointer-drawer-content {
+ overflow-y: auto;
+ padding: 16px;
+ height: calc(100% - 45px);
+ background: #1b1b1d;
+ background: var(--ifm-background-color);
+}
+
+html[data-theme="light"] .pointer-drawer-content {
+ background: #ffffff;
+}
+
+/* Mobile: full screen */
+@media (max-width: 768px) {
+ .pointer-drawer.open {
+ height: 100vh !important;
+ max-height: 100vh;
+ top: 0;
+ border-top: none;
+ }
+
+ .pointer-drawer-resize-handle {
+ display: none;
+ }
+
+ .pointer-drawer-header {
+ padding: 12px 16px;
+ }
+
+ .pointer-drawer-content {
+ height: calc(100% - 53px);
+ padding: 12px;
+ }
+}
+
+/* Dark mode */
+[data-theme="dark"] .pointer-drawer {
+ border-top-color: var(--ifm-color-emphasis-400);
+ box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.4);
+}
+
+[data-theme="dark"] .pointer-drawer-header {
+ border-bottom-color: var(--ifm-color-emphasis-400);
+}
diff --git a/packages/web/src/theme/PointersExample/PointerDrawer.tsx b/packages/web/src/theme/PointersExample/PointerDrawer.tsx
new file mode 100644
index 000000000..271475f50
--- /dev/null
+++ b/packages/web/src/theme/PointersExample/PointerDrawer.tsx
@@ -0,0 +1,208 @@
+/**
+ * A pull-out drawer for the pointer resolution widget.
+ *
+ * - Fixed at bottom of viewport
+ * - Resizable by dragging the top edge
+ * - Full-screen on mobile
+ */
+
+import React, { useState, useCallback, useRef, useEffect } from "react";
+import BrowserOnly from "@docusaurus/BrowserOnly";
+import {
+ PointerResolverProvider,
+ ResolutionVisualizer,
+ usePointerResolverContext,
+} from "@ethdebug/pointers-react";
+import { usePointerPlayground } from "./PointerPlaygroundContext";
+
+import "./PointerDrawer.css";
+
+const MIN_HEIGHT = 200;
+const MAX_HEIGHT_RATIO = 0.85;
+const DEFAULT_HEIGHT_RATIO = 0.4;
+
+export function PointerDrawer(): JSX.Element {
+ return (
+ {() => }
+ );
+}
+
+function PointerDrawerContent(): JSX.Element {
+ const { pointer, stateSpec, isOpen, toggleDrawer, closeDrawer } =
+ usePointerPlayground();
+
+ const [height, setHeight] = useState(() =>
+ Math.round(window.innerHeight * DEFAULT_HEIGHT_RATIO),
+ );
+ const [isResizing, setIsResizing] = useState(false);
+ const drawerRef = useRef(null);
+
+ // Key to force re-mount of provider when example changes
+ const [providerKey, setProviderKey] = useState(0);
+
+ // Re-mount provider when pointer/state changes from context
+ useEffect(() => {
+ setProviderKey((k) => k + 1);
+ }, [pointer, stateSpec]);
+
+ // Handle resize drag
+ const handleResizeStart = useCallback(
+ (e: React.MouseEvent | React.TouchEvent) => {
+ e.preventDefault();
+ setIsResizing(true);
+ },
+ [],
+ );
+
+ useEffect(() => {
+ if (!isResizing) return;
+
+ const handleMove = (e: MouseEvent | TouchEvent) => {
+ const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
+ const windowHeight = window.innerHeight;
+ const newHeight = windowHeight - clientY;
+ const maxHeight = windowHeight * MAX_HEIGHT_RATIO;
+
+ setHeight(Math.max(MIN_HEIGHT, Math.min(newHeight, maxHeight)));
+ };
+
+ const handleEnd = () => {
+ setIsResizing(false);
+ };
+
+ document.addEventListener("mousemove", handleMove);
+ document.addEventListener("mouseup", handleEnd);
+ document.addEventListener("touchmove", handleMove);
+ document.addEventListener("touchend", handleEnd);
+
+ return () => {
+ document.removeEventListener("mousemove", handleMove);
+ document.removeEventListener("mouseup", handleEnd);
+ document.removeEventListener("touchmove", handleMove);
+ document.removeEventListener("touchend", handleEnd);
+ };
+ }, [isResizing]);
+
+ // Handle escape key to close
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape" && isOpen) {
+ closeDrawer();
+ }
+ };
+
+ document.addEventListener("keydown", handleKeyDown);
+ return () => document.removeEventListener("keydown", handleKeyDown);
+ }, [isOpen, closeDrawer]);
+
+ // When closed, render a simple bar
+ if (!isOpen) {
+ return (
+
+
+
+ ▲
+ Pointer Playground
+
+
+
+ );
+ }
+
+ // When open, show full drawer with provider
+ return (
+
+ {/* Resize handle */}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+interface DrawerHeaderProps {
+ toggleDrawer: () => void;
+ closeDrawer: () => void;
+}
+
+/**
+ * Header with resolve button that needs access to resolver context.
+ */
+function DrawerHeader({
+ toggleDrawer,
+ closeDrawer,
+}: DrawerHeaderProps): JSX.Element {
+ const { pointer, isResolving, resolve } = usePointerResolverContext();
+
+ return (
+
+
+ ▼
+ Pointer Playground
+
+
+
+ resolve()}
+ disabled={isResolving || !pointer}
+ type="button"
+ >
+ {isResolving ? "Resolving..." : "Resolve Pointer"}
+
+
+
+ ✕
+
+
+
+ );
+}
+
+export default PointerDrawer;
diff --git a/packages/web/src/theme/PointersExample/PointerExample.css b/packages/web/src/theme/PointersExample/PointerExample.css
new file mode 100644
index 000000000..1ecb69459
--- /dev/null
+++ b/packages/web/src/theme/PointersExample/PointerExample.css
@@ -0,0 +1,72 @@
+/**
+ * Pointer Example Styles
+ */
+
+.pointer-example {
+ margin: 1rem 0;
+}
+
+.pointer-example-title {
+ font-weight: 600;
+ margin-bottom: 0.5rem;
+ color: var(--ifm-color-content);
+}
+
+.pointer-example-description {
+ font-size: 0.9rem;
+ color: var(--ifm-color-emphasis-700);
+ margin-bottom: 0.5rem;
+}
+
+.pointer-example-content {
+ position: relative;
+}
+
+/* Position the Try It button */
+.pointer-example-try-btn {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 12px;
+ background: var(--ifm-color-primary);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition:
+ background 0.2s,
+ transform 0.1s;
+ z-index: 1;
+}
+
+.pointer-example-try-btn:hover {
+ background: var(--ifm-color-primary-dark);
+}
+
+.pointer-example-try-btn:active {
+ transform: scale(0.97);
+}
+
+.pointer-example-try-btn.standalone {
+ position: static;
+ margin-top: 0.5rem;
+}
+
+.try-btn-icon {
+ font-size: 10px;
+}
+
+/* Ensure code block has room for button */
+.pointer-example-code {
+ padding-right: 100px !important;
+}
+
+/* Dark mode */
+[data-theme="dark"] .pointer-example-description {
+ color: var(--ifm-color-emphasis-600);
+}
diff --git a/packages/web/src/theme/PointersExample/PointerExample.tsx b/packages/web/src/theme/PointersExample/PointerExample.tsx
new file mode 100644
index 000000000..8fd1aca87
--- /dev/null
+++ b/packages/web/src/theme/PointersExample/PointerExample.tsx
@@ -0,0 +1,94 @@
+/**
+ * A pointer example with a "Try it" button that loads into the drawer.
+ */
+
+import React from "react";
+import BrowserOnly from "@docusaurus/BrowserOnly";
+import CodeBlock from "@theme/CodeBlock";
+import type { Pointer } from "@ethdebug/format";
+import type { MockStateSpec } from "@ethdebug/pointers-react";
+import { usePointerPlaygroundOptional } from "./PointerPlaygroundContext";
+
+import "./PointerExample.css";
+
+export interface PointerExampleProps {
+ /** The pointer to demonstrate */
+ pointer: Pointer;
+ /** Optional machine state for the example */
+ state?: MockStateSpec;
+ /** Optional title for the example */
+ title?: string;
+ /** Optional description */
+ description?: string;
+ /** Whether to show the JSON code block (default: true) */
+ showCode?: boolean;
+}
+
+export function PointerExample({
+ pointer,
+ state = {},
+ title,
+ description,
+ showCode = true,
+}: PointerExampleProps): JSX.Element {
+ const pointerJson = JSON.stringify(pointer, null, 2);
+
+ return (
+
+ {title &&
{title}
}
+ {description && (
+
{description}
+ )}
+
+
+ {showCode && (
+
+ {pointerJson}
+
+ )}
+
+
+ {() => (
+
+ )}
+
+
+
+ );
+}
+
+interface TryItButtonProps {
+ pointer: Pointer;
+ state: MockStateSpec;
+ showCode: boolean;
+}
+
+function TryItButton({
+ pointer,
+ state,
+ showCode,
+}: TryItButtonProps): JSX.Element | null {
+ const playground = usePointerPlaygroundOptional();
+
+ if (!playground) {
+ return null;
+ }
+
+ const handleClick = () => {
+ playground.loadExample(pointer, state);
+ };
+
+ return (
+
+ ▶
+ Try it
+
+ );
+}
+
+export default PointerExample;
diff --git a/packages/web/src/theme/PointersExample/PointerPlayground.tsx b/packages/web/src/theme/PointersExample/PointerPlayground.tsx
new file mode 100644
index 000000000..d0cc41c5a
--- /dev/null
+++ b/packages/web/src/theme/PointersExample/PointerPlayground.tsx
@@ -0,0 +1,47 @@
+/**
+ * Wrapper component that provides the playground context and drawer.
+ *
+ * Use this to wrap page content that contains PointerExample components.
+ */
+
+import React, { type ReactNode } from "react";
+import BrowserOnly from "@docusaurus/BrowserOnly";
+import type { Pointer } from "@ethdebug/format";
+import type { MockStateSpec } from "@ethdebug/pointers-react";
+import { PointerPlaygroundProvider } from "./PointerPlaygroundContext";
+import { PointerDrawer } from "./PointerDrawer";
+
+import "./PointerDrawer.css";
+
+export interface PointerPlaygroundProps {
+ children: ReactNode;
+ /** Initial pointer to show in drawer (optional) */
+ initialPointer?: Pointer;
+ /** Initial state for the drawer (optional) */
+ initialState?: MockStateSpec;
+}
+
+export function PointerPlayground({
+ children,
+ initialPointer,
+ initialState,
+}: PointerPlaygroundProps): JSX.Element {
+ return (
+ {children}}
+ >
+ {() => (
+
+ {children}
+
+
+ )}
+
+ );
+}
+
+export default PointerPlayground;
diff --git a/packages/web/src/theme/PointersExample/PointerPlaygroundContext.tsx b/packages/web/src/theme/PointersExample/PointerPlaygroundContext.tsx
new file mode 100644
index 000000000..ca785202a
--- /dev/null
+++ b/packages/web/src/theme/PointersExample/PointerPlaygroundContext.tsx
@@ -0,0 +1,108 @@
+/**
+ * Context for coordinating between pointer examples and the drawer widget.
+ */
+
+import React, {
+ createContext,
+ useContext,
+ useState,
+ useCallback,
+ type ReactNode,
+} from "react";
+import type { Pointer } from "@ethdebug/format";
+import type { MockStateSpec } from "@ethdebug/pointers-react";
+
+export interface PointerPlaygroundState {
+ /** Current pointer in the playground */
+ pointer: Pointer | null;
+ /** Current machine state */
+ stateSpec: MockStateSpec;
+ /** Whether the drawer is open */
+ isOpen: boolean;
+ /** Load a pointer example into the playground */
+ loadExample(pointer: Pointer, state?: MockStateSpec): void;
+ /** Open the drawer */
+ openDrawer(): void;
+ /** Close the drawer */
+ closeDrawer(): void;
+ /** Toggle the drawer */
+ toggleDrawer(): void;
+ /** Update the pointer */
+ setPointer(pointer: Pointer | null): void;
+ /** Update the state */
+ setStateSpec(state: MockStateSpec): void;
+}
+
+const PointerPlaygroundContext = createContext(
+ null,
+);
+
+export interface PointerPlaygroundProviderProps {
+ children: ReactNode;
+ /** Initial pointer */
+ initialPointer?: Pointer | null;
+ /** Initial state */
+ initialState?: MockStateSpec;
+ /** Whether drawer starts open */
+ initialOpen?: boolean;
+}
+
+export function PointerPlaygroundProvider({
+ children,
+ initialPointer = null,
+ initialState = {},
+ initialOpen = false,
+}: PointerPlaygroundProviderProps): JSX.Element {
+ const [pointer, setPointer] = useState(initialPointer);
+ const [stateSpec, setStateSpec] = useState(initialState);
+ const [isOpen, setIsOpen] = useState(initialOpen);
+
+ const openDrawer = useCallback(() => setIsOpen(true), []);
+ const closeDrawer = useCallback(() => setIsOpen(false), []);
+ const toggleDrawer = useCallback(() => setIsOpen((prev) => !prev), []);
+
+ const loadExample = useCallback(
+ (newPointer: Pointer, newState: MockStateSpec = {}) => {
+ setPointer(newPointer);
+ setStateSpec(newState);
+ setIsOpen(true);
+ },
+ [],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function usePointerPlayground(): PointerPlaygroundState {
+ const context = useContext(PointerPlaygroundContext);
+ if (!context) {
+ throw new Error(
+ "usePointerPlayground must be used within a PointerPlaygroundProvider",
+ );
+ }
+ return context;
+}
+
+/**
+ * Hook that returns null if not in a playground context.
+ * Useful for components that can work both inside and outside the playground.
+ */
+export function usePointerPlaygroundOptional(): PointerPlaygroundState | null {
+ return useContext(PointerPlaygroundContext);
+}
diff --git a/packages/web/src/theme/PointersExample/PointerResolver.tsx b/packages/web/src/theme/PointersExample/PointerResolver.tsx
new file mode 100644
index 000000000..6823c95a9
--- /dev/null
+++ b/packages/web/src/theme/PointersExample/PointerResolver.tsx
@@ -0,0 +1,116 @@
+/**
+ * PointerResolver component for the Docusaurus site.
+ *
+ * Provides an interactive pointer resolution visualizer with theming.
+ */
+
+import React from "react";
+import BrowserOnly from "@docusaurus/BrowserOnly";
+import type { Pointer } from "@ethdebug/format";
+import {
+ PointerResolverProvider,
+ ResolutionVisualizer,
+ type MockStateSpec,
+} from "@ethdebug/pointers-react";
+
+// Import CSS for styling
+import "./variables.css";
+import "./ResolutionVisualizer.css";
+import "./RegionMap.css";
+import "./RegionOutput.css";
+
+export interface PointerResolverProps {
+ /** Initial pointer definition to resolve */
+ initialPointer?: Pointer;
+ /** Initial machine state specification */
+ initialState?: MockStateSpec;
+ /** Whether to show the pointer JSON input */
+ showPointerInput?: boolean;
+ /** Whether to show the state editor */
+ showStateEditor?: boolean;
+ /** Whether to show full hex values */
+ showFullValues?: boolean;
+ /** Custom height for the component */
+ height?: string;
+}
+
+/**
+ * Interactive pointer resolution visualizer.
+ */
+export function PointerResolver({
+ initialPointer,
+ initialState = {},
+ showPointerInput = true,
+ showStateEditor = true,
+ showFullValues = false,
+ height,
+}: PointerResolverProps): JSX.Element {
+ return (
+
+ Loading pointer resolver...
+
+ }
+ >
+ {() => (
+
+ )}
+
+ );
+}
+
+export interface SimplePointerDisplayProps {
+ /** Pointer definition to display */
+ pointer: Pointer;
+ /** Machine state to resolve against */
+ state: MockStateSpec;
+ /** Whether to show full hex values */
+ showFullValues?: boolean;
+}
+
+/**
+ * Simple read-only pointer display without editing capabilities.
+ */
+export function SimplePointerDisplay({
+ pointer,
+ state,
+ showFullValues = false,
+}: SimplePointerDisplayProps): JSX.Element {
+ return (
+ Loading...}
+ >
+ {() => (
+
+ )}
+
+ );
+}
diff --git a/packages/web/src/theme/PointersExample/RegionMap.css b/packages/web/src/theme/PointersExample/RegionMap.css
new file mode 100644
index 000000000..0a68a7982
--- /dev/null
+++ b/packages/web/src/theme/PointersExample/RegionMap.css
@@ -0,0 +1,117 @@
+/**
+ * Styles for RegionMap component.
+ */
+
+.region-map {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.region-map-empty {
+ padding: 1.5rem;
+ text-align: center;
+}
+
+.region-map-empty-text {
+ font-size: 0.875rem;
+ color: var(--pointers-text-muted);
+}
+
+.region-map-entry {
+ border: 1px solid var(--pointers-border-secondary);
+ border-radius: 6px;
+ overflow: hidden;
+}
+
+.region-map-entry.selected {
+ border-color: var(--pointers-border-highlight);
+}
+
+.region-map-name {
+ padding: 0.5rem 0.75rem;
+ font-size: 0.8125rem;
+ font-weight: 600;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ background: var(--pointers-bg-secondary);
+ color: var(--pointers-text-primary);
+}
+
+.region-map-regions {
+ display: flex;
+ flex-direction: column;
+}
+
+.region-map-region {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 0.75rem;
+ border-top: 1px solid var(--pointers-border-secondary);
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.region-map-region:hover {
+ background: var(--pointers-bg-hover);
+}
+
+.region-map-region:focus {
+ outline: none;
+ background: var(--pointers-bg-highlight);
+}
+
+.region-location-badge {
+ display: inline-block;
+ padding: 0.125rem 0.375rem;
+ font-size: 0.6875rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ border-radius: 3px;
+ white-space: nowrap;
+}
+
+/* Region type colors */
+.region-storage .region-location-badge {
+ color: var(--pointers-region-storage);
+ background: var(--pointers-region-storage-bg);
+}
+
+.region-memory .region-location-badge {
+ color: var(--pointers-region-memory);
+ background: var(--pointers-region-memory-bg);
+}
+
+.region-stack .region-location-badge {
+ color: var(--pointers-region-stack);
+ background: var(--pointers-region-stack-bg);
+}
+
+.region-calldata .region-location-badge {
+ color: var(--pointers-region-calldata);
+ background: var(--pointers-region-calldata-bg);
+}
+
+.region-returndata .region-location-badge {
+ color: var(--pointers-region-returndata);
+ background: var(--pointers-region-returndata-bg);
+}
+
+.region-code .region-location-badge {
+ color: var(--pointers-region-code);
+ background: var(--pointers-region-code-bg);
+}
+
+.region-transient .region-location-badge {
+ color: var(--pointers-region-transient);
+ background: var(--pointers-region-transient-bg);
+}
+
+.region-details {
+ font-size: 0.75rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ color: var(--pointers-text-secondary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
diff --git a/packages/web/src/theme/PointersExample/RegionOutput.css b/packages/web/src/theme/PointersExample/RegionOutput.css
new file mode 100644
index 000000000..c9112a4b0
--- /dev/null
+++ b/packages/web/src/theme/PointersExample/RegionOutput.css
@@ -0,0 +1,149 @@
+/**
+ * Styles for RegionOutput component.
+ */
+
+.region-output {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.region-output-empty {
+ padding: 1.5rem;
+ text-align: center;
+}
+
+.region-output-empty-text {
+ font-size: 0.875rem;
+ color: var(--pointers-text-muted);
+}
+
+.region-output-summary {
+ font-size: 0.75rem;
+ color: var(--pointers-text-secondary);
+ padding: 0 0.25rem;
+}
+
+.region-output-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.region-output-item {
+ border: 1px solid var(--pointers-border-secondary);
+ border-radius: 6px;
+ padding: 0.75rem;
+ background: var(--pointers-bg-secondary);
+}
+
+.region-output-header {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-bottom: 0.5rem;
+}
+
+.region-output-index {
+ font-size: 0.6875rem;
+ font-weight: 600;
+ color: var(--pointers-text-muted);
+ font-family: var(--ifm-font-family-monospace, monospace);
+}
+
+.region-name-badge {
+ display: inline-block;
+ padding: 0.125rem 0.375rem;
+ font-size: 0.6875rem;
+ font-weight: 500;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ border-radius: 3px;
+ background: var(--pointers-accent-blue-bg);
+ color: var(--pointers-accent-blue);
+}
+
+.region-output-details {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ margin-bottom: 0.5rem;
+}
+
+.region-output-field {
+ display: flex;
+ align-items: baseline;
+ gap: 0.25rem;
+}
+
+.region-field-label {
+ font-size: 0.6875rem;
+ color: var(--pointers-text-muted);
+}
+
+.region-field-value {
+ font-size: 0.75rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ color: var(--pointers-syntax-number);
+ background: transparent;
+ padding: 0;
+}
+
+.region-output-value {
+ display: flex;
+ align-items: baseline;
+ gap: 0.5rem;
+ padding: 0.5rem;
+ background: var(--pointers-data-bg);
+ border: 1px solid var(--pointers-data-border);
+ border-radius: 4px;
+}
+
+.region-value-label {
+ font-size: 0.6875rem;
+ font-weight: 600;
+ color: var(--pointers-text-secondary);
+ white-space: nowrap;
+}
+
+.region-value-data {
+ font-size: 0.8125rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ color: var(--pointers-text-code);
+ word-break: break-all;
+ background: transparent;
+ padding: 0;
+}
+
+.region-value-data.no-value {
+ color: var(--pointers-text-muted);
+ font-style: italic;
+}
+
+/* Region type borders */
+.region-output-item.region-storage {
+ border-left: 3px solid var(--pointers-region-storage);
+}
+
+.region-output-item.region-memory {
+ border-left: 3px solid var(--pointers-region-memory);
+}
+
+.region-output-item.region-stack {
+ border-left: 3px solid var(--pointers-region-stack);
+}
+
+.region-output-item.region-calldata {
+ border-left: 3px solid var(--pointers-region-calldata);
+}
+
+.region-output-item.region-returndata {
+ border-left: 3px solid var(--pointers-region-returndata);
+}
+
+.region-output-item.region-code {
+ border-left: 3px solid var(--pointers-region-code);
+}
+
+.region-output-item.region-transient {
+ border-left: 3px solid var(--pointers-region-transient);
+}
diff --git a/packages/web/src/theme/PointersExample/ResolutionVisualizer.css b/packages/web/src/theme/PointersExample/ResolutionVisualizer.css
new file mode 100644
index 000000000..0365d2776
--- /dev/null
+++ b/packages/web/src/theme/PointersExample/ResolutionVisualizer.css
@@ -0,0 +1,252 @@
+/**
+ * Styles for ResolutionVisualizer component.
+ */
+
+.resolution-visualizer {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ padding: 1rem;
+ font-family: var(--ifm-font-family-base, system-ui, sans-serif);
+}
+
+.resolution-visualizer-inputs {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 1rem;
+}
+
+@media (max-width: 768px) {
+ .resolution-visualizer-inputs {
+ grid-template-columns: 1fr;
+ }
+}
+
+.resolution-visualizer-panel {
+ border: 1px solid var(--pointers-border-primary);
+ border-radius: 6px;
+ background: var(--pointers-bg-primary);
+ padding: 1rem;
+}
+
+.panel-title {
+ margin: 0 0 0.75rem 0;
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--pointers-text-primary);
+}
+
+/* Pointer Input */
+.pointer-input {
+ width: 100%;
+ min-height: 150px;
+ padding: 0.75rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ border: 1px solid var(--pointers-border-secondary);
+ border-radius: 4px;
+ background: var(--pointers-bg-code);
+ color: var(--pointers-text-code);
+ resize: vertical;
+}
+
+.pointer-input:focus {
+ outline: none;
+ border-color: var(--pointers-border-highlight);
+}
+
+.pointer-parse-error {
+ margin-top: 0.5rem;
+ padding: 0.5rem;
+ font-size: 0.75rem;
+ color: var(--pointers-accent-red);
+ background: rgba(207, 34, 46, 0.1);
+ border-radius: 4px;
+}
+
+/* State Editor */
+.state-editor {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.state-editor-section {
+ border: 1px solid var(--pointers-border-secondary);
+ border-radius: 4px;
+ overflow: hidden;
+}
+
+.state-editor-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0.5rem 0.75rem;
+ background: var(--pointers-bg-secondary);
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--pointers-text-secondary);
+}
+
+.state-editor-add-btn {
+ padding: 0.125rem 0.5rem;
+ font-size: 0.6875rem;
+ border: 1px solid var(--pointers-border-primary);
+ border-radius: 3px;
+ background: var(--pointers-bg-primary);
+ color: var(--pointers-text-secondary);
+ cursor: pointer;
+}
+
+.state-editor-add-btn:hover {
+ background: var(--pointers-bg-hover);
+ color: var(--pointers-text-primary);
+}
+
+.state-editor-empty {
+ padding: 0.75rem;
+ font-size: 0.75rem;
+ color: var(--pointers-text-muted);
+ text-align: center;
+}
+
+.state-editor-entries {
+ padding: 0.5rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.375rem;
+}
+
+.state-editor-entry {
+ display: flex;
+ align-items: center;
+ gap: 0.375rem;
+}
+
+.state-editor-index {
+ font-size: 0.6875rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ color: var(--pointers-text-muted);
+ width: 2rem;
+}
+
+.state-editor-input {
+ flex: 1;
+ padding: 0.375rem 0.5rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ font-size: 0.75rem;
+ border: 1px solid var(--pointers-border-secondary);
+ border-radius: 3px;
+ background: var(--pointers-bg-code);
+ color: var(--pointers-text-code);
+}
+
+.state-editor-input.slot {
+ flex: 0.4;
+}
+
+.state-editor-input.value {
+ flex: 0.6;
+}
+
+.state-editor-input:focus {
+ outline: none;
+ border-color: var(--pointers-border-highlight);
+}
+
+.state-editor-arrow {
+ color: var(--pointers-text-muted);
+ font-family: var(--ifm-font-family-monospace, monospace);
+}
+
+.state-editor-remove-btn {
+ padding: 0.125rem 0.375rem;
+ font-size: 0.875rem;
+ line-height: 1;
+ border: none;
+ background: transparent;
+ color: var(--pointers-text-muted);
+ cursor: pointer;
+}
+
+.state-editor-remove-btn:hover {
+ color: var(--pointers-accent-red);
+}
+
+.state-editor-textarea {
+ width: 100%;
+ padding: 0.5rem;
+ font-family: var(--ifm-font-family-monospace, monospace);
+ font-size: 0.75rem;
+ border: none;
+ background: var(--pointers-bg-code);
+ color: var(--pointers-text-code);
+ resize: vertical;
+}
+
+.state-editor-textarea:focus {
+ outline: none;
+}
+
+/* Controls */
+.resolution-visualizer-controls {
+ display: flex;
+ justify-content: center;
+ padding: 0.5rem 0;
+}
+
+.resolve-button {
+ padding: 0.625rem 1.5rem;
+ font-size: 0.875rem;
+ font-weight: 500;
+ border: none;
+ border-radius: 6px;
+ background: var(--ifm-color-primary);
+ color: white;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.resolve-button:hover:not(:disabled) {
+ background: var(--ifm-color-primary-dark);
+}
+
+.resolve-button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Output */
+.resolution-visualizer-output {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.resolution-error {
+ padding: 1rem;
+ border: 1px solid var(--pointers-accent-red);
+ border-radius: 6px;
+ background: rgba(207, 34, 46, 0.05);
+}
+
+.resolution-error h4 {
+ margin: 0 0 0.5rem 0;
+ font-size: 0.875rem;
+ color: var(--pointers-accent-red);
+}
+
+.resolution-error pre {
+ margin: 0;
+ padding: 0.5rem;
+ font-size: 0.75rem;
+ background: var(--pointers-bg-code);
+ border-radius: 4px;
+ overflow-x: auto;
+}
+
+.regions-panel,
+.output-panel {
+ background: var(--pointers-bg-primary);
+}
diff --git a/packages/web/src/theme/PointersExample/index.ts b/packages/web/src/theme/PointersExample/index.ts
new file mode 100644
index 000000000..0dbebb44f
--- /dev/null
+++ b/packages/web/src/theme/PointersExample/index.ts
@@ -0,0 +1,27 @@
+// Re-export from @ethdebug/pointers-react
+export {
+ PointerResolverProvider,
+ usePointerResolverContext,
+ ResolutionVisualizer,
+ RegionMap,
+ RegionOutput,
+ usePointerResolution,
+ createMockState,
+ formatData,
+ formatDataShort,
+ type PointerResolverProviderProps,
+ type PointerResolutionState,
+ type ResolutionVisualizerProps,
+ type RegionMapProps,
+ type RegionOutputProps,
+ type UsePointerResolutionOptions,
+ type ResolutionResult,
+ type MockStateSpec,
+} from "@ethdebug/pointers-react";
+
+// Local Docusaurus-specific components
+export * from "./PointerResolver";
+export * from "./PointerPlaygroundContext";
+export * from "./PointerDrawer";
+export * from "./PointerExample";
+export * from "./PointerPlayground";
diff --git a/packages/web/src/theme/PointersExample/variables.css b/packages/web/src/theme/PointersExample/variables.css
new file mode 100644
index 000000000..57812fc89
--- /dev/null
+++ b/packages/web/src/theme/PointersExample/variables.css
@@ -0,0 +1,129 @@
+/**
+ * CSS custom properties for @ethdebug/pointers-react components.
+ *
+ * These variables provide theme-aware styling that integrates with
+ * Docusaurus/Infima theming.
+ */
+
+:root {
+ /* Background colors */
+ --pointers-bg-primary: #ffffff;
+ --pointers-bg-secondary: #f5f6f7;
+ --pointers-bg-code: #f5f6f7;
+ --pointers-bg-hover: rgba(0, 0, 0, 0.05);
+ --pointers-bg-highlight: rgba(9, 105, 218, 0.1);
+
+ /* Border colors */
+ --pointers-border-primary: #dadde1;
+ --pointers-border-secondary: #eaecef;
+ --pointers-border-highlight: #0969da;
+
+ /* Text colors */
+ --pointers-text-primary: #1c1e21;
+ --pointers-text-secondary: #606770;
+ --pointers-text-muted: #898989;
+ --pointers-text-code: #1c1e21;
+
+ /* Syntax highlighting - light theme */
+ --pointers-syntax-keyword: #0550ae;
+ --pointers-syntax-type: #6f42c1;
+ --pointers-syntax-string: #0a3069;
+ --pointers-syntax-number: #0550ae;
+ --pointers-syntax-property: #8250df;
+
+ /* Region type colors */
+ --pointers-region-storage: #1a7f37;
+ --pointers-region-storage-bg: rgba(26, 127, 55, 0.1);
+ --pointers-region-memory: #0969da;
+ --pointers-region-memory-bg: rgba(9, 105, 218, 0.1);
+ --pointers-region-stack: #8250df;
+ --pointers-region-stack-bg: rgba(130, 80, 223, 0.1);
+ --pointers-region-calldata: #bf8700;
+ --pointers-region-calldata-bg: rgba(191, 135, 0, 0.1);
+ --pointers-region-returndata: #cf222e;
+ --pointers-region-returndata-bg: rgba(207, 34, 46, 0.1);
+ --pointers-region-code: #6e7781;
+ --pointers-region-code-bg: rgba(110, 119, 129, 0.1);
+ --pointers-region-transient: #953800;
+ --pointers-region-transient-bg: rgba(149, 56, 0, 0.1);
+
+ /* Accent colors */
+ --pointers-accent-blue: #0969da;
+ --pointers-accent-blue-bg: rgba(9, 105, 218, 0.1);
+ --pointers-accent-green: #1a7f37;
+ --pointers-accent-green-bg: rgba(26, 127, 55, 0.1);
+ --pointers-accent-red: #cf222e;
+ --pointers-accent-purple: #8250df;
+
+ /* Data display */
+ --pointers-data-bg: #f6f8fa;
+ --pointers-data-border: #d0d7de;
+}
+
+[data-theme="dark"] {
+ /* Background colors */
+ --pointers-bg-primary: #1e1e1e;
+ --pointers-bg-secondary: #2d2d30;
+ --pointers-bg-code: #2d2d30;
+ --pointers-bg-hover: rgba(255, 255, 255, 0.05);
+ --pointers-bg-highlight: rgba(86, 156, 214, 0.15);
+
+ /* Border colors */
+ --pointers-border-primary: #3e3e42;
+ --pointers-border-secondary: #454545;
+ --pointers-border-highlight: #569cd6;
+
+ /* Text colors */
+ --pointers-text-primary: #cccccc;
+ --pointers-text-secondary: #969696;
+ --pointers-text-muted: #858585;
+ --pointers-text-code: #d4d4d4;
+
+ /* Syntax highlighting - dark theme */
+ --pointers-syntax-keyword: #569cd6;
+ --pointers-syntax-type: #4ec9b0;
+ --pointers-syntax-string: #ce9178;
+ --pointers-syntax-number: #b5cea8;
+ --pointers-syntax-property: #9cdcfe;
+
+ /* Region type colors */
+ --pointers-region-storage: #4ec9b0;
+ --pointers-region-storage-bg: rgba(78, 201, 176, 0.15);
+ --pointers-region-memory: #569cd6;
+ --pointers-region-memory-bg: rgba(86, 156, 214, 0.15);
+ --pointers-region-stack: #c586c0;
+ --pointers-region-stack-bg: rgba(197, 134, 192, 0.15);
+ --pointers-region-calldata: #dcdcaa;
+ --pointers-region-calldata-bg: rgba(220, 220, 170, 0.15);
+ --pointers-region-returndata: #f14c4c;
+ --pointers-region-returndata-bg: rgba(241, 76, 76, 0.15);
+ --pointers-region-code: #858585;
+ --pointers-region-code-bg: rgba(133, 133, 133, 0.15);
+ --pointers-region-transient: #ce9178;
+ --pointers-region-transient-bg: rgba(206, 145, 120, 0.15);
+
+ /* Accent colors */
+ --pointers-accent-blue: #569cd6;
+ --pointers-accent-blue-bg: rgba(86, 156, 214, 0.15);
+ --pointers-accent-green: #4ec9b0;
+ --pointers-accent-green-bg: rgba(78, 201, 176, 0.2);
+ --pointers-accent-red: #f14c4c;
+ --pointers-accent-purple: #c586c0;
+
+ /* Data display */
+ --pointers-data-bg: #252526;
+ --pointers-data-border: #3e3e42;
+}
+
+/* Pointer resolver container */
+.pointer-resolver {
+ border: 1px solid var(--ifm-color-emphasis-300);
+ border-radius: var(--ifm-global-radius);
+ background: var(--ifm-background-color);
+ overflow: hidden;
+}
+
+.pointer-resolver-simple {
+ border: none;
+ background: transparent;
+}
diff --git a/packages/web/src/theme/ProgramExample/Details.tsx b/packages/web/src/theme/ProgramExample/Details.tsx
index 68bb9cee6..c99452080 100644
--- a/packages/web/src/theme/ProgramExample/Details.tsx
+++ b/packages/web/src/theme/ProgramExample/Details.tsx
@@ -1,8 +1,10 @@
import Admonition from "@theme/Admonition";
import Link from "@docusaurus/Link";
import { Program } from "@ethdebug/format";
-import { useProgramExampleContext } from "./ProgramExampleContext";
-import { HighlightedInstruction } from "./HighlightedInstruction";
+import {
+ useProgramExampleContext,
+ HighlightedInstruction,
+} from "@ethdebug/programs-react";
// imported for style legend
import "./SourceContents.css";
diff --git a/packages/web/src/theme/ProgramExample/TraceControls.css b/packages/web/src/theme/ProgramExample/TraceControls.css
new file mode 100644
index 000000000..6b0b232fd
--- /dev/null
+++ b/packages/web/src/theme/ProgramExample/TraceControls.css
@@ -0,0 +1,123 @@
+/**
+ * Styles for TraceControls component in Docusaurus context.
+ */
+
+/* CSS variables for programs-react components */
+:root {
+ --programs-bg-primary: var(--ifm-background-color);
+ --programs-bg-secondary: var(--ifm-background-surface-color);
+ --programs-bg-code: var(--ifm-code-background);
+ --programs-bg-hover: var(--ifm-hover-overlay);
+ --programs-border-primary: var(--ifm-color-emphasis-300);
+ --programs-border-secondary: var(--ifm-color-emphasis-200);
+ --programs-border-highlight: var(--ifm-color-primary);
+ --programs-text-primary: var(--ifm-font-color-base);
+ --programs-text-secondary: var(--ifm-color-emphasis-700);
+ --programs-text-muted: var(--ifm-color-emphasis-600);
+ --programs-accent-blue: var(--ifm-color-primary);
+ --programs-accent-blue-bg: var(--ifm-color-primary-contrast-background);
+ --programs-accent-red: var(--ifm-color-danger);
+ --programs-accent-purple: #8250df;
+ --programs-accent-purple-bg: rgba(130, 80, 223, 0.1);
+ --programs-syntax-number: var(--ifm-color-success-dark);
+}
+
+[data-theme="dark"] {
+ --programs-accent-purple: #c586c0;
+ --programs-accent-purple-bg: rgba(197, 134, 192, 0.15);
+}
+
+.trace-controls {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.trace-controls-navigation {
+ display: flex;
+ gap: 0.25rem;
+ justify-content: center;
+}
+
+.trace-control-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.25rem;
+ height: 2.25rem;
+ font-size: 1rem;
+ border: 1px solid var(--ifm-color-emphasis-300);
+ border-radius: 4px;
+ background: var(--ifm-background-color);
+ color: var(--ifm-font-color-base);
+ cursor: pointer;
+ transition: all 0.15s;
+}
+
+.trace-control-btn:hover:not(:disabled) {
+ background: var(--ifm-hover-overlay);
+ border-color: var(--ifm-color-primary);
+}
+
+.trace-control-btn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+.trace-controls-info {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 1rem;
+ font-size: 0.8125rem;
+}
+
+.trace-step-counter {
+ color: var(--ifm-color-emphasis-700);
+}
+
+.trace-current-opcode code {
+ padding: 0.125rem 0.375rem;
+ font-size: 0.75rem;
+ background: var(--ifm-color-primary-contrast-background);
+ color: var(--ifm-color-primary);
+ border-radius: 3px;
+}
+
+/* Progress bar */
+.trace-progress {
+ position: relative;
+ height: 8px;
+ background: var(--ifm-color-emphasis-200);
+ border-radius: 4px;
+ cursor: pointer;
+ margin: 0.5rem 0;
+}
+
+.trace-progress:focus {
+ outline: 2px solid var(--ifm-color-primary);
+ outline-offset: 2px;
+}
+
+.trace-progress-fill {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 100%;
+ background: var(--ifm-color-primary);
+ border-radius: 4px;
+ transition: width 0.1s ease-out;
+}
+
+.trace-progress-handle {
+ position: absolute;
+ top: 50%;
+ width: 14px;
+ height: 14px;
+ background: var(--ifm-color-primary);
+ border: 2px solid var(--ifm-background-color);
+ border-radius: 50%;
+ transform: translate(-50%, -50%);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
+ transition: left 0.1s ease-out;
+}
diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.css b/packages/web/src/theme/ProgramExample/TraceDrawer.css
new file mode 100644
index 000000000..3de56f99f
--- /dev/null
+++ b/packages/web/src/theme/ProgramExample/TraceDrawer.css
@@ -0,0 +1,346 @@
+/**
+ * Trace Drawer Styles
+ */
+
+/* Layout */
+.trace-drawer .drawer-content {
+ padding: 0;
+}
+
+.trace-drawer-layout {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ height: 100%;
+ overflow: hidden;
+}
+
+.trace-drawer-editor {
+ border-right: 1px solid var(--ifm-color-emphasis-300);
+ overflow: hidden;
+}
+
+.trace-drawer-output {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+/* Header buttons */
+.trace-drawer-btn {
+ padding: 6px 14px;
+ border: none;
+ border-radius: 4px;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition:
+ background 0.2s,
+ opacity 0.2s;
+}
+
+.trace-drawer-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.compile-btn {
+ background: var(--ifm-color-emphasis-200);
+ color: var(--ifm-color-content);
+}
+
+.compile-btn:hover:not(:disabled) {
+ background: var(--ifm-color-emphasis-300);
+}
+
+.trace-btn {
+ background: var(--ifm-color-primary);
+ color: white;
+}
+
+.trace-btn:hover:not(:disabled) {
+ background: var(--ifm-color-primary-dark);
+}
+
+/* Error display */
+.trace-drawer-error {
+ padding: 12px 16px;
+ background: var(--ifm-color-danger-contrast-background);
+ color: var(--ifm-color-danger-darkest);
+ font-size: 13px;
+ border-bottom: 1px solid var(--ifm-color-danger-light);
+}
+
+/* Placeholder */
+.trace-drawer-placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: var(--ifm-color-content-secondary);
+ font-size: 14px;
+ padding: 20px;
+ text-align: center;
+}
+
+/* Trace viewer */
+.trace-viewer {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: hidden;
+}
+
+.trace-controls {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 8px 12px;
+ background: var(--ifm-background-surface-color);
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+ flex-shrink: 0;
+}
+
+.trace-nav-btn {
+ width: 28px;
+ height: 28px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--ifm-color-emphasis-100);
+ border: 1px solid var(--ifm-color-emphasis-300);
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 12px;
+ transition: background 0.15s;
+}
+
+.trace-nav-btn:hover:not(:disabled) {
+ background: var(--ifm-color-emphasis-200);
+}
+
+.trace-nav-btn:disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+}
+
+.trace-step-info {
+ padding: 0 12px;
+ font-size: 13px;
+ font-weight: 500;
+ font-variant-numeric: tabular-nums;
+ color: var(--ifm-color-content);
+ min-width: 80px;
+ text-align: center;
+}
+
+/* Trace panels */
+.trace-panels {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ flex: 1;
+ overflow: hidden;
+ gap: 1px;
+ background: var(--ifm-color-emphasis-200);
+}
+
+.trace-panel {
+ background: var(--ifm-background-color);
+ overflow: auto;
+}
+
+.panel-header {
+ padding: 6px 12px;
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--ifm-color-content-secondary);
+ background: var(--ifm-background-surface-color);
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+ position: sticky;
+ top: 0;
+ z-index: 1;
+}
+
+.current-opcode {
+ padding: 10px 12px;
+ background: var(--ifm-color-primary-lightest);
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.current-opcode code {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--ifm-color-primary-darkest);
+}
+
+.current-opcode .opcode-pc {
+ font-size: 12px;
+ color: var(--ifm-color-content-secondary);
+}
+
+/* Opcode list */
+.opcode-list {
+ padding: 4px 0;
+}
+
+.opcode-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 4px 12px;
+ font-size: 12px;
+ cursor: pointer;
+ transition: background 0.1s;
+}
+
+.opcode-item:hover {
+ background: var(--ifm-color-emphasis-100);
+}
+
+.opcode-item.active {
+ background: var(--ifm-color-primary-lightest);
+}
+
+.opcode-index {
+ min-width: 28px;
+ color: var(--ifm-color-content-secondary);
+ font-size: 11px;
+ text-align: right;
+}
+
+.opcode-pc {
+ min-width: 48px;
+ color: var(--ifm-color-content-secondary);
+ font-family: var(--ifm-font-family-monospace);
+ font-size: 11px;
+}
+
+.opcode-name {
+ font-family: var(--ifm-font-family-monospace);
+ font-size: 12px;
+ font-weight: 500;
+ color: var(--ifm-color-content);
+}
+
+.opcode-ellipsis {
+ padding: 4px 12px;
+ font-size: 11px;
+ color: var(--ifm-color-content-secondary);
+ font-style: italic;
+}
+
+/* Stack display */
+.stack-list {
+ padding: 8px 12px;
+}
+
+.stack-empty {
+ padding: 8px 12px;
+ color: var(--ifm-color-content-secondary);
+ font-style: italic;
+ font-size: 12px;
+}
+
+.stack-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 2px 0;
+ font-size: 12px;
+}
+
+.stack-index {
+ min-width: 24px;
+ color: var(--ifm-color-content-secondary);
+ font-size: 11px;
+}
+
+.stack-value {
+ font-family: var(--ifm-font-family-monospace);
+ color: var(--ifm-color-content);
+ word-break: break-all;
+}
+
+.stack-more {
+ color: var(--ifm-color-content-secondary);
+ font-style: italic;
+}
+
+/* Variables display */
+.variables-list {
+ padding: 8px 12px;
+}
+
+.variable-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 2px 0;
+ font-size: 12px;
+}
+
+.variable-name {
+ font-family: var(--ifm-font-family-monospace);
+ font-weight: 500;
+ color: var(--ifm-color-content);
+}
+
+.variable-type {
+ font-family: var(--ifm-font-family-monospace);
+ font-size: 11px;
+ color: var(--ifm-color-content-secondary);
+}
+
+/* Storage display */
+.storage-list {
+ padding: 8px 12px;
+}
+
+.storage-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 0;
+ font-size: 12px;
+}
+
+.storage-slot {
+ font-family: var(--ifm-font-family-monospace);
+ color: var(--ifm-color-content-secondary);
+}
+
+.storage-arrow {
+ color: var(--ifm-color-content-secondary);
+ font-size: 10px;
+}
+
+.storage-value {
+ font-family: var(--ifm-font-family-monospace);
+ color: var(--ifm-color-content);
+ word-break: break-all;
+}
+
+/* Mobile responsive */
+@media (max-width: 768px) {
+ .trace-drawer-layout {
+ grid-template-columns: 1fr;
+ grid-template-rows: 40% 60%;
+ }
+
+ .trace-drawer-editor {
+ border-right: none;
+ border-bottom: 1px solid var(--ifm-color-emphasis-300);
+ }
+
+ .trace-panels {
+ grid-template-columns: 1fr;
+ grid-template-rows: 1fr 1fr;
+ }
+}
diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx
new file mode 100644
index 000000000..31c4ae350
--- /dev/null
+++ b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx
@@ -0,0 +1,558 @@
+/**
+ * Trace Drawer - A pull-out drawer for compiling and tracing BUG code.
+ *
+ * Features:
+ * - BUG code editor
+ * - Compiles via @ethdebug/bugc
+ * - Executes via @ethdebug/evm
+ * - Step-through trace visualization
+ */
+
+import React, { useState, useCallback, useEffect, useMemo } from "react";
+import BrowserOnly from "@docusaurus/BrowserOnly";
+import { compile as bugCompile, Severity, type Evm } from "@ethdebug/bugc";
+import {
+ Editor,
+ type BytecodeOutput,
+ type SourceRange,
+ extractSourceRange,
+} from "@ethdebug/bugc-react";
+import { Executor, createTraceCollector, type TraceStep } from "@ethdebug/evm";
+import { Drawer } from "@theme/Drawer";
+import { useTracePlayground } from "./TracePlaygroundContext";
+
+import "./TraceDrawer.css";
+
+export function TraceDrawer(): JSX.Element {
+ return (
+ {() => }
+ );
+}
+
+interface CompileResult {
+ success: boolean;
+ error?: string;
+ bytecode?: BytecodeOutput;
+}
+
+function TraceDrawerContent(): JSX.Element {
+ const { example, isOpen, toggleDrawer, closeDrawer, setSource } =
+ useTracePlayground();
+
+ const [source, setLocalSource] = useState(example?.source ?? "");
+ const [compileResult, setCompileResult] = useState(
+ null,
+ );
+ const [isCompiling, setIsCompiling] = useState(false);
+ const [trace, setTrace] = useState([]);
+ const [currentStep, setCurrentStep] = useState(0);
+ const [isTracing, setIsTracing] = useState(false);
+ const [traceError, setTraceError] = useState(null);
+ const [storage, setStorage] = useState>({});
+
+ // Build PC -> instruction map for source highlighting
+ const pcToInstruction = useMemo(() => {
+ const map = new Map();
+ if (!compileResult?.bytecode?.runtimeInstructions) return map;
+
+ let pc = 0;
+ for (const instruction of compileResult.bytecode.runtimeInstructions) {
+ map.set(pc, instruction);
+ pc += 1 + (instruction.immediates?.length || 0);
+ }
+ return map;
+ }, [compileResult?.bytecode?.runtimeInstructions]);
+
+ // Get highlighted source ranges for current trace step
+ const highlightedRanges = useMemo((): SourceRange[] => {
+ if (trace.length === 0 || currentStep >= trace.length) return [];
+
+ const step = trace[currentStep];
+ const instruction = pcToInstruction.get(step.pc);
+ if (!instruction?.debug?.context) return [];
+
+ return extractSourceRange(instruction.debug.context);
+ }, [trace, currentStep, pcToInstruction]);
+
+ // Extract variables from current instruction context
+ const currentVariables = useMemo(() => {
+ if (trace.length === 0 || currentStep >= trace.length) return [];
+
+ const step = trace[currentStep];
+ const instruction = pcToInstruction.get(step.pc);
+ if (!instruction?.debug?.context) return [];
+
+ return extractVariables(instruction.debug.context);
+ }, [trace, currentStep, pcToInstruction]);
+
+ // Sync source from context when example changes
+ useEffect(() => {
+ if (example?.source) {
+ setLocalSource(example.source);
+ // Reset state when loading new example
+ setCompileResult(null);
+ setTrace([]);
+ setCurrentStep(0);
+ setTraceError(null);
+ setStorage({});
+ }
+ }, [example]);
+
+ const handleSourceChange = useCallback(
+ (newSource: string) => {
+ setLocalSource(newSource);
+ setSource(newSource);
+ },
+ [setSource],
+ );
+
+ const handleCompile = useCallback(async () => {
+ setIsCompiling(true);
+ setCompileResult(null);
+ setTrace([]);
+ setTraceError(null);
+
+ try {
+ const result = await bugCompile({
+ to: "bytecode",
+ source,
+ optimizer: { level: 0 },
+ });
+
+ if (!result.success) {
+ const errors = result.messages[Severity.Error] || [];
+ setCompileResult({
+ success: false,
+ error: errors[0]?.message || "Compilation failed",
+ });
+ } else {
+ setCompileResult({
+ success: true,
+ bytecode: {
+ runtime: result.value.bytecode.runtime,
+ create: result.value.bytecode.create,
+ runtimeInstructions: result.value.bytecode.runtimeInstructions,
+ createInstructions: result.value.bytecode.createInstructions,
+ },
+ });
+ }
+ } catch (e) {
+ setCompileResult({
+ success: false,
+ error: e instanceof Error ? e.message : String(e),
+ });
+ } finally {
+ setIsCompiling(false);
+ }
+ }, [source]);
+
+ const handleTrace = useCallback(async () => {
+ if (!compileResult?.bytecode) return;
+
+ setIsTracing(true);
+ setTraceError(null);
+ setTrace([]);
+ setStorage({});
+
+ try {
+ const executor = new Executor();
+
+ // Deploy using create bytecode
+ if (compileResult.bytecode.create) {
+ const createHex = Array.from(compileResult.bytecode.create)
+ .map((b) => b.toString(16).padStart(2, "0"))
+ .join("");
+ await executor.deploy(createHex);
+ }
+
+ // Execute runtime bytecode with trace collection
+ const [handler, getTrace] = createTraceCollector();
+ await executor.execute({}, handler);
+
+ const collectedTrace = getTrace();
+ setTrace(collectedTrace.steps);
+ setCurrentStep(0);
+
+ // Read storage after execution
+ const storageEntries: Record = {};
+ for (let i = 0n; i < 16n; i++) {
+ const value = await executor.getStorage(i);
+ if (value !== 0n) {
+ const slot = `0x${i.toString(16).padStart(2, "0")}`;
+ storageEntries[slot] = `0x${value.toString(16).padStart(64, "0")}`;
+ }
+ }
+ setStorage(storageEntries);
+ } catch (e) {
+ setTraceError(e instanceof Error ? e.message : String(e));
+ } finally {
+ setIsTracing(false);
+ }
+ }, [compileResult]);
+
+ const stepForward = () => {
+ setCurrentStep((prev) => Math.min(prev + 1, trace.length - 1));
+ };
+
+ const stepBackward = () => {
+ setCurrentStep((prev) => Math.max(prev - 1, 0));
+ };
+
+ const jumpToStart = () => setCurrentStep(0);
+ const jumpToEnd = () => setCurrentStep(trace.length - 1);
+
+ const currentTraceStep = trace[currentStep];
+ const hasTrace = trace.length > 0;
+ const canTrace = compileResult?.success && compileResult.bytecode;
+
+ const headerActions = (
+ <>
+
+ {isCompiling ? "Compiling..." : "Compile"}
+
+
+ {isTracing ? "Tracing..." : "Run Trace"}
+
+ >
+ );
+
+ return (
+
+
+ {/* Left: Editor */}
+
+
+
+
+ {/* Right: Output */}
+
+ {compileResult && !compileResult.success && (
+
+ Compile Error: {compileResult.error}
+
+ )}
+
+ {traceError && (
+
+ Trace Error: {traceError}
+
+ )}
+
+ {!hasTrace && canTrace && (
+
+ Click Run Trace to execute the bytecode
+
+ )}
+
+ {!canTrace && !compileResult?.error && (
+
+ Click Compile to compile the BUG code
+
+ )}
+
+ {hasTrace && (
+
+
+
+ ⏮
+
+
+ ◀
+
+
+ {currentStep + 1} / {trace.length}
+
+ = trace.length - 1}
+ className="trace-nav-btn"
+ title="Step forward"
+ >
+ ▶
+
+ = trace.length - 1}
+ className="trace-nav-btn"
+ title="Jump to end"
+ >
+ ⏭
+
+
+
+
+
+
+
+ {currentTraceStep && (
+ <>
+
+ {currentTraceStep.opcode}
+
+ @ 0x{currentTraceStep.pc.toString(16)}
+
+
+
+
Stack
+
+
+ {currentVariables.length > 0 && (
+ <>
+
Variables
+
+ >
+ )}
+
+ {Object.keys(storage).length > 0 && (
+ <>
+
Storage
+
+ >
+ )}
+ >
+ )}
+
+
+
+ )}
+
+
+
+ );
+}
+
+interface OpcodeListProps {
+ trace: TraceStep[];
+ currentStep: number;
+ onStepClick: (index: number) => void;
+}
+
+function OpcodeList({
+ trace,
+ currentStep,
+ onStepClick,
+}: OpcodeListProps): JSX.Element {
+ // Show a window around the current step
+ const windowSize = 8;
+ const start = Math.max(0, currentStep - windowSize);
+ const end = Math.min(trace.length, currentStep + windowSize + 1);
+ const visibleSteps = trace.slice(start, end);
+
+ return (
+
+ {start > 0 &&
... {start} above
}
+ {visibleSteps.map((step, i) => {
+ const index = start + i;
+ const isActive = index === currentStep;
+ return (
+
onStepClick(index)}
+ >
+ {index + 1}
+
+ 0x{step.pc.toString(16).padStart(4, "0")}
+
+ {step.opcode}
+
+ );
+ })}
+ {end < trace.length && (
+
... {trace.length - end} below
+ )}
+
+ );
+}
+
+interface StackDisplayProps {
+ stack: bigint[];
+}
+
+function StackDisplay({ stack }: StackDisplayProps): JSX.Element {
+ if (stack.length === 0) {
+ return (empty)
;
+ }
+
+ return (
+
+ {stack.slice(0, 6).map((value, i) => (
+
+ [{i}]
+ {formatBigInt(value)}
+
+ ))}
+ {stack.length > 6 && (
+
... {stack.length - 6} more
+ )}
+
+ );
+}
+
+interface StorageDisplayProps {
+ storage: Record;
+}
+
+function StorageDisplay({ storage }: StorageDisplayProps): JSX.Element {
+ const entries = Object.entries(storage);
+ return (
+
+ {entries.map(([slot, value]) => (
+
+ {slot}
+ →
+
+ {value.length > 18 ? value.slice(0, 18) + "..." : value}
+
+
+ ))}
+
+ );
+}
+
+function formatBigInt(value: bigint): string {
+ const hex = value.toString(16);
+ if (hex.length <= 8) {
+ return `0x${hex}`;
+ }
+ return `0x${hex.slice(0, 6)}...${hex.slice(-4)}`;
+}
+
+// Variable type extracted from debug context
+interface Variable {
+ identifier: string;
+ type?: string;
+}
+
+interface VariablesDisplayProps {
+ variables: Variable[];
+}
+
+function VariablesDisplay({ variables }: VariablesDisplayProps): JSX.Element {
+ return (
+
+ {variables.map((variable, i) => (
+
+ {variable.identifier}
+ {variable.type && (
+ {variable.type}
+ )}
+
+ ))}
+
+ );
+}
+
+/**
+ * Extract variables from an ethdebug format context object.
+ */
+function extractVariables(context: unknown): Variable[] {
+ if (!context || typeof context !== "object") {
+ return [];
+ }
+
+ const ctx = context as Record;
+ const variables: Variable[] = [];
+
+ // Handle direct variables array
+ if (ctx.variables && Array.isArray(ctx.variables)) {
+ for (const v of ctx.variables) {
+ if (v && typeof v === "object" && "identifier" in v) {
+ const variable = v as Record;
+ variables.push({
+ identifier: String(variable.identifier),
+ type: variable.type ? formatType(variable.type) : undefined,
+ });
+ }
+ }
+ }
+
+ // Handle "gather" context
+ if (ctx.gather && Array.isArray(ctx.gather)) {
+ for (const item of ctx.gather) {
+ variables.push(...extractVariables(item));
+ }
+ }
+
+ // Handle "frame" context
+ if (ctx.frame && typeof ctx.frame === "object") {
+ variables.push(...extractVariables(ctx.frame));
+ }
+
+ // Handle nested context
+ if (ctx.context) {
+ variables.push(...extractVariables(ctx.context));
+ }
+
+ return variables;
+}
+
+/**
+ * Format a type definition for display.
+ */
+function formatType(type: unknown): string {
+ if (!type || typeof type !== "object") {
+ return String(type);
+ }
+
+ const t = type as Record;
+
+ if (t.kind === "uint" && typeof t.bits === "number") {
+ return `uint${t.bits}`;
+ }
+ if (t.kind === "int" && typeof t.bits === "number") {
+ return `int${t.bits}`;
+ }
+ if (t.kind === "bool") {
+ return "bool";
+ }
+ if (t.kind === "address") {
+ return "address";
+ }
+ if (t.kind === "bytes" && typeof t.size === "number") {
+ return `bytes${t.size}`;
+ }
+
+ return JSON.stringify(type);
+}
+
+export default TraceDrawer;
diff --git a/packages/web/src/theme/ProgramExample/TraceExample.css b/packages/web/src/theme/ProgramExample/TraceExample.css
new file mode 100644
index 000000000..3c923e495
--- /dev/null
+++ b/packages/web/src/theme/ProgramExample/TraceExample.css
@@ -0,0 +1,92 @@
+/**
+ * Trace Example Card Styles
+ */
+
+.trace-example {
+ margin: 1rem 0;
+ border: 1px solid var(--ifm-color-emphasis-300);
+ border-radius: var(--ifm-global-radius);
+ background: var(--ifm-background-color);
+ overflow: hidden;
+}
+
+.trace-example-title {
+ padding: 0.75rem 1rem;
+ font-weight: 600;
+ font-size: 0.9rem;
+ background: var(--ifm-background-surface-color);
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+}
+
+.trace-example-description {
+ padding: 0.5rem 1rem;
+ font-size: 0.85rem;
+ color: var(--ifm-color-content-secondary);
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+}
+
+.trace-example-content {
+ position: relative;
+}
+
+.trace-example-code {
+ margin: 0;
+}
+
+/* Remove default margins from code block */
+.trace-example-code pre {
+ margin: 0;
+ border-radius: 0;
+}
+
+.trace-example-try-btn {
+ position: absolute;
+ top: 8px;
+ right: 8px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 12px;
+ background: var(--ifm-color-primary);
+ color: white;
+ border: none;
+ border-radius: 4px;
+ font-size: 12px;
+ font-weight: 500;
+ cursor: pointer;
+ transition:
+ background 0.2s,
+ transform 0.1s;
+ z-index: 5;
+}
+
+.trace-example-try-btn:hover {
+ background: var(--ifm-color-primary-dark);
+ transform: translateY(-1px);
+}
+
+.trace-example-try-btn:active {
+ transform: translateY(0);
+}
+
+.try-btn-icon {
+ font-size: 10px;
+ line-height: 1;
+}
+
+.try-btn-text {
+ font-family: var(--ifm-font-family-base);
+}
+
+/* Dark mode adjustments */
+[data-theme="dark"] .trace-example {
+ border-color: var(--ifm-color-emphasis-400);
+}
+
+[data-theme="dark"] .trace-example-title {
+ border-bottom-color: var(--ifm-color-emphasis-400);
+}
+
+[data-theme="dark"] .trace-example-description {
+ border-bottom-color: var(--ifm-color-emphasis-400);
+}
diff --git a/packages/web/src/theme/ProgramExample/TraceExample.tsx b/packages/web/src/theme/ProgramExample/TraceExample.tsx
new file mode 100644
index 000000000..54b8d71fd
--- /dev/null
+++ b/packages/web/src/theme/ProgramExample/TraceExample.tsx
@@ -0,0 +1,80 @@
+/**
+ * A trace example card with BUG code and a "Try it" button.
+ */
+
+import React from "react";
+import BrowserOnly from "@docusaurus/BrowserOnly";
+import CodeBlock from "@theme/CodeBlock";
+import { useTracePlaygroundOptional } from "./TracePlaygroundContext";
+
+import "./TraceExample.css";
+
+export interface TraceExampleProps {
+ /** BUG source code */
+ source: string;
+ /** Optional title for the example */
+ title?: string;
+ /** Optional description */
+ description?: string;
+ /** Whether to show the code preview (default: true) */
+ showPreview?: boolean;
+}
+
+export function TraceExample({
+ source,
+ title,
+ description,
+ showPreview = true,
+}: TraceExampleProps): JSX.Element {
+ return (
+
+ {title &&
{title}
}
+ {description && (
+
{description}
+ )}
+
+
+ {showPreview && (
+
+ {source.trim()}
+
+ )}
+
+
+ {() => }
+
+
+
+ );
+}
+
+interface TryItButtonProps {
+ source: string;
+ title?: string;
+}
+
+function TryItButton({ source, title }: TryItButtonProps): JSX.Element | null {
+ const playground = useTracePlaygroundOptional();
+
+ if (!playground) {
+ return null;
+ }
+
+ const handleClick = () => {
+ playground.loadExample({ source, title });
+ };
+
+ return (
+
+ ▶
+ Try it
+
+ );
+}
+
+export default TraceExample;
diff --git a/packages/web/src/theme/ProgramExample/TracePlayground.tsx b/packages/web/src/theme/ProgramExample/TracePlayground.tsx
new file mode 100644
index 000000000..14feca75d
--- /dev/null
+++ b/packages/web/src/theme/ProgramExample/TracePlayground.tsx
@@ -0,0 +1,42 @@
+/**
+ * Wrapper component that provides the trace playground context and drawer.
+ *
+ * Use this to wrap page content that contains TraceExample components.
+ */
+
+import React, { type ReactNode } from "react";
+import BrowserOnly from "@docusaurus/BrowserOnly";
+import {
+ TracePlaygroundProvider,
+ type TraceExampleData,
+} from "./TracePlaygroundContext";
+import { TraceDrawer } from "./TraceDrawer";
+
+export interface TracePlaygroundProps {
+ children: ReactNode;
+ /** Initial example to load (optional) */
+ initialExample?: TraceExampleData;
+}
+
+export function TracePlayground({
+ children,
+ initialExample,
+}: TracePlaygroundProps): JSX.Element {
+ return (
+ {children}}
+ >
+ {() => (
+
+ {children}
+
+
+ )}
+
+ );
+}
+
+export default TracePlayground;
diff --git a/packages/web/src/theme/ProgramExample/TracePlaygroundContext.tsx b/packages/web/src/theme/ProgramExample/TracePlaygroundContext.tsx
new file mode 100644
index 000000000..6b62e6d60
--- /dev/null
+++ b/packages/web/src/theme/ProgramExample/TracePlaygroundContext.tsx
@@ -0,0 +1,108 @@
+/**
+ * Context for coordinating between trace examples and the drawer.
+ *
+ * Unlike mock trace data, this shares BUG source code that gets
+ * compiled and executed in the drawer.
+ */
+
+import React, {
+ createContext,
+ useContext,
+ useState,
+ useCallback,
+ type ReactNode,
+} from "react";
+
+export interface TraceExampleData {
+ /** BUG source code to compile and trace */
+ source: string;
+ /** Optional title for display */
+ title?: string;
+ /** Optional description */
+ description?: string;
+}
+
+export interface TracePlaygroundState {
+ /** Current example data */
+ example: TraceExampleData | null;
+ /** Whether the drawer is open */
+ isOpen: boolean;
+ /** Load an example into the playground */
+ loadExample(data: TraceExampleData): void;
+ /** Update the source code */
+ setSource(source: string): void;
+ /** Open the drawer */
+ openDrawer(): void;
+ /** Close the drawer */
+ closeDrawer(): void;
+ /** Toggle the drawer */
+ toggleDrawer(): void;
+}
+
+const TracePlaygroundContext = createContext(null);
+
+export interface TracePlaygroundProviderProps {
+ children: ReactNode;
+ /** Initial example */
+ initialExample?: TraceExampleData | null;
+ /** Whether drawer starts open */
+ initialOpen?: boolean;
+}
+
+export function TracePlaygroundProvider({
+ children,
+ initialExample = null,
+ initialOpen = false,
+}: TracePlaygroundProviderProps): JSX.Element {
+ const [example, setExample] = useState(
+ initialExample,
+ );
+ const [isOpen, setIsOpen] = useState(initialOpen);
+
+ const openDrawer = useCallback(() => setIsOpen(true), []);
+ const closeDrawer = useCallback(() => setIsOpen(false), []);
+ const toggleDrawer = useCallback(() => setIsOpen((prev) => !prev), []);
+
+ const loadExample = useCallback((data: TraceExampleData) => {
+ setExample(data);
+ setIsOpen(true);
+ }, []);
+
+ const setSource = useCallback((source: string) => {
+ setExample((prev) => (prev ? { ...prev, source } : { source }));
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+export function useTracePlayground(): TracePlaygroundState {
+ const context = useContext(TracePlaygroundContext);
+ if (!context) {
+ throw new Error(
+ "useTracePlayground must be used within a TracePlaygroundProvider",
+ );
+ }
+ return context;
+}
+
+/**
+ * Hook that returns null if not in a playground context.
+ * Useful for components that can work both inside and outside the playground.
+ */
+export function useTracePlaygroundOptional(): TracePlaygroundState | null {
+ return useContext(TracePlaygroundContext);
+}
diff --git a/packages/web/src/theme/ProgramExample/TraceViewer.css b/packages/web/src/theme/ProgramExample/TraceViewer.css
new file mode 100644
index 000000000..341f4603b
--- /dev/null
+++ b/packages/web/src/theme/ProgramExample/TraceViewer.css
@@ -0,0 +1,203 @@
+/**
+ * Styles for TraceViewer component.
+ */
+
+.trace-viewer {
+ border: 1px solid var(--ifm-color-emphasis-300);
+ border-radius: var(--ifm-global-radius);
+ background: var(--ifm-background-color);
+ overflow: hidden;
+}
+
+.trace-viewer-content {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+.trace-viewer-header {
+ padding: 0.75rem 1rem;
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+ background: var(--ifm-background-surface-color);
+}
+
+.trace-viewer-body {
+ display: grid;
+ grid-template-columns: 1fr 300px;
+ gap: 1rem;
+ padding: 1rem;
+ flex: 1;
+ overflow: hidden;
+}
+
+@media (max-width: 900px) {
+ .trace-viewer-body {
+ grid-template-columns: 1fr;
+ }
+}
+
+.trace-viewer-left,
+.trace-viewer-right {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ overflow: hidden;
+}
+
+.trace-viewer-panel {
+ border: 1px solid var(--ifm-color-emphasis-200);
+ border-radius: 6px;
+ background: var(--ifm-background-color);
+ overflow: hidden;
+}
+
+.trace-viewer-panel .panel-title {
+ margin: 0;
+ padding: 0.5rem 0.75rem;
+ font-size: 0.75rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--ifm-color-emphasis-600);
+ background: var(--ifm-background-surface-color);
+ border-bottom: 1px solid var(--ifm-color-emphasis-200);
+}
+
+/* Source panel */
+.source-panel {
+ flex: 1;
+ min-height: 150px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.source-code {
+ margin: 0;
+ padding: 0.75rem;
+ font-size: 0.8125rem;
+ line-height: 1.5;
+ overflow: auto;
+ flex: 1;
+ background: var(--ifm-code-background);
+}
+
+.source-highlight {
+ background: var(--ifm-color-warning-contrast-background);
+ border-radius: 2px;
+ padding: 0 2px;
+}
+
+/* Opcodes panel */
+.opcodes-panel {
+ min-height: 200px;
+ max-height: 300px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.opcode-list {
+ padding: 0.5rem;
+ overflow: auto;
+ flex: 1;
+}
+
+.opcode-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+ font-family: var(--ifm-font-family-monospace);
+ border-radius: 3px;
+}
+
+.opcode-item.active {
+ background: var(--ifm-color-primary-contrast-background);
+ font-weight: 600;
+}
+
+.opcode-offset {
+ color: var(--ifm-color-emphasis-600);
+ min-width: 3.5rem;
+}
+
+.opcode-mnemonic {
+ color: var(--ifm-color-primary);
+ min-width: 5rem;
+}
+
+.opcode-args {
+ color: var(--ifm-color-emphasis-700);
+}
+
+/* Variables panel */
+.variables-panel {
+ max-height: 250px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.variables-panel .variable-inspector {
+ padding: 0.5rem;
+ overflow: auto;
+ flex: 1;
+}
+
+/* Stack panel */
+.stack-panel {
+ max-height: 200px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.stack-panel .stack-inspector {
+ padding: 0.5rem;
+ overflow: auto;
+ flex: 1;
+}
+
+/* Storage panel */
+.storage-panel {
+ max-height: 150px;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+.storage-list {
+ padding: 0.5rem;
+ overflow: auto;
+ flex: 1;
+}
+
+.storage-entry {
+ display: flex;
+ align-items: center;
+ gap: 0.375rem;
+ padding: 0.25rem 0;
+ font-size: 0.75rem;
+ font-family: var(--ifm-font-family-monospace);
+}
+
+.storage-slot {
+ color: var(--ifm-color-emphasis-600);
+ min-width: 4rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.storage-arrow {
+ color: var(--ifm-color-emphasis-500);
+}
+
+.storage-value {
+ color: var(--ifm-color-success-dark);
+ background: transparent;
+ padding: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/packages/web/src/theme/ProgramExample/TraceViewer.tsx b/packages/web/src/theme/ProgramExample/TraceViewer.tsx
new file mode 100644
index 000000000..59ee577f0
--- /dev/null
+++ b/packages/web/src/theme/ProgramExample/TraceViewer.tsx
@@ -0,0 +1,224 @@
+/**
+ * TraceViewer component for the Docusaurus site.
+ *
+ * Provides an interactive execution trace viewer with source highlighting.
+ */
+
+import React from "react";
+import type { Program, Materials } from "@ethdebug/format";
+import {
+ TraceProvider,
+ TraceControls,
+ TraceProgress,
+ VariableInspector,
+ StackInspector,
+ useTraceContext,
+ type TraceStep,
+} from "@ethdebug/programs-react";
+
+// Import CSS for styling
+import "./TraceViewer.css";
+import "./TraceControls.css";
+import "./VariableInspector.css";
+
+export interface TraceViewerProps {
+ /** The execution trace */
+ trace: TraceStep[];
+ /** The program definition */
+ program: Program;
+ /** Source materials for highlighting */
+ sources?: Materials.Source[];
+ /** Initial step index */
+ initialStepIndex?: number;
+ /** Whether to show variables panel */
+ showVariables?: boolean;
+ /** Whether to show stack panel */
+ showStack?: boolean;
+ /** Custom height */
+ height?: string;
+}
+
+/**
+ * Interactive execution trace viewer.
+ */
+export function TraceViewer({
+ trace,
+ program,
+ sources = [],
+ initialStepIndex = 0,
+ showVariables = true,
+ showStack = true,
+ height,
+}: TraceViewerProps): JSX.Element {
+ return (
+
+
+
+
+
+ );
+}
+
+interface TraceViewerContentProps {
+ sources: Materials.Source[];
+ showVariables: boolean;
+ showStack: boolean;
+}
+
+function TraceViewerContent({
+ sources,
+ showVariables,
+ showStack,
+}: TraceViewerContentProps): JSX.Element {
+ const { currentStep, currentInstruction } = useTraceContext();
+
+ // Find source range for current instruction
+ const sourceRange =
+ currentInstruction?.context && "code" in currentInstruction.context
+ ? currentInstruction.context.code
+ : undefined;
+
+ // Find the source content to display
+ const sourceContent =
+ sourceRange && sources.length > 0
+ ? sources.find((s) => s.id === sourceRange.source.id)?.contents
+ : sources[0]?.contents;
+
+ return (
+
+
+
+
+
+
+
+
+ {sourceContent && (
+
+
Source
+
+
+ )}
+
+
+
Instructions
+
+
+
+
+
+ {showVariables && (
+
+
Variables
+
+
+ )}
+
+ {showStack && (
+
+
Stack
+
+
+ )}
+
+ {currentStep?.storage &&
+ Object.keys(currentStep.storage).length > 0 && (
+
+
Storage
+
+
+ )}
+
+
+
+ );
+}
+
+interface SourceDisplayProps {
+ source: string;
+ highlightRange?: { offset: number; length: number };
+}
+
+function SourceDisplay({
+ source,
+ highlightRange,
+}: SourceDisplayProps): JSX.Element {
+ if (!highlightRange) {
+ return {source} ;
+ }
+
+ const { offset, length } = highlightRange;
+ const before = source.slice(0, offset);
+ const highlighted = source.slice(offset, offset + length);
+ const after = source.slice(offset + length);
+
+ return (
+
+ {before}
+ {highlighted}
+ {after}
+
+ );
+}
+
+interface OpcodeListProps {
+ program: Program;
+ currentPc?: number;
+}
+
+function OpcodeList({ program, currentPc }: OpcodeListProps): JSX.Element {
+ const instructions = program.instructions || [];
+
+ return (
+
+ {instructions.map((instr) => {
+ const isActive = instr.offset === currentPc;
+ const offset = `0x${instr.offset.toString(16).padStart(4, "0")}`;
+ const mnemonic = instr.operation?.mnemonic || "???";
+ const args = instr.operation?.arguments?.join(" ") || "";
+
+ return (
+
+ {offset}
+ {mnemonic}
+ {args && {args} }
+
+ );
+ })}
+
+ );
+}
+
+interface StorageDisplayProps {
+ storage: Record;
+}
+
+function StorageDisplay({ storage }: StorageDisplayProps): JSX.Element {
+ const entries = Object.entries(storage);
+
+ return (
+
+ {entries.map(([slot, value]) => (
+
+ {slot}
+ =
+ {value}
+
+ ))}
+
+ );
+}
diff --git a/packages/web/src/theme/ProgramExample/VariableInspector.css b/packages/web/src/theme/ProgramExample/VariableInspector.css
new file mode 100644
index 000000000..4d233fe16
--- /dev/null
+++ b/packages/web/src/theme/ProgramExample/VariableInspector.css
@@ -0,0 +1,151 @@
+/**
+ * Styles for VariableInspector component in Docusaurus context.
+ */
+
+.variable-inspector {
+ display: flex;
+ flex-direction: column;
+}
+
+.variable-inspector-empty {
+ padding: 1rem;
+ text-align: center;
+}
+
+.variable-inspector-empty-text {
+ font-size: 0.875rem;
+ color: var(--ifm-color-emphasis-600);
+}
+
+.variable-inspector-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.variable-item {
+ padding: 0.75rem;
+ background: var(--ifm-background-surface-color);
+ border: 1px solid var(--ifm-color-emphasis-200);
+ border-radius: 6px;
+}
+
+.variable-item.has-error {
+ border-color: var(--ifm-color-danger);
+ background: var(--ifm-color-danger-contrast-background);
+}
+
+.variable-header {
+ display: flex;
+ align-items: baseline;
+ gap: 0.5rem;
+ margin-bottom: 0.375rem;
+}
+
+.variable-name {
+ font-family: var(--ifm-font-family-monospace);
+ font-size: 0.875rem;
+ font-weight: 600;
+ color: var(--ifm-font-color-base);
+}
+
+.variable-type {
+ font-size: 0.6875rem;
+ padding: 0.125rem 0.375rem;
+ background: var(--programs-accent-purple-bg);
+ color: var(--programs-accent-purple);
+ border-radius: 3px;
+}
+
+.variable-value {
+ font-size: 0.8125rem;
+}
+
+.variable-resolved {
+ font-family: var(--ifm-font-family-monospace);
+ color: var(--ifm-color-success-dark);
+ word-break: break-all;
+}
+
+.variable-pending {
+ color: var(--ifm-color-emphasis-600);
+ font-style: italic;
+}
+
+.variable-error {
+ color: var(--ifm-color-danger);
+ font-size: 0.75rem;
+}
+
+.variable-pointer {
+ margin-top: 0.5rem;
+}
+
+.variable-pointer details {
+ font-size: 0.75rem;
+}
+
+.variable-pointer summary {
+ cursor: pointer;
+ color: var(--ifm-color-emphasis-700);
+}
+
+.variable-pointer-json {
+ margin-top: 0.25rem;
+ padding: 0.5rem;
+ font-size: 0.6875rem;
+ background: var(--ifm-code-background);
+ border-radius: 4px;
+ overflow-x: auto;
+}
+
+/* Stack Inspector */
+.stack-inspector {
+ display: flex;
+ flex-direction: column;
+}
+
+.stack-inspector-empty {
+ padding: 1rem;
+ text-align: center;
+}
+
+.stack-inspector-empty-text {
+ font-size: 0.875rem;
+ color: var(--ifm-color-emphasis-600);
+}
+
+.stack-inspector-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+}
+
+.stack-entry {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.375rem 0.5rem;
+ background: var(--ifm-background-surface-color);
+ border-radius: 4px;
+}
+
+.stack-index {
+ font-size: 0.6875rem;
+ font-family: var(--ifm-font-family-monospace);
+ color: var(--ifm-color-emphasis-600);
+ min-width: 1.5rem;
+}
+
+.stack-value {
+ font-size: 0.75rem;
+ font-family: var(--ifm-font-family-monospace);
+ color: var(--ifm-color-success-dark);
+ word-break: break-all;
+}
+
+.stack-more {
+ font-size: 0.75rem;
+ color: var(--ifm-color-emphasis-600);
+ font-style: italic;
+}
diff --git a/packages/web/src/theme/ProgramExample/Variables.tsx b/packages/web/src/theme/ProgramExample/Variables.tsx
index da7099881..82624934d 100644
--- a/packages/web/src/theme/ProgramExample/Variables.tsx
+++ b/packages/web/src/theme/ProgramExample/Variables.tsx
@@ -1,10 +1,12 @@
import React from "react";
import Admonition from "@theme/Admonition";
import Link from "@docusaurus/Link";
-import { useProgramExampleContext } from "./ProgramExampleContext";
+import {
+ useProgramExampleContext,
+ ShikiCodeBlock,
+} from "@ethdebug/programs-react";
import { Program } from "@ethdebug/format";
-import { ShikiCodeBlock } from "@theme/ShikiCodeBlock";
export function Variables(): JSX.Element {
const { highlightedInstruction } = useProgramExampleContext();
diff --git a/packages/web/src/theme/ProgramExample/Viewer.tsx b/packages/web/src/theme/ProgramExample/Viewer.tsx
index 94adc29f3..defbb1276 100644
--- a/packages/web/src/theme/ProgramExample/Viewer.tsx
+++ b/packages/web/src/theme/ProgramExample/Viewer.tsx
@@ -1,7 +1,9 @@
-import { SourceContents } from "./SourceContents";
-import { Opcodes } from "./Opcodes";
+import { SourceContents, Opcodes } from "@ethdebug/programs-react";
import { Details } from "./Details";
+// Import CSS for the components (programs-react expects consumers to provide)
+import "./Opcodes.css";
+import "./SourceContents.css";
import "./Viewer.css";
export interface Props {}
diff --git a/packages/web/src/theme/ProgramExample/index.ts b/packages/web/src/theme/ProgramExample/index.ts
index 02f07f310..47a0e1113 100644
--- a/packages/web/src/theme/ProgramExample/index.ts
+++ b/packages/web/src/theme/ProgramExample/index.ts
@@ -1,6 +1,52 @@
-export * from "./ProgramExampleContext";
-export * from "./SourceContents";
-export * from "./Opcodes";
-export * from "./HighlightedInstruction";
+// Re-export from @ethdebug/programs-react
+export {
+ ProgramExampleContextProvider,
+ useProgramExampleContext,
+ type ProgramExampleState,
+ type ProgramExampleProps,
+ SourceContents,
+ Opcodes,
+ HighlightedInstruction,
+} from "@ethdebug/programs-react";
+// Trace components
+export {
+ TraceProvider,
+ useTraceContext,
+ TraceControls,
+ TraceProgress,
+ VariableInspector,
+ StackInspector,
+ type TraceState,
+ type TraceProviderProps,
+ type ResolvedVariable,
+ type TraceControlsProps,
+ type TraceProgressProps,
+ type VariableInspectorProps,
+ type StackInspectorProps,
+} from "@ethdebug/programs-react";
+
+// Also re-export utilities for convenience
+export {
+ computeOffsets,
+ resolveDynamicInstruction,
+ createMockTrace,
+ findInstructionAtPc,
+ extractVariablesFromInstruction,
+ buildPcToInstructionMap,
+ type DynamicInstruction,
+ type DynamicContext,
+ type ContextThunk,
+ type TraceStep,
+ type MockTraceSpec,
+} from "@ethdebug/programs-react";
+
+// Local Docusaurus-specific components
export * from "./Viewer";
+export * from "./TraceViewer";
+
+// Trace playground components
+export * from "./TracePlaygroundContext";
+export * from "./TracePlayground";
+export * from "./TraceDrawer";
+export * from "./TraceExample";
diff --git a/packages/web/src/theme/ShikiCodeBlock/ShikiCodeBlock.tsx b/packages/web/src/theme/ShikiCodeBlock/ShikiCodeBlock.tsx
deleted file mode 100644
index d1319152f..000000000
--- a/packages/web/src/theme/ShikiCodeBlock/ShikiCodeBlock.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from "react";
-import { type HighlightOptions, useHighlighter } from "./useHighlighter";
-
-export interface Props extends HighlightOptions {
- code: string;
-}
-
-export function ShikiCodeBlock({
- code,
- ...highlightOptions
-}: Props): JSX.Element {
- const highlighter = useHighlighter();
-
- if (!highlighter) {
- return <>Loading...>;
- }
-
- const html = highlighter.highlight(code, highlightOptions);
-
- return
;
-}
diff --git a/packages/web/src/theme/ShikiCodeBlock/index.ts b/packages/web/src/theme/ShikiCodeBlock/index.ts
index 987f6d64f..76301ac84 100644
--- a/packages/web/src/theme/ShikiCodeBlock/index.ts
+++ b/packages/web/src/theme/ShikiCodeBlock/index.ts
@@ -1,6 +1,12 @@
-export * from "./useHighlighter";
-export * from "./ShikiCodeBlock";
+// Re-export from @ethdebug/programs-react
+export {
+ useHighlighter,
+ ShikiCodeBlock,
+ type Highlighter,
+ type HighlightOptions,
+ type ShikiCodeBlockProps,
+} from "@ethdebug/programs-react";
-import { ShikiCodeBlock } from "./ShikiCodeBlock";
+import { ShikiCodeBlock } from "@ethdebug/programs-react";
export default ShikiCodeBlock;
diff --git a/yarn.lock b/yarn.lock
index 13395320a..3e885c2d9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -213,6 +213,17 @@
jsonpointer "^5.0.0"
leven "^3.1.0"
+"@asamuzakjp/css-color@^3.2.0":
+ version "3.2.0"
+ resolved "https://registry.yarnpkg.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz#cc42f5b85c593f79f1fa4f25d2b9b321e61d1794"
+ integrity sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==
+ dependencies:
+ "@csstools/css-calc" "^2.1.3"
+ "@csstools/css-color-parser" "^3.0.9"
+ "@csstools/css-parser-algorithms" "^3.0.4"
+ "@csstools/css-tokenizer" "^3.0.3"
+ lru-cache "^10.4.3"
+
"@babel/code-frame@^7.0.0":
version "7.23.5"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz"
@@ -221,15 +232,7 @@
"@babel/highlight" "^7.23.4"
chalk "^2.4.2"
-"@babel/code-frame@^7.24.7":
- version "7.24.7"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465"
- integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==
- dependencies:
- "@babel/highlight" "^7.24.7"
- picocolors "^1.0.0"
-
-"@babel/code-frame@^7.28.6":
+"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.28.6":
version "7.28.6"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.28.6.tgz#72499312ec58b1e2245ba4a4f550c132be4982f7"
integrity sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==
@@ -238,6 +241,14 @@
js-tokens "^4.0.0"
picocolors "^1.1.1"
+"@babel/code-frame@^7.24.7":
+ version "7.24.7"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465"
+ integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==
+ dependencies:
+ "@babel/highlight" "^7.24.7"
+ picocolors "^1.0.0"
+
"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5":
version "7.23.5"
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz"
@@ -274,7 +285,7 @@
json5 "^2.2.3"
semver "^6.3.1"
-"@babel/core@^7.25.9", "@babel/core@^7.28.0", "@babel/core@^7.28.6":
+"@babel/core@^7.24.4", "@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,7 +747,7 @@
js-tokens "^4.0.0"
picocolors "^1.0.0"
-"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.25.4", "@babel/parser@^7.28.6":
+"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.24.4", "@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==
@@ -2251,12 +2262,12 @@
resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz#106c54c808cabfd1ab4c602d8505ee584c2996ef"
integrity sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==
-"@csstools/css-calc@^2.1.4":
+"@csstools/css-calc@^2.1.3", "@csstools/css-calc@^2.1.4":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.4.tgz#8473f63e2fcd6e459838dd412401d5948f224c65"
integrity sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==
-"@csstools/css-color-parser@^3.1.0":
+"@csstools/css-color-parser@^3.0.9", "@csstools/css-color-parser@^3.1.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz#4e386af3a99dd36c46fef013cfe4c1c341eed6f0"
integrity sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==
@@ -2264,12 +2275,12 @@
"@csstools/color-helpers" "^5.1.0"
"@csstools/css-calc" "^2.1.4"
-"@csstools/css-parser-algorithms@^3.0.5":
+"@csstools/css-parser-algorithms@^3.0.4", "@csstools/css-parser-algorithms@^3.0.5":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz#5755370a9a29abaec5515b43c8b3f2cf9c2e3076"
integrity sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==
-"@csstools/css-tokenizer@^3.0.4":
+"@csstools/css-tokenizer@^3.0.3", "@csstools/css-tokenizer@^3.0.4":
version "3.0.4"
resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz#333fedabc3fd1a8e5d0100013731cf19e6a8c5d3"
integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==
@@ -3406,18 +3417,41 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz#9e585ab6086bef994c6e8a5b3a0481219ada862b"
integrity sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==
-"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.9.1":
+"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.8.0", "@eslint-community/eslint-utils@^4.9.1":
version "4.9.1"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595"
integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==
dependencies:
eslint-visitor-keys "^3.4.3"
-"@eslint-community/regexpp@^4.12.2", "@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1":
+"@eslint-community/regexpp@^4.12.1", "@eslint-community/regexpp@^4.12.2", "@eslint-community/regexpp@^4.6.1":
version "4.12.2"
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b"
integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==
+"@eslint/config-array@^0.21.1":
+ version "0.21.1"
+ resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.21.1.tgz#7d1b0060fea407f8301e932492ba8c18aff29713"
+ integrity sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==
+ dependencies:
+ "@eslint/object-schema" "^2.1.7"
+ debug "^4.3.1"
+ minimatch "^3.1.2"
+
+"@eslint/config-helpers@^0.4.2":
+ version "0.4.2"
+ resolved "https://registry.yarnpkg.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz#1bd006ceeb7e2e55b2b773ab318d300e1a66aeda"
+ integrity sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==
+ dependencies:
+ "@eslint/core" "^0.17.0"
+
+"@eslint/core@^0.17.0":
+ version "0.17.0"
+ resolved "https://registry.yarnpkg.com/@eslint/core/-/core-0.17.0.tgz#77225820413d9617509da9342190a2019e78761c"
+ integrity sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==
+ dependencies:
+ "@types/json-schema" "^7.0.15"
+
"@eslint/eslintrc@^2.1.4":
version "2.1.4"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad"
@@ -3433,11 +3467,44 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
+"@eslint/eslintrc@^3.3.1":
+ version "3.3.3"
+ resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.3.3.tgz#26393a0806501b5e2b6a43aa588a4d8df67880ac"
+ integrity sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==
+ dependencies:
+ ajv "^6.12.4"
+ debug "^4.3.2"
+ espree "^10.0.1"
+ globals "^14.0.0"
+ ignore "^5.2.0"
+ import-fresh "^3.2.1"
+ js-yaml "^4.1.1"
+ minimatch "^3.1.2"
+ strip-json-comments "^3.1.1"
+
"@eslint/js@8.57.1":
version "8.57.1"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.1.tgz#de633db3ec2ef6a3c89e2f19038063e8a122e2c2"
integrity sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==
+"@eslint/js@9.39.2", "@eslint/js@^9.39.2":
+ version "9.39.2"
+ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.39.2.tgz#2d4b8ec4c3ea13c1b3748e0c97ecd766bdd80599"
+ integrity sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==
+
+"@eslint/object-schema@^2.1.7":
+ version "2.1.7"
+ resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.7.tgz#6e2126a1347e86a4dedf8706ec67ff8e107ebbad"
+ integrity sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==
+
+"@eslint/plugin-kit@^0.4.1":
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz#9779e3fd9b7ee33571a57435cf4335a1794a6cb2"
+ integrity sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==
+ dependencies:
+ "@eslint/core" "^0.17.0"
+ levn "^0.4.1"
+
"@ethereumjs/binarytree@^10.1.0":
version "10.1.0"
resolved "https://registry.yarnpkg.com/@ethereumjs/binarytree/-/binarytree-10.1.0.tgz#d5c7b19975fba762ab84937120ec56f6a657ccb3"
@@ -3596,6 +3663,19 @@
dependencies:
"@hapi/hoek" "^9.0.0"
+"@humanfs/core@^0.19.1":
+ version "0.19.1"
+ resolved "https://registry.yarnpkg.com/@humanfs/core/-/core-0.19.1.tgz#17c55ca7d426733fe3c561906b8173c336b40a77"
+ integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==
+
+"@humanfs/node@^0.16.6":
+ version "0.16.7"
+ resolved "https://registry.yarnpkg.com/@humanfs/node/-/node-0.16.7.tgz#822cb7b3a12c5a240a24f621b5a2413e27a45f26"
+ integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==
+ dependencies:
+ "@humanfs/core" "^0.19.1"
+ "@humanwhocodes/retry" "^0.4.0"
+
"@humanwhocodes/config-array@^0.13.0":
version "0.13.0"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748"
@@ -3615,6 +3695,11 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
+"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2":
+ version "0.4.3"
+ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba"
+ integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
+
"@hutson/parse-repository-url@^3.0.0":
version "3.0.2"
resolved "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz"
@@ -4820,7 +4905,7 @@
"@shikijs/types" "2.5.0"
"@shikijs/vscode-textmate" "^10.0.2"
-"@shikijs/langs@2.5.0":
+"@shikijs/langs@2.5.0", "@shikijs/langs@^2.5.0":
version "2.5.0"
resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-2.5.0.tgz#97ab50c495922cc1ca06e192985b28dc73de5d50"
integrity sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==
@@ -5098,6 +5183,27 @@
dependencies:
defer-to-connect "^2.0.1"
+"@testing-library/dom@^10.0.0":
+ version "10.4.1"
+ resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.1.tgz#d444f8a889e9a46e9a3b4f3b88e0fcb3efb6cf95"
+ integrity sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==
+ dependencies:
+ "@babel/code-frame" "^7.10.4"
+ "@babel/runtime" "^7.12.5"
+ "@types/aria-query" "^5.0.1"
+ aria-query "5.3.0"
+ dom-accessibility-api "^0.5.9"
+ lz-string "^1.5.0"
+ picocolors "1.1.1"
+ pretty-format "^27.0.2"
+
+"@testing-library/react@^16.0.0":
+ version "16.3.1"
+ resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-16.3.1.tgz#60a9f1f6a930399d9e41b506a8bf68dbf4831fe8"
+ integrity sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==
+ dependencies:
+ "@babel/runtime" "^7.12.5"
+
"@trufflesuite/bigint-buffer@1.1.10":
version "1.1.10"
resolved "https://registry.yarnpkg.com/@trufflesuite/bigint-buffer/-/bigint-buffer-1.1.10.tgz#a1d9ca22d3cad1a138b78baaf15543637a3e1692"
@@ -5177,6 +5283,11 @@
dependencies:
"@types/estree" "*"
+"@types/aria-query@^5.0.1":
+ version "5.0.4"
+ resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.4.tgz#1a31c3d378850d2778dabb6374d036dcba4ba708"
+ integrity sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==
+
"@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"
@@ -5510,7 +5621,7 @@
resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz"
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
-"@types/estree@1.0.8", "@types/estree@^1.0.8":
+"@types/estree@1.0.8", "@types/estree@^1.0.6", "@types/estree@^1.0.8":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
@@ -5625,7 +5736,7 @@
dependencies:
"@types/istanbul-lib-report" "*"
-"@types/json-schema@*", "@types/json-schema@^7.0.12", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
+"@types/json-schema@*", "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9":
version "7.0.15"
resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
@@ -5796,11 +5907,6 @@
resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-3.0.1.tgz#1254750a4fec4aff2ebec088ccd0bb02e91fedb4"
integrity sha512-giB9gzDeiCeloIXDgzFBCgjj1k4WxcDrZtGl6h1IqmUPlxF+Nx8Ve+96QCyDZ/HseB/uvDsKbpib9hU5cU53pw==
-"@types/semver@^7.5.0":
- version "7.7.1"
- resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.7.1.tgz#3ce3af1a5524ef327d2da9e4fd8b6d95c8d70528"
- integrity sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==
-
"@types/send@*":
version "0.17.4"
resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz"
@@ -5883,24 +5989,7 @@
dependencies:
"@types/yargs-parser" "*"
-"@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==
- dependencies:
- "@eslint-community/regexpp" "^4.5.1"
- "@typescript-eslint/scope-manager" "6.21.0"
- "@typescript-eslint/type-utils" "6.21.0"
- "@typescript-eslint/utils" "6.21.0"
- "@typescript-eslint/visitor-keys" "6.21.0"
- debug "^4.3.4"
- graphemer "^1.4.0"
- ignore "^5.2.4"
- natural-compare "^1.4.0"
- semver "^7.5.4"
- ts-api-utils "^1.0.1"
-
-"@typescript-eslint/eslint-plugin@^8.21.0":
+"@typescript-eslint/eslint-plugin@8.53.0", "@typescript-eslint/eslint-plugin@^8.21.0":
version "8.53.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.0.tgz#afb966c66a2fdc6158cf81118204a971a36d0fc5"
integrity sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg==
@@ -5914,18 +6003,7 @@
natural-compare "^1.4.0"
ts-api-utils "^2.4.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==
- dependencies:
- "@typescript-eslint/scope-manager" "6.21.0"
- "@typescript-eslint/types" "6.21.0"
- "@typescript-eslint/typescript-estree" "6.21.0"
- "@typescript-eslint/visitor-keys" "6.21.0"
- debug "^4.3.4"
-
-"@typescript-eslint/parser@^8.21.0":
+"@typescript-eslint/parser@8.53.0", "@typescript-eslint/parser@^8.21.0":
version "8.53.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.53.0.tgz#d8bed6f12dc74e03751e5f947510ff2b165990c6"
integrity sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==
@@ -5945,14 +6023,6 @@
"@typescript-eslint/types" "^8.53.0"
debug "^4.4.3"
-"@typescript-eslint/scope-manager@6.21.0":
- version "6.21.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1"
- integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==
- dependencies:
- "@typescript-eslint/types" "6.21.0"
- "@typescript-eslint/visitor-keys" "6.21.0"
-
"@typescript-eslint/scope-manager@8.53.0":
version "8.53.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.53.0.tgz#f922fcbf0d42e72f065297af31779ccf19de9a97"
@@ -5966,16 +6036,6 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.0.tgz#105279d7969a7abdc8345cc9c57cff83cf910f8f"
integrity sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA==
-"@typescript-eslint/type-utils@6.21.0":
- version "6.21.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e"
- integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==
- dependencies:
- "@typescript-eslint/typescript-estree" "6.21.0"
- "@typescript-eslint/utils" "6.21.0"
- debug "^4.3.4"
- ts-api-utils "^1.0.1"
-
"@typescript-eslint/type-utils@8.53.0":
version "8.53.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.53.0.tgz#81a0de5c01fc68f6df0591d03cd8226bda01c91f"
@@ -5987,30 +6047,11 @@
debug "^4.4.3"
ts-api-utils "^2.4.0"
-"@typescript-eslint/types@6.21.0":
- version "6.21.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d"
- integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==
-
"@typescript-eslint/types@8.53.0", "@typescript-eslint/types@^8.53.0":
version "8.53.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.53.0.tgz#1adcad3fa32bc2c4cbf3785ba07a5e3151819efb"
integrity sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ==
-"@typescript-eslint/typescript-estree@6.21.0":
- version "6.21.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46"
- integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==
- dependencies:
- "@typescript-eslint/types" "6.21.0"
- "@typescript-eslint/visitor-keys" "6.21.0"
- debug "^4.3.4"
- globby "^11.1.0"
- is-glob "^4.0.3"
- minimatch "9.0.3"
- semver "^7.5.4"
- ts-api-utils "^1.0.1"
-
"@typescript-eslint/typescript-estree@8.53.0":
version "8.53.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.0.tgz#7805b46b7a8ce97e91b7bb56fc8b1ba26ca8ef52"
@@ -6026,19 +6067,6 @@
tinyglobby "^0.2.15"
ts-api-utils "^2.4.0"
-"@typescript-eslint/utils@6.21.0":
- version "6.21.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134"
- integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==
- dependencies:
- "@eslint-community/eslint-utils" "^4.4.0"
- "@types/json-schema" "^7.0.12"
- "@types/semver" "^7.5.0"
- "@typescript-eslint/scope-manager" "6.21.0"
- "@typescript-eslint/types" "6.21.0"
- "@typescript-eslint/typescript-estree" "6.21.0"
- semver "^7.5.4"
-
"@typescript-eslint/utils@8.53.0":
version "8.53.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.53.0.tgz#bf0a4e2edaf1afc9abce209fc02f8cab0b74af13"
@@ -6049,14 +6077,6 @@
"@typescript-eslint/types" "8.53.0"
"@typescript-eslint/typescript-estree" "8.53.0"
-"@typescript-eslint/visitor-keys@6.21.0":
- version "6.21.0"
- resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47"
- integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==
- dependencies:
- "@typescript-eslint/types" "6.21.0"
- eslint-visitor-keys "^3.4.1"
-
"@typescript-eslint/visitor-keys@8.53.0":
version "8.53.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.0.tgz#9a785664ddae7e3f7e570ad8166e48dbc9c6cf02"
@@ -6657,6 +6677,11 @@ agent-base@^7.0.2, agent-base@^7.1.0:
dependencies:
debug "^4.3.4"
+agent-base@^7.1.2:
+ version "7.1.4"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.4.tgz#e3cd76d4c548ee895d3c3fd8dc1f6c5b9032e7a8"
+ integrity sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==
+
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz"
@@ -6861,6 +6886,13 @@ argparse@^2.0.1:
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+aria-query@5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
+ integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==
+ dependencies:
+ dequal "^2.0.3"
+
array-differ@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz"
@@ -8139,7 +8171,7 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"
-cross-spawn@^7.0.2:
+cross-spawn@^7.0.2, cross-spawn@^7.0.6:
version "7.0.6"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
@@ -8329,6 +8361,14 @@ csso@^5.0.5:
dependencies:
css-tree "~2.2.0"
+cssstyle@^4.2.1:
+ version "4.6.0"
+ resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.6.0.tgz#ea18007024e3167f4f105315f3ec2d982bf48ed9"
+ integrity sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==
+ dependencies:
+ "@asamuzakjp/css-color" "^3.2.0"
+ rrweb-cssom "^0.8.0"
+
csstype@^3.0.2:
version "3.1.3"
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz"
@@ -8414,6 +8454,14 @@ dargs@^7.0.0:
resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz"
integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==
+data-urls@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde"
+ integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==
+ dependencies:
+ whatwg-mimetype "^4.0.0"
+ whatwg-url "^14.0.0"
+
date-fns@^2.30.0:
version "2.30.0"
resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz"
@@ -8472,6 +8520,11 @@ decamelize@^1.1.0:
resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
+decimal.js@^10.5.0:
+ version "10.6.0"
+ resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a"
+ integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==
+
decode-named-character-reference@^1.0.0:
version "1.0.2"
resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz"
@@ -8665,6 +8718,11 @@ docusaurus-json-schema-plugin@^1.15.0:
monaco-editor-webpack-plugin "^7.1.0"
react-monaco-editor "^0.59.0"
+dom-accessibility-api@^0.5.9:
+ version "0.5.16"
+ resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
+ integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
+
dom-converter@^0.2.0:
version "0.2.0"
resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz"
@@ -8917,6 +8975,11 @@ entities@^4.2.0, entities@^4.4.0:
resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz"
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
+entities@^6.0.0:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/entities/-/entities-6.0.1.tgz#c28c34a43379ca7f61d074130b2f5f7020a30694"
+ integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==
+
env-paths@^2.2.0, env-paths@^2.2.1:
version "2.2.1"
resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz"
@@ -9092,6 +9155,17 @@ eslint-plugin-react-hooks@^4.6.0:
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-hooks@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz#66e258db58ece50723ef20cc159f8aa908219169"
+ integrity sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==
+ dependencies:
+ "@babel/core" "^7.24.4"
+ "@babel/parser" "^7.24.4"
+ hermes-parser "^0.25.1"
+ zod "^3.25.0 || ^4.0.0"
+ zod-validation-error "^3.5.0 || ^4.0.0"
+
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"
@@ -9113,6 +9187,14 @@ eslint-scope@^7.2.2:
esrecurse "^4.3.0"
estraverse "^5.2.0"
+eslint-scope@^8.4.0:
+ version "8.4.0"
+ resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.4.0.tgz#88e646a207fad61436ffa39eb505147200655c82"
+ integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==
+ dependencies:
+ esrecurse "^4.3.0"
+ estraverse "^5.2.0"
+
eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3:
version "3.4.3"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
@@ -9123,7 +9205,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.55.0, eslint@^8.57.1:
+eslint@^8.0.0, eslint@^8.55.0:
version "8.57.1"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.1.tgz#7df109654aba7e3bbe5c8eae533c5e461d3c6ca9"
integrity sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==
@@ -9167,6 +9249,55 @@ eslint@^8.0.0, eslint@^8.55.0, eslint@^8.57.1:
strip-ansi "^6.0.1"
text-table "^0.2.0"
+eslint@^9.0.0:
+ version "9.39.2"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.39.2.tgz#cb60e6d16ab234c0f8369a3fe7cc87967faf4b6c"
+ integrity sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==
+ dependencies:
+ "@eslint-community/eslint-utils" "^4.8.0"
+ "@eslint-community/regexpp" "^4.12.1"
+ "@eslint/config-array" "^0.21.1"
+ "@eslint/config-helpers" "^0.4.2"
+ "@eslint/core" "^0.17.0"
+ "@eslint/eslintrc" "^3.3.1"
+ "@eslint/js" "9.39.2"
+ "@eslint/plugin-kit" "^0.4.1"
+ "@humanfs/node" "^0.16.6"
+ "@humanwhocodes/module-importer" "^1.0.1"
+ "@humanwhocodes/retry" "^0.4.2"
+ "@types/estree" "^1.0.6"
+ ajv "^6.12.4"
+ chalk "^4.0.0"
+ cross-spawn "^7.0.6"
+ debug "^4.3.2"
+ escape-string-regexp "^4.0.0"
+ eslint-scope "^8.4.0"
+ eslint-visitor-keys "^4.2.1"
+ espree "^10.4.0"
+ esquery "^1.5.0"
+ esutils "^2.0.2"
+ fast-deep-equal "^3.1.3"
+ file-entry-cache "^8.0.0"
+ find-up "^5.0.0"
+ glob-parent "^6.0.2"
+ ignore "^5.2.0"
+ imurmurhash "^0.1.4"
+ is-glob "^4.0.0"
+ json-stable-stringify-without-jsonify "^1.0.1"
+ lodash.merge "^4.6.2"
+ minimatch "^3.1.2"
+ natural-compare "^1.4.0"
+ optionator "^0.9.3"
+
+espree@^10.0.1, espree@^10.4.0:
+ version "10.4.0"
+ resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837"
+ integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==
+ dependencies:
+ acorn "^8.15.0"
+ acorn-jsx "^5.3.2"
+ eslint-visitor-keys "^4.2.1"
+
espree@^9.6.0, espree@^9.6.1:
version "9.6.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
@@ -9181,7 +9312,7 @@ esprima@^4.0.0:
resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
-esquery@^1.4.2:
+esquery@^1.4.2, esquery@^1.5.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.7.0.tgz#08d048f261f0ddedb5bae95f46809463d9c9496d"
integrity sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==
@@ -9531,6 +9662,13 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"
+file-entry-cache@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f"
+ integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==
+ dependencies:
+ flat-cache "^4.0.0"
+
file-loader@^6.2.0:
version "6.2.0"
resolved "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz"
@@ -9621,6 +9759,14 @@ flat-cache@^3.0.4:
keyv "^4.5.3"
rimraf "^3.0.2"
+flat-cache@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c"
+ integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==
+ dependencies:
+ flatted "^3.2.9"
+ keyv "^4.5.4"
+
flat@^5.0.2:
version "5.0.2"
resolved "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz"
@@ -10002,6 +10148,16 @@ globals@^13.19.0:
dependencies:
type-fest "^0.20.2"
+globals@^14.0.0:
+ version "14.0.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e"
+ integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
+
+globals@^17.0.0:
+ version "17.0.0"
+ resolved "https://registry.yarnpkg.com/globals/-/globals-17.0.0.tgz#a4196d9cfeb4d627ba165b4647b1f5853bf90a30"
+ integrity sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==
+
globby@^11.1.0:
version "11.1.0"
resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz"
@@ -10322,6 +10478,18 @@ he@^1.2.0:
resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+hermes-estree@0.25.1:
+ version "0.25.1"
+ resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
+ integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==
+
+hermes-parser@^0.25.1:
+ version "0.25.1"
+ resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1"
+ integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==
+ dependencies:
+ hermes-estree "0.25.1"
+
highlight.js@^10.7.1:
version "10.7.3"
resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz"
@@ -10391,6 +10559,13 @@ hpack.js@^2.1.6:
readable-stream "^2.0.1"
wbuf "^1.1.0"
+html-encoding-sniffer@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448"
+ integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==
+ dependencies:
+ whatwg-encoding "^3.1.1"
+
html-escaper@^2.0.0, html-escaper@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz"
@@ -10512,6 +10687,14 @@ http-proxy-agent@^7.0.0:
agent-base "^7.1.0"
debug "^4.3.4"
+http-proxy-agent@^7.0.2:
+ version "7.0.2"
+ resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e"
+ integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==
+ dependencies:
+ agent-base "^7.1.0"
+ debug "^4.3.4"
+
http-proxy-middleware@^2.0.9:
version "2.0.9"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz#e9e63d68afaa4eee3d147f39149ab84c0c2815ef"
@@ -10548,6 +10731,14 @@ https-proxy-agent@^7.0.1:
agent-base "^7.0.2"
debug "4"
+https-proxy-agent@^7.0.6:
+ version "7.0.6"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9"
+ integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==
+ dependencies:
+ agent-base "^7.1.2"
+ debug "4"
+
human-signals@^2.1.0:
version "2.1.0"
resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz"
@@ -10568,6 +10759,13 @@ hyperdyperid@^1.2.0:
resolved "https://registry.yarnpkg.com/hyperdyperid/-/hyperdyperid-1.2.0.tgz#59668d323ada92228d2a869d3e474d5a33b69e6b"
integrity sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==
+iconv-lite@0.6.3, iconv-lite@^0.6.2:
+ version "0.6.3"
+ resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
iconv-lite@^0.4.24, iconv-lite@~0.4.24:
version "0.4.24"
resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz"
@@ -10575,13 +10773,6 @@ iconv-lite@^0.4.24, iconv-lite@~0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
-iconv-lite@^0.6.2:
- version "0.6.3"
- resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz"
- integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
- dependencies:
- safer-buffer ">= 2.1.2 < 3.0.0"
-
icss-utils@^5.0.0, icss-utils@^5.1.0:
version "5.1.0"
resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz"
@@ -10951,6 +11142,11 @@ is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
+is-potential-custom-element-name@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5"
+ integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==
+
is-reference@^3.0.0:
version "3.0.2"
resolved "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz"
@@ -11197,6 +11393,39 @@ js-yaml@^3.10.0, js-yaml@^3.13.1, js-yaml@^3.8.4:
argparse "^1.0.7"
esprima "^4.0.0"
+js-yaml@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.1.tgz#854c292467705b699476e1a2decc0c8a3458806b"
+ integrity sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==
+ dependencies:
+ argparse "^2.0.1"
+
+jsdom@^26.0.0:
+ version "26.1.0"
+ resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-26.1.0.tgz#ab5f1c1cafc04bd878725490974ea5e8bf0c72b3"
+ integrity sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==
+ dependencies:
+ cssstyle "^4.2.1"
+ data-urls "^5.0.0"
+ decimal.js "^10.5.0"
+ html-encoding-sniffer "^4.0.0"
+ http-proxy-agent "^7.0.2"
+ https-proxy-agent "^7.0.6"
+ is-potential-custom-element-name "^1.0.1"
+ nwsapi "^2.2.16"
+ parse5 "^7.2.1"
+ rrweb-cssom "^0.8.0"
+ saxes "^6.0.0"
+ symbol-tree "^3.2.4"
+ tough-cookie "^5.1.1"
+ w3c-xmlserializer "^5.0.0"
+ webidl-conversions "^7.0.0"
+ whatwg-encoding "^3.1.1"
+ whatwg-mimetype "^4.0.0"
+ whatwg-url "^14.1.1"
+ ws "^8.18.0"
+ xml-name-validator "^5.0.0"
+
jsesc@^2.5.1:
version "2.5.2"
resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz"
@@ -11335,7 +11564,7 @@ keccak@3.0.2:
node-gyp-build "^4.2.0"
readable-stream "^3.6.0"
-keyv@^4.5.3:
+keyv@^4.5.3, keyv@^4.5.4:
version "4.5.4"
resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz"
integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
@@ -11730,7 +11959,7 @@ lru-cache@^10.0.1, "lru-cache@^9.1.1 || ^10.0.0":
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.1.0.tgz"
integrity sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==
-lru-cache@^10.2.0, lru-cache@^10.2.2:
+lru-cache@^10.2.0, lru-cache@^10.2.2, lru-cache@^10.4.3:
version "10.4.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
@@ -11749,6 +11978,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
+lz-string@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
+ integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
+
magic-string@^0.30.12:
version "0.30.21"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
@@ -13218,6 +13452,11 @@ null-loader@^4.0.1:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
+nwsapi@^2.2.16:
+ version "2.2.23"
+ resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.23.tgz#59712c3a88e6de2bb0b6ccc1070397267019cf6c"
+ integrity sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==
+
"nx@>=17.1.2 < 21":
version "20.8.4"
resolved "https://registry.yarnpkg.com/nx/-/nx-20.8.4.tgz#bdbb4e41963fa7833c2aa3c972b5832f8b56983d"
@@ -13692,6 +13931,13 @@ parse5@^7.0.0:
dependencies:
entities "^4.4.0"
+parse5@^7.2.1:
+ version "7.3.0"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.3.0.tgz#d7e224fa72399c7a175099f45fc2ad024b05ec05"
+ integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==
+ dependencies:
+ entities "^6.0.0"
+
parseurl@~1.3.2, parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz"
@@ -13824,6 +14070,11 @@ periscopic@^3.0.0:
estree-walker "^3.0.0"
is-reference "^3.0.0"
+picocolors@1.1.1, picocolors@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
+ integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
+
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz"
@@ -13834,11 +14085,6 @@ picocolors@^1.0.1:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1"
integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==
-picocolors@^1.1.1:
- version "1.1.1"
- resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
- integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
-
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
@@ -14513,6 +14759,15 @@ pretty-error@^4.0.0:
lodash "^4.17.20"
renderkid "^3.0.0"
+pretty-format@^27.0.2:
+ version "27.5.1"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
+ integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
+ dependencies:
+ ansi-regex "^5.0.1"
+ ansi-styles "^5.0.0"
+ react-is "^17.0.1"
+
pretty-format@^29.7.0:
version "29.7.0"
resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz"
@@ -14787,6 +15042,11 @@ react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0:
resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+react-is@^17.0.1:
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
+ integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
+
react-is@^18.0.0:
version "18.2.0"
resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz"
@@ -15369,6 +15629,11 @@ rollup@^4.20.0, rollup@^4.43.0:
"@rollup/rollup-win32-x64-msvc" "4.55.1"
fsevents "~2.3.2"
+rrweb-cssom@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz#3021d1b4352fbf3b614aaeed0bc0d5739abe0bc2"
+ integrity sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==
+
rtlcss@^4.1.0:
version "4.1.1"
resolved "https://registry.npmjs.org/rtlcss/-/rtlcss-4.1.1.tgz"
@@ -15428,6 +15693,13 @@ sax@^1.2.4:
resolved "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz"
integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==
+saxes@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"
+ integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==
+ dependencies:
+ xmlchars "^2.2.0"
+
scheduler@^0.23.2:
version "0.23.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
@@ -16252,6 +16524,11 @@ swr@^2.2.5:
dequal "^2.0.3"
use-sync-external-store "^1.6.0"
+symbol-tree@^3.2.4:
+ version "3.2.4"
+ resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2"
+ integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==
+
tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0:
version "2.2.1"
resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz"
@@ -16471,6 +16748,18 @@ tinyspy@^4.0.3:
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-4.0.4.tgz#d77a002fb53a88aa1429b419c1c92492e0c81f78"
integrity sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==
+tldts-core@^6.1.86:
+ version "6.1.86"
+ resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.86.tgz#a93e6ed9d505cb54c542ce43feb14c73913265d8"
+ integrity sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==
+
+tldts@^6.1.32:
+ version "6.1.86"
+ resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.86.tgz#087e0555b31b9725ee48ca7e77edc56115cd82f7"
+ integrity sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==
+ dependencies:
+ tldts-core "^6.1.86"
+
tmp@0.0.33, tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz"
@@ -16514,6 +16803,20 @@ touch@^3.1.0:
dependencies:
nopt "~1.0.10"
+tough-cookie@^5.1.1:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.1.2.tgz#66d774b4a1d9e12dc75089725af3ac75ec31bed7"
+ integrity sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==
+ dependencies:
+ tldts "^6.1.32"
+
+tr46@^5.1.0:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.1.1.tgz#96ae867cddb8fdb64a49cc3059a8d428bcf238ca"
+ integrity sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==
+ dependencies:
+ punycode "^2.3.1"
+
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz"
@@ -16549,11 +16852,6 @@ trough@^2.0.0:
resolved "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz"
integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==
-ts-api-utils@^1.0.1:
- version "1.4.3"
- resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz#bfc2215fe6528fecab2b0fba570a2e8a4263b064"
- integrity sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==
-
ts-api-utils@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.4.0.tgz#2690579f96d2790253bdcf1ca35d569ad78f9ad8"
@@ -16703,6 +17001,16 @@ typedarray@^0.0.6:
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
+typescript-eslint@^8.53.0:
+ version "8.53.0"
+ resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.53.0.tgz#c35ca6403cd381753aee325f67e10d6101d55f04"
+ integrity sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw==
+ dependencies:
+ "@typescript-eslint/eslint-plugin" "8.53.0"
+ "@typescript-eslint/parser" "8.53.0"
+ "@typescript-eslint/typescript-estree" "8.53.0"
+ "@typescript-eslint/utils" "8.53.0"
+
"typescript@>=3 < 6":
version "5.3.3"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz"
@@ -17159,6 +17467,13 @@ vitest@^3.2.4:
vite-node "3.2.4"
why-is-node-running "^2.3.0"
+w3c-xmlserializer@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c"
+ integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==
+ dependencies:
+ xml-name-validator "^5.0.0"
+
walk-up-path@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/walk-up-path/-/walk-up-path-3.0.1.tgz#c8d78d5375b4966c717eb17ada73dbd41490e886"
@@ -17204,6 +17519,11 @@ webidl-conversions@^3.0.0:
resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
+webidl-conversions@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz#256b4e1882be7debbf01d05f0aa2039778ea080a"
+ integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==
+
webpack-bundle-analyzer@^4.10.2:
version "4.10.2"
resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz#633af2862c213730be3dbdf40456db171b60d5bd"
@@ -17385,6 +17705,26 @@ websocket-extensions@>=0.1.1:
resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz"
integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==
+whatwg-encoding@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5"
+ integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==
+ dependencies:
+ iconv-lite "0.6.3"
+
+whatwg-mimetype@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a"
+ integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==
+
+whatwg-url@^14.0.0, whatwg-url@^14.1.1:
+ version "14.2.0"
+ resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.2.0.tgz#4ee02d5d725155dae004f6ae95c73e7ef5d95663"
+ integrity sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==
+ dependencies:
+ tr46 "^5.1.0"
+ webidl-conversions "^7.0.0"
+
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz"
@@ -17576,6 +17916,16 @@ xml-js@^1.6.11:
dependencies:
sax "^1.2.4"
+xml-name-validator@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673"
+ integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==
+
+xmlchars@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
+ integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
+
xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz"
@@ -17659,7 +18009,12 @@ yocto-queue@^1.0.0:
resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz"
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
-zod@^4.1.8:
+"zod-validation-error@^3.5.0 || ^4.0.0":
+ version "4.0.2"
+ resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918"
+ integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==
+
+"zod@^3.25.0 || ^4.0.0", zod@^4.1.8:
version "4.3.5"
resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.5.tgz#aeb269a6f9fc259b1212c348c7c5432aaa474d2a"
integrity sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==