From d1669f209b8f73b51164176776ebc80afeabe170 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath <2+defunkt@users.noreply.github.com> Date: Mon, 6 Oct 2025 15:53:19 -0700 Subject: [PATCH] debugger --- bin/debug | 355 ++++++++++++++++++++++++++++++++++++++++++ examples/counter.reef | 23 +++ examples/simple.reef | 7 + src/bytecode.ts | 8 + src/vm.ts | 2 + 5 files changed, 395 insertions(+) create mode 100755 bin/debug create mode 100644 examples/counter.reef create mode 100644 examples/simple.reef diff --git a/bin/debug b/bin/debug new file mode 100755 index 0000000..cd7d9a7 --- /dev/null +++ b/bin/debug @@ -0,0 +1,355 @@ +#!/usr/bin/env bun + +import { VM, toBytecode } from "../src/index" +import { OpCode } from "../src/opcode" +import type { Value } from "../src/value" + +// ANSI color codes +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + dim: '\x1b[2m', + cyan: '\x1b[36m', + yellow: '\x1b[33m', + green: '\x1b[32m', + red: '\x1b[31m', + blue: '\x1b[34m', + magenta: '\x1b[35m', +} + +type DebugOptions = { + delay: number + step: boolean + noClear: boolean +} + +async function parseArgs(): Promise<{ bytecode: string, options: DebugOptions }> { + const args = process.argv.slice(2) + + const options: DebugOptions = { + delay: 300, + step: false, + noClear: false, + } + + let bytecodeArg: string | undefined + + for (let i = 0; i < args.length; i++) { + const arg = args[i]! + + if (arg === '--delay' || arg === '-d') { + options.delay = parseInt(args[++i]!) + } else if (arg === '--step' || arg === '-s') { + options.step = true + } else if (arg === '--no-clear') { + options.noClear = true + } else if (arg === '--help' || arg === '-h') { + console.log(` +${colors.bright}ReefVM Debugger${colors.reset} + +Usage: bun bin/debug [options] + +Options: + -d, --delay Delay between instructions (default: 300ms) + -s, --step Step through one instruction at a time (press Enter) + -h, --help Show this help message + --no-clear Don't clear screen between instructions + +Examples: + bun bin/debug program.reef + bun bin/debug --delay 500 program.reef + bun bin/debug --step program.reef +`) + process.exit(0) + } else { + bytecodeArg = arg + } + } + + if (!bytecodeArg) { + console.error(`${colors.red}Error: No bytecode file specified${colors.reset}`) + console.error(`Run with --help for usage information`) + process.exit(1) + } + + // Read bytecode file + const file = Bun.file(bytecodeArg) + if (!file.exists()) { + console.error(`${colors.red}Error: File not found: ${bytecodeArg}${colors.reset}`) + process.exit(1) + } + + const bytecode = file.text().then(text => text) + + return { bytecode: await bytecode, options } +} + +function formatValue(value: Value): string { + if (value.type === 'string') { + return `${colors.green}"${value.value}"${colors.reset}` + } else if (value.type === 'number') { + return `${colors.cyan}${value.value}${colors.reset}` + } else if (value.type === 'boolean') { + return `${colors.yellow}${value.value}${colors.reset}` + } else if (value.type === 'null') { + return `${colors.dim}null${colors.reset}` + } else if (value.type === 'array') { + const items = value.value.map(v => formatValue(v)).join(', ') + return `${colors.blue}[${items}]${colors.reset}` + } else if (value.type === 'dict') { + const entries = Array.from(value.value.entries()) + .map(([k, v]) => `${k}: ${formatValue(v)}`) + .join(', ') + return `${colors.magenta}{${entries}}${colors.reset}` + } else if (value.type === 'function') { + const params = value.params.join(', ') + return `${colors.dim}${colors.reset}` + } + return String(value) +} + +function formatStack(stack: Value[]): string { + if (stack.length === 0) { + return `${colors.dim}[empty]${colors.reset}` + } + return stack.map((v, i) => ` ${colors.dim}[${i}]${colors.reset} ${formatValue(v)}`).join('\n') +} + +function formatVariables(scope: any): string { + const vars: string[] = [] + + function collectVars(s: any, depth = 0) { + if (!s) return + + const prefix = depth > 0 ? `${colors.dim}(parent)${colors.reset} ` : '' + + for (const [name, value] of s.locals.entries()) { + vars.push(` ${prefix}${colors.bright}${name}${colors.reset} = ${formatValue(value)}`) + } + + if (s.parent) { + collectVars(s.parent, depth + 1) + } + } + + collectVars(scope) + + if (vars.length === 0) { + return `${colors.dim}[no variables]${colors.reset}` + } + + return vars.join('\n') +} + +function getOpcodeName(op: OpCode): string { + return OpCode[op] || `UNKNOWN(${op})` +} + +function formatInstructionAt(vm: VM, pc: number): string { + const instruction = vm.instructions[pc] + if (!instruction) { + return `${colors.dim}[END]${colors.reset}` + } + + const opName = getOpcodeName(instruction.op) + let operandStr = '' + + if (instruction.operand !== undefined) { + const operand = instruction.operand + + // Format operand based on type + if (typeof operand === 'number') { + // Check if it's a constant reference for PUSH + if (instruction.op === OpCode.PUSH) { + const constant = vm.constants[operand] + if (constant && constant.type !== 'function_def') { + operandStr = ` ${formatValue(constant)}` + } + } else if (instruction.op === OpCode.MAKE_FUNCTION) { + const fnDef = vm.constants[operand] + if (fnDef && fnDef.type === 'function_def') { + const params = fnDef.params.join(' ') + // Check if body address has a label + const bodyLabel = vm.labels.get(fnDef.body) + const bodyStr = bodyLabel ? `.${bodyLabel}` : `#${fnDef.body}` + operandStr = ` ${colors.dim}(${params}) ${bodyStr}${colors.reset}` + } + } else if (instruction.op === OpCode.JUMP || instruction.op === OpCode.JUMP_IF_FALSE || instruction.op === OpCode.JUMP_IF_TRUE) { + // Relative jump - calculate target PC + const targetPC = pc + 1 + operand + const targetLabel = vm.labels.get(targetPC) + if (targetLabel) { + operandStr = ` ${colors.cyan}.${targetLabel}${colors.reset}` + } else { + operandStr = ` ${colors.cyan}${operand}${colors.reset}` + } + } else if (instruction.op === OpCode.PUSH_TRY || instruction.op === OpCode.PUSH_FINALLY) { + // Absolute address + const targetLabel = vm.labels.get(operand) + if (targetLabel) { + operandStr = ` ${colors.cyan}.${targetLabel}${colors.reset}` + } else { + operandStr = ` ${colors.cyan}${operand}${colors.reset}` + } + } else { + operandStr = ` ${colors.cyan}${operand}${colors.reset}` + } + } else { + operandStr = ` ${colors.yellow}${operand}${colors.reset}` + } + } + + return `${colors.bright}${opName}${colors.reset}${operandStr}` +} + +function formatInstructionWindow(vm: VM, highlightPC: number, contextLines: number = 5): string { + const lines: string[] = [] + const totalInstructions = vm.instructions.length + + // Calculate window bounds + const start = Math.max(0, highlightPC - contextLines) + const end = Math.min(totalInstructions, highlightPC + contextLines + 1) + + for (let i = start; i < end; i++) { + const isCurrent = i === highlightPC + const pcStr = `${i}`.padStart(4, ' ') + const instruction = formatInstructionAt(vm, i) + + // Check if there's a label at this instruction + const label = vm.labels.get(i) + const labelStr = label ? `${colors.dim}.${label}:${colors.reset}` : '' + + if (isCurrent) { + // Current instruction with arrow + if (label) { + lines.push(`${colors.bright}${colors.blue} ${pcStr} → ${labelStr} ${instruction}${colors.reset}`) + } else { + lines.push(`${colors.bright}${colors.blue} ${pcStr} → ${instruction}${colors.reset}`) + } + } else { + // Other instructions + if (label) { + lines.push(`${colors.dim} ${pcStr} ${labelStr} ${instruction}${colors.reset}`) + } else { + lines.push(`${colors.dim} ${pcStr} ${instruction}${colors.reset}`) + } + } + } + + return lines.join('\n') +} + +function clearScreen() { + console.write('\x1b[2J\x1b[H') +} + +function displayState(vm: VM, options: DebugOptions, stepNum: number, highlightPC: number) { + if (!options.noClear) { + clearScreen() + } + + console.log(`${colors.bright}${colors.cyan}═══════════════════════════════════════════════════════════════${colors.reset}`) + console.log(`${colors.bright}🪸 ReefVM Debugger${colors.reset} ${colors.dim}(Step ${stepNum})${colors.reset}`) + console.log(`${colors.bright}${colors.cyan}═══════════════════════════════════════════════════════════════${colors.reset}`) + console.log() + + // Instruction window + console.log(`${colors.bright}Program:${colors.reset}`) + console.log(formatInstructionWindow(vm, highlightPC, 5)) + console.log() + + // Stack + console.log(`${colors.bright}Stack:${colors.reset}`) + console.log(formatStack(vm.stack)) + console.log() + + // Variables + console.log(`${colors.bright}Variables:${colors.reset}`) + console.log(formatVariables(vm.scope)) + console.log() + + // Call stack + console.log(`${colors.bright}Call Stack Depth:${colors.reset} ${colors.cyan}${vm.callStack.length}${colors.reset}`) + + // Exception handlers + if (vm.exceptionHandlers.length > 0) { + console.log(`${colors.bright}Exception Handlers:${colors.reset} ${colors.cyan}${vm.exceptionHandlers.length}${colors.reset}`) + } + + console.log() + console.log(`${colors.dim}${'─'.repeat(63)}${colors.reset}`) + + if (options.step) { + console.log(`${colors.dim}Press Enter to continue...${colors.reset}`) + } +} + +async function waitForInput(): Promise { + return new Promise((resolve) => { + process.stdin.setRawMode(true) + process.stdin.resume() + process.stdin.once('data', (data) => { + process.stdin.setRawMode(false) + process.stdin.pause() + + // Handle Ctrl+C + if (data[0] === 3) { + console.log(`\n${colors.yellow}Interrupted${colors.reset}`) + process.exit(0) + } + + resolve() + }) + }) +} + +async function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +async function debug(bytecodeStr: string, options: DebugOptions) { + const bytecode = toBytecode(bytecodeStr) + const vm = new VM(bytecode) + + let stepNum = 0 + + // Execute step by step + while (!vm.stopped && vm.pc < vm.instructions.length) { + const instruction = vm.instructions[vm.pc]! + const executedPC = vm.pc // Save PC of instruction we're about to execute + + try { + await vm.execute(instruction) + vm.pc++ + stepNum++ + + // Display state showing the instruction we just executed + displayState(vm, options, stepNum, executedPC) + + if (options.step) { + await waitForInput() + } else { + await sleep(options.delay) + } + } catch (error) { + console.log() + console.log(`${colors.red}${colors.bright}ERROR:${colors.reset} ${colors.red}${error}${colors.reset}`) + process.exit(1) + } + } + + // Final result + console.log() + console.log(`${colors.bright}${colors.green}═══════════════════════════════════════════════════════════════${colors.reset}`) + console.log(`${colors.bright}${colors.green}Execution Complete${colors.reset}`) + console.log(`${colors.bright}${colors.green}═══════════════════════════════════════════════════════════════${colors.reset}`) + console.log() + + const result = vm.stack[vm.stack.length - 1] || { type: 'null', value: null } as Value + console.log(`${colors.bright}Final Result:${colors.reset} ${formatValue(result)}`) + console.log() +} + +// Main +const { bytecode, options } = await parseArgs() +await debug(bytecode, options) diff --git a/examples/counter.reef b/examples/counter.reef new file mode 100644 index 0000000..864be66 --- /dev/null +++ b/examples/counter.reef @@ -0,0 +1,23 @@ +; Simple counter example +PUSH 0 +STORE counter + +PUSH 10 +STORE max + +.loop: + LOAD counter + LOAD max + LT + JUMP_IF_FALSE .end + + LOAD counter + PUSH 1 + ADD + STORE counter + + JUMP .loop + +.end: + LOAD counter + HALT diff --git a/examples/simple.reef b/examples/simple.reef new file mode 100644 index 0000000..0302b31 --- /dev/null +++ b/examples/simple.reef @@ -0,0 +1,7 @@ +; Simple arithmetic +PUSH 5 +PUSH 10 +ADD +STORE result +LOAD result +HALT diff --git a/src/bytecode.ts b/src/bytecode.ts index 2e6e200..d157b94 100644 --- a/src/bytecode.ts +++ b/src/bytecode.ts @@ -4,6 +4,7 @@ import { OpCode } from "./opcode" export type Bytecode = { instructions: Instruction[] constants: Constant[] + labels?: Map // Maps instruction index to label name } export type Instruction = { @@ -240,5 +241,12 @@ export function toBytecode(str: string): Bytecode /* throws */ { }) } + // Invert labels map: name->index becomes index->name for debugger display + const labelsByIndex = new Map() + for (const [name, index] of labels.entries()) { + labelsByIndex.set(index, name) + } + bytecode.labels = labelsByIndex + return bytecode } diff --git a/src/vm.ts b/src/vm.ts index 6de55a8..cbbe609 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -16,11 +16,13 @@ export class VM { scope: Scope constants: Constant[] = [] instructions: Instruction[] = [] + labels: Map = new Map() nativeFunctions: Map = new Map() constructor(bytecode: Bytecode) { this.instructions = bytecode.instructions this.constants = bytecode.constants + this.labels = bytecode.labels || new Map() this.scope = new Scope() }