Compare commits
1 Commits
5988e75939
...
831966323a
| Author | SHA1 | Date | |
|---|---|---|---|
| 831966323a |
27
CLAUDE.md
27
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
|
3. **src/compiler/compiler.ts** - CST to bytecode transformation
|
||||||
|
|
||||||
- See how functions become labels in `fnLabels` map
|
- See how functions emit inline with JUMP wrappers
|
||||||
- Check short-circuit logic for `and`/`or` (lines 267-282)
|
- Check short-circuit logic for `and`/`or`
|
||||||
- Notice `TRY_CALL` emission for bare identifiers (line 152)
|
- Notice `TRY_CALL` emission for bare identifiers
|
||||||
|
|
||||||
4. **packages/ReefVM/src/vm.ts** - Bytecode execution
|
4. **packages/ReefVM/src/vm.ts** - Bytecode execution
|
||||||
- See `TRY_CALL` fall-through to `CALL` (lines 357-375)
|
- See `TRY_CALL` fall-through to `CALL` (lines 357-375)
|
||||||
|
|
@ -211,14 +211,23 @@ Implementation files:
|
||||||
|
|
||||||
## Compiler Architecture
|
## 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
|
JUMP .after_.func_0 # Skip over body during definition
|
||||||
3. Appends all function bodies to the end of bytecode with RETURN instructions (compiler.ts:36-41)
|
.func_0: # Function body label
|
||||||
4. Emits `MAKE_FUNCTION` with parameters and label reference
|
(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:
|
**Short-circuit logic**: ReefVM has no AND/OR opcodes. The compiler implements short-circuit evaluation using:
|
||||||
|
|
||||||
|
|
|
||||||
261
bin/repl
Executable file
261
bin/repl
Executable 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()
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun generate-parser && bun --hot src/server/server.tsx",
|
"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": {
|
"dependencies": {
|
||||||
"reefvm": "workspace:*",
|
"reefvm": "workspace:*",
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ function processEscapeSeq(escapeSeq: string): string {
|
||||||
|
|
||||||
export class Compiler {
|
export class Compiler {
|
||||||
instructions: ProgramItem[] = []
|
instructions: ProgramItem[] = []
|
||||||
fnLabels = new Map<Label, ProgramItem[]>()
|
fnLabelCount = 0
|
||||||
ifLabelCount = 0
|
ifLabelCount = 0
|
||||||
bytecode: Bytecode
|
bytecode: Bytecode
|
||||||
pipeCounter = 0
|
pipeCounter = 0
|
||||||
|
|
@ -64,14 +64,6 @@ export class Compiler {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.#compileCst(cst, input)
|
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)
|
this.bytecode = toBytecode(this.instructions)
|
||||||
|
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
|
|
@ -254,18 +246,20 @@ export class Compiler {
|
||||||
case terms.FunctionDef: {
|
case terms.FunctionDef: {
|
||||||
const { paramNames, bodyNodes } = getFunctionDefParts(node, input)
|
const { paramNames, bodyNodes } = getFunctionDefParts(node, input)
|
||||||
const instructions: ProgramItem[] = []
|
const instructions: ProgramItem[] = []
|
||||||
const functionLabel: Label = `.func_${this.fnLabels.size}`
|
const functionLabel: Label = `.func_${this.fnLabelCount++}`
|
||||||
const bodyInstructions: ProgramItem[] = []
|
const afterLabel: Label = `.after_${functionLabel}`
|
||||||
if (this.fnLabels.has(functionLabel)) {
|
|
||||||
throw new CompilerError(`Function name collision: ${functionLabel}`, node.from, node.to)
|
|
||||||
}
|
|
||||||
|
|
||||||
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])
|
instructions.push(['MAKE_FUNCTION', paramNames, functionLabel])
|
||||||
bodyNodes.forEach((bodyNode) => {
|
|
||||||
bodyInstructions.push(...this.#compileNode(bodyNode, input))
|
|
||||||
})
|
|
||||||
|
|
||||||
return instructions
|
return instructions
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user