Skip to content

refactor(viz): byte-memory model + hover highlight + ByteMap overlay - #5

Merged
zojize merged 49 commits into
mainfrom
refactor/byte-memory
Apr 18, 2026
Merged

zojize merged 49 commits into
mainfrom
refactor/byte-memory

Conversation

@zojize

@zojize zojize commented Apr 16, 2026

Copy link
Copy Markdown
Owner

Summary

Two large refactors consolidated on one branch:

  • Byte-addressed memory model (30 commits, 87436f2..8441222): interpreter storage moves from cell-indexed slots to a Uint8Array + DataView backed arena with natural alignment, struct padding, and a sparse allocation registry. Adds an allocation vs bytes view toggle, live endianness switching, and a byte-level CompArch visualization.
  • Hover highlight composable + SVG overlay (10 commits, cee0192..8934887): replaces prop-drilling of hover state with a useHoverHighlight() provide/inject composable. ByteMap gains an SVG overlay that renders execution highlights as contiguous staircase paths per allocation, with byte ↔ DS view hover propagation. Splitpane drag is deferred to drag-end for perf.

See docs/superpowers/specs/ for design docs if relevant. Pre-merge checklist green: typecheck, lint, knip, 177/177 tests, build.

Test plan

  • Run singly-insertBack, doubly-insertFront, btree, tree-skewed from start through breakpoint in both Allocation and Bytes views
  • Verify execution highlights (LHS blue / RHS green / changed amber) render in both views
  • Hover from DS view → Allocation cell + ByteMap overlay both highlight the target
  • Hover a byte → DS view node highlights; hovering a padding byte highlights the enclosing struct
  • Toggle endianness (LE/BE) in Bytes view; scalars byte-swap, DS values stay semantically correct
  • Drag ByteMap splitpane; drag feels smooth, bytes reflow on release
  • Window resize still reflows bytes (non-drag path)
  • Dark mode

🤖 Generated with Claude Code

zojize and others added 30 commits April 15, 2026 02:21
- Replace manual click-outside/clipboard/timeout handlers with VueUse composables in index.vue
- Drop unused ghost-fade logic in DataStructureView, unused SvgArrow component
- Use formatAddr helper in AddressLink/FieldTable for consistent hex formatting
- Replace queue.shift() with index walk in usePointerGraph BFS
- Add avl-rotations and doubly-reverse templates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ocation registry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…lization

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a struct definition is registered in the interpreter, immediately
compute and cache its layout via computeStructLayout, store it in
context.structLayouts, and call mem.registerStructLayout to wire the
memory manager.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Translate all old-API call sites (mem.read/write/readField, structFieldOffset,
space.cells) to the new byte-addressed MemoryManager API. Also fixes memory.ts
to correctly handle pointer alloc init, inline arrays/structs within allocations,
multi-dimensional arrays, and stride-aware pointer arithmetic.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add two reactive Vue composables as part of the byte-addressed memory
refactor (Task 12): useAllocationQuery wraps MemoryManager allocation
lookups with version-reactive computed properties, and useStructLayout
resolves struct names to typed field rows and byte sizes via
InterpreterContext.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change InterpreterContext.memory from AddressSpace to MemoryManager so
  components can call findAllocation(), readScalar(), space.version, etc.
- Update interpreter/index.ts to store markRaw(mem) and drop the
  spread-copy reactivity trick (version bump on mem.space is sufficient)
- Port usePointerGraph to use space.allocations + layout tree for edges
- Port useStatementAddresses to use findAllocation/readScalar/elementAddress
- Rewrite DSValue.vue: prop changed from cell:MemoryCell to address+type
- Rewrite FieldTable.vue: same prop change; uses useMemoryDecoder for display
- Rewrite DataStructureView.vue: builds standaloneItems from allocations
- Stub MemoryMap.vue (Task 16 replacement) and useMemoryDiff.ts (Task 14)
- Fix MemoryCell.vue: local MemoryCell interface replaces deleted type
- Update test fixtures: memory: mem (not mem.space), use space.allocations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the no-op useMemoryDiff stub with a real implementation that
snapshots AddressSpace before each step and computes byte-range diffs
and allocation deltas (newlyAllocated/newlyFreed) reactively via a
computed ref. index.vue wires in snapshotSpace + previousSnapshot and
derives the changedAddresses set consumed by MemoryMap and FieldTable.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace Task 13 stub with three-component MemoryMap decomposition:
MemoryMapRow (per-row byte grid with address label), MemoryMapTooltip
(hover/pin detail pane), and MemoryMap outer grid (gap-aware row
layout, pinned/hover address tracking). Update index.vue call sites to
pass `mem: MemoryManager` + `changedBytes` instead of the old
address-set props.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restore the original MemoryMap as AllocationMap.vue (ported to the new
byte-addressed API) and make it the default. The byte-level view from
the refactor lives at ByteMap.vue and is opt-in via a toolbar toggle
persisted in localStorage. MemoryMap.vue is now a thin dispatcher.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… dark-mode aware

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ring-inset on changed-byte amber ring so it no longer bleeds past the
  cell boundary at the top scroll row
