diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.css b/packages/web/src/theme/ProgramExample/TraceDrawer.css index 30f474b48..bba152028 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.css +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.css @@ -549,6 +549,87 @@ border: 1px solid var(--ifm-color-success); } +/* Instructions panel header: title + a small grouping control. */ +.opcodes-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.band-control { + display: inline-flex; + border: 1px solid var(--ifm-color-emphasis-300); + border-radius: 5px; + overflow: hidden; +} + +.band-btn { + appearance: none; + border: none; + background: var(--ifm-background-color); + color: var(--ifm-color-content-secondary); + font-family: var(--ifm-font-family-base); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.02em; + text-transform: none; + padding: 2px 7px; + cursor: pointer; + transition: + background 0.15s, + color 0.15s; +} + +.band-btn + .band-btn { + border-left: 1px solid var(--ifm-color-emphasis-300); +} + +.band-btn:hover:not(.active) { + background: var(--ifm-color-emphasis-100); + color: var(--ifm-color-content); +} + +.band-btn.active { + background: var(--ifm-color-emphasis-200); + color: var(--ifm-color-content); +} + +/* Subtle grouped-range banding: a faint tint + hairline accent on + alternating groups so grouped ranges read at a glance, with a light + label naming each group. No indentation, no collapsing. */ +.opcode-group { + position: relative; +} + +.opcode-list-banded .opcode-group:nth-of-type(odd) { + background: var(--ifm-color-emphasis-100); + box-shadow: inset 2px 0 0 var(--ifm-color-emphasis-300); +} + +[data-theme="dark"] .opcode-list-banded .opcode-group:nth-of-type(odd) { + background: rgba(255, 255, 255, 0.03); +} + +.opcode-group-label { + padding: 3px 12px 1px; + font-family: var(--ifm-font-family-base); + font-size: 10.5px; + font-weight: 600; + line-height: 1.3; + color: var(--ifm-color-content-secondary); + /* Long source-range labels must never widen the panel or wrap: clip to + one line with an ellipsis; the full text is on the element's title. */ + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.opcode-group-label.label-function { + color: var(--ifm-color-primary); +} + /* Instruction object footer - user-resizable, scrolls internally */ .instruction-object-panel { position: relative; diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx index 1d734c43f..cc90209e2 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx @@ -58,6 +58,37 @@ interface CompileResult { bytecode?: BytecodeOutput; } +/** + * How the instruction list groups consecutive rows for the subtle + * background banding: off, by source statement/expression range, or by the + * enclosing function activation. + */ +type BandMode = "none" | "statement" | "function"; +const BAND_MODES: readonly { value: BandMode; label: string; title: string }[] = + [ + { value: "none", label: "off", title: "No grouping" }, + { + value: "statement", + label: "source", + title: "Band instructions by source statement", + }, + { + value: "function", + label: "function", + title: "Band instructions by enclosing function", + }, + ]; + +/** A banded group of consecutive instructions in the instruction list. */ +interface InstructionGroup { + /** Row indices (into `trace`) in this group. */ + rows: number[]; + /** Light label for the group, or null for unlabeled glue/top-level. */ + label: string | null; + /** What the label names, for styling. */ + labelKind: "function" | "source"; +} + /** bugc optimizer levels the tracer can compile at. */ type OptLevel = 0 | 1 | 2 | 3; const OPT_LEVELS: readonly OptLevel[] = [0, 1, 2, 3]; @@ -83,6 +114,8 @@ function TraceDrawerContent(): JSX.Element { const [traceError, setTraceError] = useState(null); const [storage, setStorage] = useState>({}); const [showInstructionObject, setShowInstructionObject] = useState(false); + // Subtle grouped-range highlighting of the instruction list. + const [banding, setBanding] = useState("function"); const [objectHeight, setObjectHeight] = useState(OBJECT_DEFAULT_HEIGHT); const [isResizingObject, setIsResizingObject] = useState(false); @@ -285,6 +318,70 @@ function TraceDrawerContent(): JSX.Element { }); }, [trace, formatPcToInstruction]); + // Group consecutive instructions for the subtle background banding: by + // source range ("statement") or by enclosing function ("function"). + // Presentation only — the same source ranges and invoke/return contexts + // that already drive the highlight and call-info banner. + const instructionGroups = useMemo(() => { + if (banding === "none" || trace.length === 0) return null; + + // Track the active function per step via a single invoke/return pass + // (avoids re-running buildCallStack for every row). + const activeFn: (string | null)[] = []; + const stack: string[] = []; + for (const s of trace) { + const fi = formatPcToInstruction.get(s.pc); + const info = fi ? extractCallInfoFromInstruction(fi) : undefined; + const top = stack.length ? stack[stack.length - 1] : null; + if (info?.kind === "invoke") { + const id = info.identifier ?? "?"; + if (top !== id) stack.push(id); + activeFn.push(id); + } else if (info?.kind === "return") { + activeFn.push(top); + stack.pop(); + } else { + activeFn.push(top); + } + } + + const groups: InstructionGroup[] = []; + let cur: InstructionGroup | null = null; + let curKey = ""; + trace.forEach((s, i) => { + let key: string; + let label: string | null; + let labelKind: "function" | "source"; + if (banding === "function") { + const fn = activeFn[i]; + key = fn ?? "·top"; + label = fn ? `${fn}()` : null; + labelKind = "function"; + } else { + const inst = pcToInstruction.get(s.pc); + const ranges = inst?.debug?.context + ? extractSourceRange(inst.debug.context) + : []; + const r = ranges[0]; + key = r ? `${r.offset}:${r.length}` : "·none"; + label = r + ? source + .slice(r.offset, r.offset + r.length) + .replace(/\s+/g, " ") + .trim() + : null; + labelKind = "source"; + } + if (!cur || key !== curKey) { + cur = { rows: [], label, labelKind }; + curKey = key; + groups.push(cur); + } + cur.rows.push(i); + }); + return groups; + }, [banding, trace, formatPcToInstruction, pcToInstruction, source]); + // Resolve argument values for call stack frames const argCacheRef = useRef>(new Map()); @@ -767,13 +864,34 @@ function TraceDrawerContent(): JSX.Element {
-
Instructions
+
+ Instructions + + {BAND_MODES.map((m) => ( + + ))} + +
@@ -895,6 +1013,8 @@ interface OpcodeListProps { transformsByStep: string[][]; /** Function-call boundary badge per trace step (same index as `trace`). */ callBadgeByStep: (CallBadge | null)[]; + /** Banded groups for subtle grouped-range highlighting, or null (flat). */ + groups: InstructionGroup[] | null; } function OpcodeList({ @@ -903,6 +1023,7 @@ function OpcodeList({ onStepClick, transformsByStep, callBadgeByStep, + groups, }: OpcodeListProps): JSX.Element { // Render the full instruction list; the panel scrolls internally. // Keep the active step scrolled into view as the trace advances. @@ -912,72 +1033,95 @@ function OpcodeList({ activeRef.current?.scrollIntoView({ block: "nearest" }); }, [currentStep]); - return ( -
- {trace.map((step, index) => { - const isActive = index === currentStep; - const transforms = transformsByStep[index] ?? []; - const isInline = transforms.includes("inline"); - const isTailCall = transforms.includes("tailcall"); - // Show a call badge only when the row isn't already carrying a - // transform tag (an inlined call keeps its ⧉ inline marker). - const callBadge = - !isInline && !isTailCall ? (callBadgeByStep[index] ?? null) : null; - const className = [ - "opcode-item", - isActive ? "active" : "", - isInline ? "opcode-item-inline" : "", - isTailCall ? "opcode-item-tailcall" : "", - ] - .filter(Boolean) - .join(" "); - return ( -
onStepClick(index)} + const renderRow = (index: number): JSX.Element => { + const step = trace[index]; + const isActive = index === currentStep; + const transforms = transformsByStep[index] ?? []; + const isInline = transforms.includes("inline"); + const isTailCall = transforms.includes("tailcall"); + // Show a call badge only when the row isn't already carrying a + // transform tag (an inlined call keeps its ⧉ inline marker). + const callBadge = + !isInline && !isTailCall ? (callBadgeByStep[index] ?? null) : null; + const className = [ + "opcode-item", + isActive ? "active" : "", + isInline ? "opcode-item-inline" : "", + isTailCall ? "opcode-item-tailcall" : "", + ] + .filter(Boolean) + .join(" "); + return ( +
onStepClick(index)} + > + {index + 1} + + 0x{step.pc.toString(16).padStart(4, "0")} + + {step.opcode} + {isInline && ( + - {index + 1} - - 0x{step.pc.toString(16).padStart(4, "0")} - - {step.opcode} - {isInline && ( - - ⧉ inline - - )} - {isTailCall && ( - - ⮌ tailcall - - )} - {callBadge && ( - - {callBadge.kind === "invoke" - ? `➜ call ${callBadge.id ?? ""}`.trim() - : `↵ return ${callBadge.id ?? ""}`.trim()} - - )} -
- ); - })} + ⧉ inline + + )} + {isTailCall && ( + + ⮌ tailcall + + )} + {callBadge && ( + + {callBadge.kind === "invoke" + ? `➜ call ${callBadge.id ?? ""}`.trim() + : `↵ return ${callBadge.id ?? ""}`.trim()} + + )} +
+ ); + }; + + if (!groups) { + return ( +
+ {trace.map((_, index) => renderRow(index))} +
+ ); + } + + // Grouped: a subtle band + light label per grouped range. No indentation + // or collapsing — the rows read the same, just quietly delineated. + return ( +
+ {groups.map((group, gi) => ( +
+ {group.label && ( +
+ {group.label} +
+ )} + {group.rows.map((index) => renderRow(index))} +
+ ))}
); }