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
79 changes: 58 additions & 21 deletions src/components/DSValue.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import type { CppType, MemoryCell } from '~/composables/interpreter/types'
import { computed } from 'vue'
import AddressLink from '~/components/AddressLink.vue'
import { formatType } from '~/composables/interpreter/helpers'
import { structFieldOffset } from '~/composables/interpreter/memory'
import { useInterpreterContext } from '~/composables/useInterpreterContext'

const props = defineProps<{
Expand All @@ -10,6 +12,9 @@ const props = defineProps<{
indexPrefix?: string
/** Field address to highlight (from referenced-by hover or arrow hover) */
highlightedFieldAddress?: number | null
/** Statement LHS/RHS addresses for per-field code highlighting */
statementLhsAddresses?: ReadonlySet<number>
statementRhsAddresses?: ReadonlySet<number>
}>()

const emit = defineEmits<{
Expand All @@ -19,18 +24,6 @@ const emit = defineEmits<{

const context = useInterpreterContext()

function formatType(type: CppType): string {
if (typeof type === 'string')
return type
if (type.type === 'pointer')
return `${formatType(type.to)}*`
if (type.type === 'array')
return `${formatType(type.of)}[${type.size}]`
if (type.type === 'struct')
return type.name
return '?'
}

// ---- Computed data ----

/** Fresh cell value — re-reads from reactive memory each step */
Expand Down Expand Up @@ -78,11 +71,37 @@ const structFields = computed((): FieldEntry[] => {
const structDef = context.structs[v.name]
if (!structDef)
return []
return Object.keys(structDef).map((name, i) => ({
name,
cell: context.memory.cells.get(v.base + 1 + i),
type: structDef[name],
}))
const entries: FieldEntry[] = []

/** Recursively flatten array elements with subscript prefix (handles n-d arrays) */
function flattenArray(prefix: string, elemType: CppType, cell: MemoryCell | undefined) {
if (!cell)
return
const arrVal = cell.value
if (typeof arrVal !== 'object' || arrVal.type !== 'array')
return
for (let i = 0; i < arrVal.length; i++) {
const elemCell = context.memory.cells.get(arrVal.base + 1 + i)
const subscript = `${prefix}[${i}]`
// Nested array: recurse
if (typeof elemType === 'object' && elemType.type === 'array' && elemCell) {
flattenArray(subscript, elemType.of, elemCell)
}
else {
entries.push({ name: subscript, cell: elemCell, type: elemType })
}
}
}

for (const [name, fieldType] of Object.entries(structDef)) {
const fieldCell = context.memory.cells.get(v.base + 1 + structFieldOffset(name, structDef))
if (typeof fieldType === 'object' && fieldType.type === 'array' && fieldCell) {
flattenArray(name, fieldType.of, fieldCell)
continue
}
entries.push({ name, cell: fieldCell, type: fieldType })
}
return entries
})

interface ArrayEntry {
Expand Down Expand Up @@ -135,7 +154,15 @@ const arrayElements = computed((): ArrayEntry[] => {
/>
</template>
<!-- Leaf element -->
<div v-else class="flex items-baseline justify-between gap-2 rounded px-1 py-0.5">
<div
v-else
class="flex items-baseline justify-between gap-2 rounded px-1 py-0.5"
:class="[
highlightedFieldAddress === elem.cell?.address ? 'bg-blue-500/15' : '',
statementLhsAddresses?.has(elem.cell?.address ?? -1) ? 'bg-blue-500/10' : '',
statementRhsAddresses?.has(elem.cell?.address ?? -1) && !statementLhsAddresses?.has(elem.cell?.address ?? -1) ? 'bg-green-500/10' : '',
]"
>
<span class="text-gray-500 font-mono">{{ elem.prefix }}</span>
<DSValue
v-if="elem.cell"
Expand All @@ -159,14 +186,18 @@ const arrayElements = computed((): ArrayEntry[] => {
<template v-if="field.cell && typeof field.cell.value === 'object' && (field.cell.value.type === 'struct' || field.cell.value.type === 'array')">
<div
class="py-0.5 pl-3"
:class="highlightedFieldAddress === field.cell?.address ? 'bg-blue-500/15 rounded' : ''"
:class="[
highlightedFieldAddress === field.cell?.address ? 'bg-blue-500/15 rounded' : '',
statementLhsAddresses?.has(field.cell?.address ?? -1) ? 'bg-blue-500/10 rounded' : '',
statementRhsAddresses?.has(field.cell?.address ?? -1) && !statementLhsAddresses?.has(field.cell?.address ?? -1) ? 'bg-green-500/10 rounded' : '',
]"
:data-field-addr="field.cell?.address"
>
<span class="text-gray-500 font-mono">{{ field.name }}:</span>
<div class="pl-2">
<DSValue
:cell="field.cell"

:highlighted-field-address="highlightedFieldAddress"
@navigate="emit('navigate', $event)"
@hover-node="emit('hoverNode', $event)"
/>
Expand All @@ -177,14 +208,20 @@ const arrayElements = computed((): ArrayEntry[] => {
<div
v-else
class="flex items-baseline justify-between gap-4 rounded px-1 py-0.5 pl-3"
:class="highlightedFieldAddress === field.cell?.address ? 'bg-blue-500/15' : ''"
:class="[
highlightedFieldAddress === field.cell?.address ? 'bg-blue-500/15' : '',
statementLhsAddresses?.has(field.cell?.address ?? -1) ? 'bg-blue-500/10' : '',
statementRhsAddresses?.has(field.cell?.address ?? -1) && !statementLhsAddresses?.has(field.cell?.address ?? -1) ? 'bg-green-500/10' : '',
]"
:data-field-addr="field.cell?.address"
>
<span class="shrink-0 text-gray-500 font-mono">{{ field.name }}:</span>
<DSValue
v-if="field.cell"
:cell="field.cell"
:highlighted-field-address="highlightedFieldAddress"
:statement-lhs-addresses="statementLhsAddresses"
:statement-rhs-addresses="statementRhsAddresses"
@navigate="emit('navigate', $event)"
@hover-node="emit('hoverNode', $event)"
/>
Expand Down
116 changes: 56 additions & 60 deletions src/components/DataStructureView.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<script setup lang="ts">
import type { ArrowAnchor, ArrowStyle, CppType, CppValue, FieldDirection, MemoryCell } from '~/composables/interpreter/types'
import type { ArrowAnchor, ArrowStyle, FieldDirection, MemoryCell } from '~/composables/interpreter/types'
import { computed, nextTick, onUpdated, shallowRef, watch } from 'vue'
import CanvasArrow from '~/components/CanvasArrow.vue'
import DSValue from '~/components/DSValue.vue'
import { NULL_ADDRESS } from '~/composables/interpreter/types'
import { formatAddr, formatType, formatValue } from '~/composables/interpreter/helpers'
import { structTotalSize } from '~/composables/interpreter/memory'
import { useInterpreterContext } from '~/composables/useInterpreterContext'
import { usePannableCanvas } from '~/composables/usePannableCanvas'
import { usePlacementEngine } from '~/composables/usePlacementEngine'
Expand All @@ -27,38 +28,6 @@ const emit = defineEmits<{
const context = useInterpreterContext()
const pointerGraph = usePointerGraph(context)

// ---- Format helpers ----

function formatAddr(addr: number): string {
return `0x${addr.toString(16).padStart(3, '0')}`
}

function formatValue(value: CppValue): string {
if (typeof value === 'number' || typeof value === 'boolean')
return String(value)
if (typeof value === 'object') {
if (value.type === 'pointer')
return value.address === NULL_ADDRESS ? 'NULL' : formatAddr(value.address)
if (value.type === 'struct')
return `${value.name} {...}`
if (value.type === 'array')
return `[${value.length}]`
}
return String(value)
}

function formatType(type: CppType): string {
if (typeof type === 'string')
return type
if (type.type === 'pointer')
return `${formatType(type.to)}*`
if (type.type === 'array')
return `${formatType(type.of)}[${type.size}]`
if (type.type === 'struct')
return type.name
return '?'
}

// ---- Standalone data items (in-scope stack + live heap) ----

interface DataItem {
Expand All @@ -73,7 +42,7 @@ interface DataItem {
}

/** Addresses that are struct fields or array elements (sub-cells) */
function getSubCellAddresses(): Set<number> {
const subCellAddresses = computed(() => {
const sub = new Set<number>()
for (const cell of context.memory.cells.values()) {
if (cell.dead)
Expand All @@ -82,8 +51,9 @@ function getSubCellAddresses(): Set<number> {
if (typeof v === 'object' && v.type === 'struct') {
const structDef = context.structs[v.name]
if (structDef) {
for (let i = 0; i < Object.keys(structDef).length; i++)
sub.add(v.base + 1 + i)
const total = structTotalSize(structDef)
for (let i = 1; i < total; i++)
sub.add(v.base + i)
}
}
else if (typeof v === 'object' && v.type === 'array') {
Expand All @@ -92,7 +62,7 @@ function getSubCellAddresses(): Set<number> {
}
}
return sub
}
})

function getKind(cell: MemoryCell): DataItem['kind'] {
const t = cell.type
Expand All @@ -114,7 +84,7 @@ function getKind(cell: MemoryCell): DataItem['kind'] {

const standaloneItems = computed((): DataItem[] => {
const items: DataItem[] = []
const subCells = getSubCellAddresses()
const subCells = subCellAddresses.value
const seen = new Set<number>()

function addItem(name: string, cell: MemoryCell, dimmed: boolean, varName: string | null = null) {
Expand Down Expand Up @@ -240,13 +210,16 @@ const placement = usePlacementEngine({
})
placementRef = placement

/** Map from struct base address to placement key */
/** Reverse map: address → placement key (O(1) lookup) */
const addressKeyMap = computed(() => {
const map = new Map<number, string>()
for (const item of standaloneItems.value)
map.set(item.address, `item-${item.address}`)
return map
})

function addressToKey(address: number): string | undefined {
for (const item of standaloneItems.value) {
if (item.address === address)
return `item-${item.address}`
}
return undefined
return addressKeyMap.value.get(address)
}

/**
Expand Down Expand Up @@ -306,8 +279,16 @@ function invalidateStaleTreePositions(
for (const [parentKey, newChildren] of newParentToChildren) {
const oldChildren = prevParentToChildren.get(parentKey)
// Compare sets: different size or different members
if (!oldChildren || oldChildren.size !== newChildren.size
|| [...newChildren].some(k => !oldChildren.has(k))) {
let childrenChanged = !oldChildren || oldChildren.size !== newChildren.size
if (!childrenChanged) {
for (const k of newChildren) {
if (!oldChildren!.has(k)) {
childrenChanged = true
break
}
}
}
if (childrenChanged) {
for (const childKey of newChildren)
toInvalidate.add(childKey)
// Also invalidate old siblings that may have been removed from the group
Expand Down Expand Up @@ -351,10 +332,10 @@ function invalidateStaleTreePositions(
prevParentToChildren.set(k, new Set(v))
}

/** Measure and place all visible items with tree-aware layout */
function measureAndPlace() {
/** Measure and place all visible items with tree-aware layout. Returns the queried elements for reuse. */
function measureAndPlace(): NodeListOf<HTMLElement> | null {
if (!contentRef.value)
return
return null
// Step 1: Measure all DOM elements
const els = contentRef.value.querySelectorAll<HTMLElement>('[data-place-key]')
const measured = new Map<string, { w: number, h: number }>()
Expand All @@ -379,13 +360,24 @@ function measureAndPlace() {
if (!rootKey || !measured.has(rootKey))
continue

// Build adjacency map once per tree (O(edges) instead of O(nodes × edges))
const edgesByParent = new Map<number, typeof tree.edges>()
for (const edge of tree.edges) {
let list = edgesByParent.get(edge.fromAddress)
if (!list) {
list = []
edgesByParent.set(edge.fromAddress, list)
}
list.push(edge)
}

// Place root normally
const rootSize = measured.get(rootKey)!
placement.placeNew(rootKey, rootSize.w, rootSize.h)
treePlacedKeys.add(rootKey)

// Recursively place children
placeTreeChildren(tree.rootAddress, rootKey, tree, measured, treePlacedKeys)
placeTreeChildren(tree.rootAddress, rootKey, edgesByParent, measured, treePlacedKeys)
}

// Step 2.5: Evict non-tree items whose positions now overlap with tree items.
Expand All @@ -408,22 +400,24 @@ function measureAndPlace() {
for (const el of els)
activeKeys.add(el.dataset.placeKey!)
placement.retainOnly(activeKeys)

return els
}

/** Recursively place children of a tree node via placeRelative */
function placeTreeChildren(
parentAddress: number,
parentKey: string,
tree: typeof pointerGraph.value.trees[number],
edgesByParent: Map<number, typeof pointerGraph.value.trees[number]['edges']>,
measured: Map<string, { w: number, h: number }>,
placedKeys: Set<string>,
) {
const parentNode = pointerGraph.value.nodes.get(parentAddress)
if (!parentNode)
return

const childEdges = tree.edges.filter(e => e.fromAddress === parentAddress)
if (childEdges.length === 0)
const childEdges = edgesByParent.get(parentAddress)
if (!childEdges || childEdges.length === 0)
return

// Collect children with their direction, skipping already-placed nodes
Expand Down Expand Up @@ -455,7 +449,7 @@ function placeTreeChildren(
const size = measured.get(child.key)!
placement.placeRelative(child.key, parentKey, size.w, size.h, i, heights, dir, arrowSizeOverride)
placedKeys.add(child.key)
placeTreeChildren(child.address, child.key, tree, measured, placedKeys)
placeTreeChildren(child.address, child.key, edgesByParent, measured, placedKeys)
}
}

Expand All @@ -467,7 +461,7 @@ function placeTreeChildren(
const size = measured.get(child.key)!
placement.placeNew(child.key, size.w, size.h)
placedKeys.add(child.key)
placeTreeChildren(child.address, child.key, tree, measured, placedKeys)
placeTreeChildren(child.address, child.key, edgesByParent, measured, placedKeys)
}
}

Expand All @@ -480,8 +474,9 @@ const settlingKeys = new Set<string>()
let prevActiveKeys = new Set<string>()

onUpdated(() => nextTick(() => {
measureAndPlace()
animateEnterLeave()
const els = measureAndPlace()
if (els)
animateEnterLeave(els)

// Auto-pan when content bounds changed and items are outside the visible viewport
const bounds = placement.getContentBounds()
Expand Down Expand Up @@ -534,10 +529,9 @@ function getItemStyle(key: string) {
// ---- DS item enter/leave animations (runs after placement in onUpdated) ----

/** Animate fade-in for newly appeared items and fade-out for removed items. */
function animateEnterLeave() {
function animateEnterLeave(els: NodeListOf<HTMLElement>) {
if (!contentRef.value)
return
const els = contentRef.value.querySelectorAll<HTMLElement>('[data-place-key]')
const currentKeys = new Set<string>()

for (const el of els) {
Expand Down Expand Up @@ -873,6 +867,8 @@ const kindBg: Record<DataItem['kind'], string> = {
<DSValue
:cell="item.cell"
:highlighted-field-address="highlightedFieldAddress"
:statement-lhs-addresses="statementLhsAddresses"
:statement-rhs-addresses="statementRhsAddresses"
@navigate="emit('selectNode', $event)"
@hover-node="emit('hoverNode', $event)"
/>
Expand Down
Loading
Loading