Compare commits

..

No commits in common. "main" and "varadic-and-named" have entirely different histories.

33 changed files with 313 additions and 5870 deletions

145
CLAUDE.md
View File

@ -55,7 +55,7 @@ No build step required - Bun runs TypeScript directly.
### Critical Design Decisions ### Critical Design Decisions
**Label-based jumps**: All JUMP instructions (`JUMP`, `JUMP_IF_FALSE`, `JUMP_IF_TRUE`) require label operands (`.label`), not numeric offsets. Labels are resolved to PC-relative offsets during compilation, making bytecode position-independent. PUSH_TRY/PUSH_FINALLY use absolute addresses and can accept either labels or numeric offsets. **Relative jumps**: All JUMP instructions use PC-relative offsets (not absolute addresses), making bytecode position-independent. PUSH_TRY/PUSH_FINALLY use absolute addresses.
**Truthiness semantics**: Only `null` and `false` are falsy. Unlike JavaScript, `0`, `""`, empty arrays, and empty dicts are truthy. **Truthiness semantics**: Only `null` and `false` are falsy. Unlike JavaScript, `0`, `""`, empty arrays, and empty dicts are truthy.
@ -137,100 +137,40 @@ Array format features:
- Function params as string arrays: `["MAKE_FUNCTION", ["x", "y=10"], ".body"]` - Function params as string arrays: `["MAKE_FUNCTION", ["x", "y=10"], ".body"]`
- See `tests/programmatic.test.ts` and `examples/programmatic.ts` for examples - See `tests/programmatic.test.ts` and `examples/programmatic.ts` for examples
### Native Function Registration and Global Values ### Native Function Registration
**Option 1**: Pass to `run()` or `VM` constructor (convenience) **Option 1**: Pass to `run()` or `VM` constructor (convenience)
```typescript ```typescript
const result = await run(bytecode, { const result = await run(bytecode, {
add: (a: number, b: number) => a + b, add: (a: number, b: number) => a + b,
greet: (name: string) => `Hello, ${name}!`, greet: (name: string) => `Hello, ${name}!`
pi: 3.14159,
config: { debug: true, port: 8080 }
}) })
// Or with VM constructor // Or with VM constructor
const vm = new VM(bytecode, { add, greet, pi, config }) const vm = new VM(bytecode, { add, greet })
``` ```
**Option 2**: Set values with `vm.set()` (manual) **Option 2**: Register with `vm.registerFunction()` (manual)
```typescript ```typescript
const vm = new VM(bytecode) const vm = new VM(bytecode)
vm.registerFunction('add', (a: number, b: number) => a + b)
// Set functions (auto-wrapped to native functions)
vm.set('add', (a: number, b: number) => a + b)
// Set any other values (auto-converted to ReefVM Values)
vm.set('pi', 3.14159)
vm.set('config', { debug: true, port: 8080 })
await vm.run() await vm.run()
``` ```
**Option 3**: Set Value-based functions with `vm.setValueFunction()` (advanced) **Option 3**: Register Value-based functions (for direct Value access)
For functions that work directly with ReefVM Value types:
```typescript ```typescript
const vm = new VM(bytecode) vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
// Set Value-based function (no wrapping, works directly with Values)
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
return toValue(toNumber(a) + toNumber(b)) return toValue(toNumber(a) + toNumber(b))
}) })
await vm.run()
``` ```
Auto-wrapping handles: Auto-wrapping handles:
- Functions: wrapped as native functions with Value ↔ native type conversion - Value ↔ native type conversion (`fromValue`/`toValue`)
- Sync and async functions - Sync and async functions
- Arrays, objects, primitives, null, RegExp - Arrays, objects, primitives, null, RegExp
- All values converted via `toValue()`
### Calling Functions from TypeScript ### Label Usage (Preferred)
Use labels instead of numeric offsets for readability:
Use `vm.call()` to invoke Reef or native functions from TypeScript:
```typescript
const bytecode = toBytecode(`
MAKE_FUNCTION (x y=10) .add
STORE add
HALT
.add:
LOAD x
LOAD y
ADD
RETURN
`)
const vm = new VM(bytecode, {
log: (msg: string) => console.log(msg) // Native function
})
await vm.run()
// Call Reef function with positional arguments
const result1 = await vm.call('add', 5, 3) // → 8
// Call Reef function with named arguments (pass final object)
const result2 = await vm.call('add', 5, { y: 20 }) // → 25
// Call Reef function with all named arguments
const result3 = await vm.call('add', { x: 10, y: 15 }) // → 25
// Call native function
await vm.call('log', 'Hello!')
```
**How it works**:
- Looks up function (Reef or native) in VM scope
- For Reef functions: converts to callable JavaScript function using `fnFromValue`
- For native functions: calls directly
- Automatically converts arguments to ReefVM Values
- Converts result back to JavaScript types
### Label Usage (Required for JUMP instructions)
All JUMP instructions must use labels:
``` ```
JUMP .skip JUMP .skip
PUSH 42 PUSH 42
@ -240,67 +180,6 @@ HALT
HALT HALT
``` ```
### Function Definition Patterns
When defining functions, you MUST prevent the PC from falling through into function bodies. Two patterns:
**Pattern 1: JUMP over function bodies (Recommended)**
```
MAKE_FUNCTION (params) .body
STORE function_name
JUMP .end ; Skip over function body
.body:
<function code>
RETURN
.end:
<continue with program>
```
**Pattern 2: Function bodies after HALT**
```
MAKE_FUNCTION (params) .body
STORE function_name
<use the function>
HALT ; Stop before function bodies
.body:
<function code>
RETURN
```
Pattern 1 is required for:
- Defining multiple functions before using them
- REPL mode
- Any case where execution continues after defining a function
Pattern 2 only works if you HALT before reaching function bodies.
### REPL Mode (Incremental Execution)
For building REPLs (like the Shrimp REPL), use `vm.continue()` and `vm.appendBytecode()`:
```typescript
const vm = new VM(toBytecode([]), natives)
await vm.run() // Initialize (empty bytecode)
// User enters: x = 42
const line1 = compileLine("x = 42") // No HALT!
vm.appendBytecode(line1)
await vm.continue() // Execute only line 1
// User enters: x + 10
const line2 = compileLine("x + 10") // No HALT!
vm.appendBytecode(line2)
await vm.continue() // Execute only line 2, result is 52
```
**Key points**:
- `vm.run()` resets PC to 0 (re-executes everything) - use for initial setup only
- `vm.continue()` resumes from current PC (executes only new bytecode)
- `vm.appendBytecode(bytecode)` properly handles constant index remapping
- Don't use HALT in REPL lines - let VM stop naturally
- Scope and variables persist across all lines
- Side effects only run once
## TypeScript Configuration ## TypeScript Configuration
- Import alias: `#reef` maps to `./src/index.ts` - Import alias: `#reef` maps to `./src/index.ts`
@ -486,7 +365,7 @@ Run `bun test` to verify all tests pass before committing.
## Common Gotchas ## Common Gotchas
**Label requirements**: JUMP/JUMP_IF_FALSE/JUMP_IF_TRUE require label operands (`.label`), not numeric offsets. The bytecode compiler resolves labels to PC-relative offsets internally. PUSH_TRY/PUSH_FINALLY can use either labels or absolute instruction indices (`#N`). **Jump offsets**: JUMP/JUMP_IF_FALSE/JUMP_IF_TRUE use relative offsets from the next instruction (PC + 1). PUSH_TRY/PUSH_FINALLY use absolute instruction indices.
**Stack operations**: Most binary operations pop in reverse order (second operand is popped first, then first operand). **Stack operations**: Most binary operations pop in reverse order (second operand is popped first, then first operand).

340
GUIDE.md
View File

