diff --git a/bin/repl b/bin/repl new file mode 100755 index 0000000..8475d82 --- /dev/null +++ b/bin/repl @@ -0,0 +1,261 @@ +#!/usr/bin/env bun + +import { Compiler } from '../src/compiler/compiler' +import { VM, type Value, Scope } from 'reefvm' +import * as readline from 'node:readline' + +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', +} + +async function repl() { + const commands = ['/clear', '/reset', '/vars', '/funcs', '/history', '/exit', '/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.green}>>${colors.reset} `, + completer, + }) + + let codeHistory: string[] = [] + let lastVm: VM | null = null + + showWelcome() + + rl.prompt() + + rl.on('line', async (line: string) => { + const trimmed = line.trim() + + if (!trimmed) { + rl.prompt() + return + } + + if (['/exit', 'exit', '/quit', 'quit'].includes(trimmed)) { + console.log(`\n${colors.yellow}Goodbye!${colors.reset}`) + process.exit(0) + } + + if (trimmed === '/clear') { + codeHistory = [] + lastVm = null + console.clear() + showWelcome() + rl.prompt() + return + } + + if (trimmed === '/reset') { + codeHistory = [] + lastVm = null + console.log(`\n${colors.yellow}State reset${colors.reset}`) + rl.prompt() + return + } + + if (trimmed === '/vars') { + const varsVm = lastVm || new VM({ instructions: [], constants: [] }, nativeFunctions) + console.log(`\n${colors.bright}Variables:${colors.reset}`) + console.log(formatVariables(varsVm.scope)) + rl.prompt() + return + } + + if (['/fn', '/fns', '/fun', '/funs', '/func', '/funcs', '/functions'].includes(trimmed)) { + const varsVm = lastVm || new VM({ instructions: [], constants: [] }, nativeFunctions) + console.log(`\n${colors.bright}Functions:${colors.reset}`) + console.log(formatVariables(varsVm.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 + } + + codeHistory.push(trimmed) + + try { + const fullCode = codeHistory.join('\n') + const compiler = new Compiler(fullCode) + + const vm = new VM(compiler.bytecode, nativeFunctions) + const result = await vm.run() + lastVm = vm + + 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): 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((v) => formatValue(v)).join(', ') + return `${colors.blue}[${items}]${colors.reset}` + } + case 'dict': { + const entries = Array.from(value.value.entries()) + .map(([k, v]) => `${k}: ${formatValue(v)}`) + .join(', ') + return `${colors.magenta}{${entries}}${colors.reset}` + } + 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.bright}${colors.cyan}═══════════════════════════════════════════════════════════════${colors.reset}` + ) + console.log(`${colors.bright}🦐 Shrimp REPL${colors.reset}`) + console.log( + `${colors.bright}${colors.cyan}═══════════════════════════════════════════════════════════════${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}/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) => { + if (end === undefined) { + 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() diff --git a/package.json b/package.json index ed111b2..839f3e3 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ ], "scripts": { "dev": "bun generate-parser && bun --hot src/server/server.tsx", - "generate-parser": "lezer-generator src/parser/shrimp.grammar --typeScript -o src/parser/shrimp.ts" + "generate-parser": "lezer-generator src/parser/shrimp.grammar --typeScript -o src/parser/shrimp.ts", + "repl": "bun generate-parser && bun bin/repl" }, "dependencies": { "reefvm": "workspace:*",