From 5988e75939418510240b37befb9d95816945c6fd Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sat, 25 Oct 2025 07:00:50 -0700 Subject: [PATCH 1/6] bun run repl --- CLAUDE.md | 27 ++-- bin/repl | 261 +++++++++++++++++++++++++++++++++++++++ package.json | 3 +- src/compiler/compiler.ts | 30 ++--- 4 files changed, 293 insertions(+), 28 deletions(-) create mode 100755 bin/repl diff --git a/CLAUDE.md b/CLAUDE.md index 581c100..356bd09 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,9 +51,9 @@ When exploring Shrimp, focus on these key files in order: 3. **src/compiler/compiler.ts** - CST to bytecode transformation - - See how functions become labels in `fnLabels` map - - Check short-circuit logic for `and`/`or` (lines 267-282) - - Notice `TRY_CALL` emission for bare identifiers (line 152) + - See how functions emit inline with JUMP wrappers + - Check short-circuit logic for `and`/`or` + - Notice `TRY_CALL` emission for bare identifiers 4. **packages/ReefVM/src/vm.ts** - Bytecode execution - See `TRY_CALL` fall-through to `CALL` (lines 357-375) @@ -211,14 +211,23 @@ Implementation files: ## Compiler Architecture -**Function compilation strategy**: The compiler doesn't create inline function objects. Instead it: +**Function compilation strategy**: Functions are compiled inline where they're defined, with JUMP instructions to skip over their bodies during linear execution: -1. Generates unique labels (`.func_0`, `.func_1`) for each function body (compiler.ts:137) -2. Stores function body instructions in `fnLabels` map during compilation -3. Appends all function bodies to the end of bytecode with RETURN instructions (compiler.ts:36-41) -4. Emits `MAKE_FUNCTION` with parameters and label reference +``` +JUMP .after_.func_0 # Skip over body during definition +.func_0: # Function body label + (function body code) + RETURN +.after_.func_0: # Resume here after jump +MAKE_FUNCTION (x) .func_0 # Create function object with label +``` -This approach keeps the main program linear and allows ReefVM to jump to function bodies by label. +This approach: +- Emits function bodies inline (no deferred collection) +- Uses JUMP to skip bodies during normal execution flow +- Each function is self-contained at its definition site +- Works seamlessly in REPL mode (important for `vm.appendBytecode()`) +- Allows ReefVM to jump to function bodies by label when called **Short-circuit logic**: ReefVM has no AND/OR opcodes. The compiler implements short-circuit evaluation using: diff --git a/bin/repl b/bin/repl new file mode 100755 index 0000000..ea49e99 --- /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', + 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}${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}/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() 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:*", diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 3efa7cd..212da55 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -48,7 +48,7 @@ function processEscapeSeq(escapeSeq: string): string { export class Compiler { instructions: ProgramItem[] = [] - fnLabels = new Map() + 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 } -- 2.50.1 From d7f613f2e4f1bd87ec28119843a8467e6693406a Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sat, 25 Oct 2025 10:26:18 -0700 Subject: [PATCH 2/6] not double quotes --- bin/repl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/repl b/bin/repl index ea49e99..8236590 100755 --- a/bin/repl +++ b/bin/repl @@ -135,7 +135,7 @@ async function repl() { function formatValue(value: Value, inner = false): string { switch (value.type) { case 'string': - return `${colors.green}"${value.value}"${colors.reset}` + return `${colors.green}'${value.value}'${colors.reset}` case 'number': return `${colors.cyan}${value.value}${colors.reset}` case 'boolean': -- 2.50.1 From 664ba821990dc7a591a245e29c37c59505f41740 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sat, 25 Oct 2025 10:32:36 -0700 Subject: [PATCH 3/6] add /bytecode --- bin/repl | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/bin/repl b/bin/repl index 8236590..6954db7 100755 --- a/bin/repl +++ b/bin/repl @@ -1,7 +1,7 @@ #!/usr/bin/env bun import { Compiler } from '../src/compiler/compiler' -import { VM, type Value, Scope } from 'reefvm' +import { VM, type Value, Scope, bytecodeToString } from 'reefvm' import * as readline from 'node:readline' const colors = { @@ -18,7 +18,7 @@ const colors = { } async function repl() { - const commands = ['/clear', '/reset', '/vars', '/funcs', '/history', '/exit', '/quit'] + const commands = ['/clear', '/reset', '/vars', '/funcs', '/history', '/bytecode', '/exit', '/quit'] function completer(line: string): [string[], string] { if (line.startsWith('/')) { @@ -101,6 +101,20 @@ async function repl() { 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 + } + codeHistory.push(trimmed) try { @@ -210,6 +224,7 @@ function showWelcome() { 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}/exit${colors.reset} - Quit REPL`) console.log(`\nExamples:`) console.log(` ${colors.cyan}5 + 10${colors.reset}`) -- 2.50.1 From ad1d7266b811452a5492556a7a1e9d75496bf18f Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sat, 25 Oct 2025 10:42:49 -0700 Subject: [PATCH 4/6] /save --- bin/repl | 51 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/bin/repl b/bin/repl index 6954db7..85a32fb 100755 --- a/bin/repl +++ b/bin/repl @@ -2,7 +2,8 @@ import { Compiler } from '../src/compiler/compiler' import { VM, type Value, Scope, bytecodeToString } from 'reefvm' -import * as readline from 'node:readline' +import * as readline from 'readline' +import * as fs from 'fs' const colors = { reset: '\x1b[0m', @@ -18,7 +19,7 @@ const colors = { } async function repl() { - const commands = ['/clear', '/reset', '/vars', '/funcs', '/history', '/bytecode', '/exit', '/quit'] + const commands = ['/clear', '/reset', '/vars', '/funcs', '/history', '/bytecode', '/exit', '/save', '/quit'] function completer(line: string): [string[], string] { if (line.startsWith('/')) { @@ -115,6 +116,37 @@ async function repl() { 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 { @@ -219,13 +251,14 @@ function showWelcome() { ) 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}/exit${colors.reset} - Quit REPL`) + 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}`) -- 2.50.1 From e95c8e5018f9adcd05a4a28e33cefbca47bc268f Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sat, 25 Oct 2025 10:42:59 -0700 Subject: [PATCH 5/6] update submodule --- packages/ReefVM | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ReefVM b/packages/ReefVM index 995487f..17d846b 160000 --- a/packages/ReefVM +++ b/packages/ReefVM @@ -1 +1 @@ -Subproject commit 995487f2d5d8bb260e223ca402220c51ceba1c4a +Subproject commit 17d846b99910a46fc1c7ab98aa41ca8afbc14097 -- 2.50.1 From 0ff0dd53804f935a70741588557ad3fe7c312b86 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sat, 25 Oct 2025 15:52:11 -0700 Subject: [PATCH 6/6] ./bin/repl --- bin/repl | 50 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/bin/repl b/bin/repl index 85a32fb..b614e26 100755 --- a/bin/repl +++ b/bin/repl @@ -3,7 +3,8 @@ import { Compiler } from '../src/compiler/compiler' import { VM, type Value, Scope, bytecodeToString } from 'reefvm' import * as readline from 'readline' -import * as fs from 'fs' +import { readFileSync, writeFileSync } from 'fs' +import { basename } from 'path' const colors = { reset: '\x1b[0m', @@ -39,6 +40,14 @@ async function repl() { 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() @@ -137,7 +146,7 @@ async function repl() { const content = codeHistory.join('\n') + '\n' try { - fs.writeFileSync(finalFilename, content, 'utf-8') + 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}`) @@ -241,6 +250,42 @@ function formatVariables(scope: Scope, onlyFunctions = false): string { 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) + 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}` @@ -250,6 +295,7 @@ function showWelcome() { `${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)`) -- 2.50.1