@ -179,25 +179,12 @@ PUSH 1 ; Named count
CALL CALL
``` ```
**Null triggers defaults**: Pass `null` to use default values:
```
; Function: greet(name='Guest', msg='Hello')
LOAD greet
PUSH null ; Use default for 'name'
PUSH "Hi" ; Provide 'msg'
PUSH 2
PUSH 0
CALL ; → "Hi, Guest"
```
## Opcodes ## Opcodes
### Stack ### Stack
- `PUSH <const>` - Push constant - `PUSH <const>` - Push constant
- `POP` - Remove top - `POP` - Remove top
- `DUP` - Duplicate top - `DUP` - Duplicate top
- `SWAP` - Swap top two values
- `TYPE` - Pop value, push its type as string
### Variables ### Variables
- `LOAD <name>` - Push variable value (throws if not found) - `LOAD <name>` - Push variable value (throws if not found)
@ -207,10 +194,6 @@ CALL ; → "Hi, Guest"
### Arithmetic ### Arithmetic
- `ADD`, `SUB`, `MUL`, `DIV`, `MOD` - Binary ops (pop 2, push result) - `ADD`, `SUB`, `MUL`, `DIV`, `MOD` - Binary ops (pop 2, push result)
### Bitwise
- `BIT_AND`, `BIT_OR`, `BIT_XOR` - Bitwise logical ops (pop 2, push result)
- `BIT_SHL`, `BIT_SHR`, `BIT_USHR` - Bitwise shift ops (pop 2, push result)
### Comparison ### Comparison
- `EQ`, `NEQ`, `LT`, `GT`, `LTE`, `GTE` - Pop 2, push boolean - `EQ`, `NEQ`, `LT`, `GT`, `LTE`, `GTE` - Pop 2, push boolean
@ -258,40 +241,6 @@ CALL ; → "Hi, Guest"
## Compiler Patterns ## Compiler Patterns
### Function Definitions
When defining functions, you must prevent the PC from "falling through" into the function body during sequential execution. There are two standard patterns:
**Pattern 1: JUMP over function bodies (Recommended)**
```
MAKE_FUNCTION (params) .body
STORE function_name
JUMP .end ; Skip over function body
.body:
<function code>
RETURN
.end:
<continue with program>
```
**Pattern 2: Function bodies after HALT**
```
MAKE_FUNCTION (params) .body
STORE function_name
<use the function>
HALT ; Stop execution before function bodies
.body:
<function code>
RETURN
```
**Important**: Pattern 2 only works if you HALT before reaching function bodies. Pattern 1 is more flexible and required for:
- Defining multiple functions before using them
- REPL mode (incremental execution)
- Any case where execution continues after defining a function
**Why?** `MAKE_FUNCTION` creates a function value but doesn't jump to the body—it just stores the body's address. Without JUMP or HALT, the PC increments into the function body and executes it as top-level code.
### If-Else ### If-Else
``` ```
<condition> <condition>
@ -377,125 +326,6 @@ POP
.end: ; Result on stack .end: ; Result on stack
``` ```
### Reversing Operand Order
Use SWAP to reverse operand order for non-commutative operations:
```
; Compute 10 / 2 when values are in reverse order
PUSH 2
PUSH 10
SWAP ; Now: [10, 2]
DIV ; 10 / 2 = 5
```
```
; Compute "hello" - "world" (subtraction with strings coerced to numbers)
PUSH "world"
PUSH "hello"
SWAP ; Now: ["hello", "world"]
SUB ; Result based on operand order
```
**Common Use Cases**:
- Division and subtraction when operands are in wrong order
- String concatenation with specific order
- Preparing arguments for functions that care about position
### Bitwise Operations
All bitwise operations work with 32-bit signed integers:
```
; Bitwise AND (masking)
PUSH 5
PUSH 3
BIT_AND ; → 1 (0101 & 0011 = 0001)
; Bitwise OR (combining flags)
PUSH 5
PUSH 3
BIT_OR ; → 7 (0101 | 0011 = 0111)
; Bitwise XOR (toggling bits)
PUSH 5
PUSH 3
BIT_XOR ; → 6 (0101 ^ 0011 = 0110)
; Left shift (multiply by power of 2)
PUSH 5
PUSH 2
BIT_SHL ; → 20 (5 << 2 = 5 * 4)
; Arithmetic right shift (divide by power of 2, preserves sign)
PUSH 20
PUSH 2
BIT_SHR ; → 5 (20 >> 2 = 20 / 4)
PUSH -20
PUSH 2
BIT_SHR ; → -5 (sign preserved)
; Logical right shift (zero-fill)
PUSH -1
PUSH 1
BIT_USHR ; → 2147483647 (unsigned shift)
```
**Common Use Cases**:
- Flags and bit masks: `flags band MASK` to test, `flags bor FLAG` to set
- Fast multiplication/division by powers of 2
- Color manipulation: extract RGB components
- Low-level bit manipulation for protocols or file formats
### Runtime Type Checking (TYPE)
Get the type of a value as a string for runtime introspection:
```
; Basic type check
PUSH 42
TYPE ; → "number"
PUSH "hello"
TYPE ; → "string"
MAKE_ARRAY #3
TYPE ; → "array"
```
**Type Guard Pattern** (check type before operation):
```
; Safe addition - only add if both are numbers
LOAD x
DUP
TYPE
PUSH "number"
EQ
JUMP_IF_FALSE .not_number
LOAD y
DUP
TYPE
PUSH "number"
EQ
JUMP_IF_FALSE .cleanup_not_number
ADD ; Safe to add
JUMP .end
.cleanup_not_number:
POP ; Remove y
.not_number:
POP ; Remove x
PUSH null
.end:
```
**Common Use Cases**:
- Type validation before operations
- Polymorphic functions that handle multiple types
- Debugging and introspection
- Dynamic dispatch in DSLs
- Safe coercion with fallbacks
### Try-Catch ### Try-Catch
``` ```
PUSH_TRY .catch PUSH_TRY .catch
@ -532,8 +362,7 @@ Functions automatically capture current scope:
PUSH 0 PUSH 0
STORE counter STORE counter
MAKE_FUNCTION () .increment MAKE_FUNCTION () .increment
STORE increment_fn RETURN
JUMP .main
.increment: .increment:
LOAD counter ; Captured variable LOAD counter ; Captured variable
@ -542,18 +371,6 @@ JUMP .main
STORE counter STORE counter
LOAD counter LOAD counter
RETURN RETURN
.main:
LOAD increment_fn
PUSH 0
PUSH 0
CALL ; Returns 1
POP
LOAD increment_fn
PUSH 0
PUSH 0
CALL ; Returns 2 (counter persists!)
HALT
``` ```
### Tail Recursion ### Tail Recursion
@ -561,7 +378,7 @@ Use TAIL_CALL instead of CALL for last call:
``` ```
MAKE_FUNCTION (n acc) .factorial MAKE_FUNCTION (n acc) .factorial
STORE factorial STORE factorial
JUMP .main <...>
.factorial: .factorial:
LOAD n LOAD n
@ -581,15 +398,6 @@ JUMP .main
PUSH 2 PUSH 2
PUSH 0 PUSH 0
TAIL_CALL ; Reuses stack frame TAIL_CALL ; Reuses stack frame
.main:
LOAD factorial
PUSH 5
PUSH 1
PUSH 2
PUSH 0
CALL ; factorial(5, 1) = 120
HALT
``` ```
### Optional Function Calls (TRY_CALL) ### Optional Function Calls (TRY_CALL)
@ -733,8 +541,6 @@ Only `null` and `false` are falsy. Everything else (including `0`, `""`, empty a
**Arithmetic ops** (ADD, SUB, MUL, DIV, MOD) coerce both operands to numbers. **Arithmetic ops** (ADD, SUB, MUL, DIV, MOD) coerce both operands to numbers.
**Bitwise ops** (BIT_AND, BIT_OR, BIT_XOR, BIT_SHL, BIT_SHR, BIT_USHR) coerce both operands to 32-bit signed integers.
**Comparison ops** (LT, GT, LTE, GTE) coerce both operands to numbers. **Comparison ops** (LT, GT, LTE, GTE) coerce both operands to numbers.
**Equality ops** (EQ, NEQ) use type-aware comparison with deep equality for arrays/dicts. **Equality ops** (EQ, NEQ) use type-aware comparison with deep equality for arrays/dicts.
@ -759,24 +565,11 @@ Variable and function parameter names support Unicode and emoji:
### Parameter Binding Priority ### Parameter Binding Priority
For function calls, parameters bound in order: For function calls, parameters bound in order:
1. Named argument (if provided and matches param name) 1. Positional argument (if provided)
2. Positional argument (if provided) 2. Named argument (if provided and matches param name)
3. Default value (if defined) 3. Default value (if defined)
4. Null 4. Null
**Null Triggering Defaults**: Passing `null` as an argument (positional or named) triggers the default value if one exists. This allows callers to explicitly "opt-in" to defaults:
```
# Function with defaults: greet(name='Guest', greeting='Hello')
LOAD greet
PUSH null # Triggers default: name='Guest'
PUSH 'Hi' # Provided: greeting='Hi'
PUSH 2
PUSH 0
CALL # Returns "Hi, Guest"
```
This works for both ReefVM functions and native TypeScript functions. If no default exists, `null` is bound as-is.
### Exception Handlers ### Exception Handlers
- PUSH_TRY uses absolute addresses for catch blocks - PUSH_TRY uses absolute addresses for catch blocks
- Nested try blocks form a stack - Nested try blocks form a stack
@ -813,18 +606,18 @@ const vm = new VM(bytecode, { add, greet })
**Method 2**: Register after construction **Method 2**: Register after construction
```typescript ```typescript
const vm = new VM(bytecode) const vm = new VM(bytecode)
vm.set('add', (a: number, b: number) => a + b) vm.registerFunction('add', (a: number, b: number) => a + b)
await vm.run() await vm.run()
``` ```
**Method 3**: Value-based functions (for full control) **Method 3**: Value-based functions (for full control)
```typescript ```typescript
vm.setValueFunction('customOp', (a: Value, b: Value): Value => { vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
return { type: 'number', value: toNumber(a) + toNumber(b) } return { type: 'number', value: toNumber(a) + toNumber(b) }
}) })
``` ```
**Auto-wrapping**: `vm.set()` automatically converts between native TypeScript types and ReefVM Value types. Both sync and async functions work. **Auto-wrapping**: `registerFunction` automatically converts between native TypeScript types and ReefVM Value types. Both sync and async functions work.
**Usage in bytecode**: **Usage in bytecode**:
``` ```
@ -853,12 +646,12 @@ CALL ; → "Hi, Alice!"
```typescript ```typescript
// Basic @named - collects all named args // Basic @named - collects all named args
vm.set('greet', (atNamed: any = {}) => { vm.registerFunction('greet', (atNamed: any = {}) => {
return `Hello, ${atNamed.name || 'World'}!` return `Hello, ${atNamed.name || 'World'}!`
}) })
// Mixed positional and @named // Mixed positional and @named
vm.set('configure', (name: string, atOptions: any = {}) => { vm.registerFunction('configure', (name: string, atOptions: any = {}) => {
return { return {
name, name,
debug: atOptions.debug || false, debug: atOptions.debug || false,
@ -883,121 +676,6 @@ CALL ; atOptions receives {debug: true, port: 8080}
Named arguments that match fixed parameter names are bound to those parameters. Remaining unmatched named arguments are collected into the `atXxx` parameter as a plain JavaScript object. Named arguments that match fixed parameter names are bound to those parameters. Remaining unmatched named arguments are collected into the `atXxx` parameter as a plain JavaScript object.
### Calling Functions from TypeScript
You can call both Reef and native functions from TypeScript using `vm.call()`:
```typescript
const bytecode = toBytecode(`
MAKE_FUNCTION (name greeting="Hello") .greet
STORE greet
HALT
.greet:
LOAD greeting
PUSH " "
LOAD name
PUSH "!"
STR_CONCAT #4
RETURN
`)
const vm = new VM(bytecode, {
log: (msg: string) => console.log(msg) // Native function
})
await vm.run()
// Call Reef function with positional arguments
const result1 = await vm.call('greet', 'Alice')
// Returns: "Hello Alice!"
// Call Reef function with named arguments (pass as final object)
const result2 = await vm.call('greet', 'Bob', { greeting: 'Hi' })
// Returns: "Hi Bob!"
// Call Reef function with only named arguments
const result3 = await vm.call('greet', { name: 'Carol', greeting: 'Hey' })
// Returns: "Hey Carol!"
// Call native function
await vm.call('log', 'Hello from TypeScript!')
```
**How it works**:
- `vm.call(functionName, ...args)` looks up the function (Reef or native) in the VM's scope
- For Reef functions: converts to callable JavaScript function
- For native functions: calls directly
- Arguments are automatically converted to ReefVM Values
- Returns the result (automatically converted back to JavaScript types)
**Named arguments**: Pass a plain object as the final argument to provide named arguments. If the last argument is a non-array object, it's treated as named arguments. All preceding arguments are treated as positional.
**Type conversion**: Arguments and return values are automatically converted between JavaScript types and ReefVM Values:
- Primitives: `number`, `string`, `boolean`, `null`
- Arrays: converted recursively
- Objects: converted to ReefVM dicts
- Functions: Reef functions are converted to callable JavaScript functions
### REPL Mode (Incremental Compilation)
ReefVM supports incremental bytecode execution for building REPLs. This allows you to execute code line-by-line while preserving scope and avoiding re-execution of side effects.
**The Problem**: By default, `vm.run()` resets the program counter (PC) to 0, re-executing all previous bytecode. This makes it impossible to implement a REPL where each line executes only once.
**The Solution**: Use `vm.continue()` to resume execution from where you left off:
```typescript
// Line 1: Define variable
const line1 = toBytecode([
["PUSH", 42],
["STORE", "x"]
])
const vm = new VM(line1)
await vm.run() // Execute first line
// Line 2: Use the variable
const line2 = toBytecode([
["LOAD", "x"],
["PUSH", 10],
["ADD"]
])
vm.appendBytecode(line2) // Append new bytecode with proper constant remapping
await vm.continue() // Execute ONLY the new bytecode
// Result: 52 (42 + 10)
// The first line never re-executed!
```
**Key methods**:
- `vm.run()`: Resets PC to 0 and runs from the beginning (normal execution)
- `vm.continue()`: Continues from current PC (REPL mode)
- `vm.appendBytecode(bytecode)`: Helper that properly appends bytecode with constant index remapping
**Important**: Don't use `HALT` in REPL mode! The VM naturally stops when it runs out of instructions. Using `HALT` sets `vm.stopped = true`, which prevents `continue()` from resuming.
**Example REPL pattern**:
```typescript
const vm = new VM(toBytecode([]), { /* native functions */ })
while (true) {
const input = await getUserInput() // Get next line from user
const bytecode = compileLine(input) // Compile to bytecode (no HALT!)
vm.appendBytecode(bytecode) // Append to VM
const result = await vm.continue() // Execute only the new code
console.log(fromValue(result)) // Show result to user
}
```
This pattern ensures:
- Variables persist between lines
- Side effects (like `echo` or function calls) only run once
- Previous bytecode never re-executes
- Scope accumulates across all lines
### Empty Stack ### Empty Stack
- RETURN with empty stack returns null - RETURN with empty stack returns null
- HALT with empty stack returns null - HALT with empty stack returns null

500
IDEAS.md
View File

@ -1,500 +0,0 @@
# ReefVM Architectural Improvement Ideas
This document contains architectural ideas for improving ReefVM. These focus on enhancing the VM's capabilities through structural improvements rather than just adding new opcodes.
## 1. Scope Resolution Optimization
**Current Issue**: Variable lookups are O(n) through the scope chain on every `LOAD`. This becomes expensive in deeply nested closures.
**Architectural Solution**: Implement **static scope analysis** with **lexical addressing**:
```typescript
// Instead of: LOAD x (runtime scope chain walk)
// Compile to: LOAD_FAST 2 1 (scope depth 2, slot 1 - O(1) lookup)
class Scope {
locals: Map<string, Value>
parent?: Scope
// NEW: Add indexed slots for fast access
slots: Value[] // Direct array access
nameToSlot: Map<string, number> // Compile-time mapping
}
```
**Benefits**:
- O(1) variable access instead of O(n)
- Critical for hot loops and deeply nested functions
- Compiler can still fall back to named lookup for dynamic cases
---
## 2. Module System Architecture
**Current Gap**: No way to organize code across multiple files or create reusable libraries.
**Architectural Solution**: Add first-class module support:
```typescript
// New opcodes: IMPORT, EXPORT, MAKE_MODULE
// New bytecode structure:
type Bytecode = {
instructions: Instruction[]
constants: Constant[]
exports?: Map<string, number> // Exported symbols
imports?: Import[] // Import declarations
}
type Import = {
modulePath: string
symbols: string[] // [] means import all
alias?: string
}
```
**Pattern**:
```
MAKE_MODULE .module_body
EXPORT add
EXPORT subtract
HALT
.module_body:
MAKE_FUNCTION (x y) .add_impl
RETURN
```
**Benefits**:
- Code organization and reusability
- Circular dependency detection at load time
- Natural namespace isolation
- Enables standard library architecture
---
## 3. Source Map Integration
**Current Issue**: Runtime errors show bytecode addresses, not source locations.
**Architectural Solution**: Add source mapping layer:
```typescript
type Bytecode = {
instructions: Instruction[]
constants: Constant[]
sourceMap?: SourceMap // NEW
}
type SourceMap = {
file?: string
mappings: SourceMapping[] // Instruction index → source location
}
type SourceMapping = {
instruction: number
line: number
column: number
source?: string // Original source text
}
```
**Benefits**:
- Meaningful error messages with line/column
- Debugger can show original source
- Stack traces map to source code
- Critical for production debugging
---
## 4. Debugger Hook Architecture
**Current Gap**: No way to pause execution, inspect state, or step through code.
**Architectural Solution**: Add debug event system:
```typescript
class VM {
debugger?: Debugger
async execute(instruction: Instruction) {
// Before execution
await this.debugger?.onInstruction(this.pc, instruction, this)
// Execute
switch (instruction.op) { ... }
// After execution
await this.debugger?.afterInstruction(this.pc, this)
}
}
interface Debugger {
breakpoints: Set<number>
onInstruction(pc: number, instruction: Instruction, vm: VM): Promise<void>
afterInstruction(pc: number, vm: VM): Promise<void>
onCall(fn: Value, args: Value[]): Promise<void>
onReturn(value: Value): Promise<void>
onException(error: Value): Promise<void>
}
```
**Benefits**:
- Step-through debugging
- Breakpoints at any instruction
- State inspection at any point
- Non-invasive (no bytecode modification)
- Can build IDE integrations
---
## 5. Bytecode Optimization Pass Framework
**Current Gap**: Bytecode is emitted directly, no optimization.
**Architectural Solution**: Add optimization pipeline:
```typescript
type Optimizer = (bytecode: Bytecode) => Bytecode
// Framework for composable optimization passes
class BytecodeOptimizer {
passes: Optimizer[] = []
add(pass: Optimizer): this {
this.passes.push(pass)
return this
}
optimize(bytecode: Bytecode): Bytecode {
return this.passes.reduce((bc, pass) => pass(bc), bytecode)
}
}
// Example passes:
const optimizer = new BytecodeOptimizer()
.add(constantFolding) // PUSH 2; PUSH 3; ADD → PUSH 5
.add(deadCodeElimination) // Remove unreachable code after HALT/RETURN
.add(jumpChaining) // JUMP .a → .a: JUMP .b → JUMP .b directly
.add(peepholeOptimization) // DUP; POP → (nothing)
```
**Benefits**:
- Faster execution without changing compiler
- Can add passes without modifying VM
- Composable and testable
- Enables aggressive optimizations (inlining, constant folding, etc.)
---
## 6. Value Memory Management Architecture
**Current Issue**: No tracking of memory usage, no GC hooks, unbounded growth.
**Architectural Solution**: Add memory management layer:
```typescript
class MemoryManager {
allocatedBytes: number = 0
maxBytes?: number
allocateValue(value: Value): Value {
const size = this.sizeOf(value)
if (this.maxBytes && this.allocatedBytes + size > this.maxBytes) {
throw new Error('Out of memory')
}
this.allocatedBytes += size
return value
}
sizeOf(value: Value): number {
// Estimate memory footprint
}
// Hook for custom GC
gc?: () => void
}
class VM {
memory: MemoryManager
// All value-creating operations check memory
push(value: Value) {
this.memory.allocateValue(value)
this.stack.push(value)
}
}
```
**Benefits**:
- Memory limits for sandboxing
- Memory profiling
- Custom GC strategies
- Prevents runaway memory usage
---
## 7. Instruction Profiler Architecture
**Current Gap**: No way to identify performance bottlenecks in bytecode.
**Architectural Solution**: Add instrumentation layer:
```typescript
class Profiler {
instructionCounts: Map<number, number> = new Map()
instructionTime: Map<number, number> = new Map()
hotFunctions: Map<number, FunctionProfile> = new Map()
recordInstruction(pc: number, duration: number) {
this.instructionCounts.set(pc, (this.instructionCounts.get(pc) || 0) + 1)
this.instructionTime.set(pc, (this.instructionTime.get(pc) || 0) + duration)
}
getHotSpots(): HotSpot[] {
// Identify most-executed instructions
}
generateReport(): ProfileReport {
// Human-readable performance report
}
}
class VM {
profiler?: Profiler
async execute(instruction: Instruction) {
const start = performance.now()
// ... execute ...
const duration = performance.now() - start
this.profiler?.recordInstruction(this.pc, duration)
}
}
```
**Benefits**:
- Identify hot loops and functions
- Guide optimization efforts
- Measure impact of changes
- Can feed into JIT compiler (future)
---
## 8. Standard Library Plugin Architecture
**Current Issue**: Native functions registered manually, no standard library structure.
**Architectural Solution**: Module-based native libraries:
```typescript
interface NativeModule {
name: string
exports: Record<string, any>
init?(vm: VM): void
}
class VM {
modules: Map<string, NativeModule> = new Map()
registerModule(module: NativeModule) {
this.modules.set(module.name, module)
module.init?.(this)
// Auto-register exports to global scope
for (const [name, value] of Object.entries(module.exports)) {
this.set(name, value)
}
}
loadModule(name: string): NativeModule {
return this.modules.get(name) || throw new Error(`Module ${name} not found`)
}
}
// Example usage:
const mathModule: NativeModule = {
name: 'math',
exports: {
sin: Math.sin,
cos: Math.cos,
sqrt: Math.sqrt,
PI: Math.PI
}
}
vm.registerModule(mathModule)
```
**Benefits**:
- Organized standard library
- Lazy loading of modules
- Third-party plugin system
- Clear namespace boundaries
---
## 9. Streaming Bytecode Execution
**Current Limitation**: Must load entire bytecode before execution.
**Architectural Solution**: Incremental bytecode loading:
```typescript
class StreamingBytecode {
chunks: BytecodeChunk[] = []
append(chunk: BytecodeChunk) {
// Remap addresses, merge constants
this.chunks.push(chunk)
}
getInstruction(pc: number): Instruction | undefined {
// Resolve across chunks
}
}
class VM {
async runStreaming(stream: ReadableStream<BytecodeChunk>) {
for await (const chunk of stream) {
this.bytecode.append(chunk)
await this.continue() // Execute new chunk
}
}
}
```
**Benefits**:
- Execute before full load (faster startup)
- Network streaming of bytecode
- Incremental compilation
- Better REPL experience
---
## 10. Type Annotation System (Optional Runtime Types)
**Current Gap**: All values dynamically typed, no way to enforce types.
**Architectural Solution**: Optional type metadata:
```typescript
type TypedValue = Value & {
typeAnnotation?: TypeAnnotation
}
type TypeAnnotation =
| { kind: 'number' }
| { kind: 'string' }
| { kind: 'array', elementType?: TypeAnnotation }
| { kind: 'dict', valueType?: TypeAnnotation }
| { kind: 'function', params: TypeAnnotation[], return: TypeAnnotation }
// New opcodes: TYPE_CHECK, TYPE_ASSERT
// Functions can declare parameter types:
MAKE_FUNCTION (x:number y:string) .body
```
**Benefits**:
- Catch type errors earlier
- Self-documenting code
- Enables static analysis tools
- Optional (doesn't break existing code)
- Can enable optimizations (known number type → skip toNumber())
---
## 11. VM State Serialization
**Current Gap**: Can't save/restore VM execution state.
**Architectural Solution**: Serializable VM state:
```typescript
class VM {
serialize(): SerializedState {
return {
instructions: this.instructions,
constants: this.constants,
pc: this.pc,
stack: this.stack.map(serializeValue),
callStack: this.callStack.map(serializeFrame),
scope: serializeScope(this.scope),
handlers: this.handlers
}
}
static deserialize(state: SerializedState): VM {
const vm = new VM(/* ... */)
vm.restore(state)
return vm
}
}
```
**Benefits**:
- Save/restore execution state
- Distributed computing (send state to workers)
- Crash recovery
- Time-travel debugging
- Checkpoint/restart
---
## 12. Async Iterator Support
**Current Gap**: Iterators work via break, but no async iteration.
**Architectural Solution**: First-class async iteration:
```typescript
// New value type:
type Value = ... | { type: 'async_iterator', value: AsyncIterableIterator<Value> }
// New opcodes: MAKE_ASYNC_ITERATOR, AWAIT_NEXT, YIELD_ASYNC
// Pattern:
for_await (item in asyncIterable) {
// Compiles to AWAIT_NEXT loop
}
```
**Benefits**:
- Stream processing
- Async I/O without blocking
- Natural async patterns
- Matches JavaScript async iterators
---
## Priority Recommendations
### Tier 1 (Highest Impact):
1. **Source Map Integration** - Critical for usability
2. **Module System** - Essential for scaling beyond toy programs
3. **Scope Resolution Optimization** - Performance multiplier
### Tier 2 (High Value):
4. **Debugger Hook Architecture** - Developer experience game-changer
5. **Standard Library Plugin Architecture** - Enables ecosystem
6. **Bytecode Optimization Framework** - Performance without complexity
### Tier 3 (Nice to Have):
7. **Instruction Profiler** - Guides future optimization
8. **Memory Management** - Important for production use
9. **VM State Serialization** - Enables advanced use cases
### Tier 4 (Future/Experimental):
10. **Type Annotations** - Optional, doesn't break existing code
11. **Streaming Bytecode** - Mostly useful for large programs
12. **Async Iterators** - Specialized use case
---
## Design Principles
These improvements focus on:
- **Performance** (scope optimization, bytecode optimization)
- **Developer Experience** (source maps, debugger, profiler)
- **Scalability** (modules, standard library architecture)
- **Production Readiness** (memory management, serialization)
All ideas maintain ReefVM's core design philosophy of simplicity, orthogonality, and explicit behavior.

View File

@ -44,14 +44,11 @@ Commands: `clear`, `reset`, `exit`.
- Variadic functions with positional rest parameters (`...rest`) - Variadic functions with positional rest parameters (`...rest`)
- Named arguments (named) that collect unmatched named args into a dict (`@named`) - Named arguments (named) that collect unmatched named args into a dict (`@named`)
- Mixed positional and named arguments with proper priority binding - Mixed positional and named arguments with proper priority binding
- Default parameter values with null-triggering: passing `null` explicitly uses the default value
- Tail call optimization with unbounded recursion (10,000+ iterations without stack overflow) - Tail call optimization with unbounded recursion (10,000+ iterations without stack overflow)
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding - Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
- Native function interop with auto-wrapping for native TypeScript types - Native function interop with auto-wrapping for native TypeScript types
- Native functions stored in scope, called via LOAD + CALL - Native functions stored in scope, called via LOAD + CALL
- Native functions support `atXxx` parameters (e.g., `atOptions`) to collect unmatched named args
- Pass functions directly to `run(bytecode, { fnName: fn })` or `new VM(bytecode, { fnName: fn })` - Pass functions directly to `run(bytecode, { fnName: fn })` or `new VM(bytecode, { fnName: fn })`
- Call Reef functions from TypeScript with `vm.call(name, ...args)` with automatic type conversion
## Design Decisions ## Design Decisions
@ -61,4 +58,3 @@ Commands: `clear`, `reset`, `exit`.
- Variadic parameters: Functions can collect remaining positional arguments into an array using `...rest` syntax - Variadic parameters: Functions can collect remaining positional arguments into an array using `...rest` syntax
- Named parameters: Functions can collect unmatched named arguments into a dict using `@named` syntax - Named parameters: Functions can collect unmatched named arguments into a dict using `@named` syntax
- Argument binding priority: Named args bind to regular params first, with unmatched ones going to `@named` - Argument binding priority: Named args bind to regular params first, with unmatched ones going to `@named`
- Null triggers defaults: Passing `null` to a parameter with a default value explicitly uses that default (applies to both ReefVM and native functions)

196
SPEC.md
View File

@ -138,24 +138,6 @@ type ExceptionHandler = {
**Effect**: Duplicate top of stack **Effect**: Duplicate top of stack
**Stack**: [value] → [value, value] **Stack**: [value] → [value, value]
#### SWAP
**Operand**: None
**Effect**: Swap the top two values on the stack
**Stack**: [value1, value2] → [value2, value1]
#### TYPE
**Operand**: None
**Effect**: Pop value from stack, push its type as a string
**Stack**: [value] → [typeString]
Returns the type of a value as a string.
**Example**:
```
PUSH 42
TYPE ; Pushes "number"
```
### Variable Operations ### Variable Operations
#### LOAD #### LOAD
@ -197,29 +179,7 @@ All arithmetic operations pop two values, perform operation, push result as numb
#### ADD #### ADD
**Stack**: [a, b] → [a + b] **Stack**: [a, b] → [a + b]
**Note**: Only for numbers (use separate string concat if needed)
Performs different operations depending on operand types:
- If either operand is a string, converts both to strings and concatenates
- Else if both operands are arrays, concatenates the arrays
- Else if both operands are dicts, merges them (b's keys overwrite a's keys on conflict)
- Else if both operands are numbers, performs numeric addition
- Otherwise, throws an error
**Examples**:
- `5 + 3``8` (numeric addition)
- `"hello" + " world"``"hello world"` (string concatenation)
- `"count: " + 42``"count: 42"` (string concatenation)
- `100 + " items"``"100 items"` (string concatenation)
- `[1, 2, 3] + [4]``[1, 2, 3, 4]` (array concatenation)
- `[1, 2] + [3, 4]``[1, 2, 3, 4]` (array concatenation)
- `{a: 1} + {b: 2}``{a: 1, b: 2}` (dict merge)
- `{a: 1, b: 2} + {b: 99}``{a: 1, b: 99}` (dict merge, b overwrites)
**Invalid operations** (throw errors):
- `true + false` → Error
- `null + 5` → Error
- `[1] + 5` → Error
- `{a: 1} + 5` → Error
#### SUB #### SUB
**Stack**: [a, b] → [a - b] **Stack**: [a, b] → [a - b]
@ -233,62 +193,6 @@ Performs different operations depending on operand types:
#### MOD #### MOD
**Stack**: [a, b] → [a % b] **Stack**: [a, b] → [a % b]
### Bitwise Operations
All bitwise operations coerce operands to 32-bit signed integers, perform the operation, and push the result as a number.
#### BIT_AND
**Operand**: None
**Stack**: [a, b] → [a & b]
Performs bitwise AND operation. Both operands are coerced to 32-bit signed integers.
**Example**: `5 & 3``1` (binary: `0101 & 0011``0001`)
#### BIT_OR
**Operand**: None
**Stack**: [a, b] → [a | b]
Performs bitwise OR operation. Both operands are coerced to 32-bit signed integers.
**Example**: `5 | 3``7` (binary: `0101 | 0011``0111`)
#### BIT_XOR
**Operand**: None
**Stack**: [a, b] → [a ^ b]
Performs bitwise XOR (exclusive OR) operation. Both operands are coerced to 32-bit signed integers.
**Example**: `5 ^ 3``6` (binary: `0101 ^ 0011``0110`)
#### BIT_SHL
**Operand**: None
**Stack**: [a, b] → [a << b]
Performs left shift operation. Left operand is coerced to 32-bit signed integer, right operand determines shift amount (masked to 0-31).
**Example**: `5 << 2``20` (binary: `0101` shifted left 2 positions → `10100`)
#### BIT_SHR
**Operand**: None
**Stack**: [a, b] → [a >> b]
Performs sign-preserving right shift operation. Left operand is coerced to 32-bit signed integer, right operand determines shift amount (masked to 0-31). The sign bit is preserved (arithmetic shift).
**Example**:
- `20 >> 2``5` (binary: `10100` shifted right 2 positions → `0101`)
- `-20 >> 2``-5` (sign bit preserved)
#### BIT_USHR
**Operand**: None
**Stack**: [a, b] → [a >>> b]
Performs zero-fill right shift operation. Left operand is coerced to 32-bit signed integer, right operand determines shift amount (masked to 0-31). Zeros are shifted in from the left (logical shift).
**Example**:
- `-1 >>> 1``2147483647` (all bits shift right, zero fills from left)
- `-8 >>> 1``2147483644`
### Comparison Operations ### Comparison Operations
All comparison operations pop two values, compare, push boolean result. All comparison operations pop two values, compare, push boolean result.
@ -327,45 +231,39 @@ All comparison operations pop two values, compare, push boolean result.
``` ```
<evaluate left> <evaluate left>
DUP DUP
JUMP_IF_FALSE .end JUMP_IF_FALSE #2 # skip POP and <evaluate right>
POP POP
<evaluate right> <evaluate right>
.end: end:
``` ```
**OR pattern** (short-circuits if left side is true): **OR pattern** (short-circuits if left side is true):
``` ```
<evaluate left> <evaluate left>
DUP DUP
JUMP_IF_TRUE .end JUMP_IF_TRUE #2 # skip POP and <evaluate right>
POP POP
<evaluate right> <evaluate right>
.end: end:
``` ```
### Control Flow ### Control Flow
#### JUMP #### JUMP
**Operand**: Label (string) **Operand**: Offset (number)
**Effect**: Jump to the specified label **Effect**: Add offset to PC (relative jump)
**Stack**: No change **Stack**: No change
**Note**: JUMP only accepts label operands (`.label`), not numeric offsets. The VM resolves labels to relative offsets internally.
#### JUMP_IF_FALSE #### JUMP_IF_FALSE
**Operand**: Label (string) **Operand**: Offset (number)
**Effect**: If top of stack is falsy, jump to the specified label **Effect**: If top of stack is falsy, add offset to PC (relative jump)
**Stack**: [condition] → [] **Stack**: [condition] → []
**Note**: JUMP_IF_FALSE only accepts label operands (`.label`), not numeric offsets.
#### JUMP_IF_TRUE #### JUMP_IF_TRUE
**Operand**: Label (string) **Operand**: Offset (number)
**Effect**: If top of stack is truthy, jump to the specified label **Effect**: If top of stack is truthy, add offset to PC (relative jump)
**Stack**: [condition] → [] **Stack**: [condition] → []
**Note**: JUMP_IF_TRUE only accepts label operands (`.label`), not numeric offsets.
#### BREAK #### BREAK
**Operand**: None **Operand**: None
**Effect**: Unwind call stack until frame with `isBreakTarget = true`, resume there **Effect**: Unwind call stack until frame with `isBreakTarget = true`, resume there
@ -480,19 +378,11 @@ The created function captures `currentScope` as its `parentScope`.
3. Default value (if defined) 3. Default value (if defined)
4. Null 4. Null
**Null Value Semantics**:
- Passing `null` as an argument explicitly triggers the default value (if one exists)
- This allows callers to "opt-in" to defaults even when providing arguments positionally
- If no default exists, `null` is bound as-is
- This applies to both ReefVM functions and native TypeScript functions
- Example: `fn(null, 20)` where `fn(x=10, y)` binds `x=10` (default triggered), `y=20`
**Named Args Handling**: **Named Args Handling**:
- Named args that match fixed parameter names are bound to those params - Named args that match fixed parameter names are bound to those params
- If the function has `named: true`, remaining named args (that don't match any fixed param) are collected into the last parameter as a dict - If the function has `named: true`, remaining named args (that don't match any fixed param) are collected into the last parameter as a dict
- This allows flexible calling: `fn(x=10, y=20, extra=30)` where `extra` goes to the named args dict - This allows flexible calling: `fn(x=10, y=20, extra=30)` where `extra` goes to the named args dict
- **Native functions support named arguments** - parameter names are extracted from the function signature at call time - **Native functions support named arguments** - parameter names are extracted from the function signature at call time
- Passing `null` via named args also triggers defaults: `fn(x=null)` triggers `x`'s default
**Errors**: Throws if top of stack is not a function (or native function) **Errors**: Throws if top of stack is not a function (or native function)
@ -732,7 +622,7 @@ const vm = new VM(bytecode, {
}) })
// Or after construction: // Or after construction:
vm.set('multiply', (a: number, b: number) => a * b) vm.registerFunction('multiply', (a: number, b: number) => a * b)
``` ```
**Usage in Bytecode**: **Usage in Bytecode**:
@ -747,9 +637,9 @@ CALL ; Call it like any other function
**Native Function Types**: **Native Function Types**:
1. **Auto-wrapped functions** (via `vm.set()`): Accept and return native TypeScript types (number, string, boolean, array, object, etc.). The VM automatically converts between Value types and native types. 1. **Auto-wrapped functions** (via `registerFunction`): Accept and return native TypeScript types (number, string, boolean, array, object, etc.). The VM automatically converts between Value types and native types.
2. **Value-based functions** (via `vm.setValueFunction()`): Accept and return `Value` types directly for full control over type handling. 2. **Value-based functions** (via `registerValueFunction`): Accept and return `Value` types directly for full control over type handling.
**Auto-Wrapping Behavior**: **Auto-Wrapping Behavior**:
- Parameters: `Value` → native type (number, string, boolean, array, object, null, RegExp) - Parameters: `Value` → native type (number, string, boolean, array, object, null, RegExp)
@ -765,27 +655,27 @@ CALL ; Call it like any other function
**Examples**: **Examples**:
```typescript ```typescript
// Auto-wrapped native types // Auto-wrapped native types
vm.set('add', (a: number, b: number) => a + b) vm.registerFunction('add', (a: number, b: number) => a + b)
vm.set('greet', (name: string) => `Hello, ${name}!`) vm.registerFunction('greet', (name: string) => `Hello, ${name}!`)
vm.set('range', (n: number) => Array.from({ length: n }, (_, i) => i)) vm.registerFunction('range', (n: number) => Array.from({ length: n }, (_, i) => i))
// With defaults // With defaults
vm.set('greet', (name: string, greeting = 'Hello') => { vm.registerFunction('greet', (name: string, greeting = 'Hello') => {
return `${greeting}, ${name}!` return `${greeting}, ${name}!`
}) })
// Variadic functions // Variadic functions
vm.set('sum', (...nums: number[]) => { vm.registerFunction('sum', (...nums: number[]) => {
return nums.reduce((acc, n) => acc + n, 0) return nums.reduce((acc, n) => acc + n, 0)
}) })
// Value-based for custom logic // Value-based for custom logic
vm.setValueFunction('customOp', (a: Value, b: Value): Value => { vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
return { type: 'number', value: toNumber(a) + toNumber(b) } return { type: 'number', value: toNumber(a) + toNumber(b) }
}) })
// Async functions // Async functions
vm.set('fetchData', async (url: string) => { vm.registerFunction('fetchData', async (url: string) => {
const response = await fetch(url) const response = await fetch(url)
return response.json() return response.json()
}) })
@ -820,16 +710,14 @@ CALL ; → "Hi, Bob!"
## Label Syntax ## Label Syntax
The bytecode format requires labels for control flow jumps: The bytecode format supports labels for improved readability:
**Label Definition**: `.label_name:` marks an instruction position **Label Definition**: `.label_name:` marks an instruction position
**Label Reference**: `.label_name` in operands (e.g., `JUMP .loop_start`) **Label Reference**: `.label_name` in operands (e.g., `JUMP .loop_start`)
Labels are resolved to relative PC offsets during bytecode compilation. All JUMP instructions (`JUMP`, `JUMP_IF_FALSE`, `JUMP_IF_TRUE`) require label operands. Labels are resolved to numeric offsets during parsing. The original numeric offset syntax (`#N`) is still supported for backwards compatibility.
**Note**: Exception handling instructions (`PUSH_TRY`, `PUSH_FINALLY`) and function definitions (`MAKE_FUNCTION`) can use either labels or absolute instruction indices (`#N`). Example with labels:
Example:
``` ```
JUMP .skip JUMP .skip
.middle: .middle:
@ -840,6 +728,15 @@ JUMP .skip
HALT HALT
``` ```
Equivalent with numeric offsets:
```
JUMP #2
PUSH 999
HALT
PUSH 42
HALT
```
## Common Bytecode Patterns ## Common Bytecode Patterns
### If-Else Statement ### If-Else Statement
@ -915,29 +812,6 @@ PUSH 1 # namedCount
CALL CALL
``` ```
### Null Triggering Default Values
```
# Function: greet(name='Guest', greeting='Hello')
MAKE_FUNCTION (name='Guest' greeting='Hello') .greet_body
STORE 'greet'
JUMP .main
.greet_body:
LOAD 'greeting'
PUSH ', '
ADD
LOAD 'name'
ADD
RETURN
.main:
# Call with null for first param - triggers default
LOAD 'greet'
PUSH null # name will use default 'Guest'
PUSH 'Hi' # greeting='Hi' (provided)
PUSH 2 # positionalCount
PUSH 0 # namedCount
CALL # Returns "Hi, Guest"
```
### Tail Recursive Function ### Tail Recursive Function
``` ```
MAKE_FUNCTION (n acc) .factorial_body MAKE_FUNCTION (n acc) .factorial_body
@ -1038,10 +912,10 @@ const vm = new VM(bytecode, {
}) })
// Or register after construction // Or register after construction
vm.set('multiply', (a: number, b: number) => a * b) vm.registerFunction('multiply', (a: number, b: number) => a * b)
// Or use Value-based functions // Or use Value-based functions
vm.setValueFunction('customOp', (a: Value, b: Value): Value => { vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
return { type: 'number', value: toNumber(a) + toNumber(b) } return { type: 'number', value: toNumber(a) + toNumber(b) }
}) })

