From 649c6b77f1a43e39414a4b86b8dd4b00931f345b Mon Sep 17 00:00:00 2001 From: zojize Date: Sat, 11 Apr 2026 11:49:40 -0500 Subject: [PATCH 1/7] feat(templates): add bubble sort and B-tree construction templates - bubble-sort.cpp: in-place bubble sort on int[6] array, demonstrates array visualization with element swapping - btree.cpp: manually constructed order-3 B-tree with root [10,20], three leaf nodes, arrays for keys and children pointers Co-Authored-By: Claude Opus 4.6 (1M context) --- src/templates/general/btree.cpp | 57 +++++++++++++++++++++++++++ src/templates/general/bubble-sort.cpp | 28 +++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 src/templates/general/btree.cpp create mode 100644 src/templates/general/bubble-sort.cpp diff --git a/src/templates/general/btree.cpp b/src/templates/general/btree.cpp new file mode 100644 index 0000000..56d6cf3 --- /dev/null +++ b/src/templates/general/btree.cpp @@ -0,0 +1,57 @@ +// Simple B-tree (order 3) constructed manually. +// Each node has up to 2 keys and 3 children. +struct BTreeNode { + int numKeys; + int keys[3]; + BTreeNode *children[4]; +}; + +int main() { + // Root: keys [10, 20] + BTreeNode *root = new BTreeNode; + root->numKeys = 2; + root->keys[0] = 10; + root->keys[1] = 20; + root->keys[2] = 0; + + // Left child: keys [3, 7] + BTreeNode *left = new BTreeNode; + left->numKeys = 2; + left->keys[0] = 3; + left->keys[1] = 7; + left->keys[2] = 0; + left->children[0] = nullptr; + left->children[1] = nullptr; + left->children[2] = nullptr; + left->children[3] = nullptr; + + // Middle child: keys [14] + BTreeNode *mid = new BTreeNode; + mid->numKeys = 1; + mid->keys[0] = 14; + mid->keys[1] = 0; + mid->keys[2] = 0; + mid->children[0] = nullptr; + mid->children[1] = nullptr; + mid->children[2] = nullptr; + mid->children[3] = nullptr; + + // Right child: keys [25, 30] + BTreeNode *right = new BTreeNode; + right->numKeys = 2; + right->keys[0] = 25; + right->keys[1] = 30; + right->keys[2] = 0; + right->children[0] = nullptr; + right->children[1] = nullptr; + right->children[2] = nullptr; + right->children[3] = nullptr; + + // Wire up root's children + root->children[0] = left; + root->children[1] = mid; + root->children[2] = right; + root->children[3] = nullptr; + + return 0; +} diff --git a/src/templates/general/bubble-sort.cpp b/src/templates/general/bubble-sort.cpp new file mode 100644 index 0000000..f19a0be --- /dev/null +++ b/src/templates/general/bubble-sort.cpp @@ -0,0 +1,28 @@ +// Bubble sort on a small integer array. +// Demonstrates array visualization with in-place swapping. +int main() { + int arr[6]; + arr[0] = 5; + arr[1] = 3; + arr[2] = 8; + arr[3] = 1; + arr[4] = 4; + arr[5] = 2; + + int n = 6; + int i = 0; + while (i < n - 1) { + int j = 0; + while (j < n - 1 - i) { + if (arr[j] > arr[j + 1]) { + int temp = arr[j]; + arr[j] = arr[j + 1]; + arr[j + 1] = temp; + } + j = j + 1; + } + i = i + 1; + } + + return 0; +} From 269070521e7b14829aa49af740725cdc0af0c105 Mon Sep 17 00:00:00 2001 From: zojize Date: Sat, 11 Apr 2026 11:58:50 -0500 Subject: [PATCH 2/7] refactor: extract shared format helpers, simplify components Move duplicate formatAddr/formatType/formatValue/isPointerValue to composables/interpreter/helpers.ts. Simplify DataStructureView (compute subCellAddresses as computed, O(1) addressToKey via Map), DanglingEdge as type alias, and minor cleanups across components. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/DSValue.vue | 15 +--- src/components/DataStructureView.vue | 111 ++++++++++++------------- src/components/FieldTable.vue | 30 +------ src/components/MemoryCell.vue | 33 +------- src/components/MemoryMap.vue | 35 +------- src/composables/interpreter/helpers.ts | 34 ++++++++ src/composables/useMonacoEditor.ts | 6 +- src/composables/usePlacementEngine.ts | 32 ++++--- src/composables/usePointerGraph.ts | 11 +-- src/pages/index.vue | 3 - 10 files changed, 112 insertions(+), 198 deletions(-) diff --git a/src/components/DSValue.vue b/src/components/DSValue.vue index d236df0..88fd4ac 100644 --- a/src/components/DSValue.vue +++ b/src/components/DSValue.vue @@ -2,6 +2,7 @@ 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 { useInterpreterContext } from '~/composables/useInterpreterContext' const props = defineProps<{ @@ -19,18 +20,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 */ @@ -166,7 +155,7 @@ const arrayElements = computed((): ArrayEntry[] => {
diff --git a/src/components/DataStructureView.vue b/src/components/DataStructureView.vue index d629790..775100e 100644 --- a/src/components/DataStructureView.vue +++ b/src/components/DataStructureView.vue @@ -1,9 +1,9 @@ diff --git a/src/composables/interpreter/memory.ts b/src/composables/interpreter/memory.ts index 023e1e9..22c912f 100644 --- a/src/composables/interpreter/memory.ts +++ b/src/composables/interpreter/memory.ts @@ -122,10 +122,19 @@ export function createAddressSpace(): MemoryManager { for (let i = 0; i < fields.length; i++) { const [, fieldType] = fields[i] const fieldAddress = base + 1 + i + // Array fields: allocate the array separately and point to it + let fieldValue: CppValue + if (typeof fieldType === 'object' && fieldType.type === 'array') { + const arrBase = allocArray(fieldType.of, fieldType.size, region) + fieldValue = { type: 'array', base: arrBase, length: fieldType.size } + } + else { + fieldValue = defaultValue(fieldType) + } space.cells.set(fieldAddress, { address: fieldAddress, type: fieldType, - value: defaultValue(fieldType), + value: fieldValue, region, dead: false, }) diff --git a/src/composables/useStatementAddresses.ts b/src/composables/useStatementAddresses.ts index cce263f..6f9741d 100644 --- a/src/composables/useStatementAddresses.ts +++ b/src/composables/useStatementAddresses.ts @@ -118,14 +118,15 @@ function collectIdentifierAddresses( resolveVarAddress(node.text, context, addrs) break case 'field_expression': { - // For `node->field` or `node.field`, resolve the base object AND the field - const arg = node.childForFieldName('argument') - if (arg) - collectIdentifierAddresses(arg, context, addrs) - // Also resolve the specific field address + // For `node->field` or `node.field`, resolve the specific field address resolveFieldAddress(node, context, addrs) break } + case 'subscript_expression': { + // For `arr[i]` or `node->keys[i]`, resolve the element address + resolveSubscriptAddress(node, context, addrs) + break + } case 'pointer_expression': case 'unary_expression': case 'update_expression': @@ -239,10 +240,8 @@ function resolveFieldAddress(node: SyntaxNode, context: Readonly= 0) { + if (fieldIdx >= 0) addrs.add(targetCell.value.base + 1 + fieldIdx) - addrs.add(val.address) // Also highlight the struct itself - } } else if (operator.text === '.') { // Direct struct access @@ -257,6 +256,74 @@ function resolveFieldAddress(node: SyntaxNode, context: Readonlykeys[i]`, resolve the + * actual array element address. + */ +function resolveSubscriptAddress(node: SyntaxNode, context: Readonly, addrs: Set) { + const arrayNode = node.childForFieldName('argument') + const indexNode = node.childForFieldName('indices') + if (!arrayNode || !indexNode) + return + + // Resolve the array base — could be a field_expression (node->keys) or identifier (arr) + // Try field_expression first + if (arrayNode.type === 'field_expression') { + resolveFieldAddress(arrayNode, context, addrs) + // Now find the array value at the resolved field address to get the element + const lastAddr = [...addrs].at(-1) + if (lastAddr === undefined) + return + const fieldCell = context.memory.cells.get(lastAddr) + if (!fieldCell) + return + const arrVal = fieldCell.value + if (typeof arrVal !== 'object' || arrVal.type !== 'array') + return + // Resolve index (try simple number literal) + const idxChild = indexNode.namedChildren[0] + if (!idxChild) + return + const idxText = idxChild.text + const idx = Number.parseInt(idxText) + if (Number.isNaN(idx)) + return + // Replace field address with element address + addrs.delete(lastAddr) + addrs.add(arrVal.base + 1 + idx) + return + } + + // Simple array variable: arr[i] + if (arrayNode.type === 'identifier') { + const varName = arrayNode.text + let varAddr: number | undefined + for (let i = context.envStack.length - 1; i >= 0; i--) { + if (varName in context.envStack[i]) { + varAddr = context.envStack[i][varName].address + break + } + } + if (varAddr === undefined && varName in context.globalEnv) + varAddr = context.globalEnv[varName].address + if (varAddr === undefined) + return + const cell = context.memory.cells.get(varAddr) + if (!cell) + return + const arrVal = cell.value + if (typeof arrVal !== 'object' || arrVal.type !== 'array') + return + const idxChild = indexNode.namedChildren[0] + if (!idxChild) + return + const idx = Number.parseInt(idxChild.text) + if (Number.isNaN(idx)) + return + addrs.add(arrVal.base + 1 + idx) + } +} + function extractBaseIdentifier(node: SyntaxNode): string | null { switch (node.type) { case 'identifier': diff --git a/test/interpreter/programs.test.ts b/test/interpreter/programs.test.ts index 187abdd..a06edff 100644 --- a/test/interpreter/programs.test.ts +++ b/test/interpreter/programs.test.ts @@ -252,4 +252,46 @@ describe('interpreter integration tests', () => { const sumAddr = context.globalEnv.sum.address expect(mem.read(sumAddr).value).toBe(6) }) + + it('constructs a B-tree with array fields', () => { + const { mem } = runProgram(` + struct BTreeNode { + int numKeys; + int keys[3]; + BTreeNode *children[4]; + }; + void main() { + BTreeNode *root = new BTreeNode; + root->numKeys = 2; + root->keys[0] = 10; + root->keys[1] = 20; + root->keys[2] = 0; + root->children[0] = nullptr; + root->children[1] = nullptr; + root->children[2] = nullptr; + root->children[3] = nullptr; + } + `) + + // Find the BTreeNode struct on the heap + const heapCells = [...mem.space.cells.values()].filter(c => + !c.dead && c.region === 'heap' && typeof c.value === 'object' && c.value.type === 'struct', + ) + expect(heapCells.length).toBeGreaterThanOrEqual(1) + const rootCell = heapCells[0] + const rootBase = (rootCell.value as any).base + + // numKeys = 2 + expect(mem.read(rootBase + 1).value).toBe(2) + + // keys field (base+2) should be an array with base pointing to allocated elements + const keysValue = mem.read(rootBase + 2).value + expect(typeof keysValue).toBe('object') + expect((keysValue as any).type).toBe('array') + const keysBase = (keysValue as any).base + // keys[0] = 10, keys[1] = 20, keys[2] = 0 + expect(mem.read(keysBase + 1).value).toBe(10) + expect(mem.read(keysBase + 2).value).toBe(20) + expect(mem.read(keysBase + 3).value).toBe(0) + }) }) From 2a526e943cc3be4db89af4b6c05faefb822e8c38 Mon Sep 17 00:00:00 2001 From: zojize Date: Sat, 11 Apr 2026 12:16:54 -0500 Subject: [PATCH 4/7] feat(viz): btree arrows, pointer arithmetic, n-d array rendering **Pointer graph**: Handle arrays of pointers to structs in edge building. For fields like \`BTreeNode *children[4]\`, iterate array elements and create edges for each non-null pointer. Fixes arrows not rendering for btree-style structs. **Pointer arithmetic**: Support ptr+int, int+ptr, ptr-int, ptr-ptr, pointer comparison (<, >, <=, >=), ptr++/ptr--, and ptr+=n/ptr-=n. Memory model is naturally compatible (arrays are contiguous cells). **N-d array rendering**: DSValue and FieldTable recursively flatten nested array fields with subscript prefixes (e.g. \`p[0][1]\`). **Tests**: 6 pointer arithmetic program tests (offset, subtract, difference, comparison, increment, iterator loop). Moved 3 scope cleanup tests from evaluate.test.ts to programs.test.ts. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/DSValue.vue | 35 +++-- src/components/FieldTable.vue | 42 +++--- src/composables/interpreter/evaluate.ts | 55 +++++++- src/composables/usePointerGraph.ts | 93 ++++++++------ test/interpreter/evaluate.test.ts | 66 ---------- test/interpreter/programs.test.ts | 163 ++++++++++++++++++++++++ 6 files changed, 316 insertions(+), 138 deletions(-) diff --git a/src/components/DSValue.vue b/src/components/DSValue.vue index f2ef222..cab9f69 100644 --- a/src/components/DSValue.vue +++ b/src/components/DSValue.vue @@ -71,22 +71,33 @@ const structFields = computed((): FieldEntry[] => { if (!structDef) 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) + 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 fieldIdx = Object.keys(structDef).indexOf(name) const fieldCell = context.memory.cells.get(v.base + 1 + fieldIdx) - // Flatten array fields into individual subscript entries if (typeof fieldType === 'object' && fieldType.type === 'array' && fieldCell) { - const arrVal = fieldCell.value - if (typeof arrVal === 'object' && arrVal.type === 'array') { - for (let i = 0; i < arrVal.length; i++) { - entries.push({ - name: `${name}[${i}]`, - cell: context.memory.cells.get(arrVal.base + 1 + i), - type: fieldType.of, - }) - } - continue - } + flattenArray(name, fieldType.of, fieldCell) + continue } entries.push({ name, cell: fieldCell, type: fieldType }) } diff --git a/src/components/FieldTable.vue b/src/components/FieldTable.vue index 620a869..2fdbfd6 100644 --- a/src/components/FieldTable.vue +++ b/src/components/FieldTable.vue @@ -75,29 +75,41 @@ const fields = computed((): FieldRow[] => { return [] const base = structBase.value const rows: FieldRow[] = [] + + /** Recursively flatten array elements with subscript prefix (handles n-d arrays) */ + function flattenArray(prefix: string, elemType: CppType, arrValue: { base: number, length: number }) { + for (let i = 0; i < arrValue.length; i++) { + const elemAddr = arrValue.base + 1 + i + const elemCell = context.memory.cells.get(elemAddr) + const elemValue = elemCell?.value ?? 0 + const subscript = `${prefix}[${i}]` + // Nested array: recurse + if (typeof elemType === 'object' && elemType.type === 'array' + && typeof elemValue === 'object' && elemValue.type === 'array') { + flattenArray(subscript, elemType.of, elemValue) + continue + } + rows.push({ + name: subscript, + type: elemType, + value: elemValue, + address: elemAddr, + isPointer: typeof elemValue === 'object' && elemValue.type === 'pointer', + pointerAddress: (typeof elemValue === 'object' && elemValue.type === 'pointer') ? elemValue.address : 0, + changed: props.changedAddresses.has(elemAddr), + }) + } + } + for (const [name, fieldType] of Object.entries(structDef)) { const fieldIdx = Object.keys(structDef).indexOf(name) const addr = base + 1 + fieldIdx const fieldCell = context.memory.cells.get(addr) const value = fieldCell?.value ?? 0 - // Flatten array fields into individual subscript entries if (typeof fieldType === 'object' && fieldType.type === 'array' && typeof value === 'object' && value.type === 'array') { - for (let i = 0; i < value.length; i++) { - const elemAddr = value.base + 1 + i - const elemCell = context.memory.cells.get(elemAddr) - const elemValue = elemCell?.value ?? 0 - rows.push({ - name: `${name}[${i}]`, - type: fieldType.of, - value: elemValue, - address: elemAddr, - isPointer: typeof elemValue === 'object' && elemValue.type === 'pointer', - pointerAddress: (typeof elemValue === 'object' && elemValue.type === 'pointer') ? elemValue.address : 0, - changed: props.changedAddresses.has(elemAddr), - }) - } + flattenArray(name, fieldType.of, value) continue } diff --git a/src/composables/interpreter/evaluate.ts b/src/composables/interpreter/evaluate.ts index 4c03411..d0d90db 100644 --- a/src/composables/interpreter/evaluate.ts +++ b/src/composables/interpreter/evaluate.ts @@ -174,9 +174,19 @@ function* processAssignment( const currentValue = mem.read(addr).value if (op !== '=') { op = op.slice(0, -1) - asserts(op in binaryOps, `Unsupported assignment operator: ${op}`) - asserts(typeof currentValue !== 'object' && typeof value !== 'object') - value = binaryOps[op](currentValue, castIfNull(type, value) as number | boolean) + // Pointer compound assignment (ptr += n, ptr -= n) + if (typeof currentValue === 'object' && currentValue.type === 'pointer' && typeof value === 'number') { + if (op === '+') + value = { type: 'pointer', address: currentValue.address + value } + else if (op === '-') + value = { type: 'pointer', address: currentValue.address - value } + else + asserts(false, `Unsupported pointer operator: ${op}`) + } + else { + asserts(op in binaryOps, `Unsupported assignment operator: ${op}`) + value = binaryOps[op](currentValue, castIfNull(type, value) as number | boolean) + } } const newValue = castIfNull(type, value) mem.write(addr, newValue) @@ -281,6 +291,37 @@ export function* evaluate( const lhs = checksDefined(yield* evaluate(node.childForFieldName('left')!, context, mem)) const rhs = checksDefined(yield* evaluate(node.childForFieldName('right')!, context, mem)) + + // Pointer arithmetic: ptr + int, int + ptr, ptr - int, ptr - ptr + const lPtr = lhs !== null && typeof lhs === 'object' && lhs.type === 'pointer' + const rPtr = rhs !== null && typeof rhs === 'object' && rhs.type === 'pointer' + if (lPtr || rPtr) { + if (op === '+') { + if (lPtr && typeof rhs === 'number') + return { type: 'pointer', address: (lhs as { address: number }).address + rhs } + if (rPtr && typeof lhs === 'number') + return { type: 'pointer', address: (rhs as { address: number }).address + lhs } + } + if (op === '-') { + if (lPtr && typeof rhs === 'number') + return { type: 'pointer', address: (lhs as { address: number }).address - rhs } + if (lPtr && rPtr) + return (lhs as { address: number }).address - (rhs as { address: number }).address + } + // Pointer comparison + if (lPtr && rPtr && (op === '<' || op === '>' || op === '<=' || op === '>=')) { + const la = (lhs as { address: number }).address + const ra = (rhs as { address: number }).address + if (op === '<') + return la < ra + if (op === '>') + return la > ra + if (op === '<=') + return la <= ra + return la >= ra + } + } + asserts(op in binaryOps, `Unsupported binary operator: ${op}`) return binaryOps[op](lhs, rhs) } @@ -297,8 +338,14 @@ export function* evaluate( const isPostfix = node.firstChild!.isNamed const op = node.childForFieldName('operator')!.text asserts(op === '++' || op === '--') - asserts(typeof cell.value !== 'object') const old = cell.value + // Pointer increment/decrement + if (typeof old === 'object' && old.type === 'pointer') { + const delta = op === '++' ? 1 : -1 + const newVal: CppValue = { type: 'pointer', address: old.address + delta } + mem.write(addr, newVal) + return isPostfix ? old : newVal + } const newVal = binaryOps[op[0]](old, 1) mem.write(addr, newVal) return isPostfix ? old : newVal diff --git a/src/composables/usePointerGraph.ts b/src/composables/usePointerGraph.ts index b1b7bf9..aaae2a6 100644 --- a/src/composables/usePointerGraph.ts +++ b/src/composables/usePointerGraph.ts @@ -85,61 +85,72 @@ export function usePointerGraph(context: Readonly) { const fieldNames = Object.keys(structDef) for (let i = 0; i < fieldNames.length; i++) { const fieldType = structDef[fieldNames[i]] - // Only follow pointer fields that point to struct types - if (typeof fieldType !== 'object' || fieldType.type !== 'pointer') - continue - if (typeof fieldType.to !== 'object' || fieldType.to.type !== 'struct') - continue - const fieldAddr = base + 1 + i - const fieldCell = context.memory.cells.get(fieldAddr) - if (!fieldCell) - continue + const meta = context.structFieldMeta[info.structName]?.[fieldNames[i]] + + // Collect pointer-to-struct entries: either direct pointer or array of pointers + interface PtrEntry { ptrAddr: number, fieldName: string } + const ptrEntries: PtrEntry[] = [] - const val = fieldCell.value - if (typeof val !== 'object' || val.type !== 'pointer' || val.address === NULL_ADDRESS) + if (typeof fieldType === 'object' && fieldType.type === 'pointer' + && typeof fieldType.to === 'object' && fieldType.to.type === 'struct') { + ptrEntries.push({ ptrAddr: fieldAddr, fieldName: fieldNames[i] }) + } + else if (typeof fieldType === 'object' && fieldType.type === 'array' + && typeof fieldType.of === 'object' && fieldType.of.type === 'pointer' + && typeof fieldType.of.to === 'object' && fieldType.of.to.type === 'struct') { + // Array of pointers to structs — iterate elements + const fieldCell = context.memory.cells.get(fieldAddr) + if (fieldCell && typeof fieldCell.value === 'object' && fieldCell.value.type === 'array') { + for (let j = 0; j < fieldCell.value.length; j++) + ptrEntries.push({ ptrAddr: fieldCell.value.base + 1 + j, fieldName: `${fieldNames[i]}[${j}]` }) + } + } + + if (ptrEntries.length === 0) continue - const targetAddr = val.address - const meta = context.structFieldMeta[info.structName]?.[fieldNames[i]] const direction: FieldDirection = meta?.direction ?? 'right' const color = meta?.color const style = meta?.style const fallbackStyle = meta?.fallbackStyle - // Resolve target to struct base - const targetCell = context.memory.cells.get(targetAddr) - if (!targetCell) { - danglingEdges.push({ fromAddress: base, fromFieldAddress: fieldAddr, fieldName: fieldNames[i], toAddress: targetAddr, direction, color, style, fallbackStyle }) - continue - } + for (const { ptrAddr, fieldName } of ptrEntries) { + const ptrCell = context.memory.cells.get(ptrAddr) + if (!ptrCell) + continue + const val = ptrCell.value + if (typeof val !== 'object' || val.type !== 'pointer' || val.address === NULL_ADDRESS) + continue - if (targetCell.dead) { - danglingEdges.push({ fromAddress: base, fromFieldAddress: fieldAddr, fieldName: fieldNames[i], toAddress: targetAddr, direction, color, style, fallbackStyle }) - continue - } + const targetAddr = val.address + const targetCell = context.memory.cells.get(targetAddr) + if (!targetCell || targetCell.dead) { + danglingEdges.push({ fromAddress: base, fromFieldAddress: ptrAddr, fieldName, toAddress: targetAddr, direction, color, style, fallbackStyle }) + continue + } - // Resolve to struct base - const targetBase = (typeof targetCell.value === 'object' && targetCell.value.type === 'struct') - ? targetCell.value.base - : targetAddr + const targetBase = (typeof targetCell.value === 'object' && targetCell.value.type === 'struct') + ? targetCell.value.base + : targetAddr - if (!nodes.has(targetBase)) - continue + if (!nodes.has(targetBase)) + continue - const edge: PointerEdge = { - fromAddress: base, - fromFieldAddress: fieldAddr, - fieldName: fieldNames[i], - toAddress: targetBase, - direction, - color, - style, - fallbackStyle, - } + const edge: PointerEdge = { + fromAddress: base, + fromFieldAddress: ptrAddr, + fieldName, + toAddress: targetBase, + direction, + color, + style, + fallbackStyle, + } - nodes.get(base)!.outEdges.push(edge) - nodes.get(targetBase)!.inEdges.push(edge) + nodes.get(base)!.outEdges.push(edge) + nodes.get(targetBase)!.inEdges.push(edge) + } } } diff --git a/test/interpreter/evaluate.test.ts b/test/interpreter/evaluate.test.ts index b1383db..070f0b9 100644 --- a/test/interpreter/evaluate.test.ts +++ b/test/interpreter/evaluate.test.ts @@ -1381,69 +1381,3 @@ describe('field annotations (@arrow-*)', () => { expect(context.structMeta.TreeNode).toEqual({ arrowSize: 40 }) }) }) - -// ── Scope cleanup ──────────────────────────────────────────────── - -describe('scope cleanup', () => { - it('marks function parameters dead on return', () => { - const { context } = safeRunProgram(` - void foo(int x, int y) {} - void main() { - foo(10, 20); - } - `) - // After foo returns, the stack cells for x and y should be dead - const liveCells = [...context.memory.cells.values()].filter(c => !c.dead && c.region === 'stack') - // Only main's variables should remain (none in this case) - expect(liveCells).toHaveLength(0) - }) - - it('marks struct field cells dead on delete', () => { - const { context } = safeRunProgram(` - struct Node { int data; Node *next; }; - void main() { - Node *n = new Node; - n->data = 42; - n->next = nullptr; - delete n; - } - `) - // After delete, all 3 cells (header + data + next) should be dead - const liveHeap = [...context.memory.cells.values()].filter(c => !c.dead && c.region === 'heap') - expect(liveHeap).toHaveLength(0) - }) - - it('cleans up all locals after insertBack+freeAll', () => { - const { context } = safeRunProgram(` - struct ListNode { int data; ListNode *next; }; - void insertBack(ListNode **head, int data) { - ListNode *node = new ListNode; - node->data = data; - node->next = nullptr; - if (*head == nullptr) { *head = node; return; } - ListNode *cur = *head; - while (cur->next != nullptr) { cur = cur->next; } - cur->next = node; - } - void freeAll(ListNode **head) { - ListNode *curr = *head; - while (curr != nullptr) { - ListNode *next = curr->next; - delete curr; - curr = next; - } - *head = nullptr; - } - void main() { - ListNode *head = nullptr; - insertBack(&head, 10); - insertBack(&head, 20); - insertBack(&head, 30); - freeAll(&head); - } - `) - // After program completes, all cells should be dead (including main's locals) - const liveCells = [...context.memory.cells.values()].filter(c => !c.dead && c.address !== 0) - expect(liveCells).toHaveLength(0) - }) -}) diff --git a/test/interpreter/programs.test.ts b/test/interpreter/programs.test.ts index a06edff..735d7d6 100644 --- a/test/interpreter/programs.test.ts +++ b/test/interpreter/programs.test.ts @@ -295,3 +295,166 @@ describe('interpreter integration tests', () => { expect(mem.read(keysBase + 3).value).toBe(0) }) }) + +describe('scope cleanup', () => { + it('marks function parameters dead on return', () => { + const { context } = runProgram(` + void foo(int x, int y) {} + void main() { + foo(10, 20); + } + `) + const liveCells = [...context.memory.cells.values()].filter(c => !c.dead && c.region === 'stack') + expect(liveCells).toHaveLength(0) + }) + + it('marks struct field cells dead on delete', () => { + const { context } = runProgram(` + struct Node { int data; Node *next; }; + void main() { + Node *n = new Node; + n->data = 42; + n->next = nullptr; + delete n; + } + `) + const liveHeap = [...context.memory.cells.values()].filter(c => !c.dead && c.region === 'heap') + expect(liveHeap).toHaveLength(0) + }) + + it('cleans up all locals after insertBack+freeAll', () => { + const { context } = runProgram(` + struct ListNode { int data; ListNode *next; }; + void insertBack(ListNode **head, int data) { + ListNode *node = new ListNode; + node->data = data; + node->next = nullptr; + if (*head == nullptr) { *head = node; return; } + ListNode *cur = *head; + while (cur->next != nullptr) { cur = cur->next; } + cur->next = node; + } + void freeAll(ListNode **head) { + ListNode *curr = *head; + while (curr != nullptr) { + ListNode *next = curr->next; + delete curr; + curr = next; + } + *head = nullptr; + } + void main() { + ListNode *head = nullptr; + insertBack(&head, 10); + insertBack(&head, 20); + insertBack(&head, 30); + freeAll(&head); + } + `) + const liveCells = [...context.memory.cells.values()].filter(c => !c.dead && c.address !== 0) + expect(liveCells).toHaveLength(0) + }) +}) + +describe('pointer arithmetic', () => { + it('ptr + int offsets the address', () => { + const { context, mem } = runProgram(` + int sum = 0; + void main() { + int arr[3]; + arr[0] = 10; + arr[1] = 20; + arr[2] = 30; + int *p = &arr[0]; + sum = *(p + 1); + } + `) + expect(mem.read(context.globalEnv.sum.address).value).toBe(20) + }) + + it('ptr - int offsets the address backwards', () => { + const { context, mem } = runProgram(` + int result = 0; + void main() { + int arr[3]; + arr[0] = 100; + arr[1] = 200; + arr[2] = 300; + int *p = &arr[2]; + result = *(p - 2); + } + `) + expect(mem.read(context.globalEnv.result.address).value).toBe(100) + }) + + it('ptr - ptr gives integer difference', () => { + const { context, mem } = runProgram(` + int diff = 0; + void main() { + int arr[5]; + arr[0] = 0; + arr[1] = 0; + arr[2] = 0; + arr[3] = 0; + arr[4] = 0; + int *p1 = &arr[1]; + int *p2 = &arr[4]; + diff = p2 - p1; + } + `) + expect(mem.read(context.globalEnv.diff.address).value).toBe(3) + }) + + it('pointer comparison works', () => { + const { context, mem } = runProgram(` + int result = 0; + void main() { + int arr[3]; + arr[0] = 0; + arr[1] = 0; + arr[2] = 0; + int *a = &arr[0]; + int *b = &arr[2]; + if (a < b) { result = 1; } + } + `) + expect(mem.read(context.globalEnv.result.address).value).toBe(1) + }) + + it('pointer increment and decrement work', () => { + const { context, mem } = runProgram(` + int val = 0; + void main() { + int arr[3]; + arr[0] = 10; + arr[1] = 20; + arr[2] = 30; + int *p = &arr[0]; + p++; + p++; + val = *p; + } + `) + expect(mem.read(context.globalEnv.val.address).value).toBe(30) + }) + + it('iterates array with pointer loop', () => { + const { context, mem } = runProgram(` + int sum = 0; + void main() { + int arr[4]; + arr[0] = 1; + arr[1] = 2; + arr[2] = 3; + arr[3] = 4; + int *p = &arr[0]; + int *end = &arr[3] + 1; + while (p < end) { + sum = sum + *p; + p++; + } + } + `) + expect(mem.read(context.globalEnv.sum.address).value).toBe(10) + }) +}) From 94e3080a2625c9e3a8cf2aacb468961e3f0b7207 Mon Sep 17 00:00:00 2001 From: zojize Date: Sat, 11 Apr 2026 12:33:08 -0500 Subject: [PATCH 5/7] fix(viz): inline struct arrays, variable-index highlighting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Inline arrays**: Struct array fields are now allocated contiguously within the struct's memory block instead of as separate heap allocations. BTreeNode layout: [header][numKeys][keys_hdr][k0][k1][k2] [children_hdr][c0][c1][c2][c3] — all 11 cells contiguous. This eliminates dangling array allocations after struct deletion. Refactored field addressing from `base + 1 + fieldIndex` to `base + 1 + structFieldOffset(name, defs)` across all consumers (usePointerGraph, DataStructureView, DSValue, FieldTable, MemoryMap, useStatementAddresses, declarations). **Variable-index highlighting**: `resolveSubscriptAddress` now resolves variable indices (e.g. `arr[j]`) by looking up the variable's current value in the env stack. Also handles simple binary expressions like `j + 1`. Fixes bubble sort array element highlighting. **Tests**: Updated btree test to verify contiguous inline memory layout with exact cell offsets. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/DSValue.vue | 4 +- src/components/DataStructureView.vue | 7 +- src/components/FieldTable.vue | 4 +- src/components/MemoryMap.vue | 13 +-- src/composables/interpreter/declarations.ts | 7 +- src/composables/interpreter/memory.ts | 90 +++++++++++++++------ src/composables/usePointerGraph.ts | 3 +- src/composables/useStatementAddresses.ts | 80 +++++++++++------- test/interpreter/programs.test.ts | 39 +++++++-- 9 files changed, 172 insertions(+), 75 deletions(-) diff --git a/src/components/DSValue.vue b/src/components/DSValue.vue index cab9f69..a375385 100644 --- a/src/components/DSValue.vue +++ b/src/components/DSValue.vue @@ -3,6 +3,7 @@ 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<{ @@ -93,8 +94,7 @@ const structFields = computed((): FieldEntry[] => { } for (const [name, fieldType] of Object.entries(structDef)) { - const fieldIdx = Object.keys(structDef).indexOf(name) - const fieldCell = context.memory.cells.get(v.base + 1 + fieldIdx) + 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 diff --git a/src/components/DataStructureView.vue b/src/components/DataStructureView.vue index 121a522..7bc477c 100644 --- a/src/components/DataStructureView.vue +++ b/src/components/DataStructureView.vue @@ -4,6 +4,7 @@ import { computed, nextTick, onUpdated, shallowRef, watch } from 'vue' import CanvasArrow from '~/components/CanvasArrow.vue' import DSValue from '~/components/DSValue.vue' 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' @@ -50,9 +51,9 @@ const subCellAddresses = computed(() => { if (typeof v === 'object' && v.type === 'struct') { const structDef = context.structs[v.name] if (structDef) { - const fieldCount = Object.keys(structDef).length - for (let i = 0; i < fieldCount; 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') { diff --git a/src/components/FieldTable.vue b/src/components/FieldTable.vue index 2fdbfd6..0baf6da 100644 --- a/src/components/FieldTable.vue +++ b/src/components/FieldTable.vue @@ -4,6 +4,7 @@ import { computed } from 'vue' import AddressLink from '~/components/AddressLink.vue' import DSValue from '~/components/DSValue.vue' import { formatAddr, formatType, formatValue } from '~/composables/interpreter/helpers' +import { structFieldOffset } from '~/composables/interpreter/memory' import { useInterpreterContext } from '~/composables/useInterpreterContext' const props = defineProps<{ @@ -102,8 +103,7 @@ const fields = computed((): FieldRow[] => { } for (const [name, fieldType] of Object.entries(structDef)) { - const fieldIdx = Object.keys(structDef).indexOf(name) - const addr = base + 1 + fieldIdx + const addr = base + 1 + structFieldOffset(name, structDef) const fieldCell = context.memory.cells.get(addr) const value = fieldCell?.value ?? 0 diff --git a/src/components/MemoryMap.vue b/src/components/MemoryMap.vue index 2694d9d..292e141 100644 --- a/src/components/MemoryMap.vue +++ b/src/components/MemoryMap.vue @@ -5,6 +5,7 @@ import AddressLink from '~/components/AddressLink.vue' import DSValue from '~/components/DSValue.vue' import MemoryCell from '~/components/MemoryCell.vue' import { formatAddr, formatType, formatValue, isPointerValue } from '~/composables/interpreter/helpers' +import { structFieldOffset, structTotalSize } from '~/composables/interpreter/memory' import { NULL_ADDRESS } from '~/composables/interpreter/types' import { useInterpreterContext } from '~/composables/useInterpreterContext' @@ -44,12 +45,11 @@ function getFieldValues(cell: MemoryCellType): Map() - const fieldNames = Object.keys(structDef) - for (let i = 0; i < fieldNames.length; i++) { - const fieldAddr = base + 1 + i + for (const [fieldName, fieldType] of Object.entries(structDef)) { + const fieldAddr = base + 1 + structFieldOffset(fieldName, structDef) const fieldCell = context.memory.cells.get(fieldAddr) if (fieldCell) - map.set(fieldNames[i], { type: structDef[fieldNames[i]], value: fieldCell.value, address: fieldAddr }) + map.set(fieldName, { type: fieldType, value: fieldCell.value, address: fieldAddr }) } return map } @@ -193,8 +193,9 @@ const heapEntries = computed(() => { 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++) - structFieldAddresses.add(v.base + 1 + i) + const total = structTotalSize(structDef) + for (let i = 1; i < total; i++) + structFieldAddresses.add(v.base + i) } } else if (typeof v === 'object' && v.type === 'array') { diff --git a/src/composables/interpreter/declarations.ts b/src/composables/interpreter/declarations.ts index 9e06600..3faa451 100644 --- a/src/composables/interpreter/declarations.ts +++ b/src/composables/interpreter/declarations.ts @@ -2,6 +2,7 @@ import type { Node as SyntaxNode } from 'web-tree-sitter' import type { MemoryManager } from './memory' import type { CppType, CppValue, InterpreterContext } from './types' import { asserts, castIfNull, checksDefined, getGeneratorReturn } from './helpers' +import { structFieldOffset } from './memory' import { NULL_ADDRESS } from './types' // Circular dependency resolution: evaluate.ts calls processDeclaration, @@ -143,12 +144,10 @@ export function* initializeValue( const fieldDefs = context.structs[type.name] const base = mem.allocStruct(type.name, fieldDefs, 'stack') // Recursively initialize nested struct/array fields - const fieldNames = Object.keys(fieldDefs) - for (let i = 0; i < fieldNames.length; i++) { - const fieldType = fieldDefs[fieldNames[i]] + for (const [fieldName, fieldType] of Object.entries(fieldDefs)) { if (typeof fieldType === 'object' && (fieldType.type === 'struct' || fieldType.type === 'array')) { const fieldValue = yield* initializeValue(fieldType, context, mem) - mem.write(base + 1 + i, fieldValue) + mem.write(base + 1 + structFieldOffset(fieldName, fieldDefs), fieldValue) } } return { type: 'struct', name: type.name, base } diff --git a/src/composables/interpreter/memory.ts b/src/composables/interpreter/memory.ts index 22c912f..1577eae 100644 --- a/src/composables/interpreter/memory.ts +++ b/src/composables/interpreter/memory.ts @@ -45,6 +45,7 @@ export interface MemoryManager { fieldName: string, fieldDefs: Record, ) => [CppType, number] + structFieldOffset: (fieldName: string, fieldDefs: Record) => number reset: () => void } @@ -58,6 +59,32 @@ function makeNullCell(): MemoryCell { } } +/** Compute the number of cells a field type occupies (inline arrays expand) */ +function fieldCellCount(type: CppType): number { + if (typeof type === 'object' && type.type === 'array') + return 1 + type.size // header + elements + return 1 +} + +/** Compute the cell offset of fieldName within a struct (accounting for inline arrays) */ +export function structFieldOffset(fieldName: string, fieldDefs: Record): number { + let offset = 0 + for (const [name, type] of Object.entries(fieldDefs)) { + if (name === fieldName) + return offset + offset += fieldCellCount(type) + } + return -1 +} + +/** Compute the total number of cells a struct occupies (header + all fields) */ +export function structTotalSize(fieldDefs: Record): number { + let size = 1 // header + for (const type of Object.values(fieldDefs)) + size += fieldCellCount(type) + return size +} + export function createAddressSpace(): MemoryManager { const space: AddressSpace = { cells: new Map(), @@ -107,7 +134,9 @@ export function createAddressSpace(): MemoryManager { region: MemoryRegion, ): number { const fields = Object.entries(fieldDefs) - const totalSize = 1 + fields.length // header + fields + let totalSize = 1 // header + for (const [, type] of fields) + totalSize += fieldCellCount(type) const base = allocRegion(totalSize, region) // Header cell @@ -118,26 +147,43 @@ export function createAddressSpace(): MemoryManager { region, dead: false, }) - // Field cells at base+1, base+2, ... - for (let i = 0; i < fields.length; i++) { - const [, fieldType] = fields[i] - const fieldAddress = base + 1 + i - // Array fields: allocate the array separately and point to it - let fieldValue: CppValue + + // Field cells with computed offsets (inline arrays are contiguous) + let offset = 1 + for (const [, fieldType] of fields) { if (typeof fieldType === 'object' && fieldType.type === 'array') { - const arrBase = allocArray(fieldType.of, fieldType.size, region) - fieldValue = { type: 'array', base: arrBase, length: fieldType.size } + // Inline array: header at base+offset, elements at base+offset+1.. + const arrBase = base + offset + space.cells.set(arrBase, { + address: arrBase, + type: fieldType, + value: { type: 'array', base: arrBase, length: fieldType.size }, + region, + dead: false, + }) + for (let j = 0; j < fieldType.size; j++) { + const elemAddr = arrBase + 1 + j + space.cells.set(elemAddr, { + address: elemAddr, + type: fieldType.of, + value: defaultValue(fieldType.of), + region, + dead: false, + }) + } + offset += 1 + fieldType.size } else { - fieldValue = defaultValue(fieldType) + const fieldAddress = base + offset + space.cells.set(fieldAddress, { + address: fieldAddress, + type: fieldType, + value: defaultValue(fieldType), + region, + dead: false, + }) + offset += 1 } - space.cells.set(fieldAddress, { - address: fieldAddress, - type: fieldType, - value: fieldValue, - region, - dead: false, - }) } return base } @@ -221,12 +267,10 @@ export function createAddressSpace(): MemoryManager { fieldName: string, fieldDefs: Record, ): [CppType, number] { - const fields = Object.keys(fieldDefs) - const index = fields.indexOf(fieldName) - if (index === -1) + const offset = structFieldOffset(fieldName, fieldDefs) + if (offset === -1) throw new Error(`Unknown field: ${fieldName}`) - const fieldAddress = structBase + 1 + index - return [fieldDefs[fieldName], fieldAddress] + return [fieldDefs[fieldName], structBase + 1 + offset] } function reset(): void { @@ -237,5 +281,5 @@ export function createAddressSpace(): MemoryManager { space.cells.set(NULL_ADDRESS, makeNullCell()) } - return { space, alloc, allocStruct, allocArray, read, write, free, readField, reset } + return { space, alloc, allocStruct, allocArray, read, write, free, readField, structFieldOffset, reset } } diff --git a/src/composables/usePointerGraph.ts b/src/composables/usePointerGraph.ts index aaae2a6..83d09b6 100644 --- a/src/composables/usePointerGraph.ts +++ b/src/composables/usePointerGraph.ts @@ -1,5 +1,6 @@ import type { ArrowStyle, FieldDirection, InterpreterContext } from '~/composables/interpreter/types' import { computed } from 'vue' +import { structFieldOffset } from '~/composables/interpreter/memory' import { NULL_ADDRESS } from '~/composables/interpreter/types' interface PointerEdge { @@ -85,7 +86,7 @@ export function usePointerGraph(context: Readonly) { const fieldNames = Object.keys(structDef) for (let i = 0; i < fieldNames.length; i++) { const fieldType = structDef[fieldNames[i]] - const fieldAddr = base + 1 + i + const fieldAddr = base + 1 + structFieldOffset(fieldNames[i], structDef) const meta = context.structFieldMeta[info.structName]?.[fieldNames[i]] // Collect pointer-to-struct entries: either direct pointer or array of pointers diff --git a/src/composables/useStatementAddresses.ts b/src/composables/useStatementAddresses.ts index 6f9741d..270f58d 100644 --- a/src/composables/useStatementAddresses.ts +++ b/src/composables/useStatementAddresses.ts @@ -2,6 +2,7 @@ import type { Ref } from 'vue' import type { Node as SyntaxNode } from 'web-tree-sitter' import type { InterpreterContext } from '~/composables/interpreter/types' import { computed } from 'vue' +import { structFieldOffset } from '~/composables/interpreter/memory' import { NULL_ADDRESS } from '~/composables/interpreter/types' /** @@ -239,9 +240,9 @@ function resolveFieldAddress(node: SyntaxNode, context: Readonly= 0) - addrs.add(targetCell.value.base + 1 + fieldIdx) + const offset = structFieldOffset(fieldName, structDef) + if (offset >= 0) + addrs.add(targetCell.value.base + 1 + offset) } else if (operator.text === '.') { // Direct struct access @@ -260,17 +261,58 @@ function resolveFieldAddress(node: SyntaxNode, context: Readonlykeys[i]`, resolve the * actual array element address. */ +/** Try to resolve an index expression to a number (literal or variable lookup) */ +function resolveIndex(node: SyntaxNode, context: Readonly): number | undefined { + if (node.type === 'number_literal') + return Number.parseInt(node.text) + if (node.type === 'identifier') { + // Look up variable value + for (let i = context.envStack.length - 1; i >= 0; i--) { + if (node.text in context.envStack[i]) { + const val = context.memory.cells.get(context.envStack[i][node.text].address)?.value + return typeof val === 'number' ? val : undefined + } + } + if (node.text in context.globalEnv) { + const val = context.memory.cells.get(context.globalEnv[node.text].address)?.value + return typeof val === 'number' ? val : undefined + } + } + // Binary expression (e.g. j + 1): try to evaluate simple cases + if (node.type === 'binary_expression') { + const left = node.childForFieldName('left') + const right = node.childForFieldName('right') + const op = node.childForFieldName('operator')?.text + if (left && right && op) { + const l = resolveIndex(left, context) + const r = resolveIndex(right, context) + if (l !== undefined && r !== undefined) { + if (op === '+') + return l + r + if (op === '-') + return l - r + } + } + } + return undefined +} + function resolveSubscriptAddress(node: SyntaxNode, context: Readonly, addrs: Set) { const arrayNode = node.childForFieldName('argument') const indexNode = node.childForFieldName('indices') if (!arrayNode || !indexNode) return - // Resolve the array base — could be a field_expression (node->keys) or identifier (arr) - // Try field_expression first + const idxChild = indexNode.namedChildren[0] + if (!idxChild) + return + const idx = resolveIndex(idxChild, context) + if (idx === undefined) + return + + // field_expression: node->keys[i] if (arrayNode.type === 'field_expression') { resolveFieldAddress(arrayNode, context, addrs) - // Now find the array value at the resolved field address to get the element const lastAddr = [...addrs].at(-1) if (lastAddr === undefined) return @@ -280,32 +322,22 @@ function resolveSubscriptAddress(node: SyntaxNode, context: Readonly= 0; i--) { - if (varName in context.envStack[i]) { - varAddr = context.envStack[i][varName].address + if (arrayNode.text in context.envStack[i]) { + varAddr = context.envStack[i][arrayNode.text].address break } } - if (varAddr === undefined && varName in context.globalEnv) - varAddr = context.globalEnv[varName].address + if (varAddr === undefined && arrayNode.text in context.globalEnv) + varAddr = context.globalEnv[arrayNode.text].address if (varAddr === undefined) return const cell = context.memory.cells.get(varAddr) @@ -314,12 +346,6 @@ function resolveSubscriptAddress(node: SyntaxNode, context: Readonly { const rootCell = heapCells[0] const rootBase = (rootCell.value as any).base - // numKeys = 2 + // Verify inline memory layout: + // BTreeNode { int numKeys; int keys[3]; BTreeNode *children[4]; } + // Layout: [header] [numKeys] [keys_hdr] [k0] [k1] [k2] [children_hdr] [c0] [c1] [c2] [c3] + // Offsets: +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 + + // numKeys at offset 1 expect(mem.read(rootBase + 1).value).toBe(2) - // keys field (base+2) should be an array with base pointing to allocated elements + // keys array header at offset 2 (inline, base === rootBase + 2) const keysValue = mem.read(rootBase + 2).value expect(typeof keysValue).toBe('object') expect((keysValue as any).type).toBe('array') - const keysBase = (keysValue as any).base - // keys[0] = 10, keys[1] = 20, keys[2] = 0 - expect(mem.read(keysBase + 1).value).toBe(10) - expect(mem.read(keysBase + 2).value).toBe(20) - expect(mem.read(keysBase + 3).value).toBe(0) + expect((keysValue as any).base).toBe(rootBase + 2) // inline: array base IS the field address + + // keys elements are contiguous after the header + expect(mem.read(rootBase + 3).value).toBe(10) // keys[0] + expect(mem.read(rootBase + 4).value).toBe(20) // keys[1] + expect(mem.read(rootBase + 5).value).toBe(0) // keys[2] + + // children array header at offset 6 + const childrenValue = mem.read(rootBase + 6).value + expect(typeof childrenValue).toBe('object') + expect((childrenValue as any).type).toBe('array') + expect((childrenValue as any).base).toBe(rootBase + 6) + + // children[0..3] are pointers at offsets 7..10 + const c0 = mem.read(rootBase + 7).value + expect(typeof c0).toBe('object') + expect((c0 as any).type).toBe('pointer') + + // Total struct size: 1 (header) + 1 (numKeys) + 4 (keys) + 5 (children) = 11 cells + // All 11 cells should be in the same contiguous block + for (let i = 0; i < 11; i++) { + const cell = mem.space.cells.get(rootBase + i) + expect(cell).toBeDefined() + expect(cell!.region).toBe('heap') + } }) }) From 4a627a0555e4ff8c8ad8409ad14fbf66f26eb688 Mon Sep 17 00:00:00 2001 From: zojize Date: Sat, 11 Apr 2026 12:57:35 -0500 Subject: [PATCH 6/7] feat(viz): add `breakpoint()` builtin, fix array subscript highlighting Add `breakpoint()` as an interpreter builtin that pauses play mode via a context flag while still acting as a normal step when stepping manually. All templates now call `breakpoint()` before `return 0` so users can inspect the final state. Fix array element highlighting in both memory map and DS view by passing statement address props through to DSValue and adding conditional highlight classes on array leaf elements. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/components/DSValue.vue | 10 +++++++++- src/components/MemoryMap.vue | 7 ++++++- src/composables/interpreter/evaluate.ts | 9 +++++++++ src/composables/interpreter/index.ts | 7 +++++-- src/composables/interpreter/types.ts | 2 ++ src/pages/index.vue | 10 +++++----- src/templates/general/bst-delete.cpp | 1 + src/templates/general/bst-freeAll.cpp | 1 + src/templates/general/bst-insert.cpp | 1 + src/templates/general/bst-search.cpp | 1 + src/templates/general/btree.cpp | 1 + src/templates/general/bubble-sort.cpp | 1 + src/templates/general/doubly-insertBack.cpp | 1 + src/templates/general/singly-delete.cpp | 1 + src/templates/general/singly-freeAll.cpp | 1 + src/templates/general/singly-insertBack.cpp | 1 + src/templates/general/singly-insertFront.cpp | 1 + src/templates/general/singly-loop.cpp | 1 + src/templates/general/singly-reverse.cpp | 1 + src/templates/general/tree-cycle.cpp | 1 + src/templates/general/tree-dangling.cpp | 1 + src/templates/general/tree-rebuild.cpp | 1 + src/templates/general/tree-skewed.cpp | 1 + 23 files changed, 53 insertions(+), 9 deletions(-) diff --git a/src/components/DSValue.vue b/src/components/DSValue.vue index a375385..23b7c42 100644 --- a/src/components/DSValue.vue +++ b/src/components/DSValue.vue @@ -154,7 +154,15 @@ const arrayElements = computed((): ArrayEntry[] => { /> -
+
{{ elem.prefix }} props.highlightedFieldAddress, (addr) => {
@@ -336,6 +338,9 @@ watch(() => props.highlightedFieldAddress, (addr) => {