Compare commits
53 Commits
varadic-an
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b2a6021fb8 | |||
| 5350bb8c2b | |||
| 47c6d3b32f | |||
| 5ade608278 | |||
| 614f5c4f91 | |||
| 3e2e68b31f | |||
| d7a971db24 | |||
| f439c25742 | |||
| 47e227f50c | |||
| 15884ac239 | |||
| bffb83a528 | |||
| bd1736b474 | |||
| f4e24f427f | |||
| e7201e691c | |||
| 11b119a322 | |||
| 33ea94a247 | |||
| f1cc717711 | |||
| 54cd9ce8e8 | |||
| 0f39e9401e | |||
| 676f53c66b | |||
| fa021e3f18 | |||
| 4b2fd61554 | |||
| c69b172c78 | |||
| 0b5d3e634c | |||
| ba8376e2c3 | |||
| 956fd576f8 | |||
| 9618dd6414 | |||
| b58f848a65 | |||
| 3647159286 | |||
| 030eb74871 | |||
| 052f989e82 | |||
|
|
e542070677 | ||
| 97b6722a11 | |||
| d50b143c9d | |||
| e300946c48 | |||
| bf6607d368 | |||
| da61c1de50 | |||
| d359d6e27d | |||
| eb128ec831 | |||
| 286d5ff943 | |||
| aa8ecb7cf6 | |||
| bbdfcdb54a | |||
| 8d9510e9ae | |||
| 17d846b999 | |||
| 1fb5effb0a | |||
| 937861e27b | |||
| 42c0e62597 | |||
| 46829df28b | |||
| e1e7cdf1ef | |||
| f79fea33c5 | |||
| 797eb281cb | |||
| 91d3eb43e4 | |||
| eb4f103ba3 |
145
CLAUDE.md
145
CLAUDE.md
|
|
@ -55,7 +55,7 @@ No build step required - Bun runs TypeScript directly.
|
||||||
|
|
||||||
### Critical Design Decisions
|
### Critical Design Decisions
|
||||||
|
|
||||||
**Relative jumps**: All JUMP instructions use PC-relative offsets (not absolute addresses), making bytecode position-independent. PUSH_TRY/PUSH_FINALLY use absolute addresses.
|
**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.
|
||||||
|
|
||||||
**Truthiness semantics**: Only `null` and `false` are falsy. Unlike JavaScript, `0`, `""`, empty arrays, and empty dicts are truthy.
|
**Truthiness semantics**: Only `null` and `false` are falsy. Unlike JavaScript, `0`, `""`, empty arrays, and empty dicts are truthy.
|
||||||
|
|
||||||
|
|
@ -137,40 +137,100 @@ 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
|
### Native Function Registration and Global Values
|
||||||
|
|
||||||
**Option 1**: Pass to `run()` or `VM` constructor (convenience)
|
**Option 1**: Pass to `run()` or `VM` constructor (convenience)
|
||||||
```typescript
|
```typescript
|
||||||
const result = await run(bytecode, {
|
const result = await run(bytecode, {
|
||||||
add: (a: number, b: number) => a + b,
|
add: (a: number, b: number) => a + b,
|
||||||
greet: (name: string) => `Hello, ${name}!`
|
greet: (name: string) => `Hello, ${name}!`,
|
||||||
|
pi: 3.14159,
|
||||||
|
config: { debug: true, port: 8080 }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Or with VM constructor
|
// Or with VM constructor
|
||||||
const vm = new VM(bytecode, { add, greet })
|
const vm = new VM(bytecode, { add, greet, pi, config })
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option 2**: Register with `vm.registerFunction()` (manual)
|
**Option 2**: Set values with `vm.set()` (manual)
|
||||||
```typescript
|
```typescript
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
vm.registerFunction('add', (a: number, b: number) => a + b)
|
|
||||||
|
// Set functions (auto-wrapped to native functions)
|
||||||
|
vm.set('add', (a: number, b: number) => a + b)
|
||||||
|
|
||||||
|
// Set any other values (auto-converted to ReefVM Values)
|
||||||
|
vm.set('pi', 3.14159)
|
||||||
|
vm.set('config', { debug: true, port: 8080 })
|
||||||
|
|
||||||
await vm.run()
|
await vm.run()
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option 3**: Register Value-based functions (for direct Value access)
|
**Option 3**: Set Value-based functions with `vm.setValueFunction()` (advanced)
|
||||||
|
|
||||||
|
For functions that work directly with ReefVM Value types:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
|
// Set Value-based function (no wrapping, works directly with Values)
|
||||||
|
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
return toValue(toNumber(a) + toNumber(b))
|
return toValue(toNumber(a) + toNumber(b))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await vm.run()
|
||||||
```
|
```
|
||||||
|
|
||||||
Auto-wrapping handles:
|
Auto-wrapping handles:
|
||||||
- Value ↔ native type conversion (`fromValue`/`toValue`)
|
- Functions: wrapped as native functions with Value ↔ native type conversion
|
||||||
- Sync and async functions
|
- Sync and async functions
|
||||||
- Arrays, objects, primitives, null, RegExp
|
- Arrays, objects, primitives, null, RegExp
|
||||||
|
- All values converted via `toValue()`
|
||||||
|
|
||||||
### Label Usage (Preferred)
|
### Calling Functions from TypeScript
|
||||||
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
|
||||||
|
|
@ -180,6 +240,67 @@ 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`
|
||||||
|
|
@ -365,7 +486,7 @@ Run `bun test` to verify all tests pass before committing.
|
||||||
|
|
||||||
## Common Gotchas
|
## Common Gotchas
|
||||||
|
|
||||||
**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.
|
**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`).
|
||||||
|
|
||||||
**Stack operations**: Most binary operations pop in reverse order (second operand is popped first, then first operand).
|
**Stack operations**: Most binary operations pop in reverse order (second operand is popped first, then first operand).
|
||||||
|
|
||||||
|
|
|
||||||
340
GUIDE.md
340
GUIDE.md
|
|
@ -179,12 +179,25 @@ 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)
|
||||||
|
|
@ -194,6 +207,10 @@ CALL
|
||||||
### 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
|
||||||
|
|
||||||
|
|
@ -241,6 +258,40 @@ CALL
|
||||||
|
|
||||||
## 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>
|
||||||
|
|
@ -326,6 +377,125 @@ 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
|
||||||
|
|
@ -362,7 +532,8 @@ Functions automatically capture current scope:
|
||||||
PUSH 0
|
PUSH 0
|
||||||
STORE counter
|
STORE counter
|
||||||
MAKE_FUNCTION () .increment
|
MAKE_FUNCTION () .increment
|
||||||
RETURN
|
STORE increment_fn
|
||||||
|
JUMP .main
|
||||||
|
|
||||||
.increment:
|
.increment:
|
||||||
LOAD counter ; Captured variable
|
LOAD counter ; Captured variable
|
||||||
|
|
@ -371,6 +542,18 @@ RETURN
|
||||||
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
|
||||||
|
|
@ -378,7 +561,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
|
||||||
|
|
@ -398,6 +581,15 @@ STORE factorial
|
||||||
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)
|
||||||
|
|
@ -541,6 +733,8 @@ 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.
|
||||||
|
|
@ -565,11 +759,24 @@ 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. Positional argument (if provided)
|
1. Named argument (if provided and matches param name)
|
||||||
2. Named argument (if provided and matches param name)
|
2. Positional argument (if provided)
|
||||||
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
|
||||||
|
|
@ -606,18 +813,18 @@ const vm = new VM(bytecode, { add, greet })
|
||||||
**Method 2**: Register after construction
|
**Method 2**: Register after construction
|
||||||
```typescript
|
```typescript
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
vm.registerFunction('add', (a: number, b: number) => a + b)
|
vm.set('add', (a: number, b: number) => a + b)
|
||||||
await vm.run()
|
await vm.run()
|
||||||
```
|
```
|
||||||
|
|
||||||
**Method 3**: Value-based functions (for full control)
|
**Method 3**: Value-based functions (for full control)
|
||||||
```typescript
|
```typescript
|
||||||
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
**Auto-wrapping**: `registerFunction` automatically converts between native TypeScript types and ReefVM Value types. Both sync and async functions work.
|
**Auto-wrapping**: `vm.set()` automatically converts between native TypeScript types and ReefVM Value types. Both sync and async functions work.
|
||||||
|
|
||||||
**Usage in bytecode**:
|
**Usage in bytecode**:
|
||||||
```
|
```
|
||||||
|
|
@ -646,12 +853,12 @@ CALL ; → "Hi, Alice!"
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Basic @named - collects all named args
|
// Basic @named - collects all named args
|
||||||
vm.registerFunction('greet', (atNamed: any = {}) => {
|
vm.set('greet', (atNamed: any = {}) => {
|
||||||
return `Hello, ${atNamed.name || 'World'}!`
|
return `Hello, ${atNamed.name || 'World'}!`
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mixed positional and @named
|
// Mixed positional and @named
|
||||||
vm.registerFunction('configure', (name: string, atOptions: any = {}) => {
|
vm.set('configure', (name: string, atOptions: any = {}) => {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
debug: atOptions.debug || false,
|
debug: atOptions.debug || false,
|
||||||
|
|
@ -676,6 +883,121 @@ CALL ; atOptions receives {debug: true, port: 8080}
|
||||||
|
|
||||||
Named arguments that match fixed parameter names are bound to those parameters. Remaining unmatched named arguments are collected into the `atXxx` parameter as a plain JavaScript object.
|
Named arguments that match fixed parameter names are bound to those parameters. Remaining unmatched named arguments are collected into the `atXxx` parameter as a plain JavaScript object.
|
||||||
|
|
||||||
|
### Calling Functions from TypeScript
|
||||||
|
|
||||||
|
You can call both Reef and native functions from TypeScript using `vm.call()`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
MAKE_FUNCTION (name greeting="Hello") .greet
|
||||||
|
STORE greet
|
||||||
|
HALT
|
||||||
|
|
||||||
|
.greet:
|
||||||
|
LOAD greeting
|
||||||
|
PUSH " "
|
||||||
|
LOAD name
|
||||||
|
PUSH "!"
|
||||||
|
STR_CONCAT #4
|
||||||
|
RETURN
|
||||||
|
`)
|
||||||
|
|
||||||
|
const vm = new VM(bytecode, {
|
||||||
|
log: (msg: string) => console.log(msg) // Native function
|
||||||
|
})
|
||||||
|
await vm.run()
|
||||||
|
|
||||||
|
// Call Reef function with positional arguments
|
||||||
|
const result1 = await vm.call('greet', 'Alice')
|
||||||
|
// Returns: "Hello Alice!"
|
||||||
|
|
||||||
|
// Call Reef function with named arguments (pass as final object)
|
||||||
|
const result2 = await vm.call('greet', 'Bob', { greeting: 'Hi' })
|
||||||
|
// Returns: "Hi Bob!"
|
||||||
|
|
||||||
|
// Call Reef function with only named arguments
|
||||||
|
const result3 = await vm.call('greet', { name: 'Carol', greeting: 'Hey' })
|
||||||
|
// Returns: "Hey Carol!"
|
||||||
|
|
||||||
|
// Call native function
|
||||||
|
await vm.call('log', 'Hello from TypeScript!')
|
||||||
|
```
|
||||||
|
|
||||||
|
**How it works**:
|
||||||
|
- `vm.call(functionName, ...args)` looks up the function (Reef or native) in the VM's scope
|
||||||
|
- For Reef functions: converts to callable JavaScript function
|
||||||
|
- For native functions: calls directly
|
||||||
|
- Arguments are automatically converted to ReefVM Values
|
||||||
|
- Returns the result (automatically converted back to JavaScript types)
|
||||||
|
|
||||||
|
**Named arguments**: Pass a plain object as the final argument to provide named arguments. If the last argument is a non-array object, it's treated as named arguments. All preceding arguments are treated as positional.
|
||||||
|
|
||||||
|
**Type conversion**: Arguments and return values are automatically converted between JavaScript types and ReefVM Values:
|
||||||
|
- Primitives: `number`, `string`, `boolean`, `null`
|
||||||
|
- Arrays: converted recursively
|
||||||
|
- Objects: converted to ReefVM dicts
|
||||||
|
- Functions: Reef functions are converted to callable JavaScript functions
|
||||||
|
|
||||||
|
### REPL Mode (Incremental Compilation)
|
||||||
|
|
||||||
|
ReefVM supports incremental bytecode execution for building REPLs. This allows you to execute code line-by-line while preserving scope and avoiding re-execution of side effects.
|
||||||
|
|
||||||
|
**The Problem**: By default, `vm.run()` resets the program counter (PC) to 0, re-executing all previous bytecode. This makes it impossible to implement a REPL where each line executes only once.
|
||||||
|
|
||||||
|
**The Solution**: Use `vm.continue()` to resume execution from where you left off:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Line 1: Define variable
|
||||||
|
const line1 = toBytecode([
|
||||||
|
["PUSH", 42],
|
||||||
|
["STORE", "x"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const vm = new VM(line1)
|
||||||
|
await vm.run() // Execute first line
|
||||||
|
|
||||||
|
// Line 2: Use the variable
|
||||||
|
const line2 = toBytecode([
|
||||||
|
["LOAD", "x"],
|
||||||
|
["PUSH", 10],
|
||||||
|
["ADD"]
|
||||||
|
])
|
||||||
|
|
||||||
|
vm.appendBytecode(line2) // Append new bytecode with proper constant remapping
|
||||||
|
await vm.continue() // Execute ONLY the new bytecode
|
||||||
|
|
||||||
|
// Result: 52 (42 + 10)
|
||||||
|
// The first line never re-executed!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key methods**:
|
||||||
|
- `vm.run()`: Resets PC to 0 and runs from the beginning (normal execution)
|
||||||
|
- `vm.continue()`: Continues from current PC (REPL mode)
|
||||||
|
- `vm.appendBytecode(bytecode)`: Helper that properly appends bytecode with constant index remapping
|
||||||
|
|
||||||
|
**Important**: Don't use `HALT` in REPL mode! The VM naturally stops when it runs out of instructions. Using `HALT` sets `vm.stopped = true`, which prevents `continue()` from resuming.
|
||||||
|
|
||||||
|
**Example REPL pattern**:
|
||||||
|
```typescript
|
||||||
|
const vm = new VM(toBytecode([]), { /* native functions */ })
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const input = await getUserInput() // Get next line from user
|
||||||
|
const bytecode = compileLine(input) // Compile to bytecode (no HALT!)
|
||||||
|
|
||||||
|
vm.appendBytecode(bytecode) // Append to VM
|
||||||
|
const result = await vm.continue() // Execute only the new code
|
||||||
|
|
||||||
|
console.log(fromValue(result)) // Show result to user
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This pattern ensures:
|
||||||
|
- Variables persist between lines
|
||||||
|
- Side effects (like `echo` or function calls) only run once
|
||||||
|
- Previous bytecode never re-executes
|
||||||
|
- Scope accumulates across all lines
|
||||||
|
|
||||||
### Empty Stack
|
### Empty Stack
|
||||||
- RETURN with empty stack returns null
|
- RETURN with empty stack returns null
|
||||||
- HALT with empty stack returns null
|
- HALT with empty stack returns null
|
||||||
|
|
|
||||||
500
IDEAS.md
Normal file
500
IDEAS.md
Normal file
|
|
@ -0,0 +1,500 @@
|
||||||
|
# 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.
|
||||||
|
|
@ -44,11 +44,14 @@ Commands: `clear`, `reset`, `exit`.
|
||||||
- Variadic functions with positional rest parameters (`...rest`)
|
- Variadic functions with positional rest parameters (`...rest`)
|
||||||
- Named arguments (named) that collect unmatched named args into a dict (`@named`)
|
- Named arguments (named) that collect unmatched named args into a dict (`@named`)
|
||||||
- Mixed positional and named arguments with proper priority binding
|
- Mixed positional and named arguments with proper priority binding
|
||||||
|
- Default parameter values with null-triggering: passing `null` explicitly uses the default value
|
||||||
- Tail call optimization with unbounded recursion (10,000+ iterations without stack overflow)
|
- Tail call optimization with unbounded recursion (10,000+ iterations without stack overflow)
|
||||||
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
||||||
- Native function interop with auto-wrapping for native TypeScript types
|
- Native function interop with auto-wrapping for native TypeScript types
|
||||||
- Native functions stored in scope, called via LOAD + CALL
|
- Native functions stored in scope, called via LOAD + CALL
|
||||||
|
- Native functions support `atXxx` parameters (e.g., `atOptions`) to collect unmatched named args
|
||||||
- Pass functions directly to `run(bytecode, { fnName: fn })` or `new VM(bytecode, { fnName: fn })`
|
- Pass functions directly to `run(bytecode, { fnName: fn })` or `new VM(bytecode, { fnName: fn })`
|
||||||
|
- Call Reef functions from TypeScript with `vm.call(name, ...args)` with automatic type conversion
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|
||||||
|
|
@ -58,3 +61,4 @@ Commands: `clear`, `reset`, `exit`.
|
||||||
- Variadic parameters: Functions can collect remaining positional arguments into an array using `...rest` syntax
|
- Variadic parameters: Functions can collect remaining positional arguments into an array using `...rest` syntax
|
||||||
- Named parameters: Functions can collect unmatched named arguments into a dict using `@named` syntax
|
- Named parameters: Functions can collect unmatched named arguments into a dict using `@named` syntax
|
||||||
- Argument binding priority: Named args bind to regular params first, with unmatched ones going to `@named`
|
- Argument binding priority: Named args bind to regular params first, with unmatched ones going to `@named`
|
||||||
|
- Null triggers defaults: Passing `null` to a parameter with a default value explicitly uses that default (applies to both ReefVM and native functions)
|
||||||
196
SPEC.md
196
SPEC.md
|
|
@ -138,6 +138,24 @@ 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
|
||||||
|
|
@ -179,7 +197,29 @@ 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]
|
||||||
|
|
@ -193,6 +233,62 @@ All arithmetic operations pop two values, perform operation, push result as numb
|
||||||
#### 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.
|
||||||
|
|
@ -231,39 +327,45 @@ All comparison operations pop two values, compare, push boolean result.
|
||||||
```
|
```
|
||||||
<evaluate left>
|
<evaluate left>
|
||||||
DUP
|
DUP
|
||||||
JUMP_IF_FALSE #2 # skip POP and <evaluate right>
|
JUMP_IF_FALSE .end
|
||||||
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 #2 # skip POP and <evaluate right>
|
JUMP_IF_TRUE .end
|
||||||
POP
|
POP
|
||||||
<evaluate right>
|
<evaluate right>
|
||||||
end:
|
.end:
|
||||||
```
|
```
|
||||||
|
|
||||||
### Control Flow
|
### Control Flow
|
||||||
|
|
||||||
#### JUMP
|
#### JUMP
|
||||||
**Operand**: Offset (number)
|
**Operand**: Label (string)
|
||||||
**Effect**: Add offset to PC (relative jump)
|
**Effect**: Jump to the specified label
|
||||||
**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**: Offset (number)
|
**Operand**: Label (string)
|
||||||
**Effect**: If top of stack is falsy, add offset to PC (relative jump)
|
**Effect**: If top of stack is falsy, jump to the specified label
|
||||||
**Stack**: [condition] → []
|
**Stack**: [condition] → []
|
||||||
|
|
||||||
|
**Note**: JUMP_IF_FALSE only accepts label operands (`.label`), not numeric offsets.
|
||||||
|
|
||||||
#### JUMP_IF_TRUE
|
#### JUMP_IF_TRUE
|
||||||
**Operand**: Offset (number)
|
**Operand**: Label (string)
|
||||||
**Effect**: If top of stack is truthy, add offset to PC (relative jump)
|
**Effect**: If top of stack is truthy, jump to the specified label
|
||||||
**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
|
||||||
|
|
@ -378,11 +480,19 @@ The created function captures `currentScope` as its `parentScope`.
|
||||||
3. Default value (if defined)
|
3. Default value (if defined)
|
||||||
4. Null
|
4. Null
|
||||||
|
|
||||||
|
**Null Value Semantics**:
|
||||||
|
- Passing `null` as an argument explicitly triggers the default value (if one exists)
|
||||||
|
- This allows callers to "opt-in" to defaults even when providing arguments positionally
|
||||||
|
- If no default exists, `null` is bound as-is
|
||||||
|
- This applies to both ReefVM functions and native TypeScript functions
|
||||||
|
- Example: `fn(null, 20)` where `fn(x=10, y)` binds `x=10` (default triggered), `y=20`
|
||||||
|
|
||||||
**Named Args Handling**:
|
**Named Args Handling**:
|
||||||
- Named args that match fixed parameter names are bound to those params
|
- Named args that match fixed parameter names are bound to those params
|
||||||
- If the function has `named: true`, remaining named args (that don't match any fixed param) are collected into the last parameter as a dict
|
- If the function has `named: true`, remaining named args (that don't match any fixed param) are collected into the last parameter as a dict
|
||||||
- This allows flexible calling: `fn(x=10, y=20, extra=30)` where `extra` goes to the named args dict
|
- This allows flexible calling: `fn(x=10, y=20, extra=30)` where `extra` goes to the named args dict
|
||||||
- **Native functions support named arguments** - parameter names are extracted from the function signature at call time
|
- **Native functions support named arguments** - parameter names are extracted from the function signature at call time
|
||||||
|
- Passing `null` via named args also triggers defaults: `fn(x=null)` triggers `x`'s default
|
||||||
|
|
||||||
**Errors**: Throws if top of stack is not a function (or native function)
|
**Errors**: Throws if top of stack is not a function (or native function)
|
||||||
|
|
||||||
|
|
@ -622,7 +732,7 @@ const vm = new VM(bytecode, {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Or after construction:
|
// Or after construction:
|
||||||
vm.registerFunction('multiply', (a: number, b: number) => a * b)
|
vm.set('multiply', (a: number, b: number) => a * b)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Usage in Bytecode**:
|
**Usage in Bytecode**:
|
||||||
|
|
@ -637,9 +747,9 @@ CALL ; Call it like any other function
|
||||||
|
|
||||||
**Native Function Types**:
|
**Native Function Types**:
|
||||||
|
|
||||||
1. **Auto-wrapped functions** (via `registerFunction`): Accept and return native TypeScript types (number, string, boolean, array, object, etc.). The VM automatically converts between Value types and native types.
|
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 `registerValueFunction`): Accept and return `Value` types directly for full control over type handling.
|
2. **Value-based functions** (via `vm.setValueFunction()`): Accept and return `Value` types directly for full control over type handling.
|
||||||
|
|
||||||
**Auto-Wrapping Behavior**:
|
**Auto-Wrapping Behavior**:
|
||||||
- Parameters: `Value` → native type (number, string, boolean, array, object, null, RegExp)
|
- Parameters: `Value` → native type (number, string, boolean, array, object, null, RegExp)
|
||||||
|
|
@ -655,27 +765,27 @@ CALL ; Call it like any other function
|
||||||
**Examples**:
|
**Examples**:
|
||||||
```typescript
|
```typescript
|
||||||
// Auto-wrapped native types
|
// Auto-wrapped native types
|
||||||
vm.registerFunction('add', (a: number, b: number) => a + b)
|
vm.set('add', (a: number, b: number) => a + b)
|
||||||
vm.registerFunction('greet', (name: string) => `Hello, ${name}!`)
|
vm.set('greet', (name: string) => `Hello, ${name}!`)
|
||||||
vm.registerFunction('range', (n: number) => Array.from({ length: n }, (_, i) => i))
|
vm.set('range', (n: number) => Array.from({ length: n }, (_, i) => i))
|
||||||
|
|
||||||
// With defaults
|
// With defaults
|
||||||
vm.registerFunction('greet', (name: string, greeting = 'Hello') => {
|
vm.set('greet', (name: string, greeting = 'Hello') => {
|
||||||
return `${greeting}, ${name}!`
|
return `${greeting}, ${name}!`
|
||||||
})
|
})
|
||||||
|
|
||||||
// Variadic functions
|
// Variadic functions
|
||||||
vm.registerFunction('sum', (...nums: number[]) => {
|
vm.set('sum', (...nums: number[]) => {
|
||||||
return nums.reduce((acc, n) => acc + n, 0)
|
return nums.reduce((acc, n) => acc + n, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Value-based for custom logic
|
// Value-based for custom logic
|
||||||
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Async functions
|
// Async functions
|
||||||
vm.registerFunction('fetchData', async (url: string) => {
|
vm.set('fetchData', async (url: string) => {
|
||||||
const response = await fetch(url)
|
const response = await fetch(url)
|
||||||
return response.json()
|
return response.json()
|
||||||
})
|
})
|
||||||
|
|
@ -710,14 +820,16 @@ CALL ; → "Hi, Bob!"
|
||||||
|
|
||||||
## Label Syntax
|
## Label Syntax
|
||||||
|
|
||||||
The bytecode format supports labels for improved readability:
|
The bytecode format requires labels for control flow jumps:
|
||||||
|
|
||||||
**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 numeric offsets during parsing. The original numeric offset syntax (`#N`) is still supported for backwards compatibility.
|
Labels are resolved to relative PC offsets during bytecode compilation. All JUMP instructions (`JUMP`, `JUMP_IF_FALSE`, `JUMP_IF_TRUE`) require label operands.
|
||||||
|
|
||||||
Example with labels:
|
**Note**: Exception handling instructions (`PUSH_TRY`, `PUSH_FINALLY`) and function definitions (`MAKE_FUNCTION`) can use either labels or absolute instruction indices (`#N`).
|
||||||
|
|
||||||
|
Example:
|
||||||
```
|
```
|
||||||
JUMP .skip
|
JUMP .skip
|
||||||
.middle:
|
.middle:
|
||||||
|
|
@ -728,15 +840,6 @@ 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
|
||||||
|
|
@ -812,6 +915,29 @@ 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
|
||||||
|
|
@ -912,10 +1038,10 @@ const vm = new VM(bytecode, {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Or register after construction
|
// Or register after construction
|
||||||
vm.registerFunction('multiply', (a: number, b: number) => a * b)
|
vm.set('multiply', (a: number, b: number) => a * b)
|
||||||
|
|
||||||
// Or use Value-based functions
|
// Or use Value-based functions
|
||||||
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,8 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
bin/repl
2
bin/repl
|
|
@ -38,6 +38,8 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
bunfig.toml
Normal file
2
bunfig.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[test]
|
||||||
|
preload = ["./tests/setup.ts"]
|
||||||
116
examples/add-with-arrays.ts
Normal file
116
examples/add-with-arrays.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
/**
|
||||||
|
* 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]] }
|
||||||
158
examples/add-with-dicts.ts
Normal file
158
examples/add-with-dicts.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
/**
|
||||||
|
* 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' } }
|
||||||
73
examples/add-with-strings.ts
Normal file
73
examples/add-with-strings.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
/**
|
||||||
|
* 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' }
|
||||||
|
|
@ -9,7 +9,7 @@ const bytecode = toBytecode(`
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
vm.registerFunction('print', (...args: Value[]): Value => {
|
vm.set('print', (...args: Value[]): Value => {
|
||||||
console.log(...args.map(toString))
|
console.log(...args.map(toString))
|
||||||
return toNull()
|
return toNull()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,15 @@ export type Constant =
|
||||||
| Value
|
| Value
|
||||||
| FunctionDef
|
| FunctionDef
|
||||||
|
|
||||||
type Atom = number | string | boolean | null
|
type Atom = RegExp | 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]
|
||||||
|
|
@ -32,6 +34,9 @@ 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"]
|
||||||
|
|
||||||
|
|
@ -39,9 +44,9 @@ type InstructionTuple =
|
||||||
| ["NOT"]
|
| ["NOT"]
|
||||||
|
|
||||||
// Control flow
|
// Control flow
|
||||||
| ["JUMP", string | number]
|
| ["JUMP", string]
|
||||||
| ["JUMP_IF_FALSE", string | number]
|
| ["JUMP_IF_FALSE", string]
|
||||||
| ["JUMP_IF_TRUE", string | number]
|
| ["JUMP_IF_TRUE", string]
|
||||||
| ["BREAK"]
|
| ["BREAK"]
|
||||||
|
|
||||||
// Exception handling
|
// Exception handling
|
||||||
|
|
@ -51,7 +56,7 @@ type InstructionTuple =
|
||||||
| ["THROW"]
|
| ["THROW"]
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
| ["MAKE_FUNCTION", string[], string | number]
|
| ["MAKE_FUNCTION", string[], string]
|
||||||
| ["CALL"]
|
| ["CALL"]
|
||||||
| ["TAIL_CALL"]
|
| ["TAIL_CALL"]
|
||||||
| ["RETURN"]
|
| ["RETURN"]
|
||||||
|
|
@ -83,30 +88,6 @@ type LabelDefinition = [string] // Just ".label_name:"
|
||||||
|
|
||||||
export type ProgramItem = InstructionTuple | LabelDefinition
|
export type ProgramItem = InstructionTuple | LabelDefinition
|
||||||
|
|
||||||
//
|
|
||||||
// Parse bytecode from human-readable string format.
|
|
||||||
// Operand types are determined by prefix/literal:
|
|
||||||
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
|
|
||||||
// .label -> label reference (e.g., JUMP .loop_start, MAKE_FUNCTION (x y) .body)
|
|
||||||
// name -> variable/function name (e.g., LOAD x, LOAD_NATIVE add)
|
|
||||||
// 42 -> number constant (e.g., PUSH 42)
|
|
||||||
// "str" -> string constant (e.g., PUSH "hello")
|
|
||||||
// 'str' -> string constant (e.g., PUSH 'hello')
|
|
||||||
// true -> boolean constant (e.g., PUSH true)
|
|
||||||
// false -> boolean constant (e.g., PUSH false)
|
|
||||||
// null -> null constant (e.g., PUSH null)
|
|
||||||
//
|
|
||||||
// Labels:
|
|
||||||
// .label_name: -> label definition (marks current instruction position)
|
|
||||||
//
|
|
||||||
// Function definitions:
|
|
||||||
// MAKE_FUNCTION (x y) #7 -> basic function (numeric offset)
|
|
||||||
// MAKE_FUNCTION (x y) .body -> basic function (label reference)
|
|
||||||
// MAKE_FUNCTION (x y=42) #7 -> with defaults
|
|
||||||
// MAKE_FUNCTION (x ...rest) #7 -> variadic
|
|
||||||
// MAKE_FUNCTION (x @named) #7 -> named
|
|
||||||
//
|
|
||||||
|
|
||||||
function parseFunctionParams(paramStr: string, constants: Constant[]): {
|
function parseFunctionParams(paramStr: string, constants: Constant[]): {
|
||||||
params: string[]
|
params: string[]
|
||||||
defaults: Record<string, number>
|
defaults: Record<string, number>
|
||||||
|
|
@ -368,6 +349,29 @@ 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")
|
||||||
|
|
||||||
|
|
@ -386,7 +390,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
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { type Value, type NativeFunction, fromValue, toValue } from "./value"
|
import { type Value, type NativeFunction, fromValue, toValue } from "./value"
|
||||||
|
import { VM } from "./vm"
|
||||||
|
|
||||||
export type ParamInfo = {
|
export type ParamInfo = {
|
||||||
params: string[]
|
params: string[]
|
||||||
|
|
@ -9,11 +10,13 @@ export type ParamInfo = {
|
||||||
|
|
||||||
const WRAPPED_MARKER = Symbol('reef-wrapped')
|
const WRAPPED_MARKER = Symbol('reef-wrapped')
|
||||||
|
|
||||||
export function wrapNative(fn: Function): (...args: Value[]) => Promise<Value> {
|
export function wrapNative(vm: VM, fn: Function): (this: VM, ...args: Value[]) => Promise<Value> {
|
||||||
const wrapped = async (...values: Value[]) => {
|
if ((fn as any).raw) return fn as (this: VM, ...args: Value[]) => Promise<Value>
|
||||||
const nativeArgs = values.map(fromValue)
|
|
||||||
const result = await fn(...nativeArgs)
|
const wrapped = async function (this: VM, ...values: Value[]) {
|
||||||
return toValue(result)
|
const nativeArgs = values.map(arg => fromValue(arg, vm))
|
||||||
|
const result = await fn.call(this, ...nativeArgs)
|
||||||
|
return toValue(result, this)
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrappedObj = wrapped as any
|
const wrappedObj = wrapped as any
|
||||||
|
|
|
||||||
15
src/index.ts
15
src/index.ts
|
|
@ -1,14 +1,17 @@
|
||||||
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, functions?: Record<string, Function>): Promise<Value> {
|
export async function run(bytecode: Bytecode, globals?: Record<string, any>): Promise<Value> {
|
||||||
const vm = new VM(bytecode, functions)
|
const vm = new VM(bytecode, globals)
|
||||||
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 { wrapNative } from "./function"
|
|
||||||
export { type Value, toValue, toString, toNumber, fromValue, toNull } from "./value"
|
|
||||||
export { VM } from "./vm"
|
|
||||||
export { bytecodeToString } from "./format"
|
export { bytecodeToString } from "./format"
|
||||||
|
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"
|
||||||
|
|
@ -3,12 +3,16 @@ 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]
|
||||||
|
|
@ -16,6 +20,14 @@ 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]
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,15 @@ 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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,11 +50,19 @@ const OPCODES_WITH_OPERANDS = new Set([
|
||||||
const OPCODES_WITHOUT_OPERANDS = new Set([
|
const OPCODES_WITHOUT_OPERANDS = new Set([
|
||||||
OpCode.POP,
|
OpCode.POP,
|
||||||
OpCode.DUP,
|
OpCode.DUP,
|
||||||
|
OpCode.SWAP,
|
||||||
|
OpCode.TYPE,
|
||||||
OpCode.ADD,
|
OpCode.ADD,
|
||||||
OpCode.SUB,
|
OpCode.SUB,
|
||||||
OpCode.MUL,
|
OpCode.MUL,
|
||||||
OpCode.DIV,
|
OpCode.DIV,
|
||||||
OpCode.MOD,
|
OpCode.MOD,
|
||||||
|
OpCode.BIT_AND,
|
||||||
|
OpCode.BIT_OR,
|
||||||
|
OpCode.BIT_XOR,
|
||||||
|
OpCode.BIT_SHL,
|
||||||
|
OpCode.BIT_SHR,
|
||||||
|
OpCode.BIT_USHR,
|
||||||
OpCode.EQ,
|
OpCode.EQ,
|
||||||
OpCode.NEQ,
|
OpCode.NEQ,
|
||||||
OpCode.LT,
|
OpCode.LT,
|
||||||
|
|
@ -79,11 +87,15 @@ const OPCODES_WITHOUT_OPERANDS = new Set([
|
||||||
OpCode.DOT_GET,
|
OpCode.DOT_GET,
|
||||||
])
|
])
|
||||||
|
|
||||||
// immediate = immediate number, eg #5
|
// JUMP* instructions require labels only (no numeric immediates)
|
||||||
const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
|
const OPCODES_REQUIRING_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,
|
||||||
])
|
])
|
||||||
|
|
@ -189,6 +201,16 @@ 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({
|
||||||
|
|
@ -302,11 +324,11 @@ export function validateBytecode(source: string): ValidationResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate body address
|
// Validate body address (must be a label)
|
||||||
if (!bodyAddr!.startsWith('.') && !bodyAddr!.startsWith('#')) {
|
if (!bodyAddr!.startsWith('.')) {
|
||||||
errors.push({
|
errors.push({
|
||||||
line: lineNum,
|
line: lineNum,
|
||||||
message: `Invalid body address: expected .label or #offset`,
|
message: `Invalid body address: expected .label, got: ${bodyAddr}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
118
src/value.ts
118
src/value.ts
|
|
@ -1,6 +1,12 @@
|
||||||
|
import { wrapNative, getOriginalFunction } from "./function"
|
||||||
|
import { OpCode } from "./opcode"
|
||||||
import { Scope } from "./scope"
|
import { Scope } from "./scope"
|
||||||
|
import { VM } from "./vm"
|
||||||
|
|
||||||
export type NativeFunction = (...args: Value[]) => Promise<Value> | Value
|
export type NativeFunction = (...args: Value[]) => Promise<Value> | Value
|
||||||
|
export type TypeScriptFunction = (this: VM, ...args: any[]) => any
|
||||||
|
const REEF_FUNCTION = Symbol('__reefFunction')
|
||||||
|
const VALUE_TYPES = new Set(['null', 'boolean', 'number', 'string', 'array', 'dict', 'regex', 'native', 'function'])
|
||||||
|
|
||||||
export type Value =
|
export type Value =
|
||||||
| { type: 'null', value: null }
|
| { type: 'null', value: null }
|
||||||
|
|
@ -33,15 +39,19 @@ export type FunctionDef = {
|
||||||
named: boolean
|
named: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toValue(v: any): Value /* throws */ {
|
export function isValue(v: any): boolean {
|
||||||
|
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 (v && typeof v === 'object' && 'type' in v && 'value' in v)
|
if (isValue(v))
|
||||||
return v as Value
|
return v as Value
|
||||||
|
|
||||||
if (Array.isArray(v))
|
if (Array.isArray(v))
|
||||||
return { type: 'array', value: v.map(toValue) }
|
return { type: 'array', value: v.map(x => toValue(x, vm)) }
|
||||||
|
|
||||||
if (v instanceof RegExp)
|
if (v instanceof RegExp)
|
||||||
return { type: 'regex', value: v }
|
return { type: 'regex', value: v }
|
||||||
|
|
@ -54,15 +64,38 @@ export function toValue(v: any): Value /* throws */ {
|
||||||
case 'string':
|
case 'string':
|
||||||
return { type: 'string', value: v }
|
return { type: 'string', value: v }
|
||||||
case 'function':
|
case 'function':
|
||||||
throw "can't toValue() a js function yet"
|
if ((v as any)[REEF_FUNCTION])
|
||||||
|
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]))
|
for (const key of Object.keys(v)) dict.set(key, toValue(v[key], vm))
|
||||||
|
|
||||||
return { type: 'dict', value: dict }
|
return { type: 'dict', value: dict }
|
||||||
default:
|
default:
|
||||||
throw `can't toValue this: ${v}`
|
throw new Error(`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}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -154,28 +187,87 @@ export function isEqual(a: Value, b: Value): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fromValue(v: Value): any {
|
export function fromValue(v: Value, vm?: VM): 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(fromValue)
|
return v.value.map(x => fromValue(x, vm))
|
||||||
case 'dict':
|
case 'dict':
|
||||||
return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v)]))
|
return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v, vm)]))
|
||||||
case 'regex':
|
case 'regex':
|
||||||
return v.value
|
return v.value
|
||||||
case 'function':
|
case 'function':
|
||||||
|
if (!vm || !(vm instanceof VM)) {
|
||||||
|
const stack = new Error().stack || ''
|
||||||
|
const stackLines = stack.split('\n')
|
||||||
|
.slice(1)
|
||||||
|
.filter(line => !line.includes('fromValue'))
|
||||||
|
.map(line => ' ' + line.trim())
|
||||||
|
.join('\n')
|
||||||
|
throw new Error(
|
||||||
|
`VM is required for function conversion\n` +
|
||||||
|
` Function params: [${v.params.join(', ')}]\n` +
|
||||||
|
` Function body at instruction: ${v.body}\n` +
|
||||||
|
` Called from:\n${stackLines}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return fnFromValue(v, vm)
|
||||||
case 'native':
|
case 'native':
|
||||||
return '<function>'
|
return getOriginalFunction(v.fn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toNull(): Value {
|
export function toNull(): Value {
|
||||||
return toValue(null)
|
return toValue(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function fnFromValue(fn: Value, vm: VM): Function {
|
||||||
|
if (fn.type !== 'function')
|
||||||
|
throw new Error('Value is not a function')
|
||||||
|
|
||||||
|
const wrapper = async function (...args: any[]) {
|
||||||
|
let positional: any[] = args
|
||||||
|
let named: Record<string, any> = {}
|
||||||
|
|
||||||
|
if (args.length > 0 && !Array.isArray(args[args.length - 1]) && args[args.length - 1].constructor === Object) {
|
||||||
|
named = args[args.length - 1]
|
||||||
|
positional = args.slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const newVM = new VM({
|
||||||
|
instructions: vm.instructions,
|
||||||
|
constants: vm.constants,
|
||||||
|
labels: vm.labels
|
||||||
|
})
|
||||||
|
newVM.scope = fn.parentScope
|
||||||
|
|
||||||
|
newVM.stack.push(fn)
|
||||||
|
newVM.stack.push(...positional.map(x => toValue(x, vm)))
|
||||||
|
for (const [key, val] of Object.entries(named)) {
|
||||||
|
newVM.stack.push(toValue(key))
|
||||||
|
newVM.stack.push(toValue(val, vm))
|
||||||
|
}
|
||||||
|
newVM.stack.push(toValue(positional.length))
|
||||||
|
newVM.stack.push(toValue(Object.keys(named).length))
|
||||||
|
|
||||||
|
const targetDepth = newVM.callStack.length
|
||||||
|
await newVM.execute({ op: OpCode.CALL })
|
||||||
|
newVM.pc++
|
||||||
|
|
||||||
|
while (newVM.callStack.length > targetDepth && newVM.pc < newVM.instructions.length) {
|
||||||
|
await newVM.execute(newVM.instructions[newVM.pc]!)
|
||||||
|
newVM.pc++
|
||||||
|
}
|
||||||
|
|
||||||
|
return fromValue(newVM.stack.pop() || toNull(), vm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// support roundtrips, eg fromValue(toValue(fn))
|
||||||
|
; (wrapper as any)[REEF_FUNCTION] = fn
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
}
|
||||||
275
src/vm.ts
275
src/vm.ts
|
|
@ -3,8 +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, type NativeFunction, toValue, toNumber, isTrue, isEqual, toString, fromValue } from "./value"
|
import type { Value, NativeFunction, TypeScriptFunction } from "./value"
|
||||||
import { extractParamInfo, wrapNative, isWrapped, getOriginalFunction } from "./function"
|
import { toValue, toNumber, isTrue, isEqual, toString, fromValue, fnFromValue } from "./value"
|
||||||
|
import { extractParamInfo, getOriginalFunction } from "./function"
|
||||||
|
|
||||||
export class VM {
|
export class VM {
|
||||||
pc = 0
|
pc = 0
|
||||||
|
|
@ -18,26 +19,71 @@ 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, functions?: Record<string, Function>) {
|
constructor(bytecode: Bytecode, globals?: Record<string, any>) {
|
||||||
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 (functions)
|
if (globals) {
|
||||||
for (const name of Object.keys(functions))
|
for (const name of Object.keys(globals ?? {}))
|
||||||
this.registerFunction(name, functions[name]!)
|
this.set(name, globals[name])
|
||||||
|
|
||||||
|
this.scope = new Scope(this.scope)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerFunction(name: string, fn: Function) {
|
async call(name: string, ...args: any) {
|
||||||
const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(fn)
|
const value = this.scope.get(name)
|
||||||
this.scope.set(name, { type: 'native', fn: wrapped, value: '<function>' })
|
|
||||||
|
if (!value) throw new Error(`Can't find ${name}`)
|
||||||
|
if (value.type !== 'function' && value.type !== 'native')
|
||||||
|
throw new Error(`Can't call ${name}`)
|
||||||
|
|
||||||
|
if (value.type === 'native') {
|
||||||
|
return await this.callNative(value.fn, args)
|
||||||
|
} else {
|
||||||
|
const fn = fnFromValue(value, this)
|
||||||
|
return await fn(...args)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerValueFunction(name: string, fn: NativeFunction) {
|
has(name: string): boolean {
|
||||||
|
return this.scope.has(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
vars(): string[] {
|
||||||
|
return this.scope.vars()
|
||||||
|
}
|
||||||
|
|
||||||
|
get(name: string) {
|
||||||
|
return this.scope.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
set(name: string, value: any) {
|
||||||
|
this.scope.set(name, toValue(value, this))
|
||||||
|
}
|
||||||
|
|
||||||
|
setFunction(name: string, fn: TypeScriptFunction) {
|
||||||
|
this.scope.set(name, toValue(fn, this))
|
||||||
|
}
|
||||||
|
|
||||||
|
setValueFunction(name: string, fn: NativeFunction) {
|
||||||
this.scope.set(name, { type: 'native', fn, value: '<function>' })
|
this.scope.set(name, { type: 'native', fn, value: '<function>' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pushScope(locals?: Record<string, any>) {
|
||||||
|
this.scope = new Scope(this.scope)
|
||||||
|
|
||||||
|
if (locals)
|
||||||
|
for (const [name, value] of Object.entries(locals))
|
||||||
|
this.set(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
popScope() {
|
||||||
|
this.scope = this.scope.parent!
|
||||||
|
}
|
||||||
|
|
||||||
async run(): Promise<Value> {
|
async run(): Promise<Value> {
|
||||||
this.pc = 0
|
this.pc = 0
|
||||||
this.stopped = false
|
this.stopped = false
|
||||||
|
|
@ -51,6 +97,57 @@ 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:
|
||||||
|
|
@ -71,8 +168,32 @@ 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:
|
||||||
this.binaryOp((a, b) => toNumber(a) + toNumber(b))
|
const b = this.stack.pop()!
|
||||||
|
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:
|
||||||
|
|
@ -120,6 +241,31 @@ 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
|
||||||
|
|
@ -140,13 +286,18 @@ 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.stack.push(toValue(varName, this))
|
||||||
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()!
|
||||||
|
|
@ -342,10 +493,10 @@ export class VM {
|
||||||
const target = this.stack.pop()!
|
const target = this.stack.pop()!
|
||||||
|
|
||||||
if (target.type === 'array')
|
if (target.type === 'array')
|
||||||
this.stack.push(toValue(target.value?.[Number(index.value)]))
|
this.stack.push(toValue(target.value?.[Number(index.value)], this))
|
||||||
|
|
||||||
else if (target.type === 'dict')
|
else if (target.type === 'dict')
|
||||||
this.stack.push(toValue(target.value?.get(String(index.value))))
|
this.stack.push(toValue(target.value?.get(String(index.value)), this))
|
||||||
|
|
||||||
else
|
else
|
||||||
throw new Error(`DOT_GET: ${target.type} not supported`)
|
throw new Error(`DOT_GET: ${target.type} not supported`)
|
||||||
|
|
@ -451,16 +602,25 @@ export class VM {
|
||||||
let nativePositionalArgIndex = 0
|
let nativePositionalArgIndex = 0
|
||||||
|
|
||||||
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
||||||
|
// Note: null values trigger defaults (null acts as "use default")
|
||||||
for (let i = 0; i < nativeFixedParamCount; i++) {
|
for (let i = 0; i < nativeFixedParamCount; i++) {
|
||||||
const paramName = paramInfo.params[i]!
|
const paramName = paramInfo.params[i]!
|
||||||
|
let paramValue: Value | undefined
|
||||||
|
|
||||||
// Check if named argument was provided for this param
|
// Check if named argument was provided for this param
|
||||||
if (namedArgs.has(paramName)) {
|
if (namedArgs.has(paramName)) {
|
||||||
nativeArgs.push(namedArgs.get(paramName)!)
|
paramValue = namedArgs.get(paramName)!
|
||||||
namedArgs.delete(paramName) // Remove from named args so it won't go to @named
|
namedArgs.delete(paramName) // Remove from named args so it won't go to @named
|
||||||
} else if (nativePositionalArgIndex < positionalArgs.length) {
|
} else if (nativePositionalArgIndex < positionalArgs.length) {
|
||||||
nativeArgs.push(positionalArgs[nativePositionalArgIndex]!)
|
paramValue = positionalArgs[nativePositionalArgIndex]!
|
||||||
nativePositionalArgIndex++
|
nativePositionalArgIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the parameter value is null and a default exists, use the default
|
||||||
|
if (paramValue?.type === 'null' && paramInfo.defaults[paramName] !== undefined) {
|
||||||
|
nativeArgs.push(paramInfo.defaults[paramName]!)
|
||||||
|
} else if (paramValue) {
|
||||||
|
nativeArgs.push(paramValue)
|
||||||
} else if (paramInfo.defaults[paramName] !== undefined) {
|
} else if (paramInfo.defaults[paramName] !== undefined) {
|
||||||
nativeArgs.push(paramInfo.defaults[paramName]!)
|
nativeArgs.push(paramInfo.defaults[paramName]!)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -476,8 +636,8 @@ export class VM {
|
||||||
namedDict.set(key, value)
|
namedDict.set(key, value)
|
||||||
}
|
}
|
||||||
// Convert dict to plain JavaScript object for the native function
|
// Convert dict to plain JavaScript object for the native function
|
||||||
const namedObj = fromValue({ type: 'dict', value: namedDict })
|
const namedObj = fromValue({ type: 'dict', value: namedDict }, this)
|
||||||
nativeArgs.push(toValue(namedObj))
|
nativeArgs.push(toValue(namedObj, this))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle variadic parameter (TypeScript rest parameters)
|
// Handle variadic parameter (TypeScript rest parameters)
|
||||||
|
|
@ -489,13 +649,41 @@ export class VM {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the native function with bound args
|
// Call the native function with bound args
|
||||||
const result = await fn.fn(...nativeArgs)
|
try {
|
||||||
this.stack.push(result)
|
const result = await fn.fn.call(this, ...nativeArgs)
|
||||||
break
|
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: not a function')
|
throw new Error(`CALL: ${fn.value} is 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
|
||||||
|
|
@ -517,16 +705,29 @@ 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)) {
|
||||||
this.scope.set(paramName, namedArgs.get(paramName)!)
|
paramValue = 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) {
|
||||||
this.scope.set(paramName, positionalArgs[positionalArgIndex]!)
|
paramValue = 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]!
|
||||||
|
|
@ -587,7 +788,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: not a function')
|
throw new Error(`TAIL_CALL: ${tailFn.value} is not a function`)
|
||||||
|
|
||||||
this.scope = new Scope(tailFn.parentScope)
|
this.scope = new Scope(tailFn.parentScope)
|
||||||
|
|
||||||
|
|
@ -656,7 +857,7 @@ export class VM {
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw `Unknown op: ${instruction.op}`
|
throw new Error(`Unknown op: ${instruction.op}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -673,4 +874,26 @@ 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
107
tests/bitwise.test.ts
Normal file
107
tests/bitwise.test.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -15,7 +15,7 @@ test("PUSH_TRY and POP_TRY - no exception thrown", async () => {
|
||||||
PUSH 999
|
PUSH 999
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 })
|
await expect(str).toBeNumber(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
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error occurred' })
|
await expect(str).toBeString('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
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'inner error' })
|
await expect(str).toBeString('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
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error message' })
|
await expect(str).toBeString('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
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 110 })
|
await expect(str).toBeNumber(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
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'finally ran' })
|
await expect(str).toBeString('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
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 })
|
await expect(str).toBeNumber(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
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 11 })
|
await expect(str).toBeNumber(11)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("PUSH_FINALLY - error when no handler", async () => {
|
test("PUSH_FINALLY - error when no handler", async () => {
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ describe("functions parameter", () => {
|
||||||
expect(result).toEqual({ type: 'number', value: 200 })
|
expect(result).toEqual({ type: 'number', value: 200 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("can combine with manual registerFunction", async () => {
|
test("can combine with manual vm.set", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
LOAD add
|
LOAD add
|
||||||
PUSH 5
|
PUSH 5
|
||||||
|
|
@ -127,7 +127,7 @@ describe("functions parameter", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Register another function manually
|
// Register another function manually
|
||||||
vm.registerFunction('subtract', (a: number, b: number) => a - b)
|
vm.set('subtract', (a: number, b: number) => a - b)
|
||||||
|
|
||||||
const result = await vm.run()
|
const result = await vm.run()
|
||||||
expect(result).toEqual({ type: 'number', value: 6 })
|
expect(result).toEqual({ type: 'number', value: 6 })
|
||||||
|
|
@ -224,7 +224,7 @@ describe("functions parameter", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Override with manual registration
|
// Override with manual registration
|
||||||
vm.registerFunction('getValue', () => 200)
|
vm.set('getValue', () => 200)
|
||||||
|
|
||||||
const result = await vm.run()
|
const result = await vm.run()
|
||||||
expect(result).toEqual({ type: 'number', value: 200 })
|
expect(result).toEqual({ type: 'number', value: 200 })
|
||||||
|
|
|
||||||
|
|
@ -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"], ".body"],
|
["MAKE_FUNCTION", ["x=0"], ".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 inside function should be null
|
// Function is called with 0 args, so x defaults to 0
|
||||||
// Then we add 5 to null (which coerces to 0)
|
// Then we add 5 to 0
|
||||||
expect(result).toEqual({ type: 'number', value: 5 })
|
expect(result).toEqual({ type: 'number', value: 5 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -519,3 +519,206 @@ 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 })
|
||||||
|
})
|
||||||
|
|
|
||||||
1897
tests/native.test.ts
1897
tests/native.test.ts
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -159,14 +159,14 @@ describe("RegExp", () => {
|
||||||
PUSH /bar/
|
PUSH /bar/
|
||||||
NEQ
|
NEQ
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
await expect(str).toBeBoolean(true)
|
||||||
|
|
||||||
const str2 = `
|
const str2 = `
|
||||||
PUSH /test/i
|
PUSH /test/i
|
||||||
PUSH /test/i
|
PUSH /test/i
|
||||||
NEQ
|
NEQ
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
await expect(str2).toBeBoolean(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("is truthy", async () => {
|
test("is truthy", async () => {
|
||||||
|
|
@ -177,7 +177,7 @@ describe("RegExp", () => {
|
||||||
PUSH 42
|
PUSH 42
|
||||||
.end:
|
.end:
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
await expect(str).toBeNumber(42)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("NOT returns false (regex is truthy)", async () => {
|
test("NOT returns false (regex is truthy)", async () => {
|
||||||
|
|
@ -185,7 +185,7 @@ describe("RegExp", () => {
|
||||||
PUSH /pattern/
|
PUSH /pattern/
|
||||||
NOT
|
NOT
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
|
await expect(str).toBeBoolean(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("in arrays", async () => {
|
test("in arrays", async () => {
|
||||||
|
|
@ -301,7 +301,7 @@ describe("RegExp", () => {
|
||||||
PUSH /bar/i
|
PUSH /bar/i
|
||||||
STR_CONCAT #3
|
STR_CONCAT #3
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: '/foo/ and /bar/i' })
|
await expect(str).toBeString('/foo/ and /bar/i')
|
||||||
})
|
})
|
||||||
|
|
||||||
test("DUP with regex", async () => {
|
test("DUP with regex", async () => {
|
||||||
|
|
@ -311,7 +311,7 @@ describe("RegExp", () => {
|
||||||
EQ
|
EQ
|
||||||
`
|
`
|
||||||
// Same regex duplicated should be equal
|
// Same regex duplicated should be equal
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
await expect(str).toBeBoolean(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("empty pattern", async () => {
|
test("empty pattern", async () => {
|
||||||
|
|
@ -365,7 +365,7 @@ describe("RegExp", () => {
|
||||||
PUSH /xyz/
|
PUSH /xyz/
|
||||||
EQ
|
EQ
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str1))).toEqual({ type: 'boolean', value: false })
|
await expect(str1).toBeBoolean(false)
|
||||||
|
|
||||||
// Same pattern, different flags
|
// Same pattern, different flags
|
||||||
const str2 = `
|
const str2 = `
|
||||||
|
|
@ -373,7 +373,7 @@ describe("RegExp", () => {
|
||||||
PUSH /test/i
|
PUSH /test/i
|
||||||
EQ
|
EQ
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
await expect(str2).toBeBoolean(false)
|
||||||
|
|
||||||
// Different order of flags (should be equal)
|
// Different order of flags (should be equal)
|
||||||
const str3 = `
|
const str3 = `
|
||||||
|
|
@ -381,7 +381,7 @@ describe("RegExp", () => {
|
||||||
PUSH /test/gi
|
PUSH /test/gi
|
||||||
EQ
|
EQ
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true })
|
await expect(str3).toBeBoolean(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("with native functions", async () => {
|
test("with native functions", async () => {
|
||||||
|
|
@ -399,7 +399,7 @@ describe("RegExp", () => {
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
// Register a native function that takes a string and regex
|
// Register a native function that takes a string and regex
|
||||||
vm.registerFunction('match', (str: string, pattern: RegExp) => {
|
vm.set('match', (str: string, pattern: RegExp) => {
|
||||||
return pattern.test(str)
|
return pattern.test(str)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -422,7 +422,7 @@ describe("RegExp", () => {
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
vm.registerFunction('replace', (str: string, pattern: RegExp, replacement: string) => {
|
vm.set('replace', (str: string, pattern: RegExp, replacement: string) => {
|
||||||
return str.replace(pattern, replacement)
|
return str.replace(pattern, replacement)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -444,7 +444,7 @@ describe("RegExp", () => {
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
vm.registerFunction('extractNumbers', (str: string, pattern: RegExp) => {
|
vm.set('extractNumbers', (str: string, pattern: RegExp) => {
|
||||||
return str.match(pattern) || []
|
return str.match(pattern) || []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
149
tests/repl.test.ts
Normal file
149
tests/repl.test.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
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 })
|
||||||
|
})
|
||||||
243
tests/scope.test.ts
Normal file
243
tests/scope.test.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
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")
|
||||||
|
})
|
||||||
249
tests/setup.ts
Normal file
249
tests/setup.ts
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -201,17 +201,17 @@ test("formatValidationErrors produces readable output", () => {
|
||||||
expect(formatted).toContain("UNKNOWN")
|
expect(formatted).toContain("UNKNOWN")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("detects JUMP without # or .label", () => {
|
test("detects JUMP without .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 immediate (#number) or label (.label)")
|
expect(result.errors[0]!.message).toContain("JUMP requires label (.label)")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("detects JUMP_IF_TRUE without # or .label", () => {
|
test("detects JUMP_IF_TRUE without .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 # or .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 immediate (#number) or label (.label)")
|
expect(result.errors[0]!.message).toContain("JUMP_IF_TRUE requires label (.label)")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("detects JUMP_IF_FALSE without # or .label", () => {
|
test("detects JUMP_IF_FALSE without .label", () => {
|
||||||
const source = `
|
const source = `
|
||||||
PUSH false
|
PUSH false
|
||||||
JUMP_IF_FALSE 2
|
JUMP_IF_FALSE 2
|
||||||
|
|
@ -230,17 +230,18 @@ test("detects JUMP_IF_FALSE without # or .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 immediate (#number) or label (.label)")
|
expect(result.errors[0]!.message).toContain("JUMP_IF_FALSE requires label (.label)")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("allows JUMP with immediate number", () => {
|
test("rejects 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(true)
|
expect(result.valid).toBe(false)
|
||||||
|
expect(result.errors[0]!.message).toContain("JUMP requires label (.label)")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("detects MAKE_ARRAY without #", () => {
|
test("detects MAKE_ARRAY without #", () => {
|
||||||
|
|
|
||||||
302
tests/value.test.ts
Normal file
302
tests/value.test.ts
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
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:/)
|
||||||
|
})
|
||||||
54
tests/vm.test.ts
Normal file
54
tests/vm.test.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user