View File

@ -104,8 +104,6 @@ function formatValue(value: Value): string {
} else if (value.type === 'function') { } else if (value.type === 'function') {
const params = value.params.join(', ') const params = value.params.join(', ')
return `${colors.dim}<fn(${params})>${colors.reset}` return `${colors.dim}<fn(${params})>${colors.reset}`
} else if (value.type === 'native') {
return `${colors.dim}<native>${colors.reset}`
} }
return String(value) return String(value)
} }

View File

@ -38,8 +38,6 @@ function formatValue(value: Value): string {
} else if (value.type === 'function') { } else if (value.type === 'function') {
const params = value.params.join(', ') const params = value.params.join(', ')
return `${colors.dim}<fn(${params})>${colors.reset}` return `${colors.dim}<fn(${params})>${colors.reset}`
} else if (value.type === 'native') {
return `${colors.dim}<native>${colors.reset}`
} }
return String(value) return String(value)
} }

View File

@ -1,2 +0,0 @@
[test]
preload = ["./tests/setup.ts"]

View File

@ -1,116 +0,0 @@
/**
* Demonstrates the ADD opcode working with arrays
*
* ADD now handles array concatenation:
* - [1, 2, 3] + [4] === [1, 2, 3, 4]
* - If both operands are arrays, they are concatenated
*/
import { toBytecode, run } from "#reef"
// Basic array concatenation
const basicConcat = toBytecode([
["PUSH", 1],
["PUSH", 2],
["PUSH", 3],
["MAKE_ARRAY", 3],
["PUSH", 4],
["MAKE_ARRAY", 1],
["ADD"],
["HALT"]
])
console.log('Basic array concatenation ([1, 2, 3] + [4]):')
const result1 = await run(basicConcat)
console.log(result1)
// Output: { type: 'array', value: [1, 2, 3, 4] }
// Concatenate two multi-element arrays
const multiConcat = toBytecode([
["PUSH", 1],
["PUSH", 2],
["MAKE_ARRAY", 2],
["PUSH", 3],
["PUSH", 4],
["MAKE_ARRAY", 2],
["ADD"],
["HALT"]
])
console.log('\nConcatenate two arrays ([1, 2] + [3, 4]):')
const result2 = await run(multiConcat)
console.log(result2)
// Output: { type: 'array', value: [1, 2, 3, 4] }
// Concatenate multiple arrays in sequence
const multipleConcat = toBytecode([
["PUSH", 1],
["MAKE_ARRAY", 1],
["PUSH", 2],
["MAKE_ARRAY", 1],
["ADD"],
["PUSH", 3],
["MAKE_ARRAY", 1],
["ADD"],
["PUSH", 4],
["MAKE_ARRAY", 1],
["ADD"],
["HALT"]
])
console.log('\nMultiple concatenations ([1] + [2] + [3] + [4]):')
const result3 = await run(multipleConcat)
console.log(result3)
// Output: { type: 'array', value: [1, 2, 3, 4] }
// Concatenate arrays with mixed types
const mixedTypes = toBytecode([
["PUSH", 1],
["PUSH", "hello"],
["MAKE_ARRAY", 2],
["PUSH", true],
["PUSH", null],
["MAKE_ARRAY", 2],
["ADD"],
["HALT"]
])
console.log('\nConcatenate arrays with mixed types ([1, "hello"] + [true, null]):')
const result4 = await run(mixedTypes)
console.log(result4)
// Output: { type: 'array', value: [1, "hello", true, null] }
// Concatenate empty array with non-empty
const emptyConcat = toBytecode([
["MAKE_ARRAY", 0],
["PUSH", 1],
["PUSH", 2],
["PUSH", 3],
["MAKE_ARRAY", 3],
["ADD"],
["HALT"]
])
console.log('\nConcatenate empty array with [1, 2, 3] ([] + [1, 2, 3]):')
const result5 = await run(emptyConcat)
console.log(result5)
// Output: { type: 'array', value: [1, 2, 3] }
// Nested arrays
const nestedConcat = toBytecode([
["PUSH", 1],
["PUSH", 2],
["MAKE_ARRAY", 2],
["MAKE_ARRAY", 1],
["PUSH", 3],
["PUSH", 4],
["MAKE_ARRAY", 2],
["MAKE_ARRAY", 1],
["ADD"],
["HALT"]
])
console.log('\nConcatenate nested arrays ([[1, 2]] + [[3, 4]]):')
const result6 = await run(nestedConcat)
console.log(result6)
// Output: { type: 'array', value: [[1, 2], [3, 4]] }

