#!/usr/bin/env bun import { Compiler } from '../src/compiler/compiler' import { VM, type Value, Scope, bytecodeToString } from 'reefvm' import * as readline from 'readline' import * as fs from 'fs' 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', pink: '\x1b[38;2;255;105;180m' } async function repl() { const commands = ['/clear', '/reset', '/vars', '/funcs', '/history', '/bytecode', '/exit', '/save', '/quit'] function completer(line: string): [string[], string] { if (line.startsWith('/')) { const hits = commands.filter(cmd => cmd.startsWith(line)) return [hits.length ? hits : commands, line] } return [[], line] } const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: `${colors.pink}>>${colors.reset} `, completer, }) let codeHistory: string[] = [] let vm: VM | null = null showWelcome() rl.prompt() rl.on('line', async (line: string) => { const trimmed = line.trim() if (!trimmed) { rl.prompt() return } vm ||= new VM({ instructions: [], constants: [] }, nativeFunctions) if (['/exit', 'exit', '/quit', 'quit'].includes(trimmed)) { console.log(`\n${colors.yellow}Goodbye!${colors.reset}`) process.exit(0) } if (trimmed === '/clear') { codeHistory = [] vm = null console.clear() showWelcome() rl.prompt() return } if (trimmed === '/reset') { codeHistory = [] vm = null console.log(`\n${colors.yellow}State reset${colors.reset}`) rl.prompt() return } if (trimmed === '/vars') { console.log(`\n${colors.bright}Variables:${colors.reset}`) console.log(formatVariables(vm.scope)) rl.prompt() return } if (['/fn', '/fns', '/fun', '/funs', '/func', '/funcs', '/functions'].includes(trimmed)) { console.log(`\n${colors.bright}Functions:${colors.reset}`) console.log(formatVariables(vm.scope, true)) rl.prompt() return } if (trimmed === '/history') { if (codeHistory.length === 0) { console.log(`\n${colors.dim}No history yet${colors.reset}`) } else { console.log(`\n${colors.bright}History:${colors.reset}`) codeHistory.forEach((code, i) => { console.log(`${colors.dim}[${i + 1}]${colors.reset} ${code}`) }) } rl.prompt() return } if (trimmed === '/bytecode') { if (!vm || codeHistory.length === 0) { console.log(`\n${colors.dim}No history. Type some things.${colors.reset}`) } else { console.log(`\n${colors.bright}Bytecode:${colors.reset}`) console.log(bytecodeToString({ instructions: vm.instructions, constants: vm.constants })) } rl.prompt() return } if (trimmed.startsWith('/save')) { const parts = trimmed.split(/\s+/) const filename = parts[1] if (!filename) { console.log(`\n${colors.red}Usage:${colors.reset} /save `) rl.prompt() return } if (codeHistory.length === 0) { console.log(`\n${colors.dim}No history to save${colors.reset}`) rl.prompt() return } // Add .shrimp extension if no extension provided const finalFilename = filename.includes('.') ? filename : `${filename}.shrimp` const content = codeHistory.join('\n') + '\n' try { fs.writeFileSync(finalFilename, content, 'utf-8') console.log(`\n${colors.green}✓${colors.reset} Saved ${codeHistory.length} line${codeHistory.length === 1 ? '' : 's'} to ${colors.bright}${finalFilename}${colors.reset}`) } catch (error: any) { console.log(`\n${colors.red}Error:${colors.reset} Failed to save file: ${error.message}`) } rl.prompt() return } codeHistory.push(trimmed) try { const compiler = new Compiler(trimmed) vm.appendBytecode(compiler.bytecode) const result = await vm.continue() console.log(`${colors.dim}=>${colors.reset} ${formatValue(result)}`) } catch (error: any) { console.log(`\n${colors.red}Error:${colors.reset} ${error.message}`) codeHistory.pop() } rl.prompt() }) rl.on('close', () => { console.log(`\n${colors.yellow}Goodbye!${colors.reset}`) process.exit(0) }) rl.on('SIGINT', () => { rl.write(null, { ctrl: true, name: 'u' }) console.log('\n') rl.prompt() }) } function formatValue(value: Value, inner = false): string { switch (value.type) { case 'string': return `${colors.green}'${value.value}'${colors.reset}` case 'number': return `${colors.cyan}${value.value}${colors.reset}` case 'boolean': return `${colors.yellow}${value.value}${colors.reset}` case 'null': return `${colors.dim}null${colors.reset}` case 'array': { const items = value.value.map(x => formatValue(x, true)).join(' ') return `${inner ? '(' : ''}${colors.blue}list${colors.reset} ${items}${inner ? ')' : ''}` } case 'dict': { const entries = Array.from(value.value.entries()) .map(([k, v]) => `${k}=${formatValue(v, true)}`) .join(' ') return `${inner ? '(' : ''}${colors.magenta}dict${colors.reset} ${entries}${inner ? ')' : ''}` } case 'function': { const params = value.params.join(', ') return `${colors.dim}${colors.reset}` } case 'native': return `${colors.dim}${colors.reset}` case 'regex': return `${colors.magenta}${value.value}${colors.reset}` default: return String(value) } } function formatVariables(scope: Scope, onlyFunctions = false): 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()) { if (onlyFunctions && (value.type === 'function' || value.type === 'native')) { vars.push(` ${prefix}${colors.bright}${name}${colors.reset} = ${formatValue(value)}`) } else if (!onlyFunctions) { 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 showWelcome() { console.log( `${colors.pink}═══════════════════════════════════════════════════════════════${colors.reset}` ) console.log(`${colors.bright}🦐 Shrimp REPL${colors.reset}`) console.log( `${colors.pink}═══════════════════════════════════════════════════════════════${colors.reset}` ) console.log(`\nType Shrimp expressions. Press ${colors.bright}Ctrl+D${colors.reset} to exit.`) console.log(`\nCommands:`) console.log(` ${colors.bright}/clear${colors.reset} - Clear screen and reset state`) console.log(` ${colors.bright}/reset${colors.reset} - Reset state (keep history visible)`) console.log(` ${colors.bright}/vars${colors.reset} - Show all variables`) console.log(` ${colors.bright}/funcs${colors.reset} - Show all functions`) console.log(` ${colors.bright}/history${colors.reset} - Show code history`) console.log(` ${colors.bright}/bytecode${colors.reset} - Show compiled bytecode`) console.log(` ${colors.bright}/save ${colors.reset} - Save history to file`) console.log(` ${colors.bright}/exit${colors.reset} - Quit REPL`) console.log(`\nExamples:`) console.log(` ${colors.cyan}5 + 10${colors.reset}`) console.log(` ${colors.cyan}x = 42${colors.reset}`) console.log(` ${colors.cyan}echo "Hello, world!"${colors.reset}`) console.log(` ${colors.cyan}greet = do name: echo Hello name end${colors.reset}`) console.log() } const nativeFunctions = { echo: (...args: any[]) => { console.log(...args) }, len: (value: any) => { if (typeof value === 'string') return value.length if (Array.isArray(value)) return value.length if (value && typeof value === 'object') return Object.keys(value).length return 0 }, type: (value: any) => { if (value === null) return 'null' if (Array.isArray(value)) return 'array' return typeof value }, range: (start: number, end: number | null) => { if (end === null) { end = start start = 0 } const result: number[] = [] for (let i = start; i <= end; i++) { result.push(i) } return result }, join: (arr: any[], sep: string = ',') => { return arr.join(sep) }, split: (str: string, sep: string = ',') => { return str.split(sep) }, upper: (str: string) => str.toUpperCase(), lower: (str: string) => str.toLowerCase(), trim: (str: string) => str.trim(), list: (...args: any[]) => args, dict: (atNamed = {}) => atNamed } await repl()