- useElementSize on each column body to compute bytesPerRow reactively;
  rows reshuffle cleanly on resize (minimum 4 cells)
- ByteMap emits select-cell upward through MemoryMap (selectByteCell)
  and into index.vue, which resolves the allocation base and drives the
  existing FieldTable detail pane; free-byte clicks clear the selection
- FieldTable gains optional byteDetail prop rendered as a banner above
  the allocation fields (address, offset, path, type, raw byte)
- Delete ByteMapDetail.vue (superseded by the unified panel)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…l bytes

Add `memoryVersion` counter to InterpreterContext and bump it in init()/step()
so Vue components no longer depend on markRaw'd mem.space.version. Rewrite
ByteMap to render the full 16KB buffer via useVirtualList (28px row height),
splitting at MEMORY_SIZE/2 with heap pre-scrolled to the bottom.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…onMap borders

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Restore missing `flex` utility so entries get proper gaps (gap-1.5)
- Give entry cards an elevated look: white/gray-900 bg, subtle border, shadow-sm
- Distinguish card header strip from body with a darker tint and divider
- Center "No stack variables" / "No heap allocations" / "No data to display"
  in their respective panels

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ing allocation

Previously getFieldValues called findAllocation(fieldAddr) which returns the
ENCLOSING struct allocation — so every field of a struct was treated as a struct
itself, and primitive/pointer fields rendered as "ListNode {...}" instead of
their actual values. Now each field is read using its own LayoutNode from the
struct layout tree, producing int scalars, AddressLinks for pointers, etc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Byte cells now colour by leafType (from describeByte): int=blue, float=amber,
  char=purple, bool=rose, pointer=green, struct=cyan, array=orange — matching
  DataStructureView's kindColors. Continuous bytes of the same allocation are
  no longer an indistinguishable blue/green blob; each field inside a struct
  gets the colour of its own type.
- Byte-detail banner in FieldTable now shows whenever a byte was clicked in
  the byte map (previously hidden at offset 0 / allocation base).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MemoryMapByteCell reserved 2px of transparent border on the left of every
cell so that a boundary-marker border wouldn't shift layout. In dark mode the
transparent slice showed the column background through each cell as a visible
vertical separator, breaking the continuous colored blocks within an allocation;
in light mode the stripes blended in, so the two modes looked different.

Because every cell is w-8 (box-sizing: border-box), removing the reserved
border doesn't change layout — boundary cells still fit their 2px colored
border within the same 32px width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…p empty-on-resize

- Wrap stack & heap columns in Splitpanes so the user can drag the divider
  to rebalance bytes-per-row between them.
- Re-scroll heap to its last row on every bytes-per-row change (not just the
  first measurement). Previously the scrollTop stayed stuck beyond the new
  total-content height after a splitpane drag or editor/viz resize, leaving
  the heap column rendering zero rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a LE/BE toggle button in the MemoryMap toolbar (visible only in Bytes mode).
Clicking it:
  - byte-swaps every scalar in every allocation (live, dead, and the NULL
    sentinel) using each allocation's layout tree, so padding and char bytes
    are naturally skipped
  - flips the MemoryManager's internal \`littleEndian\` flag so subsequent
    reads/writes use the new order
  - bumps memoryVersion to re-render all memory-bound views