View File

@ -1,158 +0,0 @@
/**
* Demonstrates the ADD opcode working with dicts
*
* ADD now handles dict merging:
* - {a: 1} + {b: 2} === {a: 1, b: 2}
* - If both operands are dicts, they are merged
* - Keys from the second dict overwrite keys from the first on conflict
*/
import { toBytecode, run } from "#reef"
// Basic dict merge
const basicMerge = toBytecode([
["PUSH", "a"],
["PUSH", 1],
["MAKE_DICT", 1],
["PUSH", "b"],
["PUSH", 2],
["MAKE_DICT", 1],
["ADD"],
["HALT"]
])
console.log('Basic dict merge ({a: 1} + {b: 2}):')
const result1 = await run(basicMerge)
console.log(result1)
// Output: { type: 'dict', value: Map { a: 1, b: 2 } }
// Merge with overlapping keys
const overlapMerge = toBytecode([
["PUSH", "a"],
["PUSH", 1],
["PUSH", "b"],
["PUSH", 2],
["MAKE_DICT", 2],
["PUSH", "b"],
["PUSH", 99],
["PUSH", "c"],
["PUSH", 3],
["MAKE_DICT", 2],
["ADD"],
["HALT"]
])
console.log('\nMerge with overlapping keys ({a: 1, b: 2} + {b: 99, c: 3}):')
const result2 = await run(overlapMerge)
console.log(result2)
console.log('Note: b is overwritten from 2 to 99')
// Output: { type: 'dict', value: Map { a: 1, b: 99, c: 3 } }
// Merge multiple dicts in sequence
const multipleMerge = toBytecode([
["PUSH", "a"],
["PUSH", 1],
["MAKE_DICT", 1],
["PUSH", "b"],
["PUSH", 2],
["MAKE_DICT", 1],
["ADD"],
["PUSH", "c"],
["PUSH", 3],
["MAKE_DICT", 1],
["ADD"],
["PUSH", "d"],
["PUSH", 4],
["MAKE_DICT", 1],
["ADD"],
["HALT"]
])
console.log('\nMultiple merges ({a: 1} + {b: 2} + {c: 3} + {d: 4}):')
const result3 = await run(multipleMerge)
console.log(result3)
// Output: { type: 'dict', value: Map { a: 1, b: 2, c: 3, d: 4 } }
// Merge dicts with different value types
const mixedTypes = toBytecode([
["PUSH", "num"],
["PUSH", 42],
["PUSH", "str"],
["PUSH", "hello"],
["MAKE_DICT", 2],
["PUSH", "bool"],
["PUSH", true],
["PUSH", "null"],
["PUSH", null],
["MAKE_DICT", 2],
["ADD"],
["HALT"]
])
console.log('\nMerge dicts with different types ({num: 42, str: "hello"} + {bool: true, null: null}):')
const result4 = await run(mixedTypes)
console.log(result4)
// Output: { type: 'dict', value: Map { num: 42, str: 'hello', bool: true, null: null } }
// Merge empty dict with non-empty
const emptyMerge = toBytecode([
["MAKE_DICT", 0],
["PUSH", "x"],
["PUSH", 100],
["PUSH", "y"],
["PUSH", 200],
["MAKE_DICT", 2],
["ADD"],
["HALT"]
])
console.log('\nMerge empty dict with {x: 100, y: 200} ({} + {x: 100, y: 200}):')
const result5 = await run(emptyMerge)
console.log(result5)
// Output: { type: 'dict', value: Map { x: 100, y: 200 } }
// Merge dicts with nested structures
const nestedMerge = toBytecode([
["PUSH", "data"],
["PUSH", 1],
["PUSH", 2],
["MAKE_ARRAY", 2],
["MAKE_DICT", 1],
["PUSH", "config"],
["PUSH", "debug"],
["PUSH", true],
["MAKE_DICT", 1],
["MAKE_DICT", 1],
["ADD"],
["HALT"]
])
console.log('\nMerge dicts with nested structures:')
const result6 = await run(nestedMerge)
console.log(result6)
// Output: { type: 'dict', value: Map { data: [1, 2], config: { debug: true } } }
// Building configuration objects
const configBuild = toBytecode([
// Default config
["PUSH", "debug"],
["PUSH", false],
["PUSH", "port"],
["PUSH", 3000],
["PUSH", "host"],
["PUSH", "localhost"],
["MAKE_DICT", 3],
// Override with user config
["PUSH", "debug"],
["PUSH", true],
["PUSH", "port"],
["PUSH", 8080],
["MAKE_DICT", 2],
["ADD"],
["HALT"]
])
console.log('\nBuilding config (defaults + overrides):')
const result7 = await run(configBuild)
console.log(result7)
// Output: { type: 'dict', value: Map { debug: true, port: 8080, host: 'localhost' } }

View File

@ -1,73 +0,0 @@
/**
* Demonstrates the ADD opcode working with both numbers and strings
*
* ADD now behaves like JavaScript's + operator:
* - If either operand is a string, it does string concatenation
* - Otherwise, it does numeric addition
*/
import { toBytecode, run } from "#reef"
// Numeric addition
const numericAdd = toBytecode(`
PUSH 10
PUSH 5
ADD
HALT
`)
console.log('Numeric addition (10 + 5):')
console.log(await run(numericAdd))
// Output: { type: 'number', value: 15 }
// String concatenation
const stringConcat = toBytecode(`
PUSH "hello"
PUSH " world"
ADD
HALT
`)
console.log('\nString concatenation ("hello" + " world"):')
console.log(await run(stringConcat))
// Output: { type: 'string', value: 'hello world' }
// Mixed: string + number
const mixedConcat = toBytecode(`
PUSH "count: "
PUSH 42
ADD
HALT
`)
console.log('\nMixed concatenation ("count: " + 42):')
console.log(await run(mixedConcat))
// Output: { type: 'string', value: 'count: 42' }
// Building a message
const buildMessage = toBytecode(`
PUSH "You have "
PUSH 3
ADD
PUSH " new messages"
ADD
HALT
`)
console.log('\nBuilding a message:')
console.log(await run(buildMessage))
// Output: { type: 'string', value: 'You have 3 new messages' }
// Computing then concatenating
const computeAndConcat = toBytecode(`
PUSH "Result: "
PUSH 10
PUSH 5
ADD
ADD
HALT
`)
console.log('\nComputing then concatenating ("Result: " + (10 + 5)):')
console.log(await run(computeAndConcat))
// Output: { type: 'string', value: 'Result: 15' }

View File

@ -9,7 +9,7 @@ const bytecode = toBytecode(`
const vm = new VM(bytecode) const vm = new VM(bytecode)
vm.set('print', (...args: Value[]): Value => { vm.registerFunction('print', (...args: Value[]): Value => {
console.log(...args.map(toString)) console.log(...args.map(toString))
return toNull() return toNull()
}) })

View File

@ -16,15 +16,13 @@ export type Constant =
| Value | Value
| FunctionDef | FunctionDef
type Atom = RegExp | number | string | boolean | null type Atom = number | string | boolean | null
type InstructionTuple = type InstructionTuple =
// Stack // Stack
| ["PUSH", Atom] | ["PUSH", Atom]
| ["POP"] | ["POP"]
| ["DUP"] | ["DUP"]
| ["SWAP"]
| ["TYPE"]
// Variables // Variables
| ["LOAD", string] | ["LOAD", string]
@ -34,9 +32,6 @@ type InstructionTuple =
// Arithmetic // Arithmetic
| ["ADD"] | ["SUB"] | ["MUL"] | ["DIV"] | ["MOD"] | ["ADD"] | ["SUB"] | ["MUL"] | ["DIV"] | ["MOD"]
// Bitwise
| ["BIT_AND"] | ["BIT_OR"] | ["BIT_XOR"] | ["BIT_SHL"] | ["BIT_SHR"] | ["BIT_USHR"]
// Comparison // Comparison
| ["EQ"] | ["NEQ"] | ["LT"] | ["GT"] | ["LTE"] | ["GTE"] | ["EQ"] | ["NEQ"] | ["LT"] | ["GT"] | ["LTE"] | ["GTE"]
@ -44,9 +39,9 @@ type InstructionTuple =
| ["NOT"] | ["NOT"]
// Control flow // Control flow
| ["JUMP", string] | ["JUMP", string | number]
| ["JUMP_IF_FALSE", string] | ["JUMP_IF_FALSE", string | number]
| ["JUMP_IF_TRUE", string] | ["JUMP_IF_TRUE", string | number]
| ["BREAK"] | ["BREAK"]
// Exception handling // Exception handling
@ -56,7 +51,7 @@ type InstructionTuple =
| ["THROW"] | ["THROW"]
// Functions // Functions
| ["MAKE_FUNCTION", string[], string] | ["MAKE_FUNCTION", string[], string | number]
| ["CALL"] | ["CALL"]
| ["TAIL_CALL"] | ["TAIL_CALL"]
| ["RETURN"] | ["RETURN"]
@ -88,6 +83,30 @@ type LabelDefinition = [string] // Just ".label_name:"
export type ProgramItem = InstructionTuple | LabelDefinition export type ProgramItem = InstructionTuple | LabelDefinition
//
// Parse bytecode from human-readable string format.
// Operand types are determined by prefix/literal:
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
// .label -> label reference (e.g., JUMP .loop_start, MAKE_FUNCTION (x y) .body)
// name -> variable/function name (e.g., LOAD x, LOAD_NATIVE add)
// 42 -> number constant (e.g., PUSH 42)
// "str" -> string constant (e.g., PUSH "hello")
// 'str' -> string constant (e.g., PUSH 'hello')
// true -> boolean constant (e.g., PUSH true)
// false -> boolean constant (e.g., PUSH false)
// null -> null constant (e.g., PUSH null)
//
// Labels:
// .label_name: -> label definition (marks current instruction position)
//
// Function definitions:
// MAKE_FUNCTION (x y) #7 -> basic function (numeric offset)
// MAKE_FUNCTION (x y) .body -> basic function (label reference)
// MAKE_FUNCTION (x y=42) #7 -> with defaults
// MAKE_FUNCTION (x ...rest) #7 -> variadic
// MAKE_FUNCTION (x @named) #7 -> named
//
function parseFunctionParams(paramStr: string, constants: Constant[]): { function parseFunctionParams(paramStr: string, constants: Constant[]): {
params: string[] params: string[]
defaults: Record<string, number> defaults: Record<string, number>
@ -349,29 +368,6 @@ function toBytecodeFromArray(program: ProgramItem[]): Bytecode /* throws */ {
} }
} }
////
// Parse bytecode from human-readable string format.
// Operand types are determined by prefix/literal:
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
// .label -> label reference (e.g., JUMP .loop_start, MAKE_FUNCTION (x y) .body)
// name -> variable/function name (e.g., LOAD x, LOAD_NATIVE add)
// 42 -> number constant (e.g., PUSH 42)
// "str" -> string constant (e.g., PUSH "hello")
// 'str' -> string constant (e.g., PUSH 'hello')
// true -> boolean constant (e.g., PUSH true)
// false -> boolean constant (e.g., PUSH false)
// null -> null constant (e.g., PUSH null)
//
// Labels:
// .label_name: -> label definition (marks current instruction position)
//
// Function definitions:
// MAKE_FUNCTION (x y) #7 -> basic function (numeric offset)
// MAKE_FUNCTION (x y) .body -> basic function (label reference)
// MAKE_FUNCTION (x y=42) #7 -> with defaults
// MAKE_FUNCTION (x ...rest) #7 -> variadic
// MAKE_FUNCTION (x @named) #7 -> named
function toBytecodeFromString(str: string): Bytecode /* throws */ { function toBytecodeFromString(str: string): Bytecode /* throws */ {
const lines = str.trim().split("\n") const lines = str.trim().split("\n")
@ -390,7 +386,7 @@ function toBytecodeFromString(str: string): Bytecode /* throws */ {
if (!trimmed) continue if (!trimmed) continue
// Check for label definition (.label_name:) // Check for label definition (.label_name:)
if (/^\.[a-zA-Z_][a-zA-Z0-9_.]*:$/.test(trimmed)) { if (/^\.[a-zA-Z_][a-zA-Z0-9_]*:$/.test(trimmed)) {
const labelName = trimmed.slice(1, -1) const labelName = trimmed.slice(1, -1)
labels.set(labelName, cleanLines.length) labels.set(labelName, cleanLines.length)
continue continue

View File

@ -1,5 +1,4 @@
import { type Value, type NativeFunction, fromValue, toValue } from "./value" import { type Value, type NativeFunction, fromValue, toValue } from "./value"
import { VM } from "./vm"
export type ParamInfo = { export type ParamInfo = {
params: string[] params: string[]
@ -10,13 +9,11 @@ export type ParamInfo = {
const WRAPPED_MARKER = Symbol('reef-wrapped') const WRAPPED_MARKER = Symbol('reef-wrapped')
export function wrapNative(vm: VM, fn: Function): (this: VM, ...args: Value[]) => Promise<Value> { export function wrapNative(fn: Function): (...args: Value[]) => Promise<Value> {
if ((fn as any).raw) return fn as (this: VM, ...args: Value[]) => Promise<Value> const wrapped = async (...values: Value[]) => {
const nativeArgs = values.map(fromValue)
const wrapped = async function (this: VM, ...values: Value[]) { const result = await fn(...nativeArgs)
const nativeArgs = values.map(arg => fromValue(arg, vm)) return toValue(result)
const result = await fn.call(this, ...nativeArgs)
return toValue(result, this)
} }
const wrappedObj = wrapped as any const wrappedObj = wrapped as any

View File

@ -1,17 +1,14 @@
import type { Bytecode } from "./bytecode" import type { Bytecode } from "./bytecode"
import type { Value } from "./value" import { type Value } from "./value"
import { VM } from "./vm" import { VM } from "./vm"
export async function run(bytecode: Bytecode, globals?: Record<string, any>): Promise<Value> { export async function run(bytecode: Bytecode, functions?: Record<string, Function>): Promise<Value> {
const vm = new VM(bytecode, globals) const vm = new VM(bytecode, functions)
return await vm.run() return await vm.run()
} }
export { type Bytecode, toBytecode, type ProgramItem } from "./bytecode" export { type Bytecode, toBytecode, type ProgramItem } from "./bytecode"
export { bytecodeToString } from "./format" export { wrapNative } from "./function"
export { wrapNative, isWrapped, type ParamInfo, extractParamInfo, getOriginalFunction } from "./function" export { type Value, toValue, toString, toNumber, fromValue, toNull } from "./value"
export { OpCode } from "./opcode"
export { Scope } from "./scope"
export type { Value, TypeScriptFunction, NativeFunction } from "./value"
export { isValue, toValue, toString, toNumber, fromValue, toNull, fnFromValue } from "./value"
export { VM } from "./vm" export { VM } from "./vm"
export { bytecodeToString } from "./format"

View File

@ -3,16 +3,12 @@ export enum OpCode {
PUSH, // operand: constant index (number) | stack: [] → [value] PUSH, // operand: constant index (number) | stack: [] → [value]
POP, // operand: none | stack: [value] → [] POP, // operand: none | stack: [value] → []
DUP, // operand: none | stack: [value] → [value, value] DUP, // operand: none | stack: [value] → [value, value]
SWAP, // operand: none | stack: [value1, value2] → [value2, value1]
// 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 TRY_LOAD, // operand: variable name (identifier) | stack: [] → [value] | load a variable (if found) or a string
// information
TYPE, // operand: none | stack: [a] → []
// 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]
SUB, // operand: none | stack: [a, b] → [a - b] SUB, // operand: none | stack: [a, b] → [a - b]
@ -20,14 +16,6 @@ export enum OpCode {
DIV, // operand: none | stack: [a, b] → [a / b] DIV, // operand: none | stack: [a, b] → [a / b]
MOD, // operand: none | stack: [a, b] → [a % b] MOD, // operand: none | stack: [a, b] → [a % b]
// bitwise operations (coerce to 32-bit integers, pop 2, push result)
BIT_AND, // operand: none | stack: [a, b] → [a & b]
BIT_OR, // operand: none | stack: [a, b] → [a | b]
BIT_XOR, // operand: none | stack: [a, b] → [a ^ b]
BIT_SHL, // operand: none | stack: [a, b] → [a << b]
BIT_SHR, // operand: none | stack: [a, b] → [a >> b] (sign-preserving)
BIT_USHR, // operand: none | stack: [a, b] → [a >>> b] (zero-fill)
// comparison (pop 2, push boolean) // comparison (pop 2, push boolean)
EQ, // operand: none | stack: [a, b] → [a == b] (deep equality) EQ, // operand: none | stack: [a, b] → [a == b] (deep equality)
NEQ, // operand: none | stack: [a, b] → [a != b] NEQ, // operand: none | stack: [a, b] → [a != b]

View File

@ -30,15 +30,6 @@ export class Scope {
has(name: string): boolean { has(name: string): boolean {
return this.locals.has(name) || this.parent?.has(name) || false return this.locals.has(name) || this.parent?.has(name) || false
} }
vars(): string[] {
const vars = new Set(this.parent?.vars())
for (const name of this.locals.keys())
vars.add(name)
return [...vars].sort()
}
} }

View File

@ -50,19 +50,11 @@ const OPCODES_WITH_OPERANDS = new Set([
const OPCODES_WITHOUT_OPERANDS = new Set([ const OPCODES_WITHOUT_OPERANDS = new Set([
OpCode.POP, OpCode.POP,
OpCode.DUP, OpCode.DUP,
OpCode.SWAP,
OpCode.TYPE,
OpCode.ADD, OpCode.ADD,
OpCode.SUB, OpCode.SUB,
OpCode.MUL, OpCode.MUL,
OpCode.DIV, OpCode.DIV,
OpCode.MOD, OpCode.MOD,
OpCode.BIT_AND,
OpCode.BIT_OR,
OpCode.BIT_XOR,
OpCode.BIT_SHL,
OpCode.BIT_SHR,
OpCode.BIT_USHR,
OpCode.EQ, OpCode.EQ,
OpCode.NEQ, OpCode.NEQ,
OpCode.LT, OpCode.LT,
@ -87,15 +79,11 @@ const OPCODES_WITHOUT_OPERANDS = new Set([
OpCode.DOT_GET, OpCode.DOT_GET,
]) ])
// JUMP* instructions require labels only (no numeric immediates) // immediate = immediate number, eg #5
const OPCODES_REQUIRING_LABEL = new Set([ const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
OpCode.JUMP, OpCode.JUMP,
OpCode.JUMP_IF_FALSE, OpCode.JUMP_IF_FALSE,
OpCode.JUMP_IF_TRUE, OpCode.JUMP_IF_TRUE,
])
// PUSH_TRY/PUSH_FINALLY still allow immediate or label
const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
OpCode.PUSH_TRY, OpCode.PUSH_TRY,
OpCode.PUSH_FINALLY, OpCode.PUSH_FINALLY,
]) ])
@ -201,16 +189,6 @@ export function validateBytecode(source: string): ValidationResult {
// Validate specific operand formats // Validate specific operand formats
if (operand) { if (operand) {
if (OPCODES_REQUIRING_LABEL.has(opCode)) {
if (!operand.startsWith('.')) {
errors.push({
line: lineNum,
message: `${opName} requires label (.label), got: ${operand}`,
})
continue
}
}
if (OPCODES_REQUIRING_IMMEDIATE_OR_LABEL.has(opCode)) { if (OPCODES_REQUIRING_IMMEDIATE_OR_LABEL.has(opCode)) {
if (!operand.startsWith('#') && !operand.startsWith('.')) { if (!operand.startsWith('#') && !operand.startsWith('.')) {
errors.push({ errors.push({
@ -324,11 +302,11 @@ export function validateBytecode(source: string): ValidationResult {
} }
} }
// Validate body address (must be a label) // Validate body address
if (!bodyAddr!.startsWith('.')) { if (!bodyAddr!.startsWith('.') && !bodyAddr!.startsWith('#')) {
errors.push({ errors.push({
line: lineNum, line: lineNum,
message: `Invalid body address: expected .label, got: ${bodyAddr}`, message: `Invalid body address: expected .label or #offset`,
}) })
} }

View File

@ -1,12 +1,6 @@
import { wrapNative, getOriginalFunction } from "./function"
import { OpCode } from "./opcode"
import { Scope } from "./scope" import { Scope } from "./scope"
import { VM } from "./vm"
export type NativeFunction = (...args: Value[]) => Promise<Value> | Value export type NativeFunction = (...args: Value[]) => Promise<Value> | Value
export type TypeScriptFunction = (this: VM, ...args: any[]) => any
const REEF_FUNCTION = Symbol('__reefFunction')
const VALUE_TYPES = new Set(['null', 'boolean', 'number', 'string', 'array', 'dict', 'regex', 'native', 'function'])
export type Value = export type Value =
| { type: 'null', value: null } | { type: 'null', value: null }
@ -39,19 +33,15 @@ export type FunctionDef = {
named: boolean named: boolean
} }
export function isValue(v: any): boolean { export function toValue(v: any): Value /* throws */ {
return !!(v && typeof v === 'object' && VALUE_TYPES.has(v.type) && 'value' in v)
}
export function toValue(v: any, vm?: VM): Value /* throws */ {
if (v === null || v === undefined) if (v === null || v === undefined)
return { type: 'null', value: null } return { type: 'null', value: null }
if (isValue(v)) if (v && typeof v === 'object' && 'type' in v && 'value' in v)
return v as Value return v as Value
if (Array.isArray(v)) if (Array.isArray(v))
return { type: 'array', value: v.map(x => toValue(x, vm)) } return { type: 'array', value: v.map(toValue) }
if (v instanceof RegExp) if (v instanceof RegExp)
return { type: 'regex', value: v } return { type: 'regex', value: v }
@ -64,38 +54,15 @@ export function toValue(v: any, vm?: VM): Value /* throws */ {
case 'string': case 'string':
return { type: 'string', value: v } return { type: 'string', value: v }
case 'function': case 'function':
if ((v as any)[REEF_FUNCTION]) throw "can't toValue() a js function yet"
return (v as any)[REEF_FUNCTION]
let fn = vm ? wrapNative(vm, v) : cantCallFunctionWithoutVM(v)
return { type: 'native', fn, value: '<function>' }
case 'object': case 'object':
const dict: Dict = new Map() const dict: Dict = new Map()
for (const key of Object.keys(v)) dict.set(key, toValue(v[key], vm)) for (const key of Object.keys(v)) dict.set(key, toValue(v[key]))
return { type: 'dict', value: dict } return { type: 'dict', value: dict }
default: default:
throw new Error(`can't toValue this: ${v}`) throw `can't toValue this: ${v}`
}
}
function cantCallFunctionWithoutVM(fn: Function) {
const name = fn.name || '<anonymous>'
const str = fn.toString().slice(0, 100)
return (...args: Value[]) => {
const stack = new Error().stack || ''
const stackLines = stack.split('\n')
.slice(1)
.filter(line => !line.includes('toValue'))
.map(line => ' ' + line.trim())
.join('\n')
throw new Error(
`can't call function that was converted without a vm\n` +
` Function: ${name}\n` +
` Source: ${str}${str.length > 100 ? '...' : ''}\n` +
` Called from:\n${stackLines}`
)
} }
} }
@ -187,87 +154,28 @@ export function isEqual(a: Value, b: Value): boolean {
} }
} }
export function fromValue(v: Value, vm?: VM): any { export function fromValue(v: Value): any {
switch (v.type) { switch (v.type) {
case 'null': case 'null':
return null
case 'boolean': case 'boolean':
return v.value
case 'number': case 'number':
return v.value
case 'string': case 'string':
return v.value return v.value
case 'array': case 'array':
return v.value.map(x => fromValue(x, vm)) return v.value.map(fromValue)
case 'dict': case 'dict':
return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v, vm)])) return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v)]))
case 'regex': case 'regex':
return v.value return v.value
case 'function': case 'function':
if (!vm || !(vm instanceof VM)) {
const stack = new Error().stack || ''
const stackLines = stack.split('\n')
.slice(1)
.filter(line => !line.includes('fromValue'))
.map(line => ' ' + line.trim())
.join('\n')
throw new Error(
`VM is required for function conversion\n` +
` Function params: [${v.params.join(', ')}]\n` +
` Function body at instruction: ${v.body}\n` +
` Called from:\n${stackLines}`
)
}
return fnFromValue(v, vm)
case 'native': case 'native':
return getOriginalFunction(v.fn) return '<function>'
} }
} }
export function toNull(): Value { export function toNull(): Value {
return toValue(null) return toValue(null)
} }
export function fnFromValue(fn: Value, vm: VM): Function {
if (fn.type !== 'function')
throw new Error('Value is not a function')
const wrapper = async function (...args: any[]) {
let positional: any[] = args
let named: Record<string, any> = {}
if (args.length > 0 && !Array.isArray(args[args.length - 1]) && args[args.length - 1].constructor === Object) {
named = args[args.length - 1]
positional = args.slice(0, -1)
}
const newVM = new VM({
instructions: vm.instructions,
constants: vm.constants,
labels: vm.labels
})
newVM.scope = fn.parentScope
newVM.stack.push(fn)
newVM.stack.push(...positional.map(x => toValue(x, vm)))
for (const [key, val] of Object.entries(named)) {
newVM.stack.push(toValue(key))
newVM.stack.push(toValue(val, vm))
}
newVM.stack.push(toValue(positional.length))
newVM.stack.push(toValue(Object.keys(named).length))
const targetDepth = newVM.callStack.length
await newVM.execute({ op: OpCode.CALL })
newVM.pc++
while (newVM.callStack.length > targetDepth && newVM.pc < newVM.instructions.length) {
await newVM.execute(newVM.instructions[newVM.pc]!)
newVM.pc++
}
return fromValue(newVM.stack.pop() || toNull(), vm)
}
// support roundtrips, eg fromValue(toValue(fn))
; (wrapper as any)[REEF_FUNCTION] = fn
return wrapper
}

275
src/vm.ts
View File

