Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 97 additions & 6 deletions packages/web/src/theme/ProgramExample/TraceDrawer.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -409,6 +500,6 @@

.trace-panels {
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
}
}
97 changes: 86 additions & 11 deletions packages/web/src/theme/ProgramExample/TraceDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ function TraceDrawerContent(): JSX.Element {
const [isTracing, setIsTracing] = useState(false);
const [traceError, setTraceError] = useState<string | null>(null);
const [storage, setStorage] = useState<Record<string, string>>({});
const [showInstructionObject, setShowInstructionObject] = useState(false);

// Build PC -> instruction map for source highlighting
const pcToInstruction = useMemo(() => {
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -576,6 +588,36 @@ function TraceDrawerContent(): JSX.Element {
)}
</div>
</div>

<div className="instruction-object-panel">
<div className="panel-header instruction-object-header">
<label className="instruction-object-toggle">
<input
type="checkbox"
checked={showInstructionObject}
onChange={(e) =>
setShowInstructionObject(e.target.checked)
}
/>
<span>
<code>ethdebug/format/instruction</code> object
</span>
</label>
</div>
{showInstructionObject && (
<div className="instruction-object-body">
{currentFormatInstruction ? (
<pre className="instruction-object-json">
{JSON.stringify(currentFormatInstruction, null, 2)}
</pre>
) : (
<div className="instruction-object-empty">
No instruction data for this step.
</div>
)}
</div>
)}
</div>
</div>
)}
</div>
Expand All @@ -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<HTMLDivElement>(null);

useEffect(() => {
activeRef.current?.scrollIntoView({ block: "nearest" });
}, [currentStep]);

return (
<div className="opcode-list">
{start > 0 && <div className="opcode-ellipsis">... {start} above</div>}
{visibleSteps.map((step, i) => {
const index = start + i;
{trace.map((step, index) => {
const isActive = index === currentStep;
return (
<div
key={index}
ref={isActive ? activeRef : undefined}
className={`opcode-item ${isActive ? "active" : ""}`}
onClick={() => onStepClick(index)}
>
Expand All @@ -621,9 +664,6 @@ function OpcodeList({
</div>
);
})}
{end < trace.length && (
<div className="opcode-ellipsis">... {trace.length - end} below</div>
)}
</div>
);
}
Expand Down Expand Up @@ -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<string, unknown> {
const result: Record<string, unknown> = { offset };

if (instruction.mnemonic) {
const operation: Record<string, unknown> = {
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) {
Expand Down
Loading