Compare commits

...

3 Commits

Author SHA1 Message Date
146b0a2883 autocompleter 2025-10-07 22:01:22 -07:00
ec53a6420e add REPL 2025-10-07 22:01:18 -07:00
fd447abea8 TRY_LOAD opcode 2025-10-07 22:01:13 -07:00
8 changed files with 507 additions and 6 deletions

View File

@ -23,6 +23,14 @@ bun test <file> # Run specific test file
bun test --watch # Watch mode bun test --watch # Watch mode
``` ```
### Tools
```bash
./bin/reef <file.reef> # Execute bytecode file
./bin/validate <file.reef> # Validate bytecode
./bin/debug <file.reef> # Step-by-step debugger
./bin/repl # Interactive REPL
```
### Building ### Building
No build step required - Bun runs TypeScript directly. No build step required - Bun runs TypeScript directly.

View File

@ -195,7 +195,8 @@ CALL
- `DUP` - Duplicate top - `DUP` - Duplicate top
### Variables ### Variables
- `LOAD <name>` - Push variable value - `LOAD <name>` - Push variable value (throws if not found)
- `TRY_LOAD <name>` - Push variable value if found, otherwise push name as string (never throws)
- `STORE <name>` - Pop and store in variable - `STORE <name>` - Pop and store in variable
### Arithmetic ### Arithmetic

View File

@ -22,6 +22,14 @@ Run the simple debugger to see what the instructions are doing:
./bin/debug examples/loop.reef ./bin/debug examples/loop.reef
./bin/debug -h examples/loop.reef ./bin/debug -h examples/loop.reef
Interactive REPL for exploring instructions:
./bin/repl
Type opcodes interactively and see the stack and variables update in real-time.
Commands: `clear`, `reset`, `exit`.
## Features ## Features
- Stack operations (PUSH, POP, DUP) - Stack operations (PUSH, POP, DUP)

31
SPEC.md
View File

