From 1f297d68dc995712e9c94d3991d2b1bfda620e9a Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 15 Jan 2026 19:06:34 -0500 Subject: [PATCH 01/18] Add @ethdebug/evm package to monorepo Extract EVM execution and state access utilities into a standalone package: - Executor: wraps @ethereumjs/evm for contract deployment and execution - createMachineState: adapter for @ethdebug/pointers Machine.State interface - Trace utilities: capture execution traces step-by-step Extracted from packages/bugc/test/evm and packages/bugc/test/examples. --- bin/start | 3 +- package.json | 2 +- packages/evm/package.json | 53 ++++++ packages/evm/src/executor.ts | 304 ++++++++++++++++++++++++++++++++++ packages/evm/src/index.ts | 37 +++++ packages/evm/src/machine.ts | 105 ++++++++++++ packages/evm/src/trace.ts | 52 ++++++ packages/evm/tsconfig.json | 16 ++ packages/evm/vitest.config.ts | 13 ++ 9 files changed, 583 insertions(+), 2 deletions(-) create mode 100644 packages/evm/package.json create mode 100644 packages/evm/src/executor.ts create mode 100644 packages/evm/src/index.ts create mode 100644 packages/evm/src/machine.ts create mode 100644 packages/evm/src/trace.ts create mode 100644 packages/evm/tsconfig.json create mode 100644 packages/evm/vitest.config.ts diff --git a/bin/start b/bin/start index 17c20fb6..f3cc1331 100755 --- a/bin/start +++ b/bin/start @@ -10,9 +10,10 @@ else fi # Run the commands with concurrently -concurrently --names=format,pointers,bugc,playground,web,tests \ +concurrently --names=format,pointers,evm,bugc,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/playground && yarn watch" \ "cd ./packages/web && yarn start $DOCUSAURUS_NO_OPEN" \ diff --git a/package.json b/package.json index c58ffd69..aa28f0ea 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", "bundle": "tsx ./bin/bundle-schema.ts", "test": "vitest", "test:coverage": "vitest run --coverage", diff --git a/packages/evm/package.json b/packages/evm/package.json new file mode 100644 index 00000000..02c5c833 --- /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 00000000..17a29307 --- /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 00000000..1784e552 --- /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 00000000..b96aa99a --- /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 00000000..2f9e3e38 --- /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 00000000..18545ef8 --- /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 00000000..c88a255c --- /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"], + }, + }, +}); From 6047c2b6535b68162899b44c0851d9cee8acc614 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 15 Jan 2026 19:25:34 -0500 Subject: [PATCH 02/18] Add @ethdebug/programs-react package to monorepo Extract React components for program visualization from packages/web/src/theme/ProgramExample and ShikiCodeBlock. New package includes: - ProgramExampleContextProvider and useProgramExampleContext - Opcodes, SourceContents, HighlightedInstruction components - ShikiCodeBlock and useHighlighter for syntax highlighting - Utility functions: computeOffsets, resolveDynamicInstruction Web package now consumes @ethdebug/programs-react, keeping only Docusaurus-specific components (Details, Variables, Viewer) and CSS. --- bin/start | 3 +- package.json | 2 +- packages/programs-react/.eslintrc.cjs | 18 ++ packages/programs-react/package.json | 53 +++ .../components}/HighlightedInstruction.tsx | 13 +- .../programs-react/src/components/Opcodes.css | 37 +++ .../src/components}/Opcodes.tsx | 19 +- .../components/ProgramExampleContext.test.tsx | 150 +++++++++ .../src/components}/ProgramExampleContext.tsx | 49 ++- .../src/components/SourceContents.css | 14 + .../src/components}/SourceContents.tsx | 22 +- .../programs-react/src/components/index.ts | 16 + packages/programs-react/src/index.ts | 43 +++ .../src/shiki/ShikiCodeBlock.tsx | 38 +++ packages/programs-react/src/shiki/index.ts | 9 + .../src/shiki}/useHighlighter.ts | 20 +- .../programs-react/src/utils/dynamic.test.ts | 163 ++++++++++ .../src/utils}/dynamic.ts | 29 ++ packages/programs-react/src/utils/index.ts | 14 + .../programs-react/src/utils/offsets.test.ts | 93 ++++++ .../src/utils}/offsets.ts | 19 +- packages/programs-react/tsconfig.json | 18 ++ packages/programs-react/vitest.config.ts | 13 + packages/web/package.json | 1 + .../web/src/theme/ProgramExample/Details.tsx | 6 +- .../src/theme/ProgramExample/Variables.tsx | 6 +- .../web/src/theme/ProgramExample/Viewer.tsx | 6 +- .../web/src/theme/ProgramExample/index.ts | 24 +- .../theme/ShikiCodeBlock/ShikiCodeBlock.tsx | 21 -- .../web/src/theme/ShikiCodeBlock/index.ts | 12 +- yarn.lock | 304 ++++++++++++++++-- 31 files changed, 1150 insertions(+), 85 deletions(-) create mode 100644 packages/programs-react/.eslintrc.cjs create mode 100644 packages/programs-react/package.json rename packages/{web/src/theme/ProgramExample => programs-react/src/components}/HighlightedInstruction.tsx (58%) create mode 100644 packages/programs-react/src/components/Opcodes.css rename packages/{web/src/theme/ProgramExample => programs-react/src/components}/Opcodes.tsx (87%) create mode 100644 packages/programs-react/src/components/ProgramExampleContext.test.tsx rename packages/{web/src/theme/ProgramExample => programs-react/src/components}/ProgramExampleContext.tsx (66%) create mode 100644 packages/programs-react/src/components/SourceContents.css rename packages/{web/src/theme/ProgramExample => programs-react/src/components}/SourceContents.tsx (85%) create mode 100644 packages/programs-react/src/components/index.ts create mode 100644 packages/programs-react/src/index.ts create mode 100644 packages/programs-react/src/shiki/ShikiCodeBlock.tsx create mode 100644 packages/programs-react/src/shiki/index.ts rename packages/{web/src/theme/ShikiCodeBlock => programs-react/src/shiki}/useHighlighter.ts (72%) create mode 100644 packages/programs-react/src/utils/dynamic.test.ts rename packages/{web/src/theme/ProgramExample => programs-react/src/utils}/dynamic.ts (74%) create mode 100644 packages/programs-react/src/utils/index.ts create mode 100644 packages/programs-react/src/utils/offsets.test.ts rename packages/{web/src/theme/ProgramExample => programs-react/src/utils}/offsets.ts (75%) create mode 100644 packages/programs-react/tsconfig.json create mode 100644 packages/programs-react/vitest.config.ts delete mode 100644 packages/web/src/theme/ShikiCodeBlock/ShikiCodeBlock.tsx diff --git a/bin/start b/bin/start index f3cc1331..ffd64a77 100755 --- a/bin/start +++ b/bin/start @@ -10,11 +10,12 @@ else fi # Run the commands with concurrently -concurrently --names=format,pointers,evm,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/package.json b/package.json index aa28f0ea..8db61136 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "packages/*" ], "scripts": { - "build": "tsc --build packages/format packages/pointers packages/evm packages/bugc", + "build": "tsc --build packages/format packages/pointers packages/evm packages/bugc packages/programs-react", "bundle": "tsx ./bin/bundle-schema.ts", "test": "vitest", "test:coverage": "vitest run --coverage", diff --git a/packages/programs-react/.eslintrc.cjs b/packages/programs-react/.eslintrc.cjs new file mode 100644 index 00000000..6e8698b7 --- /dev/null +++ b/packages/programs-react/.eslintrc.cjs @@ -0,0 +1,18 @@ +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 }, + ], + }, +}; diff --git a/packages/programs-react/package.json b/packages/programs-react/package.json new file mode 100644 index 00000000..2d207f5b --- /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 8dcb4b47..86dddf44 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 00000000..7296ab1d --- /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 7e6debc3..3da00432 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 00000000..fecaa8c6 --- /dev/null +++ b/packages/programs-react/src/components/ProgramExampleContext.test.tsx @@ -0,0 +1,150 @@ +/** + * Tests for ProgramExampleContext. + */ + +import { describe, it, expect } 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", () => { + expect(() => { + renderHook(() => useProgramExampleContext()); + }).toThrow(/must be used within a ProgramExampleContextProvider/); + }); +}); 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 d514cdea..ff85ee02 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 00000000..5bd07a84 --- /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 82429a60..793e67e6 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/index.ts b/packages/programs-react/src/components/index.ts new file mode 100644 index 00000000..3b1f0a7f --- /dev/null +++ b/packages/programs-react/src/components/index.ts @@ -0,0 +1,16 @@ +/** + * 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"; diff --git a/packages/programs-react/src/index.ts b/packages/programs-react/src/index.ts new file mode 100644 index 00000000..ceb5c1a0 --- /dev/null +++ b/packages/programs-react/src/index.ts @@ -0,0 +1,43 @@ +/** + * @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"; + +// Shiki utilities +export { + useHighlighter, + ShikiCodeBlock, + type Highlighter, + type HighlightOptions, + type ShikiCodeBlockProps, +} from "#shiki/index"; + +// Utility functions +export { + computeOffsets, + resolveDynamicInstruction, + type DynamicInstruction, + type DynamicContext, + type ContextThunk, + type FindSourceRangeOptions, + type ResolverOptions, +} from "#utils/index"; + +// CSS - consumers should import these stylesheets +// import "@ethdebug/programs-react/components/Opcodes.css"; +// import "@ethdebug/programs-react/components/SourceContents.css"; diff --git a/packages/programs-react/src/shiki/ShikiCodeBlock.tsx b/packages/programs-react/src/shiki/ShikiCodeBlock.tsx new file mode 100644 index 00000000..ebee0764 --- /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 00000000..b4ed16f3 --- /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 96f091e6..82a2112e 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 00000000..1347f832 --- /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 e80dada7..285b7bb8 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 00000000..a58910c1 --- /dev/null +++ b/packages/programs-react/src/utils/index.ts @@ -0,0 +1,14 @@ +/** + * Utility exports. + */ + +export { computeOffsets } from "./offsets.js"; + +export { + resolveDynamicInstruction, + type DynamicInstruction, + type DynamicContext, + type ContextThunk, + type FindSourceRangeOptions, + type ResolverOptions, +} from "./dynamic.js"; 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 00000000..0f837778 --- /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 9e60320f..2b6c911e 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 00000000..cdfb74de --- /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 00000000..ab56fbfc --- /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/package.json b/packages/web/package.json index cdd5a76f..7ac5b607 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -23,6 +23,7 @@ "@docusaurus/types": "^3.9.2", "@ethdebug/format": "^0.1.0-0", "@ethdebug/pointers": "^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", diff --git a/packages/web/src/theme/ProgramExample/Details.tsx b/packages/web/src/theme/ProgramExample/Details.tsx index 68bb9cee..c9945208 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/Variables.tsx b/packages/web/src/theme/ProgramExample/Variables.tsx index da709988..82624934 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 94adc29f..defbb127 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 02f07f31..b10520a7 100644 --- a/packages/web/src/theme/ProgramExample/index.ts +++ b/packages/web/src/theme/ProgramExample/index.ts @@ -1,6 +1,22 @@ -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"; +// Also re-export utilities for convenience +export { + computeOffsets, + resolveDynamicInstruction, + type DynamicInstruction, + type DynamicContext, + type ContextThunk, +} from "@ethdebug/programs-react"; + +// Local Docusaurus-specific components export * from "./Viewer"; diff --git a/packages/web/src/theme/ShikiCodeBlock/ShikiCodeBlock.tsx b/packages/web/src/theme/ShikiCodeBlock/ShikiCodeBlock.tsx deleted file mode 100644 index d1319152..00000000 --- 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 987f6d64..76301ac8 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 13395320..695ad846 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" @@ -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== @@ -4820,7 +4831,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 +5109,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 +5209,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" @@ -6657,6 +6694,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 +6903,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" @@ -8329,6 +8378,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 +8471,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 +8537,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 +8735,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 +8992,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" @@ -10391,6 +10471,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 +10599,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 +10643,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 +10671,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 +10685,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 +11054,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 +11305,32 @@ js-yaml@^3.10.0, js-yaml@^3.13.1, js-yaml@^3.8.4: argparse "^1.0.7" esprima "^4.0.0" +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" @@ -11730,7 +11864,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 +11883,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 +13357,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 +13836,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 +13975,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 +13990,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 +14664,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 +14947,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 +15534,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 +15598,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 +16429,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 +16653,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 +16708,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" @@ -17159,6 +17367,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 +17419,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 +17605,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 +17816,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" From 23363ac862893274ae9bc7f31a845db66aa36ec7 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 15 Jan 2026 19:52:55 -0500 Subject: [PATCH 03/18] Add @ethdebug/bugc-react package to monorepo Extract visualization components from playground for BUG compiler output: - AstView, IrView, CfgView, BytecodeView components - Monaco-based Editor with BUG syntax highlighting - EthdebugTooltip for debug context display - Theme-aware CSS using Docusaurus/Infima conventions - Utility functions for debug info extraction --- packages/bugc-react/.eslintrc.cjs | 18 + packages/bugc-react/package.json | 59 ++ .../bugc-react/src/components/AstView.css | 21 + .../bugc-react/src/components/AstView.tsx | 46 + .../src/components/BytecodeView.css | 115 +++ .../src/components/BytecodeView.tsx | 205 +++++ .../bugc-react/src/components/CfgView.css | 206 +++++ .../bugc-react/src/components/CfgView.tsx | 563 ++++++++++++ packages/bugc-react/src/components/Editor.tsx | 242 +++++ .../src/components/EthdebugTooltip.css | 76 ++ .../src/components/EthdebugTooltip.tsx | 151 ++++ packages/bugc-react/src/components/IrView.css | 147 +++ packages/bugc-react/src/components/IrView.tsx | 843 ++++++++++++++++++ packages/bugc-react/src/components/index.ts | 21 + .../bugc-react/src/components/variables.css | 108 +++ packages/bugc-react/src/hooks/index.ts | 5 + .../src/hooks/useEthdebugTooltip.ts | 109 +++ packages/bugc-react/src/index.ts | 73 ++ packages/bugc-react/src/types.ts | 91 ++ packages/bugc-react/src/utils/bugLanguage.ts | 240 +++++ packages/bugc-react/src/utils/debugUtils.ts | 106 +++ .../bugc-react/src/utils/formatBytecode.ts | 193 ++++ packages/bugc-react/src/utils/index.ts | 31 + packages/bugc-react/src/utils/irDebugUtils.ts | 245 +++++ packages/bugc-react/tsconfig.json | 19 + 25 files changed, 3933 insertions(+) create mode 100644 packages/bugc-react/.eslintrc.cjs create mode 100644 packages/bugc-react/package.json create mode 100644 packages/bugc-react/src/components/AstView.css create mode 100644 packages/bugc-react/src/components/AstView.tsx create mode 100644 packages/bugc-react/src/components/BytecodeView.css create mode 100644 packages/bugc-react/src/components/BytecodeView.tsx create mode 100644 packages/bugc-react/src/components/CfgView.css create mode 100644 packages/bugc-react/src/components/CfgView.tsx create mode 100644 packages/bugc-react/src/components/Editor.tsx create mode 100644 packages/bugc-react/src/components/EthdebugTooltip.css create mode 100644 packages/bugc-react/src/components/EthdebugTooltip.tsx create mode 100644 packages/bugc-react/src/components/IrView.css create mode 100644 packages/bugc-react/src/components/IrView.tsx create mode 100644 packages/bugc-react/src/components/index.ts create mode 100644 packages/bugc-react/src/components/variables.css create mode 100644 packages/bugc-react/src/hooks/index.ts create mode 100644 packages/bugc-react/src/hooks/useEthdebugTooltip.ts create mode 100644 packages/bugc-react/src/index.ts create mode 100644 packages/bugc-react/src/types.ts create mode 100644 packages/bugc-react/src/utils/bugLanguage.ts create mode 100644 packages/bugc-react/src/utils/debugUtils.ts create mode 100644 packages/bugc-react/src/utils/formatBytecode.ts create mode 100644 packages/bugc-react/src/utils/index.ts create mode 100644 packages/bugc-react/src/utils/irDebugUtils.ts create mode 100644 packages/bugc-react/tsconfig.json diff --git a/packages/bugc-react/.eslintrc.cjs b/packages/bugc-react/.eslintrc.cjs new file mode 100644 index 00000000..6e8698b7 --- /dev/null +++ b/packages/bugc-react/.eslintrc.cjs @@ -0,0 +1,18 @@ +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 }, + ], + }, +}; diff --git a/packages/bugc-react/package.json b/packages/bugc-react/package.json new file mode 100644 index 00000000..e3e448fa --- /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 00000000..453b38ba --- /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 00000000..0af03cf8 --- /dev/null +++ b/packages/bugc-react/src/components/AstView.tsx @@ -0,0 +1,46 @@ +/** + * AstView component for displaying the AST of a BUG program. + */ + +import React from "react"; +import type { Ast } from "@ethdebug/bugc"; +import "./AstView.css"; + +/** + * 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 ( +
+
{astJson}
+
+ ); +} diff --git a/packages/bugc-react/src/components/BytecodeView.css b/packages/bugc-react/src/components/BytecodeView.css new file mode 100644 index 00000000..b66318e6 --- /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 00000000..c42ffa37 --- /dev/null +++ b/packages/bugc-react/src/components/BytecodeView.tsx @@ -0,0 +1,205 @@ +/** + * 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"; +import "./BytecodeView.css"; + +/** + * 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 +
+
+ +
+
+

Hex

+
{runtimeHex}
+
+ +
+

Instructions

+ +
+
+
+ ); +} diff --git a/packages/bugc-react/src/components/CfgView.css b/packages/bugc-react/src/components/CfgView.css new file mode 100644 index 00000000..16e51dba --- /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 00000000..a9da44db --- /dev/null +++ b/packages/bugc-react/src/components/CfgView.tsx @@ -0,0 +1,563 @@ +/** + * 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"; +import "./CfgView.css"; + +/** + * 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} + +

+
+
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

+
+
+

+ {error} +

+
+
+ ); + } + + if (!loaded || !rfModule) { + return ( +
+
+

Control Flow Graph

+
+
+

Loading...

+
+
+ ); + } + + 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 00000000..47f5c787 --- /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 00000000..d758d9d5 --- /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 00000000..0e06e730 --- /dev/null +++ b/packages/bugc-react/src/components/EthdebugTooltip.tsx @@ -0,0 +1,151 @@ +/** + * Tooltip component for displaying ethdebug debug information. + */ + +import React, { useRef, useEffect } from "react"; +import type { TooltipData } from "#types"; +import "./EthdebugTooltip.css"; + +/** + * 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 00000000..e449ed73 --- /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 00000000..69dd8632 --- /dev/null +++ b/packages/bugc-react/src/components/IrView.tsx @@ -0,0 +1,843 @@ +/** + * 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"; +import "./IrView.css"; + +/** + * 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 00000000..1eb6ca4f --- /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 00000000..490510c6 --- /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 00000000..e0c7d142 --- /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 00000000..336b1a68 --- /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 00000000..b4151f57 --- /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 00000000..a5290e6e --- /dev/null +++ b/packages/bugc-react/src/types.ts @@ -0,0 +1,91 @@ +/** + * 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 line (1-indexed) */ + startLine: number; + /** Ending line (1-indexed) */ + endLine: number; + /** Starting column (0-indexed) */ + startColumn: number; + /** Ending column (0-indexed) */ + endColumn: 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 00000000..90114f37 --- /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 00000000..a623d2aa --- /dev/null +++ b/packages/bugc-react/src/utils/debugUtils.ts @@ -0,0 +1,106 @@ +/** + * 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 ( + range.start && + range.end && + typeof range.start === "object" && + typeof range.end === "object" + ) { + const start = range.start as Record; + const end = range.end as Record; + return [ + { + startLine: start.line ?? 1, + endLine: end.line ?? 1, + startColumn: start.column ?? 0, + endColumn: end.column ?? 0, + }, + ]; + } + } + } + + // 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 00000000..c955d976 --- /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 00000000..335e2e09 --- /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 00000000..11bdcc76 --- /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 00000000..7dd5776e --- /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" }] +} From 2a6be5d538bd58b273210024da2f41e7798bf6da Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 15 Jan 2026 19:54:57 -0500 Subject: [PATCH 04/18] Suppress expected React error output in context test Mock console.error during the test that verifies the hook throws when used outside its provider, preventing React's error logging from cluttering test output. --- .../src/components/ProgramExampleContext.test.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/programs-react/src/components/ProgramExampleContext.test.tsx b/packages/programs-react/src/components/ProgramExampleContext.test.tsx index fecaa8c6..88ff691b 100644 --- a/packages/programs-react/src/components/ProgramExampleContext.test.tsx +++ b/packages/programs-react/src/components/ProgramExampleContext.test.tsx @@ -2,7 +2,7 @@ * Tests for ProgramExampleContext. */ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, vi } from "vitest"; import { renderHook, act } from "@testing-library/react"; import React from "react"; import { @@ -143,8 +143,13 @@ describe("ProgramExampleContext", () => { }); 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(); }); }); From 0a059c205917fcf9d43cec22f348d36325afa8fe Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 15 Jan 2026 20:48:13 -0500 Subject: [PATCH 05/18] Integrate @ethdebug/bugc-react into documentation site Add BUG Playground page to docs with interactive compiler visualization. The playground allows editing BUG source code and viewing AST, IR, CFG, and Bytecode output with bidirectional source highlighting. Changes to bugc-react: - Remove CSS imports from components (CSS now managed by consumers) - Fix SourceRange type to use offset/length instead of line/column - Simplify extractSourceRange to return correct format New theme components in web package: - BugcExample wrapper components with Docusaurus theming - Monaco editor decoration CSS for source highlighting --- .../bugc-react/src/components/AstView.tsx | 1 - .../src/components/BytecodeView.tsx | 1 - .../bugc-react/src/components/CfgView.tsx | 1 - .../src/components/EthdebugTooltip.tsx | 1 - packages/bugc-react/src/components/IrView.tsx | 1 - packages/bugc-react/src/types.ts | 12 +- packages/bugc-react/src/utils/debugUtils.ts | 19 +- packages/web/docs/bug-playground.mdx | 40 +++ packages/web/package.json | 2 + .../web/src/theme/BugcExample/AstView.css | 21 ++ .../src/theme/BugcExample/BugPlayground.css | 191 +++++++++++ .../src/theme/BugcExample/BugPlayground.tsx | 318 ++++++++++++++++++ .../src/theme/BugcExample/BytecodeView.css | 115 +++++++ .../web/src/theme/BugcExample/CfgView.css | 206 ++++++++++++ .../src/theme/BugcExample/EthdebugTooltip.css | 76 +++++ packages/web/src/theme/BugcExample/IrView.css | 147 ++++++++ packages/web/src/theme/BugcExample/index.ts | 46 +++ .../web/src/theme/BugcExample/variables.css | 108 ++++++ 18 files changed, 1278 insertions(+), 28 deletions(-) create mode 100644 packages/web/docs/bug-playground.mdx create mode 100644 packages/web/src/theme/BugcExample/AstView.css create mode 100644 packages/web/src/theme/BugcExample/BugPlayground.css create mode 100644 packages/web/src/theme/BugcExample/BugPlayground.tsx create mode 100644 packages/web/src/theme/BugcExample/BytecodeView.css create mode 100644 packages/web/src/theme/BugcExample/CfgView.css create mode 100644 packages/web/src/theme/BugcExample/EthdebugTooltip.css create mode 100644 packages/web/src/theme/BugcExample/IrView.css create mode 100644 packages/web/src/theme/BugcExample/index.ts create mode 100644 packages/web/src/theme/BugcExample/variables.css diff --git a/packages/bugc-react/src/components/AstView.tsx b/packages/bugc-react/src/components/AstView.tsx index 0af03cf8..b7744567 100644 --- a/packages/bugc-react/src/components/AstView.tsx +++ b/packages/bugc-react/src/components/AstView.tsx @@ -4,7 +4,6 @@ import React from "react"; import type { Ast } from "@ethdebug/bugc"; -import "./AstView.css"; /** * Props for AstView component. diff --git a/packages/bugc-react/src/components/BytecodeView.tsx b/packages/bugc-react/src/components/BytecodeView.tsx index c42ffa37..441b0f5e 100644 --- a/packages/bugc-react/src/components/BytecodeView.tsx +++ b/packages/bugc-react/src/components/BytecodeView.tsx @@ -8,7 +8,6 @@ import type { BytecodeOutput, SourceRange } from "#types"; import { extractSourceRange } from "#utils/debugUtils"; import { useEthdebugTooltip } from "#hooks/useEthdebugTooltip"; import { EthdebugTooltip } from "./EthdebugTooltip.js"; -import "./BytecodeView.css"; /** * Props for BytecodeView component. diff --git a/packages/bugc-react/src/components/CfgView.tsx b/packages/bugc-react/src/components/CfgView.tsx index a9da44db..dba9b592 100644 --- a/packages/bugc-react/src/components/CfgView.tsx +++ b/packages/bugc-react/src/components/CfgView.tsx @@ -8,7 +8,6 @@ import React, { useMemo, useCallback, useState, useEffect } from "react"; import type { Ir } from "@ethdebug/bugc"; -import "./CfgView.css"; /** * Props for CfgView component. diff --git a/packages/bugc-react/src/components/EthdebugTooltip.tsx b/packages/bugc-react/src/components/EthdebugTooltip.tsx index 0e06e730..38b7d00a 100644 --- a/packages/bugc-react/src/components/EthdebugTooltip.tsx +++ b/packages/bugc-react/src/components/EthdebugTooltip.tsx @@ -4,7 +4,6 @@ import React, { useRef, useEffect } from "react"; import type { TooltipData } from "#types"; -import "./EthdebugTooltip.css"; /** * Props for EthdebugTooltip component. diff --git a/packages/bugc-react/src/components/IrView.tsx b/packages/bugc-react/src/components/IrView.tsx index 69dd8632..fd38f1e1 100644 --- a/packages/bugc-react/src/components/IrView.tsx +++ b/packages/bugc-react/src/components/IrView.tsx @@ -15,7 +15,6 @@ import { } from "#utils/irDebugUtils"; import { useEthdebugTooltip } from "#hooks/useEthdebugTooltip"; import { EthdebugTooltip } from "./EthdebugTooltip.js"; -import "./IrView.css"; /** * Props for IrView component. diff --git a/packages/bugc-react/src/types.ts b/packages/bugc-react/src/types.ts index a5290e6e..93952407 100644 --- a/packages/bugc-react/src/types.ts +++ b/packages/bugc-react/src/types.ts @@ -8,14 +8,10 @@ import type { Ast, Ir, Evm } from "@ethdebug/bugc"; * Represents a source range in the original code. */ export interface SourceRange { - /** Starting line (1-indexed) */ - startLine: number; - /** Ending line (1-indexed) */ - endLine: number; - /** Starting column (0-indexed) */ - startColumn: number; - /** Ending column (0-indexed) */ - endColumn: number; + /** Starting byte offset */ + offset: number; + /** Length in bytes */ + length: number; } /** diff --git a/packages/bugc-react/src/utils/debugUtils.ts b/packages/bugc-react/src/utils/debugUtils.ts index a623d2aa..bf748555 100644 --- a/packages/bugc-react/src/utils/debugUtils.ts +++ b/packages/bugc-react/src/utils/debugUtils.ts @@ -29,23 +29,12 @@ export function extractSourceRange( 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; + const range = code.range as Record; if ( - range.start && - range.end && - typeof range.start === "object" && - typeof range.end === "object" + typeof range.offset === "number" && + typeof range.length === "number" ) { - const start = range.start as Record; - const end = range.end as Record; - return [ - { - startLine: start.line ?? 1, - endLine: end.line ?? 1, - startColumn: start.column ?? 0, - endColumn: end.column ?? 0, - }, - ]; + return [{ offset: range.offset, length: range.length }]; } } } diff --git a/packages/web/docs/bug-playground.mdx b/packages/web/docs/bug-playground.mdx new file mode 100644 index 00000000..53114cfb --- /dev/null +++ b/packages/web/docs/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/package.json b/packages/web/package.json index 7ac5b607..27a54cf1 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -23,6 +23,8 @@ "@docusaurus/types": "^3.9.2", "@ethdebug/format": "^0.1.0-0", "@ethdebug/pointers": "^0.1.0-0", + "@ethdebug/bugc": "^0.1.0-0", + "@ethdebug/bugc-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", diff --git a/packages/web/src/theme/BugcExample/AstView.css b/packages/web/src/theme/BugcExample/AstView.css new file mode 100644 index 00000000..453b38ba --- /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 00000000..9eae834c --- /dev/null +++ b/packages/web/src/theme/BugcExample/BugPlayground.css @@ -0,0 +1,191 @@ +/** + * 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 00000000..f2642794 --- /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 && ( + + )} + +
+
+ +
+
+ +
+ +
+ {compileResult && !compileResult.success && ( +
+

Compilation Error

+
{compileResult.error}
+
+ )} + + {compileResult?.success && ( + <> +
+ {tabs.map((tab) => ( + + ))} +
+ +
+ {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 00000000..b66318e6 --- /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 00000000..16e51dba --- /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 00000000..d758d9d5 --- /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 00000000..e449ed73 --- /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 00000000..b951609b --- /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 00000000..490510c6 --- /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); +} From e8bff5a38d16d15ce341e7537de0b4985938ca4f Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 15 Jan 2026 21:18:06 -0500 Subject: [PATCH 06/18] Restructure docs with new information architecture Phase 1 of docs improvement plan: - Create new sections: getting-started, concepts, types, pointers, programs - Move goals.mdx and known-challenges.mdx to reference/ - Move bug-playground.mdx to examples/ - Delete outdated docs/sketches/ - Add _category_.json files for sidebar organization - Add stub index pages for all new sections - Add compiler guides section under implementation-guides --- packages/web/docs/concepts/_category_.json | 4 + packages/web/docs/concepts/index.mdx | 21 + packages/web/docs/examples/_category_.json | 4 + .../docs/{ => examples}/bug-playground.mdx | 0 packages/web/docs/examples/index.mdx | 21 + .../web/docs/getting-started/_category_.json | 4 + packages/web/docs/getting-started/index.mdx | 17 + .../implementation-guides/_category_.json | 4 + .../compiler/_category_.json | 4 + .../implementation-guides/compiler/index.mdx | 21 + .../implementation-guides.mdx | 3 +- packages/web/docs/pointers/_category_.json | 4 + packages/web/docs/pointers/index.mdx | 17 + packages/web/docs/programs/_category_.json | 4 + packages/web/docs/programs/index.mdx | 19 + packages/web/docs/reference/_category_.json | 4 + .../challenges.mdx} | 2 +- packages/web/docs/{ => reference}/goals.mdx | 2 +- packages/web/docs/sketches/_category_.json | 8 - packages/web/docs/sketches/layout.mdx | 496 ------------------ packages/web/docs/sketches/prototype.mdx | 390 -------------- packages/web/docs/types/_category_.json | 4 + packages/web/docs/types/index.mdx | 18 + 23 files changed, 174 insertions(+), 897 deletions(-) create mode 100644 packages/web/docs/concepts/_category_.json create mode 100644 packages/web/docs/concepts/index.mdx create mode 100644 packages/web/docs/examples/_category_.json rename packages/web/docs/{ => examples}/bug-playground.mdx (100%) create mode 100644 packages/web/docs/examples/index.mdx create mode 100644 packages/web/docs/getting-started/_category_.json create mode 100644 packages/web/docs/getting-started/index.mdx create mode 100644 packages/web/docs/implementation-guides/_category_.json create mode 100644 packages/web/docs/implementation-guides/compiler/_category_.json create mode 100644 packages/web/docs/implementation-guides/compiler/index.mdx create mode 100644 packages/web/docs/pointers/_category_.json create mode 100644 packages/web/docs/pointers/index.mdx create mode 100644 packages/web/docs/programs/_category_.json create mode 100644 packages/web/docs/programs/index.mdx create mode 100644 packages/web/docs/reference/_category_.json rename packages/web/docs/{known-challenges.mdx => reference/challenges.mdx} (99%) rename packages/web/docs/{ => reference}/goals.mdx (99%) delete mode 100644 packages/web/docs/sketches/_category_.json delete mode 100644 packages/web/docs/sketches/layout.mdx delete mode 100644 packages/web/docs/sketches/prototype.mdx create mode 100644 packages/web/docs/types/_category_.json create mode 100644 packages/web/docs/types/index.mdx diff --git a/packages/web/docs/concepts/_category_.json b/packages/web/docs/concepts/_category_.json new file mode 100644 index 00000000..1d3167d4 --- /dev/null +++ b/packages/web/docs/concepts/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Concepts", + "position": 3 +} diff --git a/packages/web/docs/concepts/index.mdx b/packages/web/docs/concepts/index.mdx new file mode 100644 index 00000000..e555d9b4 --- /dev/null +++ b/packages/web/docs/concepts/index.mdx @@ -0,0 +1,21 @@ +--- +sidebar_position: 1 +--- + +# Concepts + +Core concepts behind ethdebug/format. + +:::note[Coming soon] +This section is under construction. It will explain the mental models +underlying the format. +::: + +## Overview + +The ethdebug/format provides structured debug information for EVM bytecode. +It consists of several main components: + +- **Types** — Describe the structure of data +- **Pointers** — Describe where data lives at runtime +- **Programs** — Describe what's in scope at each bytecode instruction diff --git a/packages/web/docs/examples/_category_.json b/packages/web/docs/examples/_category_.json new file mode 100644 index 00000000..c618bc18 --- /dev/null +++ b/packages/web/docs/examples/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Examples", + "position": 8 +} diff --git a/packages/web/docs/bug-playground.mdx b/packages/web/docs/examples/bug-playground.mdx similarity index 100% rename from packages/web/docs/bug-playground.mdx rename to packages/web/docs/examples/bug-playground.mdx diff --git a/packages/web/docs/examples/index.mdx b/packages/web/docs/examples/index.mdx new file mode 100644 index 00000000..7bedce3c --- /dev/null +++ b/packages/web/docs/examples/index.mdx @@ -0,0 +1,21 @@ +--- +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 00000000..877a378f --- /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/index.mdx b/packages/web/docs/getting-started/index.mdx new file mode 100644 index 00000000..9546c7af --- /dev/null +++ b/packages/web/docs/getting-started/index.mdx @@ -0,0 +1,17 @@ +--- +sidebar_position: 1 +--- + +# Getting Started + +Choose your path to learn about ethdebug/format. + +:::note[Coming soon] +This section is under construction. Check back soon for getting started guides +tailored to your needs. +::: + +## Choose your path + +- **Building a debugger?** Learn how to consume ethdebug/format data. +- **Building a compiler?** Learn how to emit ethdebug/format data. diff --git a/packages/web/docs/implementation-guides/_category_.json b/packages/web/docs/implementation-guides/_category_.json new file mode 100644 index 00000000..3949327e --- /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 00000000..5b1098d9 --- /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/index.mdx b/packages/web/docs/implementation-guides/compiler/index.mdx new file mode 100644 index 00000000..782a93e2 --- /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 90f42493..ebeeeedb 100644 --- a/packages/web/docs/implementation-guides/implementation-guides.mdx +++ b/packages/web/docs/implementation-guides/implementation-guides.mdx @@ -1,5 +1,6 @@ --- -sidebar_position: 4 +sidebar_position: 1 +sidebar_label: Overview pagination_prev: null pagination_next: null --- diff --git a/packages/web/docs/pointers/_category_.json b/packages/web/docs/pointers/_category_.json new file mode 100644 index 00000000..7f83e9d7 --- /dev/null +++ b/packages/web/docs/pointers/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Pointers", + "position": 5 +} diff --git a/packages/web/docs/pointers/index.mdx b/packages/web/docs/pointers/index.mdx new file mode 100644 index 00000000..9616b63e --- /dev/null +++ b/packages/web/docs/pointers/index.mdx @@ -0,0 +1,17 @@ +--- +sidebar_position: 1 +--- + +# Pointers + +How ethdebug/format describes data locations. + +:::note[Coming soon] +This section is under construction. It will explain how pointers work as +recipes for finding data at runtime. +::: + +## Overview + +Pointers in ethdebug/format describe where data lives. Unlike simple offsets, +pointers can express complex, dynamic locations that depend on runtime state. diff --git a/packages/web/docs/programs/_category_.json b/packages/web/docs/programs/_category_.json new file mode 100644 index 00000000..d5d043dd --- /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 00000000..d7672e10 --- /dev/null +++ b/packages/web/docs/programs/index.mdx @@ -0,0 +1,19 @@ +--- +sidebar_position: 1 +--- + +# Programs + +How ethdebug/format describes runtime context. + +:::note[Coming soon] +This section is under construction. It will explain how programs describe +what's in scope at each bytecode instruction. +::: + +## Overview + +Programs in ethdebug/format describe the runtime context at each point in +execution. They tell a debugger which variables are in scope, what source +code corresponds to the current instruction, and how to interpret the +current state. diff --git a/packages/web/docs/reference/_category_.json b/packages/web/docs/reference/_category_.json new file mode 100644 index 00000000..04b7db94 --- /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 c051dc4b..f1d29345 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/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 94c02203..721d1517 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 f40aabfa..00000000 --- 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 9cf98094..00000000 --- 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 789fb905..00000000 --- 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 00000000..8b257445 --- /dev/null +++ b/packages/web/docs/types/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Types", + "position": 4 +} diff --git a/packages/web/docs/types/index.mdx b/packages/web/docs/types/index.mdx new file mode 100644 index 00000000..c3999786 --- /dev/null +++ b/packages/web/docs/types/index.mdx @@ -0,0 +1,18 @@ +--- +sidebar_position: 1 +--- + +# Types + +How ethdebug/format describes data types. + +:::note[Coming soon] +This section is under construction. It will explain how the format represents +types like integers, structs, arrays, and mappings. +::: + +## Overview + +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. From fe3d7b3deb5bf5daf02f70eb3dff10d993a3f7d6 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 15 Jan 2026 21:26:09 -0500 Subject: [PATCH 07/18] Add Getting Started and Concepts documentation Phase 2 of docs restructure: - Rewrite overview.mdx with clear intro and navigation cards - Add getting-started path chooser with debugger/compiler guides - Add concepts section explaining types, pointers, programs - Add data locations reference for EVM storage areas - Update implementation-guides landing page --- packages/web/docs/concepts/data-locations.mdx | 145 ++++++++++++++++++ packages/web/docs/concepts/index.mdx | 85 ++++++++-- .../getting-started/for-compiler-authors.mdx | 136 ++++++++++++++++ .../getting-started/for-debugger-authors.mdx | 117 ++++++++++++++ packages/web/docs/getting-started/index.mdx | 68 ++++++-- .../implementation-guides.mdx | 51 +++--- packages/web/docs/overview.mdx | 73 ++++++++- 7 files changed, 621 insertions(+), 54 deletions(-) create mode 100644 packages/web/docs/concepts/data-locations.mdx create mode 100644 packages/web/docs/getting-started/for-compiler-authors.mdx create mode 100644 packages/web/docs/getting-started/for-debugger-authors.mdx diff --git a/packages/web/docs/concepts/data-locations.mdx b/packages/web/docs/concepts/data-locations.mdx new file mode 100644 index 00000000..c5cb3822 --- /dev/null +++ b/packages/web/docs/concepts/data-locations.mdx @@ -0,0 +1,145 @@ +--- +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 index e555d9b4..a366afb9 100644 --- a/packages/web/docs/concepts/index.mdx +++ b/packages/web/docs/concepts/index.mdx @@ -1,21 +1,84 @@ --- sidebar_position: 1 +sidebar_label: Overview --- # Concepts -Core concepts behind ethdebug/format. +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. -:::note[Coming soon] -This section is under construction. It will explain the mental models -underlying the format. -::: +## The main components -## Overview +ethdebug/format consists of three main kinds of information: -The ethdebug/format provides structured debug information for EVM bytecode. -It consists of several main components: +### Types -- **Types** — Describe the structure of data -- **Pointers** — Describe where data lives at runtime -- **Programs** — Describe what's in scope at each bytecode instruction +**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/getting-started/for-compiler-authors.mdx b/packages/web/docs/getting-started/for-compiler-authors.mdx new file mode 100644 index 00000000..37b71b4e --- /dev/null +++ b/packages/web/docs/getting-started/for-compiler-authors.mdx @@ -0,0 +1,136 @@ +--- +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 00000000..d5105a9b --- /dev/null +++ b/packages/web/docs/getting-started/for-debugger-authors.mdx @@ -0,0 +1,117 @@ +--- +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)** — Formal schema definitions diff --git a/packages/web/docs/getting-started/index.mdx b/packages/web/docs/getting-started/index.mdx index 9546c7af..c45b2612 100644 --- a/packages/web/docs/getting-started/index.mdx +++ b/packages/web/docs/getting-started/index.mdx @@ -1,17 +1,67 @@ --- sidebar_position: 1 +sidebar_label: Overview --- -# Getting Started +# Getting started -Choose your path to learn about ethdebug/format. - -:::note[Coming soon] -This section is under construction. Check back soon for getting started guides -tailored to your needs. -::: +Welcome to ethdebug/format! This section helps you get started based on what +you're building. ## Choose your path -- **Building a debugger?** Learn how to consume ethdebug/format data. -- **Building a compiler?** Learn how to emit ethdebug/format data. +
+
+
+
+

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)** — Read the formal schema definitions diff --git a/packages/web/docs/implementation-guides/implementation-guides.mdx b/packages/web/docs/implementation-guides/implementation-guides.mdx index ebeeeedb..527fcc70 100644 --- a/packages/web/docs/implementation-guides/implementation-guides.mdx +++ b/packages/web/docs/implementation-guides/implementation-guides.mdx @@ -7,52 +7,47 @@ 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. - -For an introduction to **ethdebug/format** pointers, please see -the Pointer specification's [Overview](/spec/pointer/overview) and -[Key concepts](/spec/pointer/concepts) pages. +A detailed walkthrough of the **@ethdebug/pointers** TypeScript reference +implementation, showing how to resolve pointers to concrete values. +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**
-
- _Guides for implementing **ethdebug/format** support inside a compiler are - planned and still need to be written._ -
+
**[Compiler guides](/docs/implementation-guides/compiler)**
+
+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 c7065468..19a54b30 100644 --- a/packages/web/docs/overview.mdx +++ b/packages/web/docs/overview.mdx @@ -4,9 +4,70 @@ 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) for formal definitions. From 9a714f68a9f97fa6e9340c8ebbe8432e1e33aa3c Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 15 Jan 2026 21:37:08 -0500 Subject: [PATCH 08/18] Add Types, Pointers, and Programs deep-dive documentation Phase 3 of docs restructure: - Types: overview, elementary types, composite types - Pointers: overview, regions guide - Programs: overview, instructions, variables - Fix broken links to /spec and /docs/known-challenges --- .../getting-started/for-compiler-authors.mdx | 2 +- .../getting-started/for-debugger-authors.mdx | 2 +- packages/web/docs/getting-started/index.mdx | 2 +- packages/web/docs/overview.mdx | 2 +- packages/web/docs/pointers/index.mdx | 204 ++++++++++++++++- packages/web/docs/pointers/regions.mdx | 197 +++++++++++++++++ packages/web/docs/programs/index.mdx | 146 ++++++++++++- packages/web/docs/programs/instructions.mdx | 179 +++++++++++++++ packages/web/docs/programs/variables.mdx | 147 +++++++++++++ packages/web/docs/types/composite.mdx | 206 ++++++++++++++++++ packages/web/docs/types/elementary.mdx | 153 +++++++++++++ packages/web/docs/types/index.mdx | 158 +++++++++++++- packages/web/docusaurus.config.ts | 2 +- 13 files changed, 1368 insertions(+), 32 deletions(-) create mode 100644 packages/web/docs/pointers/regions.mdx create mode 100644 packages/web/docs/programs/instructions.mdx create mode 100644 packages/web/docs/programs/variables.mdx create mode 100644 packages/web/docs/types/composite.mdx create mode 100644 packages/web/docs/types/elementary.mdx diff --git a/packages/web/docs/getting-started/for-compiler-authors.mdx b/packages/web/docs/getting-started/for-compiler-authors.mdx index 37b71b4e..01e262e6 100644 --- a/packages/web/docs/getting-started/for-compiler-authors.mdx +++ b/packages/web/docs/getting-started/for-compiler-authors.mdx @@ -119,7 +119,7 @@ Most compilers can add ethdebug/format support incrementally:
diff --git a/packages/web/docs/getting-started/for-debugger-authors.mdx b/packages/web/docs/getting-started/for-debugger-authors.mdx index d5105a9b..3c479138 100644 --- a/packages/web/docs/getting-started/for-debugger-authors.mdx +++ b/packages/web/docs/getting-started/for-debugger-authors.mdx @@ -114,4 +114,4 @@ To consume ethdebug/format, your debugger needs: - **[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)** — Formal schema definitions +- **[Specification](/spec/overview)** — Formal schema definitions diff --git a/packages/web/docs/getting-started/index.mdx b/packages/web/docs/getting-started/index.mdx index c45b2612..3c8b0b05 100644 --- a/packages/web/docs/getting-started/index.mdx +++ b/packages/web/docs/getting-started/index.mdx @@ -64,4 +64,4 @@ you're building. format - **[Examples](/docs/examples)** — See the format in action with interactive demos -- **[Specification](/spec)** — Read the formal schema definitions +- **[Specification](/spec/overview)** — Read the formal schema definitions diff --git a/packages/web/docs/overview.mdx b/packages/web/docs/overview.mdx index 19a54b30..ebd35dd4 100644 --- a/packages/web/docs/overview.mdx +++ b/packages/web/docs/overview.mdx @@ -70,4 +70,4 @@ The ethdebug/format specification defines schemas for: 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) for formal definitions. +[specification](/spec/overview) for formal definitions. diff --git a/packages/web/docs/pointers/index.mdx b/packages/web/docs/pointers/index.mdx index 9616b63e..77a2744d 100644 --- a/packages/web/docs/pointers/index.mdx +++ b/packages/web/docs/pointers/index.mdx @@ -4,14 +4,202 @@ sidebar_position: 1 # Pointers -How ethdebug/format describes data locations. +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. -:::note[Coming soon] -This section is under construction. It will explain how pointers work as -recipes for finding data at runtime. -::: +## Why pointers are complex -## Overview +In high-level languages, a variable like `uint256 balance` seems simple. But +at the EVM level, finding that value might require: -Pointers in ethdebug/format describe where data lives. Unlike simple offsets, -pointers can express complex, dynamic locations that depend on runtime state. +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`: unordered set of pointers +- `list`: ordered, possibly dynamic sequence +- `conditional`: pointers that apply only under certain conditions + +### Expressions make pointers dynamic + +Real allocations rarely use constant offsets. Expressions let you compute +addresses from runtime state: + +```json +{ + "location": "storage", + "slot": { + "$keccak256": [ + { "$read": "mapping-key" }, + { ".slot": "base-slot" } + ] + } +} +``` + +This computes a Solidity mapping slot by hashing the key with the base slot. + +Common expression operations: +- `$sum`, `$product`, `$quotient`, `$remainder`: arithmetic +- `$read`: read bytes from a named region +- `.offset`, `.length`, `.slot`: reference properties of named regions +- `$keccak256`: hash computation + +### Named regions enable composition + +Regions can have names that other parts of the pointer reference: + +```json +{ + "group": [ + { + "name": "array-pointer", + "location": "stack", + "slot": 0 + }, + { + "name": "array-data", + "location": "memory", + "offset": { "$read": "array-pointer" }, + "length": 32 + } + ] +} +``` + +Here, `array-data`'s offset comes from reading the value in `array-pointer`. + +## Example: Dynamic array in memory + +Here's how you might describe a `uint256[]` stored in memory: + +```json +{ + "group": [ + { + "name": "array-length", + "location": "memory", + "offset": "0x80", + "length": 32 + }, + { + "list": { + "count": { "$read": "array-length" }, + "each": "i", + "is": { + "name": "array-element", + "location": "memory", + "offset": { + "$sum": [ + { ".offset": "array-length" }, + 32, + { "$product": ["i", 32] } + ] + }, + "length": 32 + } + } + } + ] +} +``` + +This describes: +1. A region for the array length +2. A dynamic list of elements, where each element's position is computed from + the index + +## What's next + +
+
+
+
+

Regions

+
+
+

+ How to specify byte ranges in different data locations. +

+
+ +
+
+
+
+
+

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 00000000..36b47ac1 --- /dev/null +++ b/packages/web/docs/pointers/regions.mdx @@ -0,0 +1,197 @@ +--- +sidebar_position: 2 +--- + +# 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`: + +```json +{ + "location": "memory", + "offset": "0x40", + "length": 32 +} +``` + +- `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`: + +```json +{ + "location": "storage", + "slot": 5 +} +``` + +For storage and transient storage, values that don't fill a full slot can +specify sub-slot positioning: + +```json +{ + "location": "storage", + "slot": 0, + "offset": 12, + "length": 20 +} +``` + +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: + +```json +{ + "location": "memory", + "offset": "0x80", + "length": 64 +} +``` + +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: + +```json +{ + "location": "storage", + "slot": "0x0000000000000000000000000000000000000000000000000000000000000000" +} +``` + +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: + +```json +{ + "location": "stack", + "slot": 0 +} +``` + +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: + +```json +{ + "location": "calldata", + "offset": 4, + "length": 32 +} +``` + +The first 4 bytes are typically the function selector; arguments follow. + +### Returndata + +After a call, the returned data is accessible: + +```json +{ + "location": "returndata", + "offset": 0, + "length": 32 +} +``` + +### Code + +Contract bytecode can be read as data: + +```json +{ + "location": "code", + "offset": 100, + "length": 32 +} +``` + +This is used for immutable variables and other data embedded in bytecode. + +### Transient storage + +Transient storage (EIP-1153) persists only within a transaction: + +```json +{ + "location": "transient", + "slot": 0 +} +``` + +Uses the same slot-based addressing as regular storage. + +## Naming regions + +Any region can have a `name` that other parts of the pointer reference: + +```json +{ + "name": "token-balance", + "location": "storage", + "slot": 3 +} +``` + +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 accept expressions, not just literals: + +```json +{ + "location": "memory", + "offset": { + "$sum": [ + { "$read": "base-pointer" }, + { "$product": ["index", 32] } + ] + }, + "length": 32 +} +``` + +This computes the offset at runtime based on other values. See the +[expressions documentation](/spec/pointer/expression) for the full expression +language. + +## Learn more + +For complete schemas for each location type, see the +[pointer region specification](/spec/pointer/region). diff --git a/packages/web/docs/programs/index.mdx b/packages/web/docs/programs/index.mdx index d7672e10..4749455c 100644 --- a/packages/web/docs/programs/index.mdx +++ b/packages/web/docs/programs/index.mdx @@ -4,16 +4,142 @@ sidebar_position: 1 # Programs -How ethdebug/format describes runtime context. +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. -:::note[Coming soon] -This section is under construction. It will explain how programs describe -what's in scope at each bytecode instruction. -::: +## What programs contain -## Overview +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). -Programs in ethdebug/format describe the runtime context at each point in -execution. They tell a debugger which variables are in scope, what source -code corresponds to the current instruction, and how to interpret the -current state. +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. +

+
+ +
+
+
+
+
+

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 00000000..aaf2c4f3 --- /dev/null +++ b/packages/web/docs/programs/instructions.mdx @@ -0,0 +1,179 @@ +--- +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/variables.mdx b/packages/web/docs/programs/variables.mdx new file mode 100644 index 00000000..d02fd330 --- /dev/null +++ b/packages/web/docs/programs/variables.mdx @@ -0,0 +1,147 @@ +--- +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/types/composite.mdx b/packages/web/docs/types/composite.mdx new file mode 100644 index 00000000..82aff772 --- /dev/null +++ b/packages/web/docs/types/composite.mdx @@ -0,0 +1,206 @@ +--- +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 00000000..06195d0e --- /dev/null +++ b/packages/web/docs/types/elementary.mdx @@ -0,0 +1,153 @@ +--- +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 index c3999786..5e037a9b 100644 --- a/packages/web/docs/types/index.mdx +++ b/packages/web/docs/types/index.mdx @@ -4,15 +4,155 @@ sidebar_position: 1 # Types -How ethdebug/format describes data 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. -:::note[Coming soon] -This section is under construction. It will explain how the format represents -types like integers, structs, arrays, and mappings. -::: +## Why types matter -## Overview +When a debugger reads bytes from storage, memory, or the stack, those bytes +are meaningless without context. A 32-byte value could be: -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. +- 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. +

+
+ +
+
+
+
+
+

Full specification

+
+
+

+ Complete JSON schemas and detailed reference. +

+
+ +
+
+
diff --git a/packages/web/docusaurus.config.ts b/packages/web/docusaurus.config.ts index 3abec10e..9e7d9864 100644 --- a/packages/web/docusaurus.config.ts +++ b/packages/web/docusaurus.config.ts @@ -168,7 +168,7 @@ const config: Config = { }, { label: "Known challenges", - to: "/docs/known-challenges", + to: "/docs/reference/challenges", }, ], }, From 9d28cbbb33feb1b6b1d47550309825994f190a02 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 15 Jan 2026 22:06:22 -0500 Subject: [PATCH 09/18] Complete Phase 3 deep-dive documentation Add remaining documentation pages for types, pointers, and programs: - types/representation.mdx: How types map to bytes (encoding contexts, packed storage layout, dynamic data representation) - pointers/expressions.mdx: Expression language for dynamic locations ($sum, $keccak256, $read, .offset, .length, etc.) - pointers/resolution.mdx: Stub for future pointer resolution widget - programs/tracing.mdx: Stub for future execution tracing widget Update index pages with navigation cards linking to new content. --- packages/web/docs/pointers/expressions.mdx | 329 +++++++++++++++++++++ packages/web/docs/pointers/index.mdx | 35 +++ packages/web/docs/pointers/resolution.mdx | 87 ++++++ packages/web/docs/programs/index.mdx | 35 +++ packages/web/docs/programs/tracing.mdx | 81 +++++ packages/web/docs/types/index.mdx | 25 +- packages/web/docs/types/representation.mdx | 189 ++++++++++++ 7 files changed, 778 insertions(+), 3 deletions(-) create mode 100644 packages/web/docs/pointers/expressions.mdx create mode 100644 packages/web/docs/pointers/resolution.mdx create mode 100644 packages/web/docs/programs/tracing.mdx create mode 100644 packages/web/docs/types/representation.mdx diff --git a/packages/web/docs/pointers/expressions.mdx b/packages/web/docs/pointers/expressions.mdx new file mode 100644 index 00000000..f2de7858 --- /dev/null +++ b/packages/web/docs/pointers/expressions.mdx @@ -0,0 +1,329 @@ +--- +sidebar_position: 3 +--- + +# 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: + +```json +{ + "location": "memory", + "offset": { + "$sum": [ + { "$read": "array-start" }, + { "$product": ["index", 32] } + ] + }, + "length": 32 +} +``` + +## Arithmetic expressions + +Basic math operations for computing addresses: + +### `$sum` — Addition + +Adds all values in an array: + +```json +{ "$sum": [100, 32, 4] } +``` + +Result: `136` + +### `$difference` — Subtraction + +Subtracts the second value from the first (saturates at zero): + +```json +{ "$difference": [100, 32] } +``` + +Result: `68` + +### `$product` — Multiplication + +Multiplies all values in an array: + +```json +{ "$product": [32, 10] } +``` + +Result: `320` + +### `$quotient` — Division + +Integer division of first value by second: + +```json +{ "$quotient": [100, 32] } +``` + +Result: `3` + +### `$remainder` — Modulo + +Remainder after division: + +```json +{ "$remainder": [100, 32] } +``` + +Result: `4` + +## Reading values + +### `$read` — Read from a named region + +Reads the bytes from a previously defined region: + +```json +{ + "define": { + "name": "array-length-slot", + "location": "storage", + "slot": 5 + }, + "in": { + "location": "storage", + "slot": { + "$keccak256": [5] + }, + "length": { + "$product": [{ "$read": "array-length-slot" }, 32] + } + } +} +``` + +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: + +```json +{ + "group": [ + { + "name": "element-0", + "location": "memory", + "offset": "0x80", + "length": 32 + }, + { + "name": "element-1", + "location": "memory", + "offset": { + "$sum": [ + { ".offset": "element-0" }, + { ".length": "element-0" } + ] + }, + "length": 32 + } + ] +} +``` + +## 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)`: + +```json +{ + "location": "storage", + "slot": { + "$sum": [ + { "$keccak256": [5] }, + "element-index" + ] + } +} +``` + +This computes: `keccak256(5) + element-index` + +### Mapping value slots + +For a mapping at slot `n`, the value for key `k` is at `keccak256(k, n)`: + +```json +{ + "location": "storage", + "slot": { + "$keccak256": [ + { "$read": "key-region" }, + 3 + ] + } +} +``` + +This computes: `keccak256(key, 3)` — the standard Solidity mapping slot. + +### Nested mappings + +For `mapping(address => mapping(uint => uint))` at slot 2: + +```json +{ + "location": "storage", + "slot": { + "$keccak256": [ + { "$read": "inner-key" }, + { + "$keccak256": [ + { "$read": "outer-key" }, + 2 + ] + } + ] + } +} +``` + +This computes: `keccak256(inner_key, keccak256(outer_key, 2))` + +## Data manipulation + +### `$concat` — Concatenate bytes + +Joins byte sequences without padding: + +```json +{ "$concat": ["0xdead", "0xbeef"] } +``` + +Result: `0xdeadbeef` + +Useful for building hash inputs from multiple values. + +### `$sized` — Resize to N bytes + +Truncates or pads to exactly N bytes: + +```json +{ "$sized32": "0x1234" } +``` + +Result: `0x0000000000000000000000000000000000000000000000000000000000001234` + +Pads with zeros on the left; truncates from the left if too long. + +### `$wordsized` — Resize to word size + +Equivalent to `$sized32` on the EVM: + +```json +{ "$wordsized": { "$read": "some-region" } } +``` + +## Variables in expressions + +Expressions can reference variables by name. These come from list pointer +contexts: + +```json +{ + "list": { + "count": { "$read": "array-length" }, + "each": "i", + "is": { + "location": "storage", + "slot": { + "$sum": [ + { "$keccak256": [0] }, + "i" + ] + } + } + } +} +``` + +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: + +```json +{ + "define": { + "name": "array-slot", + "location": "storage", + "slot": 5 + }, + "in": { + "location": "storage", + "slot": { + "$sum": [ + { "$keccak256": [{ ".slot": "array-slot" }] }, + "element-index" + ] + } + } +} +``` + +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 index 77a2744d..2973c8b7 100644 --- a/packages/web/docs/pointers/index.mdx +++ b/packages/web/docs/pointers/index.mdx @@ -186,6 +186,41 @@ This describes:
+
+
+
+

Expressions

+
+
+

+ Computing dynamic locations with arithmetic and hashing. +

+
+ +
+
+ + +
+
+
+
+

Pointer resolution

+
+
+

+ How debuggers evaluate pointers against EVM state. +

+
+ +
+
diff --git a/packages/web/docs/pointers/resolution.mdx b/packages/web/docs/pointers/resolution.mdx new file mode 100644 index 00000000..80f5f97f --- /dev/null +++ b/packages/web/docs/pointers/resolution.mdx @@ -0,0 +1,87 @@ +--- +sidebar_position: 4 +--- + +# Pointer resolution + +Pointer resolution is the process of converting a pointer definition into +actual byte values from the EVM state. This page will feature an interactive +widget for exploring resolution step by step. + +## The resolution process + +Given a pointer and the current EVM state, resolution proceeds through several +stages: + +1. **Evaluate expressions**: Compute any dynamic values (`$sum`, `$keccak256`, + `$read`, etc.) using the current machine state +2. **Generate regions**: Produce concrete memory/storage/stack regions with + resolved offsets and lengths +3. **Read bytes**: Fetch the actual bytes from each region +4. **Combine results**: For grouped or conditional pointers, assemble the final + byte sequence + +## Example walkthrough + +Consider resolving a pointer to a dynamic array element: + +```json +{ + "define": { + "name": "base", + "location": "storage", + "slot": 3 + }, + "in": { + "location": "storage", + "slot": { + "$sum": [ + { "$keccak256": [{ ".slot": "base" }] }, + 2 + ] + } + } +} +``` + +**Step 1**: Define the base region (slot 3) + +**Step 2**: Evaluate `.slot` lookup → 3 + +**Step 3**: Compute `$keccak256([3])` → `0xc2575...` (the data slot) + +**Step 4**: Add index 2 → `0xc2575... + 2` + +**Step 5**: Read 32 bytes from that storage slot + +## Interactive exploration + +:::note Coming soon +An interactive pointer resolution widget will be added here, allowing you to: + +- Input a pointer definition +- Provide sample EVM state (storage, memory, stack values) +- Step through the resolution process +- See intermediate values at each stage +- View the final resolved bytes + +This widget will help you understand how pointers work in practice and debug +pointer definitions. +::: + +## Implementation resources + +For details on implementing pointer resolution: + +- [Evaluating expressions](/docs/implementation-guides/pointers/evaluating-expressions) + covers the expression evaluation algorithm +- [Dereference logic](/docs/implementation-guides/pointers/dereference-logic) + explains the full resolution pipeline +- [Generating regions](/docs/implementation-guides/pointers/dereference-logic/generating-regions) + details how to produce concrete regions from pointer definitions + +## Learn more + +- [Expressions documentation](./expressions) for the expression language +- [Regions documentation](./regions) for region structure +- [Pointer specification](/spec/pointer) for formal definitions diff --git a/packages/web/docs/programs/index.mdx b/packages/web/docs/programs/index.mdx index 4749455c..885ca921 100644 --- a/packages/web/docs/programs/index.mdx +++ b/packages/web/docs/programs/index.mdx @@ -126,6 +126,41 @@ runtime.
+
+
+
+

Variables

+
+
+

+ Connecting identifiers to runtime locations. +

+
+ +
+
+
+ +
+
+
+
+

Tracing execution

+
+
+

+ Using programs to inspect variables during execution. +

+
+ +
+
diff --git a/packages/web/docs/programs/tracing.mdx b/packages/web/docs/programs/tracing.mdx new file mode 100644 index 00000000..eeac49e6 --- /dev/null +++ b/packages/web/docs/programs/tracing.mdx @@ -0,0 +1,81 @@ +--- +sidebar_position: 4 +--- + +# Tracing execution + +Tracing brings together programs, pointers, and types to show what's happening +at each step of EVM execution. This page will feature an interactive widget +for exploring transaction traces with full variable inspection. + +## 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 + +## Example: Stepping through a transfer + +Consider tracing a simple ERC-20 transfer: + +``` +PC 0x1a4: SLOAD + Source: balances[msg.sender] + Variables in scope: + - sender: 0x1234...abcd (address) + - amount: 1000000000000000000 (uint256) + - senderBalance: +``` + +The trace viewer shows: +- Which storage slot is being loaded +- The source-level expression being evaluated +- Related variables and their current values + +## Interactive exploration + +:::note Coming soon +An interactive tracing widget will be added here, allowing you to: + +- Load a transaction trace +- Step forward and backward through execution +- See variables come into and out of scope +- Inspect complex data structures at each step +- Navigate between source code and bytecode views + +This widget will demonstrate how ethdebug/format enables rich debugging +experiences. +::: + +## Building a trace viewer + +The key components for trace integration: + +1. **EVM trace source**: Transaction traces from a node or simulation +2. **Program data**: The compiled program with instruction records +3. **Pointer resolver**: Engine to evaluate pointers against EVM state +4. **Type decoder**: Logic to interpret bytes according to type definitions + +## Learn more + +- [Instructions documentation](./instructions) for understanding instruction + records +- [Variables documentation](./variables) for variable structure and lifetime +- [Pointer resolution](../pointers/resolution) for resolving variable locations +- [BUG Playground](/docs/examples/bug-playground) for a live example of + compilation and debugging diff --git a/packages/web/docs/types/index.mdx b/packages/web/docs/types/index.mdx index 5e037a9b..7b5b269f 100644 --- a/packages/web/docs/types/index.mdx +++ b/packages/web/docs/types/index.mdx @@ -107,7 +107,7 @@ other (useful for recursive structures). ## What's next
-
+

Elementary types

@@ -123,7 +123,7 @@ other (useful for recursive structures).
-
+

Composite types

@@ -139,7 +139,26 @@ other (useful for recursive structures).
-
+
+ +
+
+
+
+

Representation

+
+
+

+ How types map to bytes in storage vs. memory encoding contexts. +

+
+ +
+
+

Full specification

diff --git a/packages/web/docs/types/representation.mdx b/packages/web/docs/types/representation.mdx new file mode 100644 index 00000000..85874013 --- /dev/null +++ b/packages/web/docs/types/representation.mdx @@ -0,0 +1,189 @@ +--- +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 From 0697233fb9838fb1031dcddc5ce5313e2e0eb0e8 Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 15 Jan 2026 23:02:13 -0500 Subject: [PATCH 10/18] Add Phase 4: Interactive widgets and documentation polish - Add @ethdebug/pointers-react package with: - PointerResolverProvider context and usePointerResolution hook - ResolutionVisualizer, RegionMap, RegionOutput components - createMockState utility for mock EVM state - CSS with light/dark theme support - Extend @ethdebug/programs-react with tracing: - TraceContext for execution trace state - TraceControls navigation component - VariableInspector component - mockTrace utilities - Add web integration: - PointersExample theme wrapper with BrowserOnly for SSR - ProgramExample TraceViewer wrapper - CSS theming for both light and dark modes - Add documentation: - reference/glossary.mdx with terminology definitions - implementation-guides/compiler/case-study-bug.mdx case study - Update pointers/resolution.mdx with interactive widget - Update programs/tracing.mdx with expanded documentation - Fix browser compatibility in @ethdebug/pointers: - Remove top-level await import("util") that broke SSG - Use Symbol.for("nodejs.util.inspect.custom") instead --- package.json | 2 +- packages/pointers-react/package.json | 51 +++ .../src/components/PointerResolverContext.tsx | 63 ++++ .../src/components/RegionMap.css | 117 ++++++ .../src/components/RegionMap.tsx | 130 +++++++ .../src/components/RegionOutput.css | 149 ++++++++ .../src/components/RegionOutput.tsx | 149 ++++++++ .../src/components/ResolutionVisualizer.css | 251 +++++++++++++ .../src/components/ResolutionVisualizer.tsx | 322 +++++++++++++++++ .../src/components/variables.css | 116 ++++++ .../src/hooks/usePointerResolution.ts | 179 +++++++++ packages/pointers-react/src/index.ts | 49 +++ .../pointers-react/src/utils/mockState.ts | 237 ++++++++++++ packages/pointers-react/tsconfig.json | 18 + packages/pointers/src/data.ts | 13 +- .../src/components/TraceContext.tsx | 184 ++++++++++ .../src/components/TraceControls.css | 101 ++++++ .../src/components/TraceControls.tsx | 147 ++++++++ .../src/components/VariableInspector.css | 151 ++++++++ .../src/components/VariableInspector.tsx | 175 +++++++++ .../programs-react/src/components/index.ts | 23 ++ packages/programs-react/src/index.ts | 25 ++ packages/programs-react/src/utils/index.ts | 9 + .../programs-react/src/utils/mockTrace.ts | 119 ++++++ .../compiler/case-study-bug.mdx | 339 ++++++++++++++++++ packages/web/docs/pointers/resolution.mdx | 169 +++++++-- packages/web/docs/programs/tracing.mdx | 182 ++++++++-- packages/web/docs/reference/glossary.mdx | 206 +++++++++++ packages/web/package.json | 1 + .../theme/PointersExample/PointerResolver.tsx | 116 ++++++ .../src/theme/PointersExample/RegionMap.css | 117 ++++++ .../theme/PointersExample/RegionOutput.css | 149 ++++++++ .../PointersExample/ResolutionVisualizer.css | 252 +++++++++++++ .../web/src/theme/PointersExample/index.ts | 23 ++ .../src/theme/PointersExample/variables.css | 129 +++++++ .../theme/ProgramExample/TraceControls.css | 123 +++++++ .../src/theme/ProgramExample/TraceViewer.css | 203 +++++++++++ .../src/theme/ProgramExample/TraceViewer.tsx | 224 ++++++++++++ .../ProgramExample/VariableInspector.css | 151 ++++++++ .../web/src/theme/ProgramExample/index.ts | 24 ++ 40 files changed, 5117 insertions(+), 71 deletions(-) create mode 100644 packages/pointers-react/package.json create mode 100644 packages/pointers-react/src/components/PointerResolverContext.tsx create mode 100644 packages/pointers-react/src/components/RegionMap.css create mode 100644 packages/pointers-react/src/components/RegionMap.tsx create mode 100644 packages/pointers-react/src/components/RegionOutput.css create mode 100644 packages/pointers-react/src/components/RegionOutput.tsx create mode 100644 packages/pointers-react/src/components/ResolutionVisualizer.css create mode 100644 packages/pointers-react/src/components/ResolutionVisualizer.tsx create mode 100644 packages/pointers-react/src/components/variables.css create mode 100644 packages/pointers-react/src/hooks/usePointerResolution.ts create mode 100644 packages/pointers-react/src/index.ts create mode 100644 packages/pointers-react/src/utils/mockState.ts create mode 100644 packages/pointers-react/tsconfig.json create mode 100644 packages/programs-react/src/components/TraceContext.tsx create mode 100644 packages/programs-react/src/components/TraceControls.css create mode 100644 packages/programs-react/src/components/TraceControls.tsx create mode 100644 packages/programs-react/src/components/VariableInspector.css create mode 100644 packages/programs-react/src/components/VariableInspector.tsx create mode 100644 packages/programs-react/src/utils/mockTrace.ts create mode 100644 packages/web/docs/implementation-guides/compiler/case-study-bug.mdx create mode 100644 packages/web/docs/reference/glossary.mdx create mode 100644 packages/web/src/theme/PointersExample/PointerResolver.tsx create mode 100644 packages/web/src/theme/PointersExample/RegionMap.css create mode 100644 packages/web/src/theme/PointersExample/RegionOutput.css create mode 100644 packages/web/src/theme/PointersExample/ResolutionVisualizer.css create mode 100644 packages/web/src/theme/PointersExample/index.ts create mode 100644 packages/web/src/theme/PointersExample/variables.css create mode 100644 packages/web/src/theme/ProgramExample/TraceControls.css create mode 100644 packages/web/src/theme/ProgramExample/TraceViewer.css create mode 100644 packages/web/src/theme/ProgramExample/TraceViewer.tsx create mode 100644 packages/web/src/theme/ProgramExample/VariableInspector.css diff --git a/package.json b/package.json index 8db61136..c57b1fad 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "packages/*" ], "scripts": { - "build": "tsc --build packages/format packages/pointers packages/evm packages/bugc packages/programs-react", + "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", diff --git a/packages/pointers-react/package.json b/packages/pointers-react/package.json new file mode 100644 index 00000000..86d5ba89 --- /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 00000000..9da54840 --- /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 00000000..0a68a798 --- /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 00000000..a5903949 --- /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 00000000..c9112a4b --- /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 00000000..60204d78 --- /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 00000000..2a8ea49d --- /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 00000000..0526ad88 --- /dev/null +++ b/packages/pointers-react/src/components/ResolutionVisualizer.tsx @@ -0,0 +1,322 @@ +/** + * 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 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 + +
+ {entries.length === 0 ? ( +
No storage entries
+ ) : ( +
+ {entries.map(([slot, value]) => ( +
+ handleSlotChange(slot, e.target.value)} + placeholder="slot (hex)" + /> + = + handleValueChange(slot, e.target.value)} + placeholder="value (hex)" + /> + +
+ ))} +
+ )} +
+ ); +} + +/** + * 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) + +
+ {stack.length === 0 ? ( +
Empty stack
+ ) : ( +
+ {stack.map((entry, index) => ( +
+ [{index}] + handleEntryChange(index, e.target.value)} + placeholder="value (hex)" + /> + +
+ ))} +
+ )} +
+ ); +} + +/** + * Component for editing memory contents. + */ +function MemoryEditor({ + memory, + onChange, +}: { + memory: string | undefined; + onChange: (memory: string) => void; +}): JSX.Element { + return ( +
+
+ Memory +
+