Compare commits

...

1 Commits

Author SHA1 Message Date
7d044b28a4 bun run repl 2025-10-25 10:12:09 -07:00
3 changed files with 275 additions and 19 deletions

261
bin/repl Executable file
View File

@ -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',
pink: '\x1b[38;2;255;105;180m'
}
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.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
}
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}<fn(${params})>${colors.reset}`
}
case 'native':
return `${colors.dim}<native-fn>${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}/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()

View File

@ -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:*",

View File

@ -48,7 +48,7 @@ function processEscapeSeq(escapeSeq: string): string {
export class Compiler {
instructions: ProgramItem[] = []
fnLabels = new Map<Label, ProgramItem[]>()
fnLabelCount = 0
ifLabelCount = 0
bytecode: Bytecode
pipeCounter = 0
@ -64,14 +64,6 @@ export class Compiler {
}
this.#compileCst(cst, input)
// Add the labels
for (const [label, labelInstructions] of this.fnLabels) {
this.instructions.push([`${label}:`])
this.instructions.push(...labelInstructions)
this.instructions.push(['RETURN'])
}
this.bytecode = toBytecode(this.instructions)
if (DEBUG) {
@ -254,18 +246,20 @@ export class Compiler {
case terms.FunctionDef: {
const { paramNames, bodyNodes } = getFunctionDefParts(node, input)
const instructions: ProgramItem[] = []
const functionLabel: Label = `.func_${this.fnLabels.size}`
const bodyInstructions: ProgramItem[] = []
if (this.fnLabels.has(functionLabel)) {
throw new CompilerError(`Function name collision: ${functionLabel}`, node.from, node.to)
}
const functionLabel: Label = `.func_${this.fnLabelCount++}`
const afterLabel: Label = `.after_${functionLabel}`
this.fnLabels.set(functionLabel, bodyInstructions)
instructions.push(['JUMP', afterLabel])
instructions.push([`${functionLabel}:`])
bodyNodes.forEach((bodyNode) => {
instructions.push(...this.#compileNode(bodyNode, input))
})
instructions.push(['RETURN'])
instructions.push([`${afterLabel}:`])
instructions.push(['MAKE_FUNCTION', paramNames, functionLabel])
bodyNodes.forEach((bodyNode) => {
bodyInstructions.push(...this.#compileNode(bodyNode, input))
})
return instructions
}