#!/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: 1000, 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)