@ -86,6 +86,11 @@ class Scope {
2. If not found, recursively check parent 2. If not found, recursively check parent
3. If not found anywhere, throw error 3. If not found anywhere, throw error
**Variable Resolution (TRY_LOAD)**:
1. Check current scope's locals
2. If not found, recursively check parent
3. If not found anywhere, return variable name as string (no error)
**Variable Assignment (STORE)**: **Variable Assignment (STORE)**:
1. If variable exists in current scope, update it 1. If variable exists in current scope, update it
2. Else if variable exists in any parent scope, update it there 2. Else if variable exists in any parent scope, update it there
@ -142,10 +147,32 @@ type ExceptionHandler = {
**Errors**: Throws if variable not found in scope chain **Errors**: Throws if variable not found in scope chain
#### STORE #### STORE
**Operand**: Variable name (string) **Operand**: Variable name (string)
**Effect**: Store top of stack into variable (following scope chain rules) **Effect**: Store top of stack into variable (following scope chain rules)
**Stack**: [value] → [] **Stack**: [value] → []
#### TRY_LOAD
**Operand**: Variable name (string)
**Effect**: Push variable value onto stack if found, otherwise push variable name as string
**Stack**: [] → [value | name]
**Errors**: Never throws (unlike LOAD)
**Behavior**:
1. Search for variable in scope chain (current scope and all parents)
2. If found, push the variable's value onto stack
3. If not found, push the variable name as a string value onto stack
**Use Cases**:
- Shell-like behavior where strings don't need quotes
**Example**:
```
PUSH 42
STORE x
TRY_LOAD x ; Pushes 42 (variable exists)
TRY_LOAD y ; Pushes "y" (variable doesn't exist)
```
### Arithmetic Operations ### Arithmetic Operations
All arithmetic operations pop two values, perform operation, push result as number. All arithmetic operations pop two values, perform operation, push result as number.

298
bin/repl Executable file
View File

@ -0,0 +1,298 @@
#!/usr/bin/env bun
import { VM, toBytecode } from "../src/index"
import { OpCode } from "../src/opcode"
import type { Value } from "../src/value"
import * as readline from 'node:readline'
// ANSI color codes
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',
}
function formatValue(value: Value): string {
if (value.type === 'string') {
return `${colors.green}"${value.value}"${colors.reset}`
} else if (value.type === 'number') {
return `${colors.cyan}${value.value}${colors.reset}`
} else if (value.type === 'boolean') {
return `${colors.yellow}${value.value}${colors.reset}`
} else if (value.type === 'null') {
return `${colors.dim}null${colors.reset}`
} else if (value.type === 'array') {
const items = value.value.map(v => formatValue(v)).join(', ')
return `${colors.blue}[${items}]${colors.reset}`
} else if (value.type === 'dict') {
const entries = Array.from(value.value.entries())
.map(([k, v]) => `${k}: ${formatValue(v)}`)
.join(', ')
return `${colors.magenta}{${entries}}${colors.reset}`
} else if (value.type === 'function') {
const params = value.params.join(', ')
return `${colors.dim}<fn(${params})>${colors.reset}`
}
return String(value)
}
function formatStack(stack: Value[]): string {
if (stack.length === 0) {
return ` ${colors.dim}[empty]${colors.reset}`
}
return stack.map((v, i) => ` ${colors.dim}[${i}]${colors.reset} ${formatValue(v)}`).join('\n')
}
function formatVariables(scope: any): 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()) {
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 getOpcodeName(op: OpCode): string {
return OpCode[op] || `UNKNOWN(${op})`
}
function formatInstructionAt(vm: VM, pc: number): string {
const instruction = vm.instructions[pc]
if (!instruction) {
return `${colors.dim}[END]${colors.reset}`
}
const opName = getOpcodeName(instruction.op)
let operandStr = ''
if (instruction.operand !== undefined) {
const operand = instruction.operand
// Format operand based on type
if (typeof operand === 'number') {
// Check if it's a constant reference for PUSH
if (instruction.op === OpCode.PUSH) {
const constant = vm.constants[operand]
if (constant && constant.type !== 'function_def') {
operandStr = ` ${formatValue(constant)}`
}
} else if (instruction.op === OpCode.MAKE_FUNCTION) {
const fnDef = vm.constants[operand]
if (fnDef && fnDef.type === 'function_def') {
const params = fnDef.params.join(' ')
const bodyLabel = vm.labels.get(fnDef.body)
const bodyStr = bodyLabel ? `.${bodyLabel}` : `#${fnDef.body}`
operandStr = ` ${colors.dim}(${params}) ${bodyStr}${colors.reset}`
}
} else if (instruction.op === OpCode.JUMP || instruction.op === OpCode.JUMP_IF_FALSE || instruction.op === OpCode.JUMP_IF_TRUE) {
const targetPC = pc + 1 + operand
const targetLabel = vm.labels.get(targetPC)
if (targetLabel) {
operandStr = ` ${colors.cyan}.${targetLabel}${colors.reset}`
} else {
operandStr = ` ${colors.cyan}${operand}${colors.reset}`
}
} else if (instruction.op === OpCode.PUSH_TRY || instruction.op === OpCode.PUSH_FINALLY) {
const targetLabel = vm.labels.get(operand)
if (targetLabel) {
operandStr = ` ${colors.cyan}.${targetLabel}${colors.reset}`
} else {
operandStr = ` ${colors.cyan}${operand}${colors.reset}`
}
} else {
operandStr = ` ${colors.cyan}${operand}${colors.reset}`
}
} else {
operandStr = ` ${colors.yellow}${operand}${colors.reset}`
}
}
return `${colors.bright}${opName}${colors.reset}${operandStr}`
}
function displayHistory(instructions: string[], vm: VM) {
console.log(`\n${colors.bright}Instructions:${colors.reset}`)
if (instructions.length === 0) {
console.log(` ${colors.dim}[none yet]${colors.reset}`)
} else {
instructions.forEach((_, i) => {
console.log(` ${colors.dim}[${i}]${colors.reset} ${formatInstructionAt(vm, i)}`)
})
}
}
function displayState(vm: VM) {
console.log(`\n${colors.bright}Stack:${colors.reset}`)
console.log(formatStack(vm.stack))
console.log(`\n${colors.bright}Variables:${colors.reset}`)
console.log(formatVariables(vm.scope))
if (vm.callStack.length > 0) {
console.log(`\n${colors.bright}Call Stack Depth:${colors.reset} ${colors.cyan}${vm.callStack.length}${colors.reset}`)
}
if (vm.exceptionHandlers.length > 0) {
console.log(`${colors.bright}Exception Handlers:${colors.reset} ${colors.cyan}${vm.exceptionHandlers.length}${colors.reset}`)
}
}
function clearScreen() {
console.write('\x1b[2J\x1b[H')
}
function showWelcome() {
clearScreen()
console.log(`${colors.bright}${colors.cyan}═══════════════════════════════════════════════════════════════${colors.reset}`)
console.log(`${colors.bright}🪸 ReefVM REPL${colors.reset}`)
console.log(`${colors.bright}${colors.cyan}═══════════════════════════════════════════════════════════════${colors.reset}`)
console.log(`\nType instructions one at a time. Use ${colors.bright}Tab${colors.reset} for autocomplete.`)
console.log(`\nCommands:`)
console.log(` ${colors.bright}clear${colors.reset} - Clear screen and reset state`)
console.log(` ${colors.bright}reset${colors.reset} - Reset VM state (keep history visible)`)
console.log(` ${colors.bright}exit${colors.reset} - Quit REPL`)
console.log(`\nExamples:`)
console.log(` ${colors.cyan}PUSH 42${colors.reset}`)
console.log(` ${colors.cyan}PUSH 10${colors.reset}`)
console.log(` ${colors.cyan}ADD${colors.reset}`)
console.log(` ${colors.cyan}STORE result${colors.reset}`)
console.log()
}
async function repl() {
// Get all opcode names for autocomplete
const opcodes = Object.keys(OpCode)
.filter(key => isNaN(Number(key))) // Filter out numeric keys (enum values)
const commands = ['clear', 'reset', 'exit', 'quit']
const completions = [...opcodes, ...commands]
// Autocomplete function
function completer(line: string) {
const hits = completions.filter(c => c.toLowerCase().startsWith(line.toLowerCase()))
return [hits.length ? hits : completions, line]
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: `${colors.green}reef>${colors.reset} `,
completer,
})
let instructions: string[] = []
let vm: VM | null = null
showWelcome()
rl.prompt()
rl.on('line', async (line: string) => {
let trimmed = line.trim()
// Handle empty lines
if (!trimmed) {
rl.prompt()
return
}
// Handle commands
if (trimmed === 'exit' || trimmed === 'quit') {
console.log(`\n${colors.yellow}Goodbye!${colors.reset}`)
process.exit(0)
}
if (trimmed === 'clear') {
instructions = []
vm = null
showWelcome()
rl.prompt()
return
}
if (trimmed === 'reset') {
instructions = []
vm = null
console.log(`\n${colors.yellow}VM reset${colors.reset}`)
rl.prompt()
return
}
// lowercase -> uppercase (cuz we're lazy)
if (/^[a-z]\s*/.test(trimmed)) {
const parts = trimmed.split(' ')
trimmed = [parts[0].toUpperCase(), ...parts.slice(1)].join(' ')
}
// Add instruction and execute
instructions.push(trimmed)
try {
// Rebuild bytecode with all instructions
const bytecodeStr = instructions.join('\n')
const bytecode = toBytecode(bytecodeStr)
vm = new VM(bytecode)
// Execute all instructions
while (!vm.stopped && vm.pc < vm.instructions.length) {
const instruction = vm.instructions[vm.pc]!
await vm.execute(instruction)
vm.pc++
}
// Display current state
clearScreen()
console.log(`${colors.bright}${colors.cyan}═══════════════════════════════════════════════════════════════${colors.reset}`)
console.log(`${colors.bright}🪸 ReefVM REPL${colors.reset}`)
console.log(`${colors.bright}${colors.cyan}═══════════════════════════════════════════════════════════════${colors.reset}`)
displayHistory(instructions, vm)
displayState(vm)
console.log(`\n${colors.dim}${'─'.repeat(63)}${colors.reset}`)
} catch (error: any) {
console.log(`\n${colors.red}Error: ${error.message}${colors.reset}`)
// Remove the failed instruction
instructions.pop()
}
rl.prompt()
})
rl.on('close', () => {
console.log(`\n${colors.yellow}Goodbye!${colors.reset}`)
process.exit(0)
})
// Handle Ctrl+C gracefully
rl.on('SIGINT', () => {
console.log(`\n${colors.yellow}Use 'exit' to quit${colors.reset}`)
rl.prompt()
})
}
// Main
await repl()