Values remain semantically consistent across the toggle — \`int x = 42\`
still evaluates \`x == 42\` truthy, but its bytes are now laid out differently
in the byte view. The change is exposed to the UI via a new useSetEndianness()
injection, since the context itself is readonly()-wrapped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The AllocationMap's TransitionGroup leave rule uses \`position: absolute;
width: 100%\` to pull the exiting element out of flow. Without a positioned
ancestor on the column's scroll body, the absolute element fell back to the
Splitpanes-wrapped viz panel — so leaving items briefly stretched across both
stack and heap columns before fading out. Adding \`relative\` to the scroll
body scopes the absolute position to the column, so leaving items stay
within their column's bounds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zojize and others added 11 commits April 16, 2026 14:52
…olumn

When a user hovers an AddressLink inside AllocationMap, the handler emits
hoverPointer → index.vue updates hoveredNodeAddress → AllocationMap receives
it back as the highlightedAddress prop. Both watches (the local hoveredTarget
one and the prop-driven one) end up firing. The hoveredTarget watch had a
same-column guard since the pre-refactor days, but the prop-driven watch
didn't — so the cell would scroll under the cursor and the link would run
away from the pointer.

Regression source: Task 13's port split the original single-function scroll
logic in MemoryMap.vue into two watches, preserving the guard on one of them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…posable

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… CSS cleanup

- ADDRESS_LABEL_WIDTH 62→56 px so overlay rects align with byte cells
- ByteMapOverlay uses context.memoryVersion for reactivity (mem is markRaw,
  so props.mem.space.version didn't track; overlay stayed empty on initial
  Bytes mount until a view toggle re-ran the computed)
- refThrottled on column widths (60ms) so splitpane drag doesn't cascade
  into per-frame row/overlay re-renders
- Replace mm-* CSS and scoped style blocks with UnoCSS atomics; add
  scrollbar-hidden shortcut to uno.config.ts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t clipped

SVG strokes paint centered on the rect edge. A rect at y=0 with a 2px stroke
paints from y=-1 to y=1; the scroll container clips y<0 and the top half of
the stroke disappears for allocations on the topmost row. Insetting the rect
by strokeWidth/2 on all sides keeps the full stroke inside the drawable
region without changing the visual size of the highlight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When an allocation spans multiple rows, rendering each rect individually
produces internal vertical/horizontal borders at the row boundaries that
look disconnected. Replace the per-rect rendering with a single SVG <path>
that traces the allocation's perimeter, so a multi-row highlight reads as
one continuous shape.

- allocationPath() in overlayGeometry.ts computes the perimeter d-string.
  Handles three cases: single-row rect, head+tail (connected staircase
  when tail_right > head_left, disjoint sub-paths otherwise), and
  head+middle+tail (always connected staircase).
- Inset is applied during path construction so the centered stroke fits
  inside the scroll container's clip region.
- Boost halo is now a wider stroke on the same path (drawn under the base
  stroke) instead of a separate ring of rects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When bytesPerRow changes (splitpane drag), row addresses shift — row 1
moves from addr 10 to addr 9 if bpr goes 10→9. Keying MemoryMapByteRow
by item.data (the row-start address) meant every visible row got a new
key on every bpr tick, forcing Vue to unmount+re-mount ~30 rows and
their ~300 child cells per tick. Keying by item.index keeps the same
row components alive and just updates their row-start prop, so each
drag tick is a prop diff instead of a mount cycle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Listen for Splitpanes @resize / @Resized to freeze column widths during
active drag. Mid-drag, splitpanes still resize the pane boundaries via
CSS, but bytesPerRow stays constant so the virtual list, rows, and
overlay don't recompute on every pointer move. On release we snap to
the final widths and recompute once.

Trade-off: the byte grid doesn't reflow live during drag. In practice
it only snaps once at the end, which reads as a decisive layout change
rather than a stuttery live update. Window-resize and other non-drag
size changes still flow through via the 60ms throttled watcher.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@netlify

netlify Bot commented Apr 16, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for viz-list ready!

Name Link
🔨 Latest commit d58bbe0
🔍 Latest deploy log https://app.netlify.com/projects/viz-list/deploys/69e31e622814c100082e068b
😎 Deploy Preview https://deploy-preview-5--viz-list.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

zojize and others added 8 commits April 16, 2026 18:19
The beforeEach previously slept 500ms after btn-step became visible, hoping
that was enough for tree-sitter WASM to finish loading. On CI's cold start
it sometimes wasn't, so the first test would click Step before handleStep()
could init (tree was still undefined), memoryVersion stayed at 0, and the
toPass loop timed out.

Expose `data-ready="!!tree"` on the Step button and wait on it in
beforeEach. Replaces an arbitrary timeout with a deterministic signal
tied to the actual readiness condition.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous slider topped out at 500ms which still felt too fast for
users who wanted to follow a stepping simulation. Widen the range so the
slow end is actually slow: 50ms (fastest) to 1500ms (slowest), step 50,
30 discrete positions. Label tiers updated to fastest / fast / med /
slow / slowest. Default stays at 200ms so existing preferences aren't
disrupted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The content extractor was scanning plain code tokens and greedily matching
`i--` in `for (…; i--)` loops against the `i-` icon prefix, tripping the
presetIcons loader for every occurrence. Blocklist any all-dash suffix so
decrement operators stop emitting "[unocss] failed to load icon -" warnings
on every dev boot without disabling warnings globally.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Callers that need to identify the scalar containing a given byte (arrow
anchors, byte-level pointer inspection) had to walk the layout themselves.
Return the walk's accumulated offset + size so describeByte is a sufficient
one-stop query.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce a draggable/bezier pointer-arrow overlay above the memory map,
usable in both Allocations and Bytes views. Three layered sources:
1. Hover a pointer (byte cell or AddressLink) → primary arrow to target.
2. Clicking a pointer pins its arrow via selectedAddress /
   selectedByteAddress; the arrow dismisses on view toggle without closing
   the detail panel (pinnedGateOpen).
3. A "arrows" toolbar toggle enumerates every live pointer in the arena as
   dimmer ambient arrows.

Key pieces:
- ArrowOverlay.vue: single SVG overlay sibling to the KeepAlive'd view.
  getBoundingClientRect isn't a Vue reactive dep, so a MutationObserver on
  the host plus bubbling transition/animation event listeners (+ rAF loop
  while any is active) keep endpoints tracking KeepAlive swaps and
  CSS-driven card transitions. Endpoints use shortest-segment geometry
  otherwise; anchors can opt into bottom-edge (AddressLink underline) or
  left-edge (allocation cards) via data-anchor-edge.
- useHoverHighlight: extended with pointerArrow, pinnedArrows,
  ambientArrows, and a computed activeArrowSources Set for AddressLink's
  underline state. Arrow reads are guarded with try/catch + dead-alloc
  checks so a freed selection can't throw out of a computed and leave the
  store latched.
- Color plumbing (highlightColors): new pointer-source (violet) and
  pointer-target (amber) highlight kinds for the two ends of an active
  arrow; ARROW_STYLES mirrors those so arrow line color matches the
  anchor's rectangle tint. Source/target tinting also applies to
  Allocations view cards.
- Supporting UX polish bundled in: Allocations tab renamed (plural);
  toolbar buttons now share a transparent-border box so the LE/BE toggle
  doesn't shift toolbar height; AllocationMap uses Splitpanes so its
  gutter matches ByteMap; MemoryMap wraps its view swap in KeepAlive so
  ByteMap's measured widths and scroll state survive a toggle; ByteMap
  v-show-gates its rows on a non-zero measured width to avoid an initial
  4-bpr flash; heap column re-snaps to bottom on Bytes activation; scroll-
  to-pointer-target in ByteMap mirrors AllocationMap's hover scroll.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The test deterministically reproduces a specific KeepAlive-swap bug where
ArrowOverlay's computed held stale getBoundingClientRect coords across a
view toggle. Now fixed via a MutationObserver. Keeping the spec as a
searchable record of what the MutationObserver guards, but skipped so the
e2e suite doesn't grow on every debugging session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The perpendicular displacement used signed components, so arrow direction
dictated which side of the segment the control point landed on — a
rightward arrow bowed up while its return trip bowed down, which read as
visual noise in the ambient show-all-arrows view. Force the control into
the upper-left quadrant of the midpoint so every curve arches up and left,
regardless of source/target geometry. Anti-parallel pairs now stay
separated while sharing a single consistent arch direction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Browser overscroll (the macOS/touchpad rubber-band bounce) transiently
offsets a scroll container's content during the rebound, but the arrow
overlay reads endpoints straight from getBoundingClientRect without any
bounce-aware correction — the rendered paths drift by a few pixels until
the scroll settles. Setting overscroll-behavior: none on the scrollable
containers (and the root) suppresses the bounce outright. No visual drift
during overscroll attempts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zojize
zojize merged commit d0be94f into main Apr 18, 2026
6 checks passed
@zojize
zojize deleted the refactor/byte-memory branch April 18, 2026 06:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant