Compare commits

..

No commits in common. "main" and "regex" have entirely different histories.
main ... regex

36 changed files with 1361 additions and 9094 deletions

178
CLAUDE.md
View File

@ -42,7 +42,8 @@ No build step required - Bun runs TypeScript directly.
- Stack-based execution with program counter (PC) - Stack-based execution with program counter (PC)
- Call stack for function frames - Call stack for function frames
- Exception handler stack for try/catch/finally - Exception handler stack for try/catch/finally
- Lexical scope chain with parent references (includes native functions) - Lexical scope chain with parent references
- Native function registry for TypeScript interop
**Key subsystems**: **Key subsystems**:
- **bytecode.ts**: Compiler that converts both string and array formats to executable bytecode. Handles label resolution, constant pool management, and function definition parsing. The `toBytecode()` function accepts either a string (human-readable) or typed array format (programmatic). - **bytecode.ts**: Compiler that converts both string and array formats to executable bytecode. Handles label resolution, constant pool management, and function definition parsing. The `toBytecode()` function accepts either a string (human-readable) or typed array format (programmatic).
@ -55,7 +56,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.
@ -69,22 +70,19 @@ No build step required - Bun runs TypeScript directly.
**Parameter binding priority**: Named args bind to fixed params first. Unmatched named args go to `@named` dict parameter. Fixed params bind in order: named arg > positional arg > default > null. **Parameter binding priority**: Named args bind to fixed params first. Unmatched named args go to `@named` dict parameter. Fixed params bind in order: named arg > positional arg > default > null.
**Native function calling**: Native functions are stored in scope and called via LOAD + CALL, using the same calling convention as Reef functions. Named arguments are supported by extracting parameter names from the function signature at call time. **Native function calling**: CALL_NATIVE consumes the entire stack as arguments (different from CALL which pops specific argument counts).
## Testing Strategy ## Testing Strategy
Tests are organized by feature area: Tests are organized by feature area:
- **opcodes.test.ts**: Stack ops, arithmetic, comparisons, variables, control flow - **basic.test.ts**: Stack ops, arithmetic, comparisons, variables, control flow
- **functions.test.ts**: Function creation, calls, closures, defaults, variadic, named args - **functions.test.ts**: Function creation, calls, closures, defaults, variadic, named args
- **tail-call.test.ts**: Tail call optimization and unbounded recursion - **tail-call.test.ts**: Tail call optimization and unbounded recursion
- **exceptions.test.ts**: Try/catch/finally, exception unwinding, nested handlers - **exceptions.test.ts**: Try/catch/finally, exception unwinding, nested handlers
- **native.test.ts**: Native function interop (sync and async) - **native.test.ts**: Native function interop (sync and async)
- **functions-parameter.test.ts**: Convenience parameter for passing functions to run() and VM
- **bytecode.test.ts**: Bytecode string parser, label resolution, constants - **bytecode.test.ts**: Bytecode string parser, label resolution, constants
- **programmatic.test.ts**: Array format API, typed tuples, labels, functions - **programmatic.test.ts**: Array format API, typed tuples, labels, functions
- **validator.test.ts**: Bytecode validation rules - **validator.test.ts**: Bytecode validation rules
- **unicode.test.ts**: Unicode and emoji identifiers
- **regex.test.ts**: RegExp support
- **examples.test.ts**: Integration tests for example programs - **examples.test.ts**: Integration tests for example programs
When adding features: When adding features:
@ -137,100 +135,53 @@ 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) ReefVM supports two ways to register native functions:
```typescript
const result = await run(bytecode, {
add: (a: number, b: number) => a + b,
greet: (name: string) => `Hello, ${name}!`,
pi: 3.14159,
config: { debug: true, port: 8080 }
})
// Or with VM constructor **1. Native TypeScript functions (recommended)** - Auto-converts between native TS and ReefVM types:
const vm = new VM(bytecode, { add, greet, pi, config })
```
**Option 2**: Set values with `vm.set()` (manual)
```typescript ```typescript
const vm = new VM(bytecode) const vm = new VM(bytecode)
// Set functions (auto-wrapped to native functions) // Works with native TypeScript types!
vm.set('add', (a: number, b: number) => a + b) vm.registerFunction('add', (a: number, b: number) => {
return a + b
})
// Set any other values (auto-converted to ReefVM Values) // Supports defaults (like NOSE commands)
vm.set('pi', 3.14159) vm.registerFunction('ls', (path: string, link = false) => {
vm.set('config', { debug: true, port: 8080 }) return link ? `listing ${path} with links` : `listing ${path}`
})
// Async functions work too
vm.registerFunction('fetch', async (url: string) => {
const response = await fetch(url)
return await response.text()
})
await vm.run() await vm.run()
``` ```
**Option 3**: Set Value-based functions with `vm.setValueFunction()` (advanced) **2. Value-based functions (manual)** - For functions that need direct Value access:
For functions that work directly with ReefVM Value types:
```typescript ```typescript
const vm = new VM(bytecode) const vm = new VM(bytecode)
// Set Value-based function (no wrapping, works directly with Values) vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
vm.setValueFunction('customOp', (a: Value, b: Value): Value => { // Direct access to Value types
return toValue(toNumber(a) + toNumber(b)) return toValue(toNumber(a) + toNumber(b))
}) })
await vm.run() await vm.run()
``` ```
Auto-wrapping handles: The auto-wrapping handles:
- Functions: wrapped as native functions with Value ↔ native type conversion - Converting Value → native types on input (using `fromValue`)
- Sync and async functions - Converting native types → Value on output (using `toValue`)
- Arrays, objects, primitives, null, RegExp - Both sync and async functions
- All values converted via `toValue()` - Arrays, objects, primitives, and null
### 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 +191,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,12 +376,14 @@ 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).
**MAKE_ARRAY operand**: Specifies count, not a stack index. `MAKE_ARRAY #3` pops 3 items. **MAKE_ARRAY operand**: Specifies count, not a stack index. `MAKE_ARRAY #3` pops 3 items.
**CALL_NATIVE stack behavior**: Unlike CALL, it consumes all stack values as arguments and clears the stack.
**Finally blocks**: The compiler must generate explicit JUMPs to finally blocks for successful try/catch completion. The VM only auto-jumps to finally on THROW. **Finally blocks**: The compiler must generate explicit JUMPs to finally blocks for successful try/catch completion. The VM only auto-jumps to finally on THROW.
**Variable scoping**: STORE updates existing variables in parent scopes or creates in current scope. It does NOT shadow by default. **Variable scoping**: STORE updates existing variables in parent scopes or creates in current scope. It does NOT shadow by default.

487
GUIDE.md
View File

@ -42,6 +42,9 @@ OPCODE operand ; comment
- Booleans: `PUSH true`, `PUSH false` - Booleans: `PUSH true`, `PUSH false`
- Null: `PUSH null` - Null: `PUSH null`
**Native function names**: Registered TypeScript functions
- `CALL_NATIVE print`
## Array Format ## Array Format
The programmatic array format uses TypeScript tuples for type safety: The programmatic array format uses TypeScript tuples for type safety:
@ -96,6 +99,11 @@ const result = await run(bytecode)
["MAKE_DICT", 2] // Pop 2 key-value pairs ["MAKE_DICT", 2] // Pop 2 key-value pairs
``` ```
**Native function names**: Strings for registered functions
```typescript
["CALL_NATIVE", "print"]
```
### Functions in Array Format ### Functions in Array Format
```typescript ```typescript
@ -179,25 +187,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 +202,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
@ -244,9 +235,6 @@ CALL ; → "Hi, Guest"
- `DICT_SET` - Pop value, key, dict; mutate dict - `DICT_SET` - Pop value, key, dict; mutate dict
- `DICT_HAS` - Pop key and dict, push boolean - `DICT_HAS` - Pop key and dict, push boolean
### Unified Access
- `DOT_GET` - Pop index/key and array/dict, push value (null if missing)
### Strings ### Strings
- `STR_CONCAT #N` - Pop N values, convert to strings, concatenate, push result - `STR_CONCAT #N` - Pop N values, convert to strings, concatenate, push result
@ -256,42 +244,11 @@ CALL ; → "Hi, Guest"
- `POP_TRY` - Remove handler (try succeeded) - `POP_TRY` - Remove handler (try succeeded)
- `THROW` - Throw exception (pops error value) - `THROW` - Throw exception (pops error value)
### Native
- `CALL_NATIVE <name>` - Call registered TypeScript function (consumes entire stack as args)
## 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 +334,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 +370,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 +379,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 +386,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 +406,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)
@ -658,56 +474,6 @@ PUSH "!"
STR_CONCAT #2 ; → "Hello World!" STR_CONCAT #2 ; → "Hello World!"
``` ```
### Unified Access (DOT_GET)
DOT_GET provides a single opcode for accessing both arrays and dicts:
```
; Array access
PUSH 10
PUSH 20
PUSH 30
MAKE_ARRAY #3
PUSH 1
DOT_GET ; → 20
; Dict access
PUSH 'name'
PUSH 'Alice'
MAKE_DICT #1
PUSH 'name'
DOT_GET ; → 'Alice'
```
**Chained access**:
```
; Access dict['users'][0]['name']
LOAD dict
PUSH 'users'
DOT_GET ; Get users array
PUSH 0
DOT_GET ; Get first user
PUSH 'name'
DOT_GET ; Get name field
```
**With variables**:
```
LOAD data
LOAD key ; Key can be string or number
DOT_GET ; Works for both array and dict
```
**Null safety**: Returns null for missing keys or out-of-bounds indices
```
MAKE_ARRAY #0
PUSH 0
DOT_GET ; → null (empty array)
MAKE_DICT #0
PUSH 'key'
DOT_GET ; → null (missing key)
```
## Key Concepts ## Key Concepts
### Truthiness ### Truthiness
@ -733,8 +499,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 +523,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
@ -785,7 +536,7 @@ This works for both ReefVM functions and native TypeScript functions. If no defa
- Finally execution in all cases is compiler's responsibility, not VM's - Finally execution in all cases is compiler's responsibility, not VM's
### Calling Convention ### Calling Convention
All calls (including native functions) push arguments in order: All calls push arguments in order:
1. Function 1. Function
2. Positional args (in order) 2. Positional args (in order)
3. Named args (key1, val1, key2, val2, ...) 3. Named args (key1, val1, key2, val2, ...)
@ -793,210 +544,8 @@ All calls (including native functions) push arguments in order:
5. Named count (as number) 5. Named count (as number)
6. CALL or TAIL_CALL 6. CALL or TAIL_CALL
Native functions use the same calling convention as Reef functions. They are registered into scope and called via LOAD + CALL. ### CALL_NATIVE Behavior
Unlike CALL, CALL_NATIVE consumes the **entire stack** as arguments and clears the stack. The native function receives all values that were on the stack at the time of the call.
### Registering Native Functions
Native TypeScript functions are registered into the VM's scope and accessed like regular variables.
**Method 1**: Pass to `run()` or `VM` constructor
```typescript
const result = await run(bytecode, {
add: (a: number, b: number) => a + b,
greet: (name: string) => `Hello, ${name}!`
})
// Or with VM
const vm = new VM(bytecode, { add, greet })
```
**Method 2**: Register after construction
```typescript
const vm = new VM(bytecode)
vm.set('add', (a: number, b: number) => a + b)
await vm.run()
```
**Method 3**: Value-based functions (for full control)
```typescript
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
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.
**Usage in bytecode**:
```
; Positional arguments
LOAD add ; Load native function from scope
PUSH 5
PUSH 10
PUSH 2 ; positionalCount
PUSH 0 ; namedCount
CALL ; Call like any other function
; Named arguments
LOAD greet
PUSH "name"
PUSH "Alice"
PUSH "greeting"
PUSH "Hi"
PUSH 0 ; positionalCount
PUSH 2 ; namedCount
CALL ; → "Hi, Alice!"
```
**Named Arguments**: Native functions support named arguments. Parameter names are extracted from the function signature at call time, and arguments are bound using the same priority as Reef functions (named arg > positional arg > default > null).
**@named Pattern**: Parameters starting with `at` followed by an uppercase letter (e.g., `atOptions`, `atNamed`) collect unmatched named arguments:
```typescript
// Basic @named - collects all named args
vm.set('greet', (atNamed: any = {}) => {
return `Hello, ${atNamed.name || 'World'}!`
})
// Mixed positional and @named
vm.set('configure', (name: string, atOptions: any = {}) => {
return {
name,
debug: atOptions.debug || false,
port: atOptions.port || 3000
}
})
```
Bytecode example:
```
; Call with mixed positional and named args
LOAD configure
PUSH "myApp" ; positional arg → name
PUSH "debug"
PUSH true
PUSH "port"
PUSH 8080
PUSH 1 ; 1 positional arg
PUSH 2 ; 2 named args (debug, port)
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.
### 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

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,10 @@ 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 (CALL_NATIVE) with sync and async functions
- Native functions stored in scope, called via LOAD + CALL - Write native functions with regular TypeScript types instead of Shrimp's internal Value types
- 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 })`
- Call Reef functions from TypeScript with `vm.call(name, ...args)` with automatic type conversion
## Design Decisions ## Design Decisions
@ -60,5 +56,4 @@ Commands: `clear`, `reset`, `exit`.
- Short-circuiting via compiler: No AND/OR opcodes—compilers use JUMP patterns for proper short-circuit evaluation - Short-circuiting via compiler: No AND/OR opcodes—compilers use JUMP patterns for proper short-circuit evaluation
- 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)

369
SPEC.md
View File

@ -13,9 +13,10 @@ The ReefVM is a stack-based bytecode virtual machine designed for the Shrimp pro
- **Value Stack**: Operand stack for computation - **Value Stack**: Operand stack for computation
- **Call Stack**: Call frames for function invocations - **Call Stack**: Call frames for function invocations
- **Exception Handlers**: Stack of try/catch handlers - **Exception Handlers**: Stack of try/catch handlers
- **Scope Chain**: Linked scopes for lexical variable resolution (includes native functions) - **Scope Chain**: Linked scopes for lexical variable resolution
- **Program Counter (PC)**: Current instruction index - **Program Counter (PC)**: Current instruction index
- **Constants Pool**: Immutable values and function metadata - **Constants Pool**: Immutable values and function metadata
- **Native Function Registry**: External functions callable from Shrimp
### Execution Model ### Execution Model
@ -39,7 +40,6 @@ type Value =
| { type: 'dict', value: Map<string, Value> } | { type: 'dict', value: Map<string, Value> }
| { type: 'function', params: string[], defaults: Record<string, number>, | { type: 'function', params: string[], defaults: Record<string, number>,
body: number, parentScope: Scope, variadic: boolean, named: boolean } body: number, parentScope: Scope, variadic: boolean, named: boolean }
| { type: 'native', fn: NativeFunction, value: '<function>' }
``` ```
### Type Coercion ### Type Coercion
@ -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
@ -459,20 +357,15 @@ The created function captures `currentScope` as its `parentScope`.
3. Pop named arguments (name/value pairs) from stack 3. Pop named arguments (name/value pairs) from stack
4. Pop positional arguments from stack 4. Pop positional arguments from stack
5. Pop function from stack 5. Pop function from stack
6. **If function is native**: 6. Mark current frame (if exists) as break target (`isBreakTarget = true`)
- Mark current frame (if exists) as break target 7. Push new call frame with current PC and scope
- Call native function with positional args 8. Create new scope with function's parentScope as parent
- Push return value onto stack 9. Bind parameters:
- Done (skip steps 7-11)
7. Mark current frame (if exists) as break target (`isBreakTarget = true`)
8. Push new call frame with current PC and scope
9. Create new scope with function's parentScope as parent
10. Bind parameters:
- For regular functions: bind params by position, then by name, then defaults, then null - For regular functions: bind params by position, then by name, then defaults, then null
- For variadic functions: bind fixed params, collect rest into array - For variadic functions: bind fixed params, collect rest into array
- For functions with `named: true`: bind fixed params by position/name, collect unmatched named args into dict - For functions with `named: true`: bind fixed params by position/name, collect unmatched named args into dict
11. Set currentScope to new scope 10. Set currentScope to new scope
12. Jump to function body 11. Jump to function body
**Parameter Binding Priority** (for fixed params): **Parameter Binding Priority** (for fixed params):
1. Named argument (if provided and matches param name) 1. Named argument (if provided and matches param name)
@ -480,21 +373,12 @@ 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
- 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
#### TAIL_CALL #### TAIL_CALL
**Operand**: None **Operand**: None
@ -625,51 +509,6 @@ Key is coerced to string.
Key is coerced to string. Key is coerced to string.
**Errors**: Throws if not dict **Errors**: Throws if not dict
### Unified Access
#### DOT_GET
**Operand**: None
**Effect**: Get value from array or dict
**Stack**: [array|dict, index|key] → [value]
**Behavior**:
- If target is array: coerce index to number and access `array[index]`
- If target is dict: coerce key to string and access `dict.get(key)`
- Returns null if index out of bounds or key not found
**Errors**: Throws if target is not array or dict
**Use Cases**:
- Unified syntax for accessing both arrays and dicts
- Chaining access operations: `obj.users.0.name`
- Generic accessor that works with any indexable type
**Example**:
```
; Array access
PUSH 10
PUSH 20
PUSH 30
MAKE_ARRAY #3
PUSH 1
DOT_GET ; → 20
; Dict access
PUSH 'name'
PUSH 'Alice'
MAKE_DICT #1
PUSH 'name'
DOT_GET ; → 'Alice'
; Chained access
; dict['users'][0]
LOAD dict
PUSH 'users'
DOT_GET
PUSH 0
DOT_GET
```
### String Operations ### String Operations
#### STR_CONCAT #### STR_CONCAT
@ -722,93 +561,28 @@ STR_CONCAT #4 ; → "Count: 42, Active: true"
### TypeScript Interop ### TypeScript Interop
Native TypeScript functions are registered into the VM's scope and accessed via regular LOAD/CALL operations. They behave identically to Reef functions from the bytecode perspective. #### CALL_NATIVE
**Operand**: Function name (string)
**Effect**: Call registered TypeScript function
**Stack**: [...args] → [returnValue]
**Registration**: **Behavior**:
1. Look up function by name in registry
2. Mark current frame (if exists) as break target
3. Await function call (native function receives arguments and returns a Value)
4. Push return value onto stack
**Notes**:
- TypeScript functions are passed the raw stack values as arguments
- They must return a valid Value
- They can be async (VM awaits them)
- Like CALL, but function is from TypeScript registry instead of stack
**Errors**: Throws if function not found
**TypeScript Function Signature**:
```typescript ```typescript
const vm = new VM(bytecode, { type TypeScriptFunction = (...args: Value[]) => Promise<Value> | Value;
add: (a: number, b: number) => a + b,
greet: (name: string) => `Hello, ${name}!`
})
// Or after construction:
vm.set('multiply', (a: number, b: number) => a * b)
```
**Usage in Bytecode**:
```
LOAD add ; Load native function from scope
PUSH 5
PUSH 10
PUSH 2 ; positionalCount
PUSH 0 ; namedCount
CALL ; Call it like any other function
```
**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.
2. **Value-based functions** (via `vm.setValueFunction()`): Accept and return `Value` types directly for full control over type handling.
**Auto-Wrapping Behavior**:
- Parameters: `Value` → native type (number, string, boolean, array, object, null, RegExp)
- Return value: native type → `Value`
- Supports sync and async functions
- Objects convert to dicts, arrays convert to Value arrays
**Named Arguments**:
- Native functions support named arguments by extracting parameter names from the function signature
- Parameter binding follows the same priority as Reef functions: named arg > positional arg > default > null
- TypeScript rest parameters (`...args`) are supported and behave like Reef variadic parameters
**Examples**:
```typescript
// Auto-wrapped native types
vm.set('add', (a: number, b: number) => a + b)
vm.set('greet', (name: string) => `Hello, ${name}!`)
vm.set('range', (n: number) => Array.from({ length: n }, (_, i) => i))
// With defaults
vm.set('greet', (name: string, greeting = 'Hello') => {
return `${greeting}, ${name}!`
})
// Variadic functions
vm.set('sum', (...nums: number[]) => {
return nums.reduce((acc, n) => acc + n, 0)
})
// Value-based for custom logic
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
return { type: 'number', value: toNumber(a) + toNumber(b) }
})
// Async functions
vm.set('fetchData', async (url: string) => {
const response = await fetch(url)
return response.json()
})
```
**Calling with Named Arguments**:
```
; Call with positional args
LOAD greet
PUSH "Alice"
PUSH 1
PUSH 0
CALL ; → "Hello, Alice!"
; Call with named args
LOAD greet
PUSH "name"
PUSH "Bob"
PUSH "greeting"
PUSH "Hi"
PUSH 0
PUSH 2
CALL ; → "Hi, Bob!"
``` ```
### Special ### Special
@ -820,16 +594,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 +612,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 +696,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
@ -984,9 +742,10 @@ All of these should throw errors:
6. **Break Outside Loop**: BREAK with no break target 6. **Break Outside Loop**: BREAK with no break target
7. **Continue Outside Loop**: CONTINUE with no continue target 7. **Continue Outside Loop**: CONTINUE with no continue target
8. **Return Outside Function**: RETURN with no call frame 8. **Return Outside Function**: RETURN with no call frame
9. **Mismatched Handler**: POP_TRY with no handler 9. **Unknown Function**: CALL_NATIVE with unregistered function
10. **Invalid Constant**: PUSH with invalid constant index 10. **Mismatched Handler**: POP_TRY with no handler
11. **Invalid Function Definition**: MAKE_FUNCTION with non-function_def constant 11. **Invalid Constant**: PUSH with invalid constant index
12. **Invalid Function Definition**: MAKE_FUNCTION with non-function_def constant
## Edge Cases ## Edge Cases
@ -1031,21 +790,11 @@ All of these should throw errors:
## VM Initialization ## VM Initialization
```typescript ```typescript
// Register native functions during construction const vm = new VM(bytecode);
const vm = new VM(bytecode, { vm.registerFunction('add', (a, b) => {
add: (a: number, b: number) => a + b,
greet: (name: string) => `Hello, ${name}!`
})
// Or register after construction
vm.set('multiply', (a: number, b: number) => a * b)
// Or use Value-based functions
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
return { type: 'number', value: toNumber(a) + toNumber(b) } return { type: 'number', value: toNumber(a) + toNumber(b) }
}) })
const result = await vm.execute()
const result = await vm.run()
``` ```
## Testing Considerations ## Testing Considerations

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"]
@ -78,8 +73,8 @@ type InstructionTuple =
// Strings // Strings
| ["STR_CONCAT", number] | ["STR_CONCAT", number]
// Arrays and dicts // Native
| ["DOT_GET"] | ["CALL_NATIVE", string]
// Special // Special
| ["HALT"] | ["HALT"]
@ -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, CALL_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>
@ -317,6 +336,7 @@ function toBytecodeFromArray(program: ProgramItem[]): Bytecode /* throws */ {
case "STORE": case "STORE":
case "TRY_LOAD": case "TRY_LOAD":
case "TRY_CALL": case "TRY_CALL":
case "CALL_NATIVE":
operandValue = operand as string operandValue = operand as string
break break
@ -349,29 +369,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 +387,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,202 +0,0 @@
import type { Bytecode, Constant } from "./bytecode"
import { OpCode } from "./opcode"
import type { Value, FunctionDef } from "./value"
/**
* Converts a Bytecode object back to human-readable string format.
* This is the inverse of toBytecode().
*/
export const bytecodeToString = (bytecode: Bytecode): string => {
const lines: string[] = []
const { instructions, constants, labels } = bytecode
for (let i = 0; i < instructions.length; i++) {
// Check if there's a label at this position
if (labels?.has(i)) {
lines.push(`.${labels.get(i)}:`)
}
const instr = instructions[i]!
const opName = OpCode[instr.op] // Get string name from enum
// Format based on whether operand exists
if (instr.operand === undefined) {
lines.push(opName)
} else {
const operandStr = formatOperand(instr.op, instr.operand, constants, labels, i)
lines.push(`${opName} ${operandStr}`)
}
}
return lines.join('\n')
}
/**
* Format an operand based on the opcode type
*/
const formatOperand = (
op: OpCode,
operand: number | string,
constants: Constant[],
labels: Map<number, string> | undefined,
currentIndex: number
): string => {
// Handle string operands (variable names)
if (typeof operand === 'string') {
return operand
}
// Handle numeric operands based on opcode
switch (op) {
case OpCode.PUSH: {
// Look up constant value
const value = constants[operand]
if (!value) {
throw new Error(`Invalid constant index: ${operand}`)
}
return formatConstant(value)
}
case OpCode.MAKE_FUNCTION: {
// Look up function definition and format as (params) .label
const funcDef = constants[operand]
if (!funcDef || !('type' in funcDef) || funcDef.type !== 'function_def') {
throw new Error(`Invalid function definition at constant index: ${operand}`)
}
return formatFunctionDef(funcDef as FunctionDef, labels, constants)
}
case OpCode.JUMP:
case OpCode.JUMP_IF_FALSE:
case OpCode.JUMP_IF_TRUE: {
// Convert relative offset to absolute position
const targetIndex = currentIndex + 1 + operand
const labelName = labels?.get(targetIndex)
return labelName ? `.${labelName}` : `#${operand}`
}
case OpCode.PUSH_TRY:
case OpCode.PUSH_FINALLY: {
// These use absolute positions
const labelName = labels?.get(operand)
return labelName ? `.${labelName}` : `#${operand}`
}
case OpCode.MAKE_ARRAY:
case OpCode.MAKE_DICT:
case OpCode.STR_CONCAT:
// These are just counts
return `#${operand}`
default:
return `#${operand}`
}
}
/**
* Format a constant value (from constants pool)
*/
const formatConstant = (constant: Constant): string => {
// Handle function definitions (shouldn't happen in PUSH, but be safe)
if ('type' in constant && constant.type === 'function_def') {
return '<function_def>'
}
// Handle Value types
const value = constant as Value
switch (value.type) {
case 'null':
return 'null'
case 'boolean':
return value.value.toString()
case 'number':
return value.value.toString()
case 'string':
// Use single quotes and escape special characters
return `'${escapeString(value.value)}'`
case 'regex': {
// Format as /pattern/flags
const pattern = value.value.source
const flags = value.value.flags
return `/${pattern}/${flags}`
}
case 'array': {
// Format as [item1, item2, ...]
const items = value.value.map(formatConstant).join(', ')
return `[${items}]`
}
case 'dict': {
// Format as {key1: value1, key2: value2}
const entries = Array.from(value.value.entries())
.map(([k, v]) => `${k}: ${formatConstant(v)}`)
.join(', ')
return `{${entries}}`
}
case 'function':
case 'native':
return '<function>'
default:
return '<unknown>'
}
}
/**
* Escape special characters in strings for output
*/
const escapeString = (str: string): string => {
return str
.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
.replace(/\n/g, '\\n')
.replace(/\t/g, '\\t')
.replace(/\r/g, '\\r')
.replace(/\$/g, '\\$')
}
/**
* Format a function definition as (params) .label
*/
const formatFunctionDef = (
funcDef: FunctionDef,
labels: Map<number, string> | undefined,
constants: Constant[]
): string => {
const params: string[] = []
for (let i = 0; i < funcDef.params.length; i++) {
const paramName = funcDef.params[i]!
const defaultIndex = funcDef.defaults[paramName]
if (defaultIndex !== undefined) {
// Parameter has a default value
const defaultValue = constants[defaultIndex]
if (!defaultValue) {
throw new Error(`Invalid default value index: ${defaultIndex}`)
}
params.push(`${paramName}=${formatConstant(defaultValue)}`)
} else if (i === funcDef.params.length - 1 && funcDef.variadic) {
// Last parameter and function is variadic
params.push(`...${paramName}`)
} else if (i === funcDef.params.length - 1 && funcDef.named) {
// Last parameter and function accepts named args
params.push(`@${paramName}`)
} else {
// Regular parameter
params.push(paramName)
}
}
// Format body address (prefer label name if available)
const bodyLabel = labels?.get(funcDef.body)
const bodyStr = bodyLabel ? `.${bodyLabel}` : `#${funcDef.body}`
return `(${params.join(' ')}) ${bodyStr}`
}

View File

@ -1,112 +0,0 @@
import { type Value, type NativeFunction, fromValue, toValue } from "./value"
import { VM } from "./vm"
export type ParamInfo = {
params: string[]
defaults: Record<string, Value>
variadic: boolean
named: boolean
}
const WRAPPED_MARKER = Symbol('reef-wrapped')
export function wrapNative(vm: VM, fn: Function): (this: VM, ...args: Value[]) => Promise<Value> {
if ((fn as any).raw) return fn as (this: VM, ...args: Value[]) => Promise<Value>
const wrapped = async function (this: VM, ...values: Value[]) {
const nativeArgs = values.map(arg => fromValue(arg, vm))
const result = await fn.call(this, ...nativeArgs)
return toValue(result, this)
}
const wrappedObj = wrapped as any
wrappedObj[WRAPPED_MARKER] = true
// Store the original function for param extraction
wrappedObj.originalFn = fn
return wrapped
}
export function isWrapped(fn: Function): boolean {
return !!(fn as any)[WRAPPED_MARKER]
}
export function getOriginalFunction(fn: NativeFunction): Function {
return (fn as any).originalFn || fn
}
export function extractParamInfo(fn: Function): ParamInfo {
const params: string[] = []
const defaults: Record<string, Value> = {}
let variadic = false
let named = false
const fnStr = fn.toString()
// Match function signature: function(a, b) or (a, b) => or async (a, b) =>
const match = fnStr.match(/(?:function\s*.*?\(|^\s*\(|^\s*async\s*\(|^\s*async\s+function\s*.*?\()([^)]*)\)/)
if (!match || !match[1]) {
return { params, defaults, variadic, named }
}
const paramStr = match[1].trim()
if (!paramStr) {
return { params, defaults, variadic, named }
}
// Split parameters by comma (naive - doesn't handle nested objects/arrays)
const paramParts = paramStr.split(',').map(p => p.trim())
for (const part of paramParts) {
// Check for rest parameters (...rest)
if (part.startsWith('...')) {
variadic = true
const paramName = part.slice(3).trim()
params.push(paramName)
}
// Check for default values (name = value)
else if (part.includes('=')) {
const eqIndex = part.indexOf('=')
const paramName = part.slice(0, eqIndex).trim()
const defaultStr = part.slice(eqIndex + 1).trim()
params.push(paramName)
// Check if this is a named parameter (atXxx pattern)
if (/^at[A-Z]/.test(paramName)) {
named = true
}
// Try to parse the default value (only simple literals)
try {
if (defaultStr === 'null') {
defaults[paramName] = toValue(null)
} else if (defaultStr === 'true') {
defaults[paramName] = toValue(true)
} else if (defaultStr === 'false') {
defaults[paramName] = toValue(false)
} else if (/^-?\d+(\.\d+)?$/.test(defaultStr)) {
defaults[paramName] = toValue(parseFloat(defaultStr))
} else if (/^['"].*['"]$/.test(defaultStr)) {
defaults[paramName] = toValue(defaultStr.slice(1, -1))
}
// For complex defaults (like {}), we skip them and let the function's own default be used
} catch {
// If parsing fails, ignore the default
}
} else {
// Regular parameter
const paramName = part.trim()
params.push(paramName)
// Check if this is a named parameter (atXxx pattern)
if (/^at[A-Z]/.test(paramName)) {
named = true
}
}
}
return { params, defaults, variadic, named }
}

View File

@ -1,17 +1,12 @@
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): Promise<Value> {
const vm = new VM(bytecode, globals) const vm = new VM(bytecode)
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 { type Value, toValue, toString, toNumber, fromValue, toNull, wrapNative } from "./value"
export { wrapNative, isWrapped, type ParamInfo, extractParamInfo, getOriginalFunction } from "./function"
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"

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]
@ -71,12 +59,12 @@ export enum OpCode {
DICT_SET, // operand: none | stack: [dict, key, value] → [] | mutates dict DICT_SET, // operand: none | stack: [dict, key, value] → [] | mutates dict
DICT_HAS, // operand: none | stack: [dict, key] → [boolean] DICT_HAS, // operand: none | stack: [dict, key] → [boolean]
// arrays and dicts
DOT_GET, // operand: none | stack: [array|dict, index|key] → [value] | unified accessor, returns null if missing
// strings // strings
STR_CONCAT, // operand: value count (number) | stack: [val1, ..., valN] → [string] | concatenate N values STR_CONCAT, // operand: value count (number) | stack: [val1, ..., valN] → [string] | concatenate N values
// typescript interop
CALL_NATIVE, // operand: function name (identifier) | stack: [...args] → [result] | consumes entire stack
// special // special
HALT // operand: none | stop execution HALT // operand: none | stop execution
} }

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

@ -45,24 +45,17 @@ const OPCODES_WITH_OPERANDS = new Set([
OpCode.MAKE_DICT, OpCode.MAKE_DICT,
OpCode.STR_CONCAT, OpCode.STR_CONCAT,
OpCode.MAKE_FUNCTION, OpCode.MAKE_FUNCTION,
OpCode.CALL_NATIVE,
]) ])
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,
@ -84,18 +77,13 @@ const OPCODES_WITHOUT_OPERANDS = new Set([
OpCode.DICT_GET, OpCode.DICT_GET,
OpCode.DICT_SET, OpCode.DICT_SET,
OpCode.DICT_HAS, OpCode.DICT_HAS,
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,4 @@
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 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 }
@ -16,7 +8,6 @@ export type Value =
| { type: 'array', value: Value[] } | { type: 'array', value: Value[] }
| { type: 'dict', value: Dict } | { type: 'dict', value: Dict }
| { type: 'regex', value: RegExp } | { type: 'regex', value: RegExp }
| { type: 'native', fn: NativeFunction, value: '<function>' }
| { | {
type: 'function', type: 'function',
params: string[], params: string[],
@ -39,21 +30,17 @@ 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 }
switch (typeof v) { switch (typeof v) {
@ -64,38 +51,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}`
)
} }
} }
@ -137,8 +101,6 @@ export function toString(v: Value): string {
return 'null' return 'null'
case 'function': case 'function':
return '<function>' return '<function>'
case 'native':
return '<function>'
case 'array': case 'array':
return `[${v.value.map(toString).join(', ')}]` return `[${v.value.map(toString).join(', ')}]`
case 'dict': { case 'dict': {
@ -159,7 +121,9 @@ export function isEqual(a: Value, b: Value): boolean {
case 'null': case 'null':
return true return true
case 'boolean': case 'boolean':
return a.value === b.value
case 'number': case 'number':
return a.value === b.value
case 'string': case 'string':
return a.value === b.value return a.value === b.value
case 'array': { case 'array': {
@ -180,44 +144,30 @@ export function isEqual(a: Value, b: Value): boolean {
return String(a.value) === String(b.value) return String(a.value) === String(b.value)
} }
case 'function': case 'function':
case 'native':
return false // functions never equal return false // functions never equal
default: default:
return false return false
} }
} }
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)) { return '<function>'
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':
return getOriginalFunction(v.fn)
} }
} }
@ -225,49 +175,21 @@ export function toNull(): Value {
return toValue(null) return toValue(null)
} }
export function fnFromValue(fn: Value, vm: VM): Function { const WRAPPED_MARKER = Symbol('reef-wrapped')
if (fn.type !== 'function')
throw new Error('Value is not a function')
const wrapper = async function (...args: any[]) { export function wrapNative(fn: Function): (...args: Value[]) => Promise<Value> {
let positional: any[] = args const wrapped = async (...values: Value[]) => {
let named: Record<string, any> = {} const nativeArgs = values.map(fromValue)
const result = await fn(...nativeArgs)
if (args.length > 0 && !Array.isArray(args[args.length - 1]) && args[args.length - 1].constructor === Object) { return toValue(result)
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)) const wrappedObj = wrapped as any
; (wrapper as any)[REEF_FUNCTION] = fn wrappedObj[WRAPPED_MARKER] = true
return wrapper return wrapped
} }
export function isWrapped(fn: Function): boolean {
return !!(fn as any)[WRAPPED_MARKER]
}

370
src/vm.ts
View File

@ -3,9 +3,9 @@ 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, toValue, toNumber, isTrue, isEqual, toString, wrapNative, isWrapped } from "./value"
import { toValue, toNumber, isTrue, isEqual, toString, fromValue, fnFromValue } from "./value"
import { extractParamInfo, getOriginalFunction } from "./function" type NativeFunction = (...args: Value[]) => Promise<Value> | Value
export class VM { export class VM {
pc = 0 pc = 0
@ -19,69 +19,20 @@ 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) {
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) {
for (const name of Object.keys(globals ?? {}))
this.set(name, globals[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.nativeFunctions.set(name, wrapped)
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) this.nativeFunctions.set(name, fn)
}
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>' })
}
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> {
@ -97,57 +48,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 +68,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 +117,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 +137,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()!
@ -488,22 +334,6 @@ export class VM {
this.stack.push({ type: 'boolean', value: hasDict.value.has(toString(hasKey)) }) this.stack.push({ type: 'boolean', value: hasDict.value.has(toString(hasKey)) })
break break
case OpCode.DOT_GET: {
const index = this.stack.pop()!
const target = this.stack.pop()!
if (target.type === 'array')
this.stack.push(toValue(target.value?.[Number(index.value)], this))
else if (target.type === 'dict')
this.stack.push(toValue(target.value?.get(String(index.value)), this))
else
throw new Error(`DOT_GET: ${target.type} not supported`)
break
}
case OpCode.STR_CONCAT: case OpCode.STR_CONCAT:
let count = instruction.operand as number let count = instruction.operand as number
let parts = [] let parts = []
@ -533,17 +363,16 @@ export class VM {
}) })
break break
// @ts-ignore
case OpCode.TRY_CALL: { case OpCode.TRY_CALL: {
const varName = instruction.operand as string const varName = instruction.operand as string
const value = this.scope.get(varName) const value = this.scope.get(varName)
if (value?.type === 'function' || value?.type === 'native') { if (value?.type === 'function') {
this.stack.push(value) this.stack.push(value)
this.stack.push(toValue(0)) this.stack.push(toValue(0))
this.stack.push(toValue(0)) this.stack.push(toValue(0))
this.instructions[this.pc] = { op: OpCode.CALL } // No `break` here -- we want to fall through to OpCode.CALL!
this.pc--
break
} else if (value) { } else if (value) {
this.stack.push(value) this.stack.push(value)
break break
@ -553,6 +382,8 @@ export class VM {
} }
} }
// don't put any `case` statement here - `TRY_CALL` MUST go before `CALL!`
case OpCode.CALL: { case OpCode.CALL: {
// Pop named count from stack (top) // Pop named count from stack (top)
const namedCount = toNumber(this.stack.pop()!) const namedCount = toNumber(this.stack.pop()!)
@ -580,110 +411,8 @@ export class VM {
const fn = this.stack.pop()! const fn = this.stack.pop()!
// Handle native functions
if (fn.type === 'native') {
// Mark current frame as break target (like regular CALL does)
if (this.callStack.length > 0)
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
// Extract parameter info on-demand
const originalFn = getOriginalFunction(fn.fn)
const paramInfo = extractParamInfo(originalFn)
// Bind parameters using the same priority as Reef functions
const nativeArgs: Value[] = []
// Determine how many params are fixed (excluding variadic and named)
let nativeFixedParamCount = paramInfo.params.length
if (paramInfo.variadic) nativeFixedParamCount--
if (paramInfo.named) nativeFixedParamCount--
// Track which positional args have been consumed
let nativePositionalArgIndex = 0
// 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++) {
const paramName = paramInfo.params[i]!
let paramValue: Value | undefined
// Check if named argument was provided for this param
if (namedArgs.has(paramName)) {
paramValue = namedArgs.get(paramName)!
namedArgs.delete(paramName) // Remove from named args so it won't go to @named
} else if (nativePositionalArgIndex < positionalArgs.length) {
paramValue = positionalArgs[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) {
nativeArgs.push(paramInfo.defaults[paramName]!)
} else {
nativeArgs.push(toValue(null))
}
}
// Handle named parameter (collect remaining unmatched named args)
// Parameter names matching atXxx pattern (e.g., atOptions, atNamed) collect extra named args
if (paramInfo.named) {
const namedDict = new Map<string, Value>()
for (const [key, value] of namedArgs) {
namedDict.set(key, value)
}
// Convert dict to plain JavaScript object for the native function
const namedObj = fromValue({ type: 'dict', value: namedDict }, this)
nativeArgs.push(toValue(namedObj, this))
}
// Handle variadic parameter (TypeScript rest parameters)
// For TypeScript functions with ...rest, we spread the remaining args
// rather than wrapping them in an array
if (paramInfo.variadic) {
const remainingArgs = positionalArgs.slice(nativePositionalArgIndex)
nativeArgs.push(...remainingArgs)
}
// Call the native function with bound args
try {
const result = await fn.fn.call(this, ...nativeArgs)
this.stack.push(result)
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 +434,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 +504,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)
@ -856,8 +572,30 @@ export class VM {
this.stack.push(returnValue) this.stack.push(returnValue)
break break
case OpCode.CALL_NATIVE:
const functionName = instruction.operand as string
const tsFunction = this.nativeFunctions.get(functionName)
if (!tsFunction)
throw new Error(`CALL_NATIVE: function not found: ${functionName}`)
// Mark current frame as break target (like CALL does)
if (this.callStack.length > 0)
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
// Pop all arguments from stack (TypeScript function consumes entire stack)
const tsArgs = [...this.stack]
this.stack = []
// Call the TypeScript function and await if necessary
const tsResult = await tsFunction(...tsArgs)
// Push result back onto stack
this.stack.push(tsResult)
break
default: default:
throw new Error(`Unknown op: ${instruction.op}`) throw `Unknown op: ${instruction.op}`
} }
} }
@ -874,26 +612,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)
}
} }

1064
tests/basic.test.ts Normal file

File diff suppressed because it is too large Load Diff

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

@ -1,232 +0,0 @@
import { test, expect, describe } from "bun:test"
import { run, VM } from "#index"
import { toBytecode } from "#bytecode"
describe("functions parameter", () => {
test("pass functions to run()", async () => {
const bytecode = toBytecode(`
LOAD add
PUSH 5
PUSH 3
PUSH 2
PUSH 0
CALL
HALT
`)
const result = await run(bytecode, {
add: (a: number, b: number) => a + b
})
expect(result).toEqual({ type: 'number', value: 8 })
})
test("pass functions to VM constructor", async () => {
const bytecode = toBytecode(`
LOAD multiply
PUSH 10
PUSH 2
PUSH 2
PUSH 0
CALL
HALT
`)
const vm = new VM(bytecode, {
multiply: (a: number, b: number) => a * b
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 20 })
})
test("pass multiple functions", async () => {
const bytecode = toBytecode(`
LOAD add
PUSH 10
PUSH 5
PUSH 2
PUSH 0
CALL
STORE sum
LOAD multiply
LOAD sum
PUSH 3
PUSH 2
PUSH 0
CALL
HALT
`)
const result = await run(bytecode, {
add: (a: number, b: number) => a + b,
multiply: (a: number, b: number) => a * b
})
expect(result).toEqual({ type: 'number', value: 45 })
})
test("auto-wraps native functions", async () => {
const bytecode = toBytecode(`
LOAD concat
PUSH "hello"
PUSH "world"
PUSH 2
PUSH 0
CALL
HALT
`)
const result = await run(bytecode, {
concat: (a: string, b: string) => a + " " + b
})
expect(result).toEqual({ type: 'string', value: 'hello world' })
})
test("works with async functions", async () => {
const bytecode = toBytecode(`
LOAD delay
PUSH 100
PUSH 1
PUSH 0
CALL
HALT
`)
const result = await run(bytecode, {
delay: async (n: number) => {
await new Promise(resolve => setTimeout(resolve, 1))
return n * 2
}
})
expect(result).toEqual({ type: 'number', value: 200 })
})
test("can combine with manual vm.set", async () => {
const bytecode = toBytecode(`
LOAD add
PUSH 5
PUSH 3
PUSH 2
PUSH 0
CALL
STORE sum
LOAD subtract
LOAD sum
PUSH 2
PUSH 2
PUSH 0
CALL
HALT
`)
const vm = new VM(bytecode, {
add: (a: number, b: number) => a + b
})
// Register another function manually
vm.set('subtract', (a: number, b: number) => a - b)
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 6 })
})
test("no functions parameter (undefined)", async () => {
const bytecode = toBytecode(`
PUSH 42
HALT
`)
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 42 })
})
test("empty functions object", async () => {
const bytecode = toBytecode(`
PUSH 99
HALT
`)
const result = await run(bytecode, {})
expect(result).toEqual({ type: 'number', value: 99 })
})
test("function throws error", async () => {
const bytecode = toBytecode(`
LOAD divide
PUSH 0
PUSH 1
PUSH 0
CALL
HALT
`)
try {
await run(bytecode, {
divide: (n: number) => {
if (n === 0) throw new Error("Cannot divide by zero")
return 100 / n
}
})
expect(true).toBe(false) // Should not reach here
} catch (e: any) {
expect(e.message).toContain("Cannot divide by zero")
}
})
test("complex workflow with multiple function calls", async () => {
const bytecode = toBytecode(`
LOAD add
PUSH 5
PUSH 3
PUSH 2
PUSH 0
CALL
STORE result
LOAD multiply
LOAD result
PUSH 2
PUSH 2
PUSH 0
CALL
STORE final
LOAD format
LOAD final
PUSH 1
PUSH 0
CALL
HALT
`)
const result = await run(bytecode, {
add: (a: number, b: number) => a + b,
multiply: (a: number, b: number) => a * b,
format: (n: number) => `Result: ${n}`
})
expect(result).toEqual({ type: 'string', value: 'Result: 16' })
})
test("function overriding - later registration wins", async () => {
const bytecode = toBytecode(`
LOAD getValue
PUSH 5
PUSH 1
PUSH 0
CALL
HALT
`)
const vm = new VM(bytecode, {
getValue: () => 100
})
// Override with manual registration
vm.set('getValue', () => 200)
const result = await vm.run()
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

@ -1,459 +0,0 @@
import { test, expect, describe } from "bun:test"
import { run } from "#index"
import { toBytecode } from "#bytecode"
describe("RegExp", () => {
test("basic pattern parsing", async () => {
const str = `
PUSH /hello/
`
const result = await run(toBytecode(str))
expect(result.type).toBe('regex')
if (result.type === 'regex') {
expect(result.value.source).toBe('hello')
expect(result.value.flags).toBe('')
}
})
test("pattern with flags", async () => {
const str = `
PUSH /test/gi
`
const result = await run(toBytecode(str))
expect(result.type).toBe('regex')
if (result.type === 'regex') {
expect(result.value.source).toBe('test')
expect(result.value.global).toBe(true)
expect(result.value.ignoreCase).toBe(true)
}
})
test("multiple flag combinations", async () => {
// Test i flag
const str1 = `
PUSH /pattern/i
`
const result1 = await run(toBytecode(str1))
expect(result1.type).toBe('regex')
if (result1.type === 'regex') {
expect(result1.value.ignoreCase).toBe(true)
}
// Test g flag
const str2 = `
PUSH /pattern/g
`
const result2 = await run(toBytecode(str2))
expect(result2.type).toBe('regex')
if (result2.type === 'regex') {
expect(result2.value.global).toBe(true)
}
// Test m flag
const str3 = `
PUSH /pattern/m
`
const result3 = await run(toBytecode(str3))
expect(result3.type).toBe('regex')
if (result3.type === 'regex') {
expect(result3.value.multiline).toBe(true)
}
// Test combined flags
const str4 = `
PUSH /pattern/gim
`
const result4 = await run(toBytecode(str4))
expect(result4.type).toBe('regex')
if (result4.type === 'regex') {
expect(result4.value.global).toBe(true)
expect(result4.value.ignoreCase).toBe(true)
expect(result4.value.multiline).toBe(true)
}
})
test("complex patterns", async () => {
// Character class
const str1 = `
PUSH /[a-z0-9]+/
`
const result1 = await run(toBytecode(str1))
expect(result1.type).toBe('regex')
if (result1.type === 'regex') {
expect(result1.value.source).toBe('[a-z0-9]+')
}
// Quantifiers
const str2 = `
PUSH /a{2,4}/
`
const result2 = await run(toBytecode(str2))
expect(result2.type).toBe('regex')
if (result2.type === 'regex') {
expect(result2.value.source).toBe('a{2,4}')
}
// Groups and alternation
const str3 = `
PUSH /(foo|bar)/
`
const result3 = await run(toBytecode(str3))
expect(result3.type).toBe('regex')
if (result3.type === 'regex') {
expect(result3.value.source).toBe('(foo|bar)')
}
// Anchors and special chars
const str4 = `
PUSH /^[a-z]+$/
`
const result4 = await run(toBytecode(str4))
expect(result4.type).toBe('regex')
if (result4.type === 'regex') {
expect(result4.value.source).toBe('^[a-z]+$')
}
})
test("escaping special characters", async () => {
const str = `
PUSH /\\d+\\.\\d+/
`
const result = await run(toBytecode(str))
expect(result.type).toBe('regex')
if (result.type === 'regex') {
expect(result.value.source).toBe('\\d+\\.\\d+')
}
})
test("store and load", async () => {
const str = `
PUSH /test/i
STORE pattern
LOAD pattern
`
const result = await run(toBytecode(str))
expect(result.type).toBe('regex')
if (result.type === 'regex') {
expect(result.value.source).toBe('test')
expect(result.value.ignoreCase).toBe(true)
}
})
test("TRY_LOAD with regex", async () => {
const str = `
PUSH /hello/g
STORE regex
TRY_LOAD regex
`
const result = await run(toBytecode(str))
expect(result.type).toBe('regex')
if (result.type === 'regex') {
expect(result.value.source).toBe('hello')
expect(result.value.global).toBe(true)
}
})
test("NEQ comparison", async () => {
const str = `
PUSH /foo/
PUSH /bar/
NEQ
`
await expect(str).toBeBoolean(true)
const str2 = `
PUSH /test/i
PUSH /test/i
NEQ
`
await expect(str2).toBeBoolean(false)
})
test("is truthy", async () => {
// Regex values should be truthy (not null or false)
const str = `
PUSH /test/
JUMP_IF_FALSE .end
PUSH 42
.end:
`
await expect(str).toBeNumber(42)
})
test("NOT returns false (regex is truthy)", async () => {
const str = `
PUSH /pattern/
NOT
`
await expect(str).toBeBoolean(false)
})
test("in arrays", async () => {
const str = `
PUSH /first/
PUSH /second/i
PUSH /third/g
MAKE_ARRAY #3
`
const result = await run(toBytecode(str))
expect(result.type).toBe('array')
if (result.type === 'array') {
expect(result.value).toHaveLength(3)
expect(result.value[0]!.type).toBe('regex')
if (result.value[0]!.type === 'regex') {
expect(result.value[0]!.value.source).toBe('first')
}
expect(result.value[1]!.type).toBe('regex')
if (result.value[1]!.type === 'regex') {
expect(result.value[1]!.value.source).toBe('second')
expect(result.value[1]!.value.ignoreCase).toBe(true)
}
expect(result.value[2]!.type).toBe('regex')
if (result.value[2]!.type === 'regex') {
expect(result.value[2]!.value.source).toBe('third')
expect(result.value[2]!.value.global).toBe(true)
}
}
})
test("retrieve from array", async () => {
const str = `
PUSH /pattern/i
PUSH /test/g
MAKE_ARRAY #2
PUSH 1
ARRAY_GET
`
const result = await run(toBytecode(str))
expect(result.type).toBe('regex')
if (result.type === 'regex') {
expect(result.value.source).toBe('test')
expect(result.value.global).toBe(true)
}
})
test("in dicts", async () => {
const str = `
PUSH 'email'
PUSH /^[a-z@.]+$/i
PUSH 'phone'
PUSH /\\d{3}-\\d{4}/
MAKE_DICT #2
`
const result = await run(toBytecode(str))
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.size).toBe(2)
const email = result.value.get('email')
expect(email?.type).toBe('regex')
if (email?.type === 'regex') {
expect(email.value.source).toBe('^[a-z@.]+$')
expect(email.value.ignoreCase).toBe(true)
}
const phone = result.value.get('phone')
expect(phone?.type).toBe('regex')
if (phone?.type === 'regex') {
expect(phone.value.source).toBe('\\d{3}-\\d{4}')
}
}
})
test("retrieve from dict", async () => {
const str = `
PUSH 'pattern'
PUSH /test/gim
MAKE_DICT #1
PUSH 'pattern'
DICT_GET
`
const result = await run(toBytecode(str))
expect(result.type).toBe('regex')
if (result.type === 'regex') {
expect(result.value.source).toBe('test')
expect(result.value.global).toBe(true)
expect(result.value.ignoreCase).toBe(true)
expect(result.value.multiline).toBe(true)
}
})
test("with STR_CONCAT converts to string", async () => {
const str = `
PUSH "Pattern: "
PUSH /test/gi
STR_CONCAT #2
`
const result = await run(toBytecode(str))
expect(result.type).toBe('string')
if (result.type === 'string') {
expect(result.value).toBe('Pattern: /test/gi')
}
})
test("multiple regex in STR_CONCAT", async () => {
const str = `
PUSH /foo/
PUSH " and "
PUSH /bar/i
STR_CONCAT #3
`
await expect(str).toBeString('/foo/ and /bar/i')
})
test("DUP with regex", async () => {
const str = `
PUSH /pattern/i
DUP
EQ
`
// Same regex duplicated should be equal
await expect(str).toBeBoolean(true)
})
test("empty pattern", async () => {
const str = `
PUSH //
`
const result = await run(toBytecode(str))
expect(result.type).toBe('regex')
if (result.type === 'regex') {
expect(result.value.source).toBe('(?:)')
}
})
test("pattern with forward slashes escaped", async () => {
const str = `
PUSH /https:\\/\\//
`
const result = await run(toBytecode(str))
expect(result.type).toBe('regex')
if (result.type === 'regex') {
expect(result.value.source).toBe('https:\\/\\/')
}
})
test("unicode patterns", async () => {
const str = `
PUSH //
`
const result = await run(toBytecode(str))
expect(result.type).toBe('regex')
if (result.type === 'regex') {
expect(result.value.source).toBe('こんにちは')
}
})
test("emoji in pattern", async () => {
const str = `
PUSH /🎉+/
`
const result = await run(toBytecode(str))
expect(result.type).toBe('regex')
if (result.type === 'regex') {
expect(result.value.source).toBe('🎉+')
}
})
test("comparing different regex types", async () => {
// Different patterns
const str1 = `
PUSH /abc/
PUSH /xyz/
EQ
`
await expect(str1).toBeBoolean(false)
// Same pattern, different flags
const str2 = `
PUSH /test/
PUSH /test/i
EQ
`
await expect(str2).toBeBoolean(false)
// Different order of flags (should be equal)
const str3 = `
PUSH /test/ig
PUSH /test/gi
EQ
`
await expect(str3).toBeBoolean(true)
})
test("with native functions", async () => {
const { VM } = await import("#vm")
const bytecode = toBytecode(`
LOAD match
PUSH "hello world"
PUSH /world/
PUSH 2
PUSH 0
CALL
HALT
`)
const vm = new VM(bytecode)
// Register a native function that takes a string and regex
vm.set('match', (str: string, pattern: RegExp) => {
return pattern.test(str)
})
const result = await vm.run()
expect(result).toEqual({ type: 'boolean', value: true })
})
test("native function with regex replacement", async () => {
const { VM } = await import("#vm")
const bytecode = toBytecode(`
LOAD replace
PUSH "hello world"
PUSH /o/g
PUSH "0"
PUSH 3
PUSH 0
CALL
HALT
`)
const vm = new VM(bytecode)
vm.set('replace', (str: string, pattern: RegExp, replacement: string) => {
return str.replace(pattern, replacement)
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'hell0 w0rld' })
})
test("native function extracting matches", async () => {
const { VM } = await import("#vm")
const bytecode = toBytecode(`
LOAD extractNumbers
PUSH "test123abc456"
PUSH /\\d+/g
PUSH 2
PUSH 0
CALL
HALT
`)
const vm = new VM(bytecode)
vm.set('extractNumbers', (str: string, pattern: RegExp) => {
return str.match(pattern) || []
})
const result = await vm.run()
expect(result.type).toBe('array')
if (result.type === 'array') {
expect(result.value).toHaveLength(2)
expect(result.value[0]).toEqual({ type: 'string', value: '123' })
expect(result.value[1]).toEqual({ type: 'string', value: '456' })
}
})
})

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

@ -1,119 +0,0 @@
import { test, expect, describe } from "bun:test"
import { run } from "#index"
import { toBytecode } from "#bytecode"
describe("Unicode and Emoji", () => {
test("emoji variable names - string format", async () => {
const bytecode = toBytecode(`
PUSH 5
STORE 💎
LOAD 💎
HALT
`)
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 5 })
})
test("emoji variable names - array format", async () => {
const bytecode = toBytecode([
["PUSH", 100],
["STORE", "💰"],
["LOAD", "💰"],
["PUSH", 50],
["ADD"],
["HALT"]
])
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 150 })
})
test("unicode variable names - Japanese", async () => {
const bytecode = toBytecode(`
PUSH 42
STORE
LOAD
HALT
`)
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 42 })
})
test("unicode variable names - Chinese", async () => {
const bytecode = toBytecode([
["PUSH", 888],
["STORE", "数字"],
["LOAD", "数字"],
["HALT"]
])
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 888 })
})
test("emoji in function parameters", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (💎 🌟) .add
STORE add
JUMP .after
.add:
LOAD 💎
LOAD 🌟
ADD
RETURN
.after:
LOAD add
PUSH 10
PUSH 20
PUSH 2
PUSH 0
CALL
HALT
`)
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 30 })
})
test("emoji with defaults and variadic", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["🎯=100", "...🎨"], ".fn"],
["STORE", "fn"],
["JUMP", ".after"],
[".fn:"],
["LOAD", "🎯"],
["RETURN"],
[".after:"],
["LOAD", "fn"],
["PUSH", 0],
["PUSH", 0],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 100 })
})
test("mixed emoji and regular names", async () => {
const bytecode = toBytecode([
["PUSH", 10],
["STORE", "💎"],
["PUSH", 20],
["STORE", "value"],
["PUSH", 30],
["STORE", "🌟"],
["LOAD", "💎"],
["LOAD", "value"],
["ADD"],
["LOAD", "🌟"],
["ADD"],
["HALT"]
])
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 60 })
})
})

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()
})
})