@ -3,9 +3,8 @@ import type { ExceptionHandler } from "./exception"
import { type Frame } from "./frame" import { type Frame } from "./frame"
import { OpCode } from "./opcode" import { OpCode } from "./opcode"
import { Scope } from "./scope" import { Scope } from "./scope"
import type { Value, NativeFunction, TypeScriptFunction } from "./value" import { type Value, type NativeFunction, toValue, toNumber, isTrue, isEqual, toString, fromValue } from "./value"
import { toValue, toNumber, isTrue, isEqual, toString, fromValue, fnFromValue } from "./value" import { extractParamInfo, wrapNative, isWrapped, getOriginalFunction } from "./function"
import { extractParamInfo, getOriginalFunction } from "./function"
export class VM { export class VM {
pc = 0 pc = 0
@ -19,71 +18,26 @@ export class VM {
labels: Map<number, string> = new Map() labels: Map<number, string> = new Map()
nativeFunctions: Map<string, NativeFunction> = new Map() nativeFunctions: Map<string, NativeFunction> = new Map()
constructor(bytecode: Bytecode, globals?: Record<string, any>) { constructor(bytecode: Bytecode, functions?: Record<string, Function>) {
this.instructions = bytecode.instructions this.instructions = bytecode.instructions
this.constants = bytecode.constants this.constants = bytecode.constants
this.labels = bytecode.labels || new Map() this.labels = bytecode.labels || new Map()
this.scope = new Scope() this.scope = new Scope()
if (globals) { if (functions)
for (const name of Object.keys(globals ?? {})) for (const name of Object.keys(functions))
this.set(name, globals[name]) this.registerFunction(name, functions[name]!)
this.scope = new Scope(this.scope)
}
} }
async call(name: string, ...args: any) { registerFunction(name: string, fn: Function) {
const value = this.scope.get(name) const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(fn)
this.scope.set(name, { type: 'native', fn: wrapped, value: '<function>' })
if (!value) throw new Error(`Can't find ${name}`)
if (value.type !== 'function' && value.type !== 'native')
throw new Error(`Can't call ${name}`)
if (value.type === 'native') {
return await this.callNative(value.fn, args)
} else {
const fn = fnFromValue(value, this)
return await fn(...args)
}
} }
has(name: string): boolean { registerValueFunction(name: string, fn: NativeFunction) {
return this.scope.has(name)
}
vars(): string[] {
return this.scope.vars()
}
get(name: string) {
return this.scope.get(name)
}
set(name: string, value: any) {
this.scope.set(name, toValue(value, this))
}
setFunction(name: string, fn: TypeScriptFunction) {
this.scope.set(name, toValue(fn, this))
}
setValueFunction(name: string, fn: NativeFunction) {
this.scope.set(name, { type: 'native', fn, value: '<function>' }) this.scope.set(name, { type: 'native', fn, value: '<function>' })
} }
pushScope(locals?: Record<string, any>) {
this.scope = new Scope(this.scope)
if (locals)
for (const [name, value] of Object.entries(locals))
this.set(name, value)
}
popScope() {
this.scope = this.scope.parent!
}
async run(): Promise<Value> { async run(): Promise<Value> {
this.pc = 0 this.pc = 0
this.stopped = false this.stopped = false
@ -97,57 +51,6 @@ export class VM {
return this.stack[this.stack.length - 1] || toValue(null) return this.stack[this.stack.length - 1] || toValue(null)
} }
// Resume execution from current PC without resetting
// Useful for REPL mode where you append bytecode incrementally
async continue(): Promise<Value> {
this.stopped = false
while (!this.stopped && this.pc < this.instructions.length) {
const instruction = this.instructions[this.pc]!
await this.execute(instruction)
this.pc++
}
return this.stack[this.stack.length - 1] || toValue(null)
}
// Helper for REPL mode: append new bytecode with proper constant index remapping
appendBytecode(bytecode: Bytecode): void {
const constantOffset = this.constants.length
const instructionOffset = this.instructions.length
// Remap function body addresses in constants before adding them
for (const constant of bytecode.constants) {
if (constant.type === 'function_def') {
this.constants.push({ ...constant, body: constant.body + instructionOffset })
} else {
this.constants.push(constant)
}
}
for (const instruction of bytecode.instructions) {
if (instruction.operand !== undefined && typeof instruction.operand === 'number') {
// Opcodes that reference constants need their operand adjusted
if (instruction.op === OpCode.PUSH || instruction.op === OpCode.MAKE_FUNCTION) {
this.instructions.push({
op: instruction.op,
operand: instruction.operand + constantOffset
})
} else {
this.instructions.push(instruction)
}
} else {
this.instructions.push(instruction)
}
}
if (bytecode.labels) {
for (const [addr, label] of bytecode.labels.entries()) {
this.labels.set(addr + instructionOffset, label)
}
}
}
async execute(instruction: Instruction) /* throws */ { async execute(instruction: Instruction) /* throws */ {
switch (instruction.op) { switch (instruction.op) {
case OpCode.PUSH: case OpCode.PUSH:
@ -168,32 +71,8 @@ export class VM {
this.stack.push(this.stack[this.stack.length - 1]!) this.stack.push(this.stack[this.stack.length - 1]!)
break break
case OpCode.SWAP:
const first = this.stack.pop()!
const second = this.stack.pop()!
this.stack.push(first)
this.stack.push(second)
break
case OpCode.ADD: case OpCode.ADD:
const b = this.stack.pop()! this.binaryOp((a, b) => toNumber(a) + toNumber(b))
const a = this.stack.pop()!
if (a.type === 'string' || b.type === 'string') {
this.stack.push(toValue(toString(a) + toString(b)))
} else if (a.type === 'array' && b.type === 'array') {
this.stack.push({ type: 'array', value: [...a.value, ...b.value] })
} else if (a.type === 'dict' && b.type === 'dict') {
const merged = new Map(a.value)
for (const [key, value] of b.value) {
merged.set(key, value)
}
this.stack.push({ type: 'dict', value: merged })
} else if (a.type === 'number' && b.type === 'number') {
this.stack.push(toValue(a.value + b.value))
} else {
throw new Error(`ADD: Cannot add ${a.type} and ${b.type}`)
}
break break
case OpCode.SUB: case OpCode.SUB:
@ -241,31 +120,6 @@ export class VM {
this.stack.push({ type: 'boolean', value: !isTrue(val) }) this.stack.push({ type: 'boolean', value: !isTrue(val) })
break break
// Bitwise operations
case OpCode.BIT_AND:
this.binaryOp((a, b) => (toNumber(a) | 0) & (toNumber(b) | 0))
break
case OpCode.BIT_OR:
this.binaryOp((a, b) => (toNumber(a) | 0) | (toNumber(b) | 0))
break
case OpCode.BIT_XOR:
this.binaryOp((a, b) => (toNumber(a) | 0) ^ (toNumber(b) | 0))
break
case OpCode.BIT_SHL:
this.binaryOp((a, b) => (toNumber(a) | 0) << (toNumber(b) | 0))
break
case OpCode.BIT_SHR:
this.binaryOp((a, b) => (toNumber(a) | 0) >> (toNumber(b) | 0))
break
case OpCode.BIT_USHR:
this.binaryOp((a, b) => (toNumber(a) | 0) >>> (toNumber(b) | 0))
break
case OpCode.HALT: case OpCode.HALT:
this.stopped = true this.stopped = true
break break
@ -286,18 +140,13 @@ export class VM {
const value = this.scope.get(varName) const value = this.scope.get(varName)
if (value === undefined) if (value === undefined)
this.stack.push(toValue(varName, this)) this.stack.push(toValue(varName))
else else
this.stack.push(value) this.stack.push(value)
break break
} }
case OpCode.TYPE:
const value = this.stack.pop()!
this.stack.push(toValue(value.type))
break
case OpCode.STORE: case OpCode.STORE:
const name = instruction.operand as string const name = instruction.operand as string
const toStore = this.stack.pop()! const toStore = this.stack.pop()!
@ -493,10 +342,10 @@ export class VM {
const target = this.stack.pop()! const target = this.stack.pop()!
if (target.type === 'array') if (target.type === 'array')
this.stack.push(toValue(target.value?.[Number(index.value)], this)) this.stack.push(toValue(target.value?.[Number(index.value)]))
else if (target.type === 'dict') else if (target.type === 'dict')
this.stack.push(toValue(target.value?.get(String(index.value)), this)) this.stack.push(toValue(target.value?.get(String(index.value))))
else else
throw new Error(`DOT_GET: ${target.type} not supported`) throw new Error(`DOT_GET: ${target.type} not supported`)
@ -602,25 +451,16 @@ export class VM {
let nativePositionalArgIndex = 0 let nativePositionalArgIndex = 0
// Bind fixed parameters using priority: named arg > positional arg > default > null // Bind fixed parameters using priority: named arg > positional arg > default > null
// Note: null values trigger defaults (null acts as "use default")
for (let i = 0; i < nativeFixedParamCount; i++) { for (let i = 0; i < nativeFixedParamCount; i++) {
const paramName = paramInfo.params[i]! const paramName = paramInfo.params[i]!
let paramValue: Value | undefined
// Check if named argument was provided for this param // Check if named argument was provided for this param
if (namedArgs.has(paramName)) { if (namedArgs.has(paramName)) {
paramValue = namedArgs.get(paramName)! nativeArgs.push(namedArgs.get(paramName)!)
namedArgs.delete(paramName) // Remove from named args so it won't go to @named namedArgs.delete(paramName) // Remove from named args so it won't go to @named
} else if (nativePositionalArgIndex < positionalArgs.length) { } else if (nativePositionalArgIndex < positionalArgs.length) {
paramValue = positionalArgs[nativePositionalArgIndex]! nativeArgs.push(positionalArgs[nativePositionalArgIndex]!)
nativePositionalArgIndex++ nativePositionalArgIndex++
}
// If the parameter value is null and a default exists, use the default
if (paramValue?.type === 'null' && paramInfo.defaults[paramName] !== undefined) {
nativeArgs.push(paramInfo.defaults[paramName]!)
} else if (paramValue) {
nativeArgs.push(paramValue)
} else if (paramInfo.defaults[paramName] !== undefined) { } else if (paramInfo.defaults[paramName] !== undefined) {
nativeArgs.push(paramInfo.defaults[paramName]!) nativeArgs.push(paramInfo.defaults[paramName]!)
} else { } else {
@ -636,8 +476,8 @@ export class VM {
namedDict.set(key, value) namedDict.set(key, value)
} }
// Convert dict to plain JavaScript object for the native function // Convert dict to plain JavaScript object for the native function
const namedObj = fromValue({ type: 'dict', value: namedDict }, this) const namedObj = fromValue({ type: 'dict', value: namedDict })
nativeArgs.push(toValue(namedObj, this)) nativeArgs.push(toValue(namedObj))
} }
// Handle variadic parameter (TypeScript rest parameters) // Handle variadic parameter (TypeScript rest parameters)
@ -649,41 +489,13 @@ export class VM {
} }
// Call the native function with bound args // Call the native function with bound args
try { const result = await fn.fn(...nativeArgs)
const result = await fn.fn.call(this, ...nativeArgs) this.stack.push(result)
this.stack.push(result) break
break
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const errorValue = toValue(errorMessage)
// no exception handlers, let it crash
if (this.exceptionHandlers.length === 0) {
throw new Error(errorMessage)
}
// use existing THROW logic
const throwHandler = this.exceptionHandlers.pop()!
while (this.callStack.length > throwHandler.callStackDepth)
this.callStack.pop()
this.scope = throwHandler.scope
this.stack.push(errorValue)
// Jump to `finally` if present, otherwise jump to `catch`
const targetAddress = throwHandler.finallyAddress !== undefined
? throwHandler.finallyAddress
: throwHandler.catchAddress
// subtract 1 because pc will be incremented
this.pc = targetAddress - 1
break
}
} }
if (fn.type !== 'function') if (fn.type !== 'function')
throw new Error(`CALL: ${fn.value} is not a function`) throw new Error('CALL: not a function')
if (this.callStack.length > 0) if (this.callStack.length > 0)
this.callStack[this.callStack.length - 1]!.isBreakTarget = true this.callStack[this.callStack.length - 1]!.isBreakTarget = true
@ -705,29 +517,16 @@ export class VM {
let positionalArgIndex = 0 let positionalArgIndex = 0
// Bind fixed parameters using priority: named arg > positional arg > default > null // Bind fixed parameters using priority: named arg > positional arg > default > null
// Note: null values trigger defaults (null acts as "use default")
for (let i = 0; i < fixedParamCount; i++) { for (let i = 0; i < fixedParamCount; i++) {
const paramName = fn.params[i]! const paramName = fn.params[i]!
let paramValue: Value | undefined
// Check if named argument was provided for this param // Check if named argument was provided for this param
if (namedArgs.has(paramName)) { if (namedArgs.has(paramName)) {
paramValue = namedArgs.get(paramName)! this.scope.set(paramName, namedArgs.get(paramName)!)
namedArgs.delete(paramName) // Remove from named args so it won't go to named namedArgs.delete(paramName) // Remove from named args so it won't go to named
} else if (positionalArgIndex < positionalArgs.length) { } else if (positionalArgIndex < positionalArgs.length) {
paramValue = positionalArgs[positionalArgIndex]! this.scope.set(paramName, positionalArgs[positionalArgIndex]!)
positionalArgIndex++ positionalArgIndex++
}
// If the parameter value is null and a default exists, use the default
if (paramValue && paramValue.type === 'null' && fn.defaults[paramName] !== undefined) {
const defaultIdx = fn.defaults[paramName]!
const defaultValue = this.constants[defaultIdx]!
if (defaultValue.type === 'function_def')
throw new Error('Default value cannot be a function definition')
this.scope.set(paramName, defaultValue)
} else if (paramValue) {
this.scope.set(paramName, paramValue)
} else if (fn.defaults[paramName] !== undefined) { } else if (fn.defaults[paramName] !== undefined) {
const defaultIdx = fn.defaults[paramName]! const defaultIdx = fn.defaults[paramName]!
const defaultValue = this.constants[defaultIdx]! const defaultValue = this.constants[defaultIdx]!
@ -788,7 +587,7 @@ export class VM {
const tailFn = this.stack.pop()! const tailFn = this.stack.pop()!
if (tailFn.type !== 'function') if (tailFn.type !== 'function')
throw new Error(`TAIL_CALL: ${tailFn.value} is not a function`) throw new Error('TAIL_CALL: not a function')
this.scope = new Scope(tailFn.parentScope) this.scope = new Scope(tailFn.parentScope)
@ -857,7 +656,7 @@ export class VM {
break break
default: default:
throw new Error(`Unknown op: ${instruction.op}`) throw `Unknown op: ${instruction.op}`
} }
} }
@ -874,26 +673,4 @@ export class VM {
const result = fn(a, b) const result = fn(a, b)
this.stack.push({ type: 'boolean', value: result }) this.stack.push({ type: 'boolean', value: result })
} }
async callNative(nativeFn: NativeFunction, args: any[]): Promise<Value> {
const originalFn = getOriginalFunction(nativeFn)
const lastArg = args[args.length - 1]
if (lastArg && !Array.isArray(lastArg) && typeof lastArg === 'object') {
const paramInfo = extractParamInfo(originalFn)
const positional = args.slice(0, -1)
const named = lastArg
args = [...positional]
for (let i = positional.length; i < paramInfo.params.length; i++) {
const paramName = paramInfo.params[i]!
if (named[paramName] !== undefined) {
args[i] = named[paramName]
}
}
}
const result = await originalFn.call(this, ...args)
return toValue(result, this)
}
} }

View File

@ -1,107 +0,0 @@
import { expect, describe, test } from 'bun:test'
import { toBytecode, run } from '#reef'
describe('bitwise operations', () => {
test('BIT_AND', async () => {
const bytecode = toBytecode([
["PUSH", 5], ["PUSH", 3], ["BIT_AND"], ["HALT"]
])
await expect(bytecode).toBeNumber(1)
})
test('BIT_AND with zero', async () => {
const bytecode = toBytecode([
["PUSH", 5], ["PUSH", 0], ["BIT_AND"], ["HALT"]
])
await expect(bytecode).toBeNumber(0)
})
test('BIT_OR', async () => {
const bytecode = toBytecode([
["PUSH", 5], ["PUSH", 3], ["BIT_OR"], ["HALT"]
])
await expect(bytecode).toBeNumber(7)
})
test('BIT_OR with zero', async () => {
const bytecode = toBytecode([
["PUSH", 5], ["PUSH", 0], ["BIT_OR"], ["HALT"]
])
await expect(bytecode).toBeNumber(5)
})
test('BIT_XOR', async () => {
const bytecode = toBytecode([
["PUSH", 5], ["PUSH", 3], ["BIT_XOR"], ["HALT"]
])
await expect(bytecode).toBeNumber(6)
})
test('BIT_XOR with itself returns zero', async () => {
const bytecode = toBytecode([
["PUSH", 5], ["PUSH", 5], ["BIT_XOR"], ["HALT"]
])
await expect(bytecode).toBeNumber(0)
})
test('BIT_SHL', async () => {
const bytecode = toBytecode([
["PUSH", 5], ["PUSH", 2], ["BIT_SHL"], ["HALT"]
])
await expect(bytecode).toBeNumber(20)
})
test('BIT_SHL by zero', async () => {
const bytecode = toBytecode([
["PUSH", 5], ["PUSH", 0], ["BIT_SHL"], ["HALT"]
])
await expect(bytecode).toBeNumber(5)
})
test('BIT_SHR', async () => {
const bytecode = toBytecode([
["PUSH", 20], ["PUSH", 2], ["BIT_SHR"], ["HALT"]
])
await expect(bytecode).toBeNumber(5)
})
test('BIT_SHR preserves sign for negative numbers', async () => {
const bytecode = toBytecode([
["PUSH", -20], ["PUSH", 2], ["BIT_SHR"], ["HALT"]
])
await expect(bytecode).toBeNumber(-5)
})
test('BIT_USHR', async () => {
const bytecode = toBytecode([
["PUSH", -1], ["PUSH", 1], ["BIT_USHR"], ["HALT"]
])
await expect(bytecode).toBeNumber(2147483647)
})
test('BIT_USHR does not preserve sign', async () => {
const bytecode = toBytecode([
["PUSH", -8], ["PUSH", 1], ["BIT_USHR"], ["HALT"]
])
await expect(bytecode).toBeNumber(2147483644)
})
test('compound bitwise operations', async () => {
const bytecode = toBytecode([
// (5 & 3) | (8 ^ 12)
["PUSH", 5], ["PUSH", 3], ["BIT_AND"], // stack: [1]
["PUSH", 8], ["PUSH", 12], ["BIT_XOR"], // stack: [1, 4]
["BIT_OR"], // stack: [5]
["HALT"]
])
await expect(bytecode).toBeNumber(5)
})
test('shift with large shift amounts', async () => {
const bytecode = toBytecode([
["PUSH", 1], ["PUSH", 31], ["BIT_SHL"], ["HALT"]
])
// 1 << 31 = -2147483648 (most significant bit set)
await expect(bytecode).toBeNumber(-2147483648)
})
})

View File

@ -15,7 +15,7 @@ test("PUSH_TRY and POP_TRY - no exception thrown", async () => {
PUSH 999 PUSH 999
HALT HALT
` `
await expect(str).toBeNumber(52) expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 })
}) })
test("THROW - catch exception with error value", async () => { test("THROW - catch exception with error value", async () => {
@ -29,7 +29,7 @@ test("THROW - catch exception with error value", async () => {
.catch: .catch:
HALT HALT
` `
await expect(str).toBeString('error occurred') expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error occurred' })
}) })
test("THROW - uncaught exception throws JS error", async () => { test("THROW - uncaught exception throws JS error", async () => {
@ -58,7 +58,7 @@ test("THROW - exception with nested try blocks", async () => {
PUSH "outer error" PUSH "outer error"
HALT HALT
` `
await expect(str).toBeString('inner error') expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'inner error' })
}) })
test("THROW - exception skips outer handler", async () => { test("THROW - exception skips outer handler", async () => {
@ -75,7 +75,7 @@ test("THROW - exception skips outer handler", async () => {
.outer_catch: .outer_catch:
HALT HALT
` `
await expect(str).toBeString('error message') expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error message' })
}) })
test("THROW - exception unwinds call stack", async () => { test("THROW - exception unwinds call stack", async () => {
@ -150,7 +150,7 @@ test("PUSH_FINALLY - finally executes after successful try", async () => {
ADD ADD
HALT HALT
` `
await expect(str).toBeNumber(110) expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 110 })
}) })
test("PUSH_FINALLY - finally executes after exception", async () => { test("PUSH_FINALLY - finally executes after exception", async () => {
@ -169,7 +169,7 @@ test("PUSH_FINALLY - finally executes after exception", async () => {
PUSH "finally ran" PUSH "finally ran"
HALT HALT
` `
await expect(str).toBeString('finally ran') expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'finally ran' })
}) })
test("PUSH_FINALLY - finally without catch", async () => { test("PUSH_FINALLY - finally without catch", async () => {
@ -189,7 +189,7 @@ test("PUSH_FINALLY - finally without catch", async () => {
ADD ADD
HALT HALT
` `
await expect(str).toBeNumber(52) expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 })
}) })
test("PUSH_FINALLY - nested try-finally blocks", async () => { test("PUSH_FINALLY - nested try-finally blocks", async () => {
@ -214,7 +214,7 @@ test("PUSH_FINALLY - nested try-finally blocks", async () => {
ADD ADD
HALT HALT
` `
await expect(str).toBeNumber(11) expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 11 })
}) })
test("PUSH_FINALLY - error when no handler", async () => { test("PUSH_FINALLY - error when no handler", async () => {

View File

@ -104,7 +104,7 @@ describe("functions parameter", () => {
expect(result).toEqual({ type: 'number', value: 200 }) expect(result).toEqual({ type: 'number', value: 200 })
}) })
test("can combine with manual vm.set", async () => { test("can combine with manual registerFunction", async () => {
const bytecode = toBytecode(` const bytecode = toBytecode(`
LOAD add LOAD add
PUSH 5 PUSH 5
@ -127,7 +127,7 @@ describe("functions parameter", () => {
}) })
// Register another function manually // Register another function manually
vm.set('subtract', (a: number, b: number) => a - b) vm.registerFunction('subtract', (a: number, b: number) => a - b)
const result = await vm.run() const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 6 }) expect(result).toEqual({ type: 'number', value: 6 })
@ -224,7 +224,7 @@ describe("functions parameter", () => {
}) })
// Override with manual registration // Override with manual registration
vm.set('getValue', () => 200) vm.registerFunction('getValue', () => 200)
const result = await vm.run() const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 200 }) expect(result).toEqual({ type: 'number', value: 200 })

View File

@ -487,7 +487,7 @@ test("TRY_CALL - handles null values", async () => {
test("TRY_CALL - function can access its parameters", async () => { test("TRY_CALL - function can access its parameters", async () => {
const bytecode = toBytecode([ const bytecode = toBytecode([
["MAKE_FUNCTION", ["x=0"], ".body"], ["MAKE_FUNCTION", ["x"], ".body"],
["STORE", "addFive"], ["STORE", "addFive"],
["PUSH", 10], ["PUSH", 10],
["STORE", "x"], ["STORE", "x"],
@ -501,8 +501,8 @@ test("TRY_CALL - function can access its parameters", async () => {
]) ])
const result = await run(bytecode) const result = await run(bytecode)
// Function is called with 0 args, so x defaults to 0 // Function is called with 0 args, so x inside function should be null
// Then we add 5 to 0 // Then we add 5 to null (which coerces to 0)
expect(result).toEqual({ type: 'number', value: 5 }) expect(result).toEqual({ type: 'number', value: 5 })
}) })
@ -519,206 +519,3 @@ test("TRY_CALL - with string format", async () => {
const result = await run(bytecode) const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 100 }) expect(result).toEqual({ type: 'number', value: 100 })
}) })
test("CALL - passing null triggers default value for single parameter", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x=42"], ".body"],
["STORE", "func"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["RETURN"],
[".end:"],
["LOAD", "func"],
["PUSH", null],
["PUSH", 1],
["PUSH", 0],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// Passing null should trigger the default value of 42
expect(result).toEqual({ type: 'number', value: 42 })
})
test("CALL - passing null triggers default value for multiple parameters", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["a=10", "b=20", "c=30"], ".body"],
["STORE", "func"],
["JUMP", ".end"],
[".body:"],
["LOAD", "a"],
["LOAD", "b"],
["ADD"],
["LOAD", "c"],
["ADD"],
["RETURN"],
[".end:"],
["LOAD", "func"],
["PUSH", 5],
["PUSH", null],
["PUSH", null],
["PUSH", 3],
["PUSH", 0],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// a=5 (provided), b=20 (null triggers default), c=30 (null triggers default)
// Result: 5 + 20 + 30 = 55
expect(result).toEqual({ type: 'number', value: 55 })
})
test("CALL - null in middle parameter triggers default", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x=100", "y=200", "z=300"], ".body"],
["STORE", "func"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["LOAD", "y"],
["ADD"],
["LOAD", "z"],
["ADD"],
["RETURN"],
[".end:"],
["LOAD", "func"],
["PUSH", 1],
["PUSH", null],
["PUSH", 3],
["PUSH", 3],
["PUSH", 0],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// x=1, y=200 (null triggers default), z=3
// Result: 1 + 200 + 3 = 204
expect(result).toEqual({ type: 'number', value: 204 })
})
test("CALL - null with named arguments triggers default", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x=50", "y=75"], ".body"],
["STORE", "func"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["LOAD", "y"],
["ADD"],
["RETURN"],
[".end:"],
["LOAD", "func"],
["PUSH", "x"],
["PUSH", null],
["PUSH", "y"],
["PUSH", 25],
["PUSH", 0],
["PUSH", 2],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// x=50 (null triggers default), y=25 (provided via named arg)
// Result: 50 + 25 = 75
expect(result).toEqual({ type: 'number', value: 75 })
})
test("CALL - null with string default value", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["name='Guest'"], ".body"],
["STORE", "greet"],
["JUMP", ".end"],
[".body:"],
["PUSH", "Hello, "],
["LOAD", "name"],
["ADD"],
["RETURN"],
[".end:"],
["LOAD", "greet"],
["PUSH", null],
["PUSH", 1],
["PUSH", 0],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// Passing null should trigger the default value 'Guest'
expect(result).toEqual({ type: 'string', value: 'Hello, Guest' })
})
test("CALL - null with no default still results in null", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x"], ".body"],
["STORE", "func"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["RETURN"],
[".end:"],
["LOAD", "func"],
["PUSH", null],
["PUSH", 1],
["PUSH", 0],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// No default value, so null should be returned
expect(result).toEqual({ type: 'null', value: null })
})
test("CALL - null triggers default with variadic parameters", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x=99", "...rest"], ".body"],
["STORE", "func"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["RETURN"],
[".end:"],
["LOAD", "func"],
["PUSH", null],
["PUSH", 1],
["PUSH", 2],
["PUSH", 3],
["PUSH", 4],
["PUSH", 0],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// x should be 99 (null triggers default), rest gets [1, 2, 3]
expect(result).toEqual({ type: 'number', value: 99 })
})
test("CALL - null triggers default with @named parameter", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x=777", "@named"], ".body"],
["STORE", "func"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["RETURN"],
[".end:"],
["LOAD", "func"],
["PUSH", null],
["PUSH", "foo"],
["PUSH", "bar"],
["PUSH", 1],
["PUSH", 1],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// x should be 777 (null triggers default)
expect(result).toEqual({ type: 'number', value: 777 })
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -159,14 +159,14 @@ describe("RegExp", () => {
PUSH /bar/ PUSH /bar/
NEQ NEQ
` `
await expect(str).toBeBoolean(true) expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
const str2 = ` const str2 = `
PUSH /test/i PUSH /test/i
PUSH /test/i PUSH /test/i
NEQ NEQ
` `
await expect(str2).toBeBoolean(false) expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
}) })
test("is truthy", async () => { test("is truthy", async () => {
@ -177,7 +177,7 @@ describe("RegExp", () => {
PUSH 42 PUSH 42
.end: .end:
` `
await expect(str).toBeNumber(42) expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
}) })
test("NOT returns false (regex is truthy)", async () => { test("NOT returns false (regex is truthy)", async () => {
@ -185,7 +185,7 @@ describe("RegExp", () => {
PUSH /pattern/ PUSH /pattern/
NOT NOT
` `
await expect(str).toBeBoolean(false) expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
}) })
test("in arrays", async () => { test("in arrays", async () => {
@ -301,7 +301,7 @@ describe("RegExp", () => {
PUSH /bar/i PUSH /bar/i
STR_CONCAT #3 STR_CONCAT #3
` `
await expect(str).toBeString('/foo/ and /bar/i') expect(await run(toBytecode(str))).toEqual({ type: 'string', value: '/foo/ and /bar/i' })
}) })
test("DUP with regex", async () => { test("DUP with regex", async () => {
@ -311,7 +311,7 @@ describe("RegExp", () => {
EQ EQ
` `
// Same regex duplicated should be equal // Same regex duplicated should be equal
await expect(str).toBeBoolean(true) expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
}) })
test("empty pattern", async () => { test("empty pattern", async () => {
@ -365,7 +365,7 @@ describe("RegExp", () => {
PUSH /xyz/ PUSH /xyz/
EQ EQ
` `
await expect(str1).toBeBoolean(false) expect(await run(toBytecode(str1))).toEqual({ type: 'boolean', value: false })
// Same pattern, different flags // Same pattern, different flags
const str2 = ` const str2 = `
@ -373,7 +373,7 @@ describe("RegExp", () => {
PUSH /test/i PUSH /test/i
EQ EQ
` `
await expect(str2).toBeBoolean(false) expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
// Different order of flags (should be equal) // Different order of flags (should be equal)
const str3 = ` const str3 = `
@ -381,7 +381,7 @@ describe("RegExp", () => {
PUSH /test/gi PUSH /test/gi
EQ EQ
` `
await expect(str3).toBeBoolean(true) expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true })
}) })
test("with native functions", async () => { test("with native functions", async () => {
@ -399,7 +399,7 @@ describe("RegExp", () => {
const vm = new VM(bytecode) const vm = new VM(bytecode)
// Register a native function that takes a string and regex // Register a native function that takes a string and regex
vm.set('match', (str: string, pattern: RegExp) => { vm.registerFunction('match', (str: string, pattern: RegExp) => {
return pattern.test(str) return pattern.test(str)
}) })
@ -422,7 +422,7 @@ describe("RegExp", () => {
const vm = new VM(bytecode) const vm = new VM(bytecode)
vm.set('replace', (str: string, pattern: RegExp, replacement: string) => { vm.registerFunction('replace', (str: string, pattern: RegExp, replacement: string) => {
return str.replace(pattern, replacement) return str.replace(pattern, replacement)
}) })
@ -444,7 +444,7 @@ describe("RegExp", () => {
const vm = new VM(bytecode) const vm = new VM(bytecode)
vm.set('extractNumbers', (str: string, pattern: RegExp) => { vm.registerFunction('extractNumbers', (str: string, pattern: RegExp) => {
return str.match(pattern) || [] return str.match(pattern) || []
}) })

View File

@ -1,149 +0,0 @@
import { test, expect } from "bun:test"
import { VM, toBytecode } from "#reef"
test("REPL mode - demonstrates PC reset problem", async () => {
// Track how many times each line executes
let line1Count = 0
let line2Count = 0
// Line 1: Set x = 5, track execution
const line1 = toBytecode([
["LOAD", "trackLine1"],
["PUSH", 0],
["PUSH", 0],
["CALL"],
["POP"],
["PUSH", 5],
["STORE", "x"]
])
const vm = new VM(line1, {
trackLine1: () => { line1Count++; return null },
trackLine2: () => { line2Count++; return null }
})
await vm.run()
expect(vm.scope.get("x")).toEqual({ type: "number", value: 5 })
expect(line1Count).toBe(1)
expect(line2Count).toBe(0)
// Line 2: Track execution, load x
const line2 = toBytecode([
["LOAD", "trackLine2"],
["PUSH", 0],
["PUSH", 0],
["CALL"],
["POP"],
["LOAD", "x"]
])
// Append line2 bytecode to VM (what a REPL would do)
vm.instructions.push(...line2.instructions)
for (const constant of line2.constants) {
if (!vm.constants.includes(constant)) {
vm.constants.push(constant)
}
}
// Current behavior: run() resets PC to 0, re-executing everything
await vm.run()
// PROBLEM: Line 1 ran AGAIN (count is now 2, not 1)
// This is the issue for REPL - side effects run multiple times
expect(line1Count).toBe(2) // Ran twice! Should still be 1
expect(line2Count).toBe(1) // This is correct
// What we WANT for REPL:
// - Only execute the NEW bytecode (line 2)
// - line1Count should stay at 1
// - line2Count should be 1
})
test("REPL mode - continue() executes only new bytecode", async () => {
// Track how many times each line executes
let line1Count = 0
let line2Count = 0
// Line 1: Set x = 5, track execution
const line1 = toBytecode([
["LOAD", "trackLine1"],
["PUSH", 0],
["PUSH", 0],
["CALL"],
["POP"],
["PUSH", 5],
["STORE", "x"]
])
const vm = new VM(line1, {
trackLine1: () => { line1Count++; return null },
trackLine2: () => { line2Count++; return null }
})
await vm.run()
expect(vm.scope.get("x")).toEqual({ type: "number", value: 5 })
expect(line1Count).toBe(1)
expect(line2Count).toBe(0)
// Line 2: Track execution, load x and add 10
const line2 = toBytecode([
["LOAD", "trackLine2"],
["PUSH", 0],
["PUSH", 0],
["CALL"],
["POP"],
["LOAD", "x"],
["PUSH", 10],
["ADD"]
])
// Append line2 bytecode to VM using the helper method
vm.appendBytecode(line2)
// SOLUTION: Use continue() instead of run() to resume from current PC
await vm.continue()
const result = vm.stack[vm.stack.length - 1]
// SUCCESS: Line 1 only ran once, line 2 ran once
expect(line1Count).toBe(1) // Still 1! Side effect didn't re-run
expect(line2Count).toBe(1) // Ran once as expected
expect(result).toEqual({ type: "number", value: 15 }) // 5 + 10
})
test("REPL mode - function calls work across chunks", async () => {
const vm = new VM(toBytecode([]))
await vm.run()
// Line 1: Define a function
const line1 = toBytecode([
["MAKE_FUNCTION", ["x"], ".body"],
["STORE", "add1"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["PUSH", 1],
["ADD"],
["RETURN"],
[".end:"]
])
vm.appendBytecode(line1)
await vm.continue()
expect(vm.scope.get("add1")?.type).toBe("function")
// Line 2: Call the function
const line2 = toBytecode([
["LOAD", "add1"],
["PUSH", 10],
["PUSH", 1],
["PUSH", 0],
["CALL"]
])
vm.appendBytecode(line2)
const result = await vm.continue()
expect(result).toEqual({ type: "number", value: 11 })
})

View File

@ -1,243 +0,0 @@
import { test, expect } from "bun:test"
import { Scope, toValue } from "#reef"
test("Scope - create empty scope", () => {
const scope = new Scope()
expect(scope.parent).toBeUndefined()
expect(scope.locals.size).toBe(0)
})
test("Scope - create child scope with parent", () => {
const parent = new Scope()
const child = new Scope(parent)
expect(child.parent).toBe(parent)
expect(child.locals.size).toBe(0)
})
test("Scope - set and get variable in same scope", () => {
const scope = new Scope()
const value = toValue(42)
scope.set("x", value)
expect(scope.get("x")).toBe(value)
})
test("Scope - get returns undefined for non-existent variable", () => {
const scope = new Scope()
expect(scope.get("x")).toBeUndefined()
})
test("Scope - set updates existing variable in same scope", () => {
const scope = new Scope()
scope.set("x", toValue(10))
expect(scope.get("x")).toEqual({ type: "number", value: 10 })
scope.set("x", toValue(20))
expect(scope.get("x")).toEqual({ type: "number", value: 20 })
})
test("Scope - get searches parent scope", () => {
const parent = new Scope()
const child = new Scope(parent)
parent.set("x", toValue(42))
expect(child.get("x")).toEqual({ type: "number", value: 42 })
})
test("Scope - get searches multiple levels up", () => {
const grandparent = new Scope()
const parent = new Scope(grandparent)
const child = new Scope(parent)
grandparent.set("x", toValue(100))
expect(child.get("x")).toEqual({ type: "number", value: 100 })
})
test("Scope - child updates parent variable when it exists (no shadowing)", () => {
const parent = new Scope()
const child = new Scope(parent)
parent.set("x", toValue(10))
child.set("x", toValue(20))
// set() updates parent's variable, not shadow it
expect(parent.get("x")).toEqual({ type: "number", value: 20 })
expect(child.get("x")).toEqual({ type: "number", value: 20 })
expect(child.locals.has("x")).toBe(false)
})
test("Scope - set updates parent scope variable when it exists", () => {
const parent = new Scope()
const child = new Scope(parent)
parent.set("x", toValue(10))
child.set("x", toValue(20))
// Should update parent's variable, not create new local one
expect(parent.get("x")).toEqual({ type: "number", value: 20 })
expect(child.get("x")).toEqual({ type: "number", value: 20 })
expect(child.locals.has("x")).toBe(false)
})
test("Scope - set creates new local variable when not found in parent", () => {
const parent = new Scope()
const child = new Scope(parent)
parent.set("x", toValue(10))
child.set("y", toValue(20))
expect(parent.get("x")).toEqual({ type: "number", value: 10 })
expect(parent.get("y")).toBeUndefined()
expect(child.get("x")).toEqual({ type: "number", value: 10 })
expect(child.get("y")).toEqual({ type: "number", value: 20 })
expect(child.locals.has("y")).toBe(true)
})
test("Scope - has returns true for variable in current scope", () => {
const scope = new Scope()
scope.set("x", toValue(42))
expect(scope.has("x")).toBe(true)
expect(scope.has("y")).toBe(false)
})
test("Scope - has returns true for variable in parent scope", () => {
const parent = new Scope()
const child = new Scope(parent)
parent.set("x", toValue(42))
expect(child.has("x")).toBe(true)
expect(child.has("y")).toBe(false)
})
test("Scope - has searches multiple levels up", () => {
const grandparent = new Scope()
const parent = new Scope(grandparent)
const child = new Scope(parent)
grandparent.set("x", toValue(100))
expect(child.has("x")).toBe(true)
expect(parent.has("x")).toBe(true)
expect(grandparent.has("x")).toBe(true)
})
test("Scope.vars() - returns empty array for empty scope", () => {
const scope = new Scope()
expect(scope.vars()).toEqual([])
})
test("Scope.vars() - returns single variable from current scope", () => {
const scope = new Scope()
scope.set("x", toValue(42))
expect(scope.vars()).toEqual(["x"])
})
test("Scope.vars() - returns multiple variables from current scope", () => {
const scope = new Scope()
scope.set("x", toValue(1))
scope.set("y", toValue(2))
scope.set("z", toValue(3))
const vars = scope.vars()
expect(vars.length).toBe(3)
expect(vars).toContain("x")
expect(vars).toContain("y")
expect(vars).toContain("z")
})
test("Scope.vars() - includes variables from parent scope", () => {
const parent = new Scope()
const child = new Scope(parent)
parent.set("x", toValue(1))
child.set("y", toValue(2))
const vars = child.vars()
expect(vars.length).toBe(2)
expect(vars).toContain("x")
expect(vars).toContain("y")
})
test("Scope.vars() - includes variables from multiple parent scopes", () => {
const grandparent = new Scope()
const parent = new Scope(grandparent)
const child = new Scope(parent)
grandparent.set("x", toValue(1))
parent.set("y", toValue(2))
child.set("z", toValue(3))
const vars = child.vars()
expect(vars.length).toBe(3)
expect(vars).toContain("x")
expect(vars).toContain("y")
expect(vars).toContain("z")
})
test("Scope.vars() - no duplicates when child updates parent variable", () => {
const parent = new Scope()
const child = new Scope(parent)
parent.set("x", toValue(10))
child.set("x", toValue(20)) // Updates parent, doesn't create local
// Only one "x" since child doesn't have its own local x
const vars = child.vars()
expect(vars).toEqual(["x"])
})
test("Scope.vars() - can't have duplicates if manually set in locals", () => {
const parent = new Scope()
const child = new Scope(parent)
parent.set("x", toValue(10))
child.locals.set("x", toValue(20))
const vars = child.vars()
expect(vars).toEqual(["x"])
expect(child.get("x")).toEqual({ type: "number", value: 20 })
expect(parent.get("x")).toEqual({ type: "number", value: 10 })
})
test("Scope.vars() - handles deep scope chains", () => {
let scope = new Scope()
// Create a deep chain: level0 -> level1 -> ... -> level5
for (let i = 0; i < 5; i++) {
scope.set(`var${i}`, toValue(i))
scope = new Scope(scope)
}
// Final scope should have all variables from the chain
const vars = scope.vars()
expect(vars.length).toBe(5)
expect(vars).toContain("var0")
expect(vars).toContain("var1")
expect(vars).toContain("var2")
expect(vars).toContain("var3")
expect(vars).toContain("var4")
})
test("Scope - can store and retrieve functions", () => {
const scope = new Scope()
const fnValue = {
type: 'function' as const,
value: '<function>' as const,
params: ['x'],
defaults: {},
body: 0,
variadic: false,
named: false,
parentScope: scope
}
scope.set("myFunc", fnValue)
expect(scope.get("myFunc")).toBe(fnValue)
expect(scope.get("myFunc")?.type).toBe("function")
})

View File

@ -1,249 +0,0 @@
import { expect } from "bun:test"
import { toValue, fromValue, type Value, toBytecode, run, type Bytecode } from "#reef"
import { isEqual } from "../src/value"
declare module "bun:test" {
interface Matchers<T> {
/**
* Run bytecode and assert that the result equals a JavaScript value after conversion via toValue()
* @example expect(bytecode).toEqualValue(42)
* @example expect("PUSH 5\nPUSH 3\nADD").toEqualValue(8)
* @example expect([["PUSH", 42]]).toEqualValue(42)
*/
toEqualValue(expected: any): Promise<void>
/**
* Run bytecode and assert that the result is null
* @example expect(bytecode).toBeNull()
* @example expect("PUSH null").toBeNull()
*/
toBeNull(): Promise<void>
/**
* Run bytecode and assert that the result is a boolean with the expected value
* @example expect(bytecode).toBeBoolean(true)
* @example expect("PUSH true").toBeBoolean(true)
*/
toBeBoolean(expected: boolean): Promise<void>
/**
* Run bytecode and assert that the result is a number with the expected value
* @example expect(bytecode).toBeNumber(42)
* @example expect("PUSH 42").toBeNumber(42)
*/
toBeNumber(expected: number): Promise<void>
/**
* Run bytecode and assert that the result is a string with the expected value
* @example expect(bytecode).toBeString("hello")
* @example expect("PUSH \"hello\"").toBeString("hello")
*/
toBeString(expected: string): Promise<void>
/**
* Run bytecode and assert that the result is an array with the expected values
* @example expect(bytecode).toBeArray([1, 2, 3])
*/
toBeArray(expected: any[]): Promise<void>
/**
* Run bytecode and assert that the result is a dict with the expected key-value pairs
* @example expect(bytecode).toBeDict({ x: 10, y: 20 })
*/
toBeDict(expected: Record<string, any>): Promise<void>
/**
* Run bytecode and assert that the result is a function (Reef or native)
* @example expect(bytecode).toBeFunction()
*/
toBeFunction(): Promise<void>
/**
* Run bytecode and assert that the result is truthy according to ReefVM semantics
* (only null and false are falsy)
* @example expect(bytecode).toBeTruthy()
*/
toBeTruthy(): Promise<void>
/**
* Run bytecode and assert that the result is falsy according to ReefVM semantics
* (only null and false are falsy)
* @example expect(bytecode).toBeFalsy()
*/
toBeFalsy(): Promise<void>
}
}
expect.extend({
async toEqualValue(this: void, received: unknown, expected: any) {
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
const result = await run(bytecode)
const expectedValue = toValue(expected)
const pass = isEqual(result, expectedValue)
return {
pass,
message: () =>
pass
? `Expected value NOT to equal ${formatValue(expectedValue)}, but it did`
: `Expected value to equal ${formatValue(expectedValue)}, but received ${formatValue(result)}`,
}
},
async toBeNull(this: void, received: unknown) {
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
const result = await run(bytecode)
const pass = result.type === "null"
return {
pass,
message: () =>
pass
? `Expected value NOT to be null, but it was`
: `Expected value to be null, but received ${formatValue(result)}`,
}
},
async toBeBoolean(this: void, received: unknown, expected: boolean) {
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
const result = await run(bytecode)
const pass = result.type === "boolean" && result.value === expected
return {
pass,
message: () =>
pass
? `Expected value NOT to be boolean ${expected}, but it was`
: `Expected value to be boolean ${expected}, but received ${formatValue(result)}`,
}
},
async toBeNumber(this: void, received: unknown, expected: number) {
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
const result = await run(bytecode)
const pass = result.type === "number" && result.value === expected
return {
pass,
message: () =>
pass
? `Expected value NOT to be number ${expected}, but it was`
: `Expected value to be number ${expected}, but received ${formatValue(result)}`,
}
},
async toBeString(this: void, received: unknown, expected: string) {
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
const result = await run(bytecode)
const pass = result.type === "string" && result.value === expected
return {
pass,
message: () =>
pass
? `Expected value NOT to be string "${expected}", but it was`
: `Expected value to be string "${expected}", but received ${formatValue(result)}`,
}
},
async toBeArray(this: void, received: unknown, expected: any[]) {
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
const result = await run(bytecode)
const expectedValue = toValue(expected)
const pass = result.type === "array" && isEqual(result, expectedValue)
return {
pass,
message: () =>
pass
? `Expected value NOT to be array ${formatValue(expectedValue)}, but it was`
: `Expected value to be array ${formatValue(expectedValue)}, but received ${formatValue(result)}`,
}
},
async toBeDict(this: void, received: unknown, expected: Record<string, any>) {
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
const result = await run(bytecode)
const expectedValue = toValue(expected)
const pass = result.type === "dict" && isEqual(result, expectedValue)
return {
pass,
message: () =>
pass
? `Expected value NOT to be dict ${formatValue(expectedValue)}, but it was`
: `Expected value to be dict ${formatValue(expectedValue)}, but received ${formatValue(result)}`,
}
},
async toBeFunction(this: void, received: unknown) {
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
const result = await run(bytecode)
const pass = result.type === "function" || result.type === "native"
return {
pass,
message: () =>
pass
? `Expected value NOT to be a function, but it was`
: `Expected value to be a function, but received ${formatValue(result)}`,
}
},
async toBeTruthy(this: void, received: unknown) {
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
const result = await run(bytecode)
// ReefVM semantics: only null and false are falsy
const pass = !(result.type === "null" || (result.type === "boolean" && !result.value))
return {
pass,
message: () =>
pass
? `Expected value NOT to be truthy, but it was: ${formatValue(result)}`
: `Expected value to be truthy, but received ${formatValue(result)}`,
}
},
async toBeFalsy(this: void, received: unknown) {
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
const result = await run(bytecode)
// ReefVM semantics: only null and false are falsy
const pass = result.type === "null" || (result.type === "boolean" && !result.value)
return {
pass,
message: () =>
pass
? `Expected value NOT to be falsy, but it was: ${formatValue(result)}`
: `Expected value to be falsy, but received ${formatValue(result)}`,
}
},
})
function formatValue(value: Value): string {
switch (value.type) {
case "null":
return "null"
case "boolean":
case "number":
return String(value.value)
case "string":
return `"${value.value}"`
case "array":
return `[${value.value.map(formatValue).join(", ")}]`
case "dict": {
const entries = Array.from(value.value.entries())
.map(([k, v]) => `${k}: ${formatValue(v)}`)
.join(", ")
return `{${entries}}`
}
case "regex":
return String(value.value)
case "function":
case "native":
return "<function>"
default:
return String(value)
}
}

View File

@ -201,17 +201,17 @@ test("formatValidationErrors produces readable output", () => {
expect(formatted).toContain("UNKNOWN") expect(formatted).toContain("UNKNOWN")
}) })
test("detects JUMP without .label", () => { test("detects JUMP without # or .label", () => {
const source = ` const source = `
JUMP 5 JUMP 5
HALT HALT
` `
const result = validateBytecode(source) const result = validateBytecode(source)
expect(result.valid).toBe(false) expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("JUMP requires label (.label)") expect(result.errors[0]!.message).toContain("JUMP requires immediate (#number) or label (.label)")
}) })
test("detects JUMP_IF_TRUE without .label", () => { test("detects JUMP_IF_TRUE without # or .label", () => {
const source = ` const source = `
PUSH true PUSH true
JUMP_IF_TRUE 2 JUMP_IF_TRUE 2
@ -219,10 +219,10 @@ test("detects JUMP_IF_TRUE without .label", () => {
` `
const result = validateBytecode(source) const result = validateBytecode(source)
expect(result.valid).toBe(false) expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("JUMP_IF_TRUE requires label (.label)") expect(result.errors[0]!.message).toContain("JUMP_IF_TRUE requires immediate (#number) or label (.label)")
}) })
test("detects JUMP_IF_FALSE without .label", () => { test("detects JUMP_IF_FALSE without # or .label", () => {
const source = ` const source = `
PUSH false PUSH false
JUMP_IF_FALSE 2 JUMP_IF_FALSE 2
@ -230,18 +230,17 @@ test("detects JUMP_IF_FALSE without .label", () => {
` `
const result = validateBytecode(source) const result = validateBytecode(source)
expect(result.valid).toBe(false) expect(result.valid).toBe(false)
expect(result.errors[0]!.message).toContain("JUMP_IF_FALSE requires label (.label)") expect(result.errors[0]!.message).toContain("JUMP_IF_FALSE requires immediate (#number) or label (.label)")
}) })
test("rejects JUMP with immediate number", () => { test("allows JUMP with immediate number", () => {
const source = ` const source = `
JUMP #2 JUMP #2
PUSH 999 PUSH 999
HALT HALT
` `
const result = validateBytecode(source) const result = validateBytecode(source)
expect(result.valid).toBe(false) expect(result.valid).toBe(true)
expect(result.errors[0]!.message).toContain("JUMP requires label (.label)")
}) })
test("detects MAKE_ARRAY without #", () => { test("detects MAKE_ARRAY without #", () => {

View File

@ -1,302 +0,0 @@
import { test, expect } from "bun:test"
import { isValue, toValue } from "#reef"
test("isValue - recognizes valid Value objects", () => {
expect(isValue({ type: 'number', value: 42 })).toBe(true)
expect(isValue({ type: 'number', value: 0 })).toBe(true)
expect(isValue({ type: 'string', value: 'hello' })).toBe(true)
expect(isValue({ type: 'string', value: '' })).toBe(true)
expect(isValue({ type: 'boolean', value: true })).toBe(true)
expect(isValue({ type: 'boolean', value: false })).toBe(true)
expect(isValue({ type: 'null', value: null })).toBe(true)
expect(isValue({ type: 'array', value: [] })).toBe(true)
expect(isValue({ type: 'array', value: [toValue(1), toValue(2)] })).toBe(true)
expect(isValue({ type: 'dict', value: new Map() })).toBe(true)
expect(isValue({ type: 'regex', value: /test/ })).toBe(true)
expect(isValue({
type: 'function',
value: '<function>',
params: [],
defaults: {},
body: 0,
variadic: false,
named: false,
parentScope: null as any
})).toBe(true)
expect(isValue({
type: 'native',
value: '<function>',
fn: (() => { }) as any
})).toBe(true)
})
test("isValue - rejects primitives", () => {
expect(isValue(42)).toBe(false)
expect(isValue(0)).toBe(false)
expect(isValue('hello')).toBe(false)
expect(isValue('')).toBe(false)
expect(isValue(true)).toBe(false)
expect(isValue(false)).toBe(false)
expect(isValue(null)).toBe(false)
expect(isValue(undefined)).toBe(false)
})
test("isValue - rejects plain objects", () => {
expect(isValue({})).toBe(false)
expect(isValue({ foo: 'bar' })).toBe(false)
expect(isValue({ type: 'number' })).toBe(false)
expect(isValue({ value: 42 })).toBe(false)
})
test("isValue - rejects arrays and functions", () => {
expect(isValue([])).toBe(false)
expect(isValue([1, 2, 3])).toBe(false)
expect(isValue(() => { })).toBe(false)
expect(isValue(function () { })).toBe(false)
})
test("isValue - rejects other object types", () => {
expect(isValue(new Date())).toBe(false)
expect(isValue(/regex/)).toBe(false)
expect(isValue(new Map())).toBe(false)
expect(isValue(new Set())).toBe(false)
})
test("isValue - used by toValue to detect already-converted values", () => {
const value = toValue(42)
expect(isValue(value)).toBe(true)
const result = toValue(value)
expect(result).toBe(value)
})
test("isValue - edge cases with type and value properties", () => {
expect(isValue({ type: 'number', value: 42, extra: 'data' })).toBe(true)
expect(isValue({ type: null, value: 42 })).toBe(false)
expect(isValue({ type: 'number', value: undefined })).toBe(true)
expect(isValue({ type: 'number', val: 42 })).toBe(false)
expect(isValue({ typ: 'number', value: 42 })).toBe(false)
})
test("isValue - rejects objects with invalid type values", () => {
expect(isValue({ type: 'text', value: 'Bob' })).toBe(false)
expect(isValue({ type: 'email', value: 'test@example.com' })).toBe(false)
expect(isValue({ type: 'password', value: 'secret' })).toBe(false)
expect(isValue({ type: 'checkbox', value: true })).toBe(false)
expect(isValue({ type: 'custom', value: 123 })).toBe(false)
expect(isValue({ type: 'unknown', value: null })).toBe(false)
})
test("toValue - correctly handles HTML input props", async () => {
const { VM, toBytecode, toValue } = await import("#reef")
const bytecode = toBytecode([["HALT"]])
const vm = new VM(bytecode)
const inputProps = { type: 'text', value: 'Bob' }
const converted = toValue(inputProps, vm)
expect(converted.type).toBe('dict')
expect(converted.value.get('type')).toEqual({ type: 'string', value: 'text' })
expect(converted.value.get('value')).toEqual({ type: 'string', value: 'Bob' })
})
test("toValue - converts wrapped Reef functions back to original Value", async () => {
const { VM, toBytecode, fnFromValue } = await import("#reef")
// Create a Reef function
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x"], ".body"],
["STORE", "add1"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["PUSH", 1],
["ADD"],
["RETURN"],
[".end:"],
["HALT"]
])
const vm = new VM(bytecode)
await vm.run()
const reefFunction = vm.scope.get("add1")!
expect(reefFunction.type).toBe("function")
// Convert to JS function
const jsFunction = fnFromValue(reefFunction, vm)
expect(typeof jsFunction).toBe("function")
// Convert back to Value - should return the original Reef function
const backToValue = toValue(jsFunction)
expect(backToValue).toBe(reefFunction) // Same reference
expect(backToValue.type).toBe("function")
})
test("fromValue - converts native function back to original JS function", async () => {
const { VM, toBytecode, toValue, fromValue } = await import("#reef")
const bytecode = toBytecode([["HALT"]])
const vm = new VM(bytecode)
// Create a native JS function
const originalFn = (x: number, y: number) => x * y
// Convert to Value (wraps it as a native function)
const nativeValue = toValue(originalFn, vm)
expect(nativeValue.type).toBe("native")
// Convert back to JS - should get the original function
const convertedFn = fromValue(nativeValue, vm)
expect(typeof convertedFn).toBe("function")
// Verify it's the same function
expect(convertedFn).toBe(originalFn)
// Verify it works correctly
expect(convertedFn(3, 4)).toBe(12)
})
test("fromValue - native function roundtrip preserves functionality", async () => {
const { VM, toBytecode, toValue, fromValue } = await import("#reef")
const bytecode = toBytecode([["HALT"]])
const vm = new VM(bytecode)
// Create a native function with state
let callCount = 0
const countingFn = (n: number) => {
callCount++
return n * callCount
}
// Roundtrip through Value
const nativeValue = toValue(countingFn, vm)
const roundtrippedFn = fromValue(nativeValue, vm)
// Verify it maintains state across calls
expect(roundtrippedFn(10)).toBe(10) // 10 * 1
expect(roundtrippedFn(10)).toBe(20) // 10 * 2
expect(roundtrippedFn(10)).toBe(30) // 10 * 3
expect(callCount).toBe(3)
})
test("fromValue - async native function roundtrip", async () => {
const { VM, toBytecode, toValue, fromValue } = await import("#reef")
const bytecode = toBytecode([["HALT"]])
const vm = new VM(bytecode)
const asyncFn = async (x: number, y: number) => {
await new Promise(resolve => setTimeout(resolve, 1))
return x + y
}
const nativeValue = toValue(asyncFn, vm)
expect(nativeValue.type).toBe("native")
const roundtrippedFn = fromValue(nativeValue, vm)
const result = await roundtrippedFn(5, 7)
expect(result).toBe(12)
})
test("toValue - throws helpful error when calling function converted without VM", async () => {
function myFunction(x: number) {
return x * 2
}
const value = toValue(myFunction)
expect(value.type).toBe('native')
// Error is thrown when calling the function, not when converting
await expect(async () => {
await value.fn({ type: 'number', value: 5 })
}).toThrow(/can't call function that was converted without a vm/)
await expect(async () => {
await value.fn({ type: 'number', value: 5 })
}).toThrow(/Function: myFunction/)
await expect(async () => {
await value.fn({ type: 'number', value: 5 })
}).toThrow(/Source:/)
await expect(async () => {
await value.fn({ type: 'number', value: 5 })
}).toThrow(/Called from:/)
})
test("toValue - error message shows function info for arrow functions", async () => {
const anonymousFn = (x: number) => x * 2
const value = toValue(anonymousFn)
expect(value.type).toBe('native')
// Arrow functions show as <anonymous>
await expect(async () => {
await value.fn({ type: 'number', value: 5 })
}).toThrow(/Function: <anonymous>/)
await expect(async () => {
await value.fn({ type: 'number', value: 5 })
}).toThrow(/Source:/)
})
test("toValue - error when function is nested in object without VM", async () => {
const obj = {
name: "test",
handler: (x: number) => x * 2
}
const value = toValue(obj)
expect(value.type).toBe('dict')
const handlerValue = value.value.get('handler')!
expect(handlerValue.type).toBe('native')
await expect(async () => {
await (handlerValue as any).fn({ type: 'number', value: 5 })
}).toThrow(/can't call function that was converted without a vm/)
await expect(async () => {
await (handlerValue as any).fn({ type: 'number', value: 5 })
}).toThrow(/Function: handler/)
})
test("toValue - error when function is nested in array without VM", async () => {
const arr = [1, 2, (x: number) => x * 2]
const value = toValue(arr)
expect(value.type).toBe('array')
const fnValue = value.value[2]!
expect(fnValue.type).toBe('native')
await expect(async () => {
await (fnValue as any).fn({ type: 'number', value: 5 })
}).toThrow(/can't call function that was converted without a vm/)
})
test("fromValue - throws helpful error when converting function without VM", async () => {
const { Scope, fromValue } = await import("#reef")
const reefFunction = {
type: 'function' as const,
params: ['x', 'y'],
defaults: {},
body: 10,
parentScope: new Scope(),
variadic: false,
named: false,
value: '<function>' as const
}
expect(() => fromValue(reefFunction)).toThrow(/VM is required for function conversion/)
expect(() => fromValue(reefFunction)).toThrow(/Function params: \[x, y\]/)
expect(() => fromValue(reefFunction)).toThrow(/Function body at instruction: 10/)
expect(() => fromValue(reefFunction)).toThrow(/Called from:/)
})

View File

@ -1,54 +0,0 @@
import { test, expect, describe } from "bun:test"
import { VM } from "#vm"
import { toBytecode } from "#bytecode"
import { toValue } from "#value"
describe("VM scope methods", () => {
test("pushScope creates isolated child scope", async () => {
const bytecode = toBytecode([["HALT"]])
const vm = new VM(bytecode)
vm.set("x", 42)
vm.pushScope()
const xValue = vm.scope.get("x")
expect(xValue).toEqual({ type: "number", value: 42 })
vm.set("y", 100)
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
expect(vm.scope.get("y")).toEqual({ type: "number", value: 100 })
})
test("popScope returns to parent scope and child variables are not accessible", async () => {
const bytecode = toBytecode([["HALT"]])
const vm = new VM(bytecode)
vm.set("x", 42)
vm.pushScope()
vm.set("y", 100)
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
expect(vm.scope.get("y")).toEqual({ type: "number", value: 100 })
vm.popScope()
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
expect(vm.scope.get("y")).toBeUndefined()
})
test("pushScope with locals initializes child scope with variables", async () => {
const bytecode = toBytecode([["HALT"]])
const vm = new VM(bytecode)
vm.set("x", 42)
vm.pushScope({ y: 100, z: "hello" })
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
expect(vm.scope.get("y")).toEqual({ type: "number", value: 100 })
expect(vm.scope.get("z")).toEqual({ type: "string", value: "hello" })
vm.popScope()
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
expect(vm.scope.get("y")).toBeUndefined()
expect(vm.scope.get("z")).toBeUndefined()
})
})