271 lines
8.4 KiB
Plaintext
Executable File
271 lines
8.4 KiB
Plaintext
Executable File
#!/usr/bin/env bun
|
|
|
|
import { Compiler } from '../src/compiler/compiler'
|
|
import { colors, formatValue, nativeFunctions, valueFunctions } from '../src/prelude'
|
|
import { VM, Scope, bytecodeToString } from 'reefvm'
|
|
import * as readline from 'readline'
|
|
import { readFileSync, writeFileSync } from 'fs'
|
|
import { basename } from 'path'
|
|
|
|
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
|
|
|
|
// Load file if provided as argument
|
|
const filePath = process.argv[2]
|
|
if (filePath) {
|
|
const loaded = await loadFile(filePath)
|
|
vm = loaded.vm
|
|
codeHistory = loaded.codeHistory
|
|
}
|
|
|
|
showWelcome()
|
|
|
|
rl.prompt()
|
|
|
|
rl.on('line', async (line: string) => {
|
|
const trimmed = line.trim()
|
|
|
|
if (!trimmed) {
|
|
rl.prompt()
|
|
return
|
|
}
|
|
|
|
vm ||= new VM({ instructions: [], constants: [] }, nativeFunctions, valueFunctions)
|
|
|
|
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 <filename>`)
|
|
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 {
|
|
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 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')
|
|
}
|
|
|
|
async function loadFile(filePath: string): Promise<{ vm: VM; codeHistory: string[] }> {
|
|
try {
|
|
const fileContent = readFileSync(filePath, 'utf-8')
|
|
const lines = fileContent.trim().split('\n')
|
|
|
|
console.log(`${colors.dim}Loading ${basename(filePath)}...${colors.reset}`)
|
|
|
|
const vm = new VM({ instructions: [], constants: [] }, nativeFunctions, valueFunctions)
|
|
await vm.run()
|
|
|
|
const codeHistory: string[] = []
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim()
|
|
if (!trimmed) continue
|
|
|
|
try {
|
|
const compiler = new Compiler(trimmed)
|
|
vm.appendBytecode(compiler.bytecode)
|
|
await vm.continue()
|
|
codeHistory.push(trimmed)
|
|
} catch (error: any) {
|
|
console.log(`${colors.red}Error in ${basename(filePath)}:${colors.reset} ${error.message}`)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
console.log(`${colors.green}✓${colors.reset} Loaded ${codeHistory.length} line${codeHistory.length === 1 ? '' : 's'}\n`)
|
|
|
|
return { vm, codeHistory }
|
|
} catch (error: any) {
|
|
console.log(`${colors.red}Error:${colors.reset} Could not load file: ${error.message}`)
|
|
process.exit(1)
|
|
}
|
|
}
|
|
|
|
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(`${colors.dim}Usage: bun bin/repl [file.shrimp]${colors.reset}`)
|
|
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 <file>${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()
|
|
}
|
|
|
|
await repl()
|