View File

@ -5,8 +5,9 @@ export enum OpCode {
DUP, // operand: none | stack: [value] → [value, value] DUP, // operand: none | stack: [value] → [value, value]
// variables // variables
LOAD, // operand: variable name (identifier) | stack: [] → [value] LOAD, // operand: variable name (identifier) | stack: [] → [value]
STORE, // operand: variable name (identifier) | stack: [value] → [] STORE, // operand: variable name (identifier) | stack: [value] → []
TRY_LOAD, // operand: variable name (identifier) | stack: [] → [value] | load a variable (if found) or a string
// math (coerce to number, pop 2, push result) // math (coerce to number, pop 2, push result)
ADD, // operand: none | stack: [a, b] → [a + b] ADD, // operand: none | stack: [a, b] → [a + b]

View File

@ -116,7 +116,7 @@ export class VM {
this.stopped = true this.stopped = true
break break
case OpCode.LOAD: case OpCode.LOAD: {
const varName = instruction.operand as string const varName = instruction.operand as string
const value = this.scope.get(varName) const value = this.scope.get(varName)
@ -125,6 +125,19 @@ export class VM {
this.stack.push(value) this.stack.push(value)
break break
}
case OpCode.TRY_LOAD: {
const varName = instruction.operand as string
const value = this.scope.get(varName)
if (value === undefined)
this.stack.push(toValue(varName))
else
this.stack.push(value)
break
}
case OpCode.STORE: case OpCode.STORE:
const name = instruction.operand as string const name = instruction.operand as string

View File

@ -327,6 +327,151 @@ test("STORE and LOAD - multiple variables", async () => {
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 30 }) expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 30 })
}) })
test("TRY_LOAD - variable found", async () => {
const str = `
PUSH 100
STORE count
TRY_LOAD count
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 })
const str2 = `
PUSH 'Bobby'
STORE name
TRY_LOAD name
`
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'Bobby' })
})
test("TRY_LOAD - variable missing", async () => {
const str = `
PUSH 100
STORE count
TRY_LOAD count1
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'count1' })
const str2 = `
PUSH 'Bobby'
STORE name
TRY_LOAD full-name
`
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'full-name' })
})
test("TRY_LOAD - with different value types", async () => {
// Array
const str1 = `
PUSH 1
PUSH 2
PUSH 3
MAKE_ARRAY #3
STORE arr
TRY_LOAD arr
`
const result1 = await run(toBytecode(str1))
expect(result1.type).toBe('array')
// Dict
const str2 = `
PUSH 'key'
PUSH 'value'
MAKE_DICT #1
STORE dict
TRY_LOAD dict
`
const result2 = await run(toBytecode(str2))
expect(result2.type).toBe('dict')
// Boolean
const str3 = `
PUSH true
STORE flag
TRY_LOAD flag
`
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true })
// Null
const str4 = `
PUSH null
STORE empty
TRY_LOAD empty
`
expect(await run(toBytecode(str4))).toEqual({ type: 'null', value: null })
})
test("TRY_LOAD - in nested scope", async () => {
// Function should be able to TRY_LOAD variable from parent scope
const str = `
PUSH 42
STORE outer
MAKE_FUNCTION () .fn
PUSH 0
PUSH 0
CALL
HALT
.fn:
TRY_LOAD outer
RETURN
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
})
test("TRY_LOAD - missing variable in nested scope returns name", async () => {
// If variable doesn't exist in any scope, should return name as string
const str = `
PUSH 42
STORE outer
MAKE_FUNCTION () .fn
PUSH 0
PUSH 0
CALL
HALT
.fn:
TRY_LOAD inner
RETURN
`
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'inner' })
})
test("TRY_LOAD - used for conditional variable existence check", async () => {
// Pattern: use TRY_LOAD to check if variable exists and get its value or name
const str = `
PUSH 100
STORE count
TRY_LOAD count
PUSH 'count'
EQ
`
// Variable exists, so TRY_LOAD returns 100, which != 'count'
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
const str2 = `
PUSH 100
STORE count
TRY_LOAD missing
PUSH 'missing'
EQ
`
// Variable missing, so TRY_LOAD returns 'missing', which == 'missing'
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true })
})
test("TRY_LOAD - with function value", async () => {
const str = `
MAKE_FUNCTION () .fn
STORE myFunc
JUMP .skip
.fn:
PUSH 99
RETURN
.skip:
TRY_LOAD myFunc
`
const result = await run(toBytecode(str))
expect(result.type).toBe('function')
})
test("JUMP - relative jump forward", async () => { test("JUMP - relative jump forward", async () => {
const str = ` const str = `
PUSH 1 PUSH 1