From ec53a6420ecb24ace34d8c39b3fb452dd455cec9 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Tue, 7 Oct 2025 21:53:28 -0700 Subject: [PATCH] add REPL --- CLAUDE.md | 8 ++ README.md | 8 ++ bin/repl | 283 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 299 insertions(+) create mode 100755 bin/repl diff --git a/CLAUDE.md b/CLAUDE.md index 81e09d4..aea859e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,6 +23,14 @@ bun test # Run specific test file bun test --watch # Watch mode ``` +### Tools +```bash +./bin/reef # Execute bytecode file +./bin/validate # Validate bytecode +./bin/debug # Step-by-step debugger +./bin/repl # Interactive REPL +``` + ### Building No build step required - Bun runs TypeScript directly. diff --git a/README.md b/README.md index 9dca0fb..8d1285c 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,14 @@ Run the simple debugger to see what the instructions are doing: ./bin/debug examples/loop.reef ./bin/debug -h examples/loop.reef +Interactive REPL for exploring instructions: + + ./bin/repl + +Type opcodes interactively and see the stack and variables update in real-time. + +Commands: `clear`, `reset`, `exit`. + ## Features - Stack operations (PUSH, POP, DUP) diff --git a/bin/repl b/bin/repl new file mode 100755 index 0000000..647f574 --- /dev/null +++ b/bin/repl @@ -0,0 +1,283 @@ +#!/usr/bin/env bun + +import { VM, toBytecode } from "../src/index" +import { OpCode } from "../src/opcode" +import type { Value } from "../src/value" +import * as readline from 'node:readline' + +// 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', +} + +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(' ') + 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) { + 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) { + 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 displayHistory(instructions: string[], vm: VM) { + console.log(`\n${colors.bright}Instructions:${colors.reset}`) + if (instructions.length === 0) { + console.log(` ${colors.dim}[none yet]${colors.reset}`) + } else { + instructions.forEach((_, i) => { + console.log(` ${colors.dim}[${i}]${colors.reset} ${formatInstructionAt(vm, i)}`) + }) + } +} + +function displayState(vm: VM) { + console.log(`\n${colors.bright}Stack:${colors.reset}`) + console.log(formatStack(vm.stack)) + + console.log(`\n${colors.bright}Variables:${colors.reset}`) + console.log(formatVariables(vm.scope)) + + if (vm.callStack.length > 0) { + console.log(`\n${colors.bright}Call Stack Depth:${colors.reset} ${colors.cyan}${vm.callStack.length}${colors.reset}`) + } + + if (vm.exceptionHandlers.length > 0) { + console.log(`${colors.bright}Exception Handlers:${colors.reset} ${colors.cyan}${vm.exceptionHandlers.length}${colors.reset}`) + } +} + +function clearScreen() { + console.write('\x1b[2J\x1b[H') +} + +function showWelcome() { + clearScreen() + console.log(`${colors.bright}${colors.cyan}═══════════════════════════════════════════════════════════════${colors.reset}`) + console.log(`${colors.bright}🪸 ReefVM REPL${colors.reset}`) + console.log(`${colors.bright}${colors.cyan}═══════════════════════════════════════════════════════════════${colors.reset}`) + console.log(`\nType instructions one at a time. Commands:`) + console.log(` ${colors.bright}clear${colors.reset} - Clear screen and reset state`) + console.log(` ${colors.bright}reset${colors.reset} - Reset VM state (keep history visible)`) + console.log(` ${colors.bright}exit${colors.reset} - Quit REPL`) + console.log(`\nExamples:`) + console.log(` ${colors.cyan}PUSH 42${colors.reset}`) + console.log(` ${colors.cyan}PUSH 10${colors.reset}`) + console.log(` ${colors.cyan}ADD${colors.reset}`) + console.log(` ${colors.cyan}STORE result${colors.reset}`) + console.log() +} + +async function repl() { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: `${colors.green}reef>${colors.reset} `, + }) + + let instructions: string[] = [] + let vm: VM | null = null + + showWelcome() + + rl.prompt() + + rl.on('line', async (line: string) => { + let trimmed = line.trim() + + // Handle empty lines + if (!trimmed) { + rl.prompt() + return + } + + // Handle commands + if (trimmed === 'exit' || trimmed === 'quit') { + console.log(`\n${colors.yellow}Goodbye!${colors.reset}`) + process.exit(0) + } + + if (trimmed === 'clear') { + instructions = [] + vm = null + showWelcome() + rl.prompt() + return + } + + if (trimmed === 'reset') { + instructions = [] + vm = null + console.log(`\n${colors.yellow}VM reset${colors.reset}`) + rl.prompt() + return + } + + // lowercase -> uppercase (cuz we're lazy) + if (/^[a-z]\s*/.test(trimmed)) { + const parts = trimmed.split(' ') + trimmed = [parts[0].toUpperCase(), ...parts.slice(1)].join(' ') + } + + // Add instruction and execute + instructions.push(trimmed) + + try { + // Rebuild bytecode with all instructions + const bytecodeStr = instructions.join('\n') + const bytecode = toBytecode(bytecodeStr) + vm = new VM(bytecode) + + // Execute all instructions + while (!vm.stopped && vm.pc < vm.instructions.length) { + const instruction = vm.instructions[vm.pc]! + await vm.execute(instruction) + vm.pc++ + } + + // Display current state + clearScreen() + console.log(`${colors.bright}${colors.cyan}═══════════════════════════════════════════════════════════════${colors.reset}`) + console.log(`${colors.bright}🪸 ReefVM REPL${colors.reset}`) + console.log(`${colors.bright}${colors.cyan}═══════════════════════════════════════════════════════════════${colors.reset}`) + + displayHistory(instructions, vm) + displayState(vm) + + console.log(`\n${colors.dim}${'─'.repeat(63)}${colors.reset}`) + } catch (error: any) { + console.log(`\n${colors.red}Error: ${error.message}${colors.reset}`) + // Remove the failed instruction + instructions.pop() + } + + rl.prompt() + }) + + rl.on('close', () => { + console.log(`\n${colors.yellow}Goodbye!${colors.reset}`) + process.exit(0) + }) + + // Handle Ctrl+C gracefully + rl.on('SIGINT', () => { + console.log(`\n${colors.yellow}Use 'exit' to quit${colors.reset}`) + rl.prompt() + }) +} + +// Main +await repl()