Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
9ea5e24
chore: cleanup and new templates
zojize Apr 15, 2026
87436f2
feat(interpreter): layout primitives — sizeOf/alignOf/alignUp for LP32
zojize Apr 15, 2026
56b8990
feat(interpreter): struct layout with natural alignment and padding
zojize Apr 15, 2026
f70821f
feat(interpreter): array layout + nested struct/array support
zojize Apr 15, 2026
49d361a
refactor(interpreter): reshape types for byte-addressed memory model
zojize Apr 15, 2026
0c4f719
refactor(interpreter): byte-backed memory manager with DataView + all…
zojize Apr 15, 2026
af79958
feat(interpreter): describeByte — layout-tree introspection for visua…
zojize Apr 15, 2026
0c401f0
feat(interpreter): cache struct layouts at registration time
zojize Apr 15, 2026
6e90025
refactor(interpreter): port evaluate.ts to byte-based reads/writes
zojize Apr 15, 2026
3942b08
feat(interpreter): decodeScalar helper for unguarded byte decoding
zojize Apr 15, 2026
c642f97
feat(viz): useMemoryDecoder composable for reactive typed reads
zojize Apr 15, 2026
69fe008
feat(viz): useAllocationQuery and useStructLayout composables
zojize Apr 15, 2026
60524f5
refactor(viz): port data-structure view to byte-addressed API
zojize Apr 15, 2026
fe6028c
refactor(viz): diff bytes and allocation deltas across steps
zojize Apr 15, 2026
3b14849
feat(viz): MemoryMapByteCell component
zojize Apr 15, 2026
de8c8d8
feat(viz): byte-level MemoryMap with row/tooltip decomposition
zojize Apr 15, 2026
c2c24ec
chore(viz): drop unused scaffolding flagged by knip
zojize Apr 15, 2026
666b67b
feat(viz): restore allocation view as default, add byte-view toggle
zojize Apr 16, 2026
2df7d42
feat(viz): redesign ByteMap — two-column stack/heap, 8 bytes per row,…
zojize Apr 16, 2026
c11270c
feat(viz): responsive ByteMap, inset change-ring, unified detail panel
zojize Apr 16, 2026
99ce2a4
fix(viz): make memory version reactive and virtualize ByteMap over al…
zojize Apr 16, 2026
69629a7
fix(viz): heap initial scroll, dead-allocation click safety, Allocati…
zojize Apr 16, 2026
f551899
feat(viz): AllocationMap card styling and centered empty states
zojize Apr 16, 2026
3c4c43f
fix(viz): read struct fields by their own layout node, not the enclos…
zojize Apr 16, 2026
7999bc6
feat(viz): type-coloured ByteMap cells and always-visible byte detail
zojize Apr 16, 2026
6ebc0ff
fix(viz): remove transparent border-left that showed as dark separators
zojize Apr 16, 2026
cb6f25e
feat(viz): splitpane-draggable stack/heap columns in ByteMap, fix hea…
zojize Apr 16, 2026
d4a7671
feat(viz): live endianness toggle with in-place byte-swap
zojize Apr 16, 2026
6f276b8
fix(viz): scope leave-animation to column with position: relative
zojize Apr 16, 2026
549000e
fix(viz): singularize 'allocation' and 'byte' when count is 1
zojize Apr 16, 2026
8441222
fix(viz): don't scroll when AddressLink hover target is in the same c…
zojize Apr 16, 2026
cee0192
feat(viz): useHoverHighlight composable (provide/inject)
zojize Apr 16, 2026
377fded
feat(viz): allocationRects geometry helper for ByteMap overlay
zojize Apr 16, 2026
a77af44
refactor(viz): migrate hover/highlight state to useHoverHighlight com…
zojize Apr 16, 2026
170333c
feat(viz): ByteMap SVG overlay for execution highlights
zojize Apr 16, 2026
c1c6db8
feat(viz): byte→DS hover propagation; drop per-byte amber ring
zojize Apr 16, 2026
2dce38a
fix(viz): overlay alignment, initial-mount rendering, drag throttle +…
zojize Apr 16, 2026
79ae5c7
fix(viz): inset overlay rects by half stroke-width so top border isn'…
zojize Apr 16, 2026
8039db5
feat(viz): render overlay as single staircase path per allocation
zojize Apr 16, 2026
5141d3c
perf(viz): key ByteMap virtual-list rows by index, not row address
zojize Apr 16, 2026
8934887
perf(viz): defer ByteMap bpr recompute to splitpane drag-end
zojize Apr 16, 2026
006274a
test(e2e): deterministic parser-ready wait via data-ready on Step button
zojize Apr 16, 2026
72d450c
feat(ui): extend simulation speed range to 50–1500ms, 5 labeled tiers
zojize Apr 16, 2026
a5872bd
fix(uno): ignore stray i-+ token matches from decrement operators
zojize Apr 18, 2026
ce5a739
feat(interpreter): expose leafBase/leafSize from describeByte
zojize Apr 18, 2026
b9be435
feat(viz): pointer-arrow overlay on memory views
zojize Apr 18, 2026
a96a2d9
test(e2e): skip arrow re-anchor regression, keep for documentation
zojize Apr 18, 2026
aecee02
refactor(viz): lock pointer-arrow bow to upper-left quadrant
zojize Apr 18, 2026
d58bbe0
fix(viz): disable overscroll on memory-map scroll containers
zojize Apr 18, 2026
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
28 changes: 25 additions & 3 deletions src/components/AddressLink.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, inject } from 'vue'
import { formatAddr } from '~/composables/interpreter/helpers'
import { NULL_ADDRESS } from '~/composables/interpreter/types'
import { HOVER_HIGHLIGHT_KEY } from '~/composables/useHoverHighlight'

const props = defineProps<{
address: number
/** Byte address of this link itself (its own location in memory). When
* provided, the link can act as the visible anchor for an arrow
* originating here — the ArrowOverlay looks up `[data-pointer-source]`
* so it can start a curve from the underline's center. Optional because
* AddressLink is also used in contexts where no arrow applies. */
sourceAddress?: number
}>()

const emit = defineEmits<{
Expand All @@ -12,13 +20,27 @@ const emit = defineEmits<{
}>()

const isNull = computed(() => props.address === NULL_ADDRESS)
const label = computed(() => isNull.value ? 'NULL' : `0x${props.address.toString(16).padStart(3, '0')}`)
const label = computed(() => isNull.value ? 'NULL' : formatAddr(props.address))

// Inject optionally — AddressLink is used outside MemoryMap too (Monaco side
// panels, etc.), where the hover store isn't provided.
const hover = inject(HOVER_HIGHLIGHT_KEY, null)
const isArrowSource = computed(() => {
if (!hover || props.sourceAddress === undefined)
return false
return hover.activeArrowSources.value.has(props.sourceAddress)
})
</script>

<template>
<span
class="cursor-pointer font-mono hover:underline"
:class="isNull ? 'text-red-400' : 'text-green-400'"
:class="[
isNull ? 'text-red-400' : 'text-green-400',
isArrowSource ? 'underline decoration-current decoration-2 underline-offset-2' : '',
]"
:data-pointer-source="sourceAddress"
data-anchor-edge="bottom"
@click.stop="!isNull && emit('navigate', address)"
@pointerenter="!isNull && emit('hover', address)"
@pointerleave="emit('hover', null)"
Expand Down
186 changes: 104 additions & 82 deletions src/components/DSValue.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
<script setup lang="ts">
import type { CppType, MemoryCell } from '~/composables/interpreter/types'
import type { CppType } 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<{
cell: MemoryCell
/** Base address of the memory location to render */
address: number
/** CppType for this location (scalar, pointer, struct, array) */
type: CppType
/** Optional prefix for array indices, e.g. "[0]" for nested display */
indexPrefix?: string
/** Field address to highlight (from referenced-by hover or arrow hover) */
Expand All @@ -26,116 +28,138 @@ const context = useInterpreterContext()

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

/** Fresh cell value — re-reads from reactive memory each step */
const freshValue = computed(() => {
// eslint-disable-next-line ts/no-unused-expressions
context.memory.version // reactive dependency
return context.memory.cells.get(props.cell.address)?.value ?? props.cell.value
})
const isPrimitive = computed(() => typeof props.type === 'string')
const isPointer = computed(() => typeof props.type === 'object' && props.type.type === 'pointer')
const isArray = computed(() => typeof props.type === 'object' && props.type.type === 'array')
const isStruct = computed(() => typeof props.type === 'object' && props.type.type === 'struct')

const isPrimitive = computed(() => typeof props.cell.type === 'string')
const isPointer = computed(() => {
const v = freshValue.value
return typeof v === 'object' && v.type === 'pointer'
})
const isArray = computed(() => {
const v = freshValue.value
return typeof v === 'object' && v.type === 'array'
const structName = computed(() => {
if (typeof props.type === 'object' && props.type.type === 'struct')
return props.type.name
return null
})
const isStruct = computed(() => {
const v = freshValue.value
return typeof v === 'object' && v.type === 'struct'

/** Formatted primitive value (re-reads from reactive memory each step) */
const primitiveDisplay = computed(() => {
// eslint-disable-next-line ts/no-unused-expressions
context.memoryVersion // reactive dependency
if (!isPrimitive.value)
return ''
try {
const v = context.memory.readScalar(props.address, props.type as Parameters<typeof context.memory.readScalar>[1])
return String(v)
}
catch {
return '?'
}
})

const structName = computed(() => {
if (typeof props.cell.type === 'object' && props.cell.type.type === 'struct')
return props.cell.type.name
return null
/** Pointer target address */
const pointerAddress = computed(() => {
// eslint-disable-next-line ts/no-unused-expressions
context.memoryVersion // reactive dependency
if (!isPointer.value || typeof props.type !== 'object' || props.type.type !== 'pointer')
return 0
try {
return context.memory.readScalar(props.address, props.type) as number
}
catch {
return 0
}
})

interface FieldEntry {
name: string
cell: MemoryCell | undefined
address: number
type: CppType
}

const structFields = computed((): FieldEntry[] => {
if (!structName.value)
return []
// eslint-disable-next-line ts/no-unused-expressions
context.memory.version // reactive dependency — re-evaluate each step
const freshCell = context.memory.cells.get(props.cell.address)
const v = freshCell?.value
if (!v || typeof v !== 'object' || v.type !== 'struct')
return []
const structDef = context.structs[v.name]
if (!structDef)
context.memoryVersion // reactive dependency — re-evaluate each step
const layout = context.structLayouts[structName.value]
if (!layout || layout.kind !== 'struct')
return []
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)
function flattenArray(prefix: string, elemType: CppType, elemAddr: number, arrayLength: number, stride: number) {
for (let i = 0; i < arrayLength; i++) {
const addr = elemAddr + i * stride
const subscript = `${prefix}[${i}]`
// Nested array: recurse
if (typeof elemType === 'object' && elemType.type === 'array' && elemCell) {
flattenArray(subscript, elemType.of, elemCell)
if (typeof elemType === 'object' && elemType.type === 'array') {
const innerLayout = layout.kind === 'struct' ? null : null // placeholder — use field node
void innerLayout
// For nested arrays, recurse using the element type
const innerStride = Math.floor(stride / elemType.size) || 1
flattenArray(subscript, elemType.of, addr, elemType.size, innerStride)
}
else {
entries.push({ name: subscript, cell: elemCell, type: elemType })
entries.push({ name: subscript, address: addr, 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)
for (const field of layout.fields) {
const fieldAddr = props.address + field.offset
if (field.node.kind === 'array') {
const elemType = fieldNodeToType(field.node.element)
flattenArray(field.name, elemType, fieldAddr, field.node.length, field.node.stride)
continue
}
entries.push({ name, cell: fieldCell, type: fieldType })
entries.push({ name: field.name, address: fieldAddr, type: fieldNodeToType(field.node) })
}
return entries
})

interface ArrayEntry {
index: number
cell: MemoryCell | undefined
address: number
type: CppType
prefix: string
}

const arrayElements = computed((): ArrayEntry[] => {
// eslint-disable-next-line ts/no-unused-expressions
context.memory.version // reactive dependency — re-evaluate each step
const freshCell = context.memory.cells.get(props.cell.address)
const v = freshCell?.value
if (!v || typeof v !== 'object' || v.type !== 'array')
context.memoryVersion // reactive dependency — re-evaluate each step
if (typeof props.type !== 'object' || props.type.type !== 'array')
return []
const alloc = context.memory.findAllocation(props.address)
if (!alloc || alloc.layout.kind !== 'array')
return []
const arrLayout = alloc.layout
const prefix = props.indexPrefix ?? ''
return Array.from({ length: v.length }, (_, i) => ({
return Array.from({ length: arrLayout.length }, (_, i) => ({
index: i,
cell: context.memory.cells.get(v.base + 1 + i),
address: props.address + i * arrLayout.stride,
type: fieldNodeToType(arrLayout.element),
prefix: `${prefix}[${i}]`,
}))
})

/** Convert a LayoutNode to a CppType for child rendering */
function fieldNodeToType(node: import('~/composables/interpreter/layout').LayoutNode): CppType {
if (node.kind === 'scalar')
return node.type
if (node.kind === 'array')
return { type: 'array', of: fieldNodeToType(node.element), size: node.length }
return { type: 'struct', name: node.structName }
}
</script>

<template>
<!-- Primitive value -->
<span v-if="isPrimitive" class="text-orange-600 font-semibold dark:text-orange-300">
{{ freshValue }}
{{ primitiveDisplay }}
</span>

<!-- Pointer value -->
<AddressLink
v-else-if="isPointer && typeof freshValue === 'object' && freshValue.type === 'pointer'"
:address="freshValue.address"
v-else-if="isPointer"
:address="pointerAddress"
@navigate="emit('navigate', $event)"
@hover="emit('hoverNode', $event)"
/>
Expand All @@ -144,10 +168,10 @@ const arrayElements = computed((): ArrayEntry[] => {
<div v-else-if="isArray" class="flex flex-col">
<template v-for="elem in arrayElements" :key="elem.index">
<!-- If element is itself an array, recurse without extra wrapper -->
<template v-if="elem.cell && typeof elem.cell.value === 'object' && elem.cell.value.type === 'array'">
<template v-if="typeof elem.type === 'object' && elem.type.type === 'array'">
<DSValue
:cell="elem.cell"

:address="elem.address"
:type="elem.type"
:index-prefix="elem.prefix"
@navigate="emit('navigate', $event)"
@hover-node="emit('hoverNode', $event)"
Expand All @@ -158,20 +182,18 @@ const arrayElements = computed((): ArrayEntry[] => {
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' : '',
highlightedFieldAddress === elem.address ? 'bg-blue-500/15' : '',
statementLhsAddresses?.has(elem.address) ? 'bg-blue-500/10' : '',
statementRhsAddresses?.has(elem.address) && !statementLhsAddresses?.has(elem.address) ? 'bg-green-500/10' : '',
]"
>
<span class="text-gray-500 font-mono">{{ elem.prefix }}</span>
<DSValue
v-if="elem.cell"
:cell="elem.cell"

:address="elem.address"
:type="elem.type"
@navigate="emit('navigate', $event)"
@hover-node="emit('hoverNode', $event)"
/>
<span v-else class="text-gray-500">?</span>
</div>
</template>
</div>
Expand All @@ -183,20 +205,21 @@ const arrayElements = computed((): ArrayEntry[] => {
</div>
<template v-for="field in structFields" :key="field.name">
<!-- Complex value (struct/array): field name then value below, indented -->
<template v-if="field.cell && typeof field.cell.value === 'object' && (field.cell.value.type === 'struct' || field.cell.value.type === 'array')">
<template v-if="typeof field.type === 'object' && (field.type.type === 'struct' || field.type.type === 'array')">
<div
class="py-0.5 pl-3"
: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' : '',
highlightedFieldAddress === field.address ? 'bg-blue-500/15 rounded' : '',
statementLhsAddresses?.has(field.address) ? 'bg-blue-500/10 rounded' : '',
statementRhsAddresses?.has(field.address) && !statementLhsAddresses?.has(field.address) ? 'bg-green-500/10 rounded' : '',
]"
:data-field-addr="field.cell?.address"
:data-field-addr="field.address"
>
<span class="text-gray-500 font-mono">{{ field.name }}:</span>
<div class="pl-2">
<DSValue
:cell="field.cell"
:address="field.address"
:type="field.type"
:highlighted-field-address="highlightedFieldAddress"
@navigate="emit('navigate', $event)"
@hover-node="emit('hoverNode', $event)"
Expand All @@ -209,23 +232,22 @@ const arrayElements = computed((): ArrayEntry[] => {
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' : '',
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' : '',
highlightedFieldAddress === field.address ? 'bg-blue-500/15' : '',
statementLhsAddresses?.has(field.address) ? 'bg-blue-500/10' : '',
statementRhsAddresses?.has(field.address) && !statementLhsAddresses?.has(field.address) ? 'bg-green-500/10' : '',
]"
:data-field-addr="field.cell?.address"
:data-field-addr="field.address"
>
<span class="shrink-0 text-gray-500 font-mono">{{ field.name }}:</span>
<DSValue
v-if="field.cell"
:cell="field.cell"
:address="field.address"
:type="field.type"
:highlighted-field-address="highlightedFieldAddress"
:statement-lhs-addresses="statementLhsAddresses"
:statement-rhs-addresses="statementRhsAddresses"
@navigate="emit('navigate', $event)"
@hover-node="emit('hoverNode', $event)"
/>
<span v-else class="text-gray-500">?</span>
</div>
</template>
<div class="text-accent-cyan font-semibold">
Expand All @@ -235,6 +257,6 @@ const arrayElements = computed((): ArrayEntry[] => {

<!-- Fallback -->
<span v-else class="text-gray-500">
{{ formatType(cell.type) }}
{{ formatType(type) }}
</span>
</template>
Loading
Loading