diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.css b/packages/web/src/theme/ProgramExample/TraceDrawer.css index 9f36d24c0..b3d017cde 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.css +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.css @@ -10,6 +10,9 @@ .trace-drawer-layout { display: grid; grid-template-columns: 1fr 1fr; + /* Fill the drawer height so both columns stretch full-height instead + * of collapsing to content height (which left dead space below). */ + grid-template-rows: minmax(0, 1fr); height: 100%; overflow: hidden; } @@ -204,11 +207,17 @@ border-left: 3px solid var(--ifm-color-danger); } -/* Trace panels */ +/* Trace panels - this row grows to fill the drawer's height */ .trace-panels { display: grid; grid-template-columns: 1fr 1fr; - flex: 1; + /* Explicit full-height row: without this the single implicit grid row + * is auto-sized to content, so the panels stay at content height (the + * instructions list looked "fixed height" with dead space below) even + * though .trace-panels itself flex-grows. The row must fill the grid. */ + grid-template-rows: minmax(0, 1fr); + flex: 1 1 auto; + min-height: 0; overflow: hidden; gap: 1px; background: var(--ifm-color-emphasis-200); @@ -217,6 +226,37 @@ .trace-panel { background: var(--ifm-background-color); overflow: auto; + min-height: 0; +} + +/* Instructions panel: fixed header, internally scrolling list. + * + * These selectors are scoped with `.trace-panel` on purpose. The class + * names `.opcodes-panel` / `.opcode-list` are also used by the sibling + * TraceViewer component, whose global stylesheet caps them with + * `min-height: 200px; max-height: 300px`. Docusaurus theme CSS is not + * module-scoped, so those caps leak onto this drawer and pin the + * instructions list to a fixed height. The `.trace-panel.opcodes-panel` + * selector (specificity 0,2,0) outranks the leaked `.opcodes-panel` + * (0,1,0), and `max-height: none` / `min-height: 0` remove the cap so + * the panel grows to fill its grid cell with the list scrolling inside. */ +.trace-panel.opcodes-panel { + display: flex; + flex-direction: column; + overflow: hidden; + min-height: 0; + max-height: none; +} + +.trace-panel.opcodes-panel .panel-header { + flex-shrink: 0; +} + +.trace-panel.opcodes-panel .opcode-list { + flex: 1 1 auto; + min-height: 0; + max-height: none; + overflow: auto; } .panel-header { @@ -297,11 +337,62 @@ color: var(--ifm-color-content); } -.opcode-ellipsis { - padding: 4px 12px; +/* Instruction object footer - constant height, scrolls internally */ +.instruction-object-panel { + flex-shrink: 0; + display: flex; + flex-direction: column; + border-top: 1px solid var(--ifm-color-emphasis-200); + background: var(--ifm-background-color); +} + +.instruction-object-header { + position: static; + text-transform: none; + letter-spacing: normal; +} + +.instruction-object-toggle { + display: flex; + align-items: center; + gap: 6px; + margin: 0; + cursor: pointer; + user-select: none; +} + +.instruction-object-toggle input { + cursor: pointer; + margin: 0; +} + +.instruction-object-toggle code { font-size: 11px; - color: var(--ifm-color-content-secondary); +} + +.instruction-object-body { + flex-shrink: 0; + min-height: 0; + max-height: 180px; + overflow: auto; +} + +.instruction-object-json { + margin: 0; + padding: 10px 12px; + font-family: var(--ifm-font-family-monospace); + font-size: 11px; + line-height: 1.5; + white-space: pre; + color: var(--ifm-color-content); + background: transparent; +} + +.instruction-object-empty { + padding: 10px 12px; + font-size: 12px; font-style: italic; + color: var(--ifm-color-content-secondary); } /* Stack display */ @@ -409,6 +500,6 @@ .trace-panels { grid-template-columns: 1fr; - grid-template-rows: 1fr 1fr; + grid-template-rows: minmax(0, 1fr) minmax(0, 1fr); } } diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx index cd9b37416..488ff6850 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx @@ -56,6 +56,7 @@ function TraceDrawerContent(): JSX.Element { const [isTracing, setIsTracing] = useState(false); const [traceError, setTraceError] = useState(null); const [storage, setStorage] = useState>({}); + const [showInstructionObject, setShowInstructionObject] = useState(false); // Build PC -> instruction map for source highlighting const pcToInstruction = useMemo(() => { @@ -105,6 +106,17 @@ function TraceDrawerContent(): JSX.Element { return extractCallInfo(instruction.debug.context); }, [trace, currentStep, pcToInstruction]); + // Build the ethdebug/format instruction object for the current step + const currentFormatInstruction = useMemo(() => { + if (trace.length === 0 || currentStep >= trace.length) return undefined; + + const step = trace[currentStep]; + const instruction = pcToInstruction.get(step.pc); + if (!instruction) return undefined; + + return toFormatInstruction(instruction, step.pc); + }, [trace, currentStep, pcToInstruction]); + // Build call stack by scanning invoke/return/revert up to // current step const callStack = useMemo(() => { @@ -576,6 +588,36 @@ function TraceDrawerContent(): JSX.Element { )} + +
+
+ +
+ {showInstructionObject && ( +
+ {currentFormatInstruction ? ( +
+                        {JSON.stringify(currentFormatInstruction, null, 2)}
+                      
+ ) : ( +
+ No instruction data for this step. +
+ )} +
+ )} +
)} @@ -595,21 +637,22 @@ function OpcodeList({ currentStep, onStepClick, }: OpcodeListProps): JSX.Element { - // Show a window around the current step - const windowSize = 8; - const start = Math.max(0, currentStep - windowSize); - const end = Math.min(trace.length, currentStep + windowSize + 1); - const visibleSteps = trace.slice(start, end); + // Render the full instruction list; the panel scrolls internally. + // Keep the active step scrolled into view as the trace advances. + const activeRef = useRef(null); + + useEffect(() => { + activeRef.current?.scrollIntoView({ block: "nearest" }); + }, [currentStep]); return (
- {start > 0 &&
... {start} above
} - {visibleSteps.map((step, i) => { - const index = start + i; + {trace.map((step, index) => { const isActive = index === currentStep; return (
onStepClick(index)} > @@ -621,9 +664,6 @@ function OpcodeList({
); })} - {end < trace.length && ( -
... {trace.length - end} below
- )}
); } @@ -673,6 +713,41 @@ function StorageDisplay({ storage }: StorageDisplayProps): JSX.Element { ); } +/** + * Convert an Evm.Instruction into an ethdebug/format/instruction object + * for display (offset, operation, context). Mirrors the compiler's + * program-builder so the drawer shows the canonical format shape. + */ +function toFormatInstruction( + instruction: Evm.Instruction, + offset: number, +): Record { + const result: Record = { offset }; + + if (instruction.mnemonic) { + const operation: Record = { + mnemonic: instruction.mnemonic, + }; + + if (instruction.immediates && instruction.immediates.length > 0) { + operation.arguments = [ + "0x" + + instruction.immediates + .map((b) => b.toString(16).padStart(2, "0")) + .join(""), + ]; + } + + result.operation = operation; + } + + if (instruction.debug?.context) { + result.context = instruction.debug.context; + } + + return result; +} + function formatBigInt(value: bigint): string { const hex = value.toString(16); if (hex.length <= 8) {