Compare commits
No commits in common. "main" and "varadic-and-named" have entirely different histories.
main
...
varadic-an
145
CLAUDE.md
145
CLAUDE.md
|
|
@ -55,7 +55,7 @@ No build step required - Bun runs TypeScript directly.
|
||||||
|
|
||||||
### Critical Design Decisions
|
### Critical Design Decisions
|
||||||
|
|
||||||
**Label-based jumps**: All JUMP instructions (`JUMP`, `JUMP_IF_FALSE`, `JUMP_IF_TRUE`) require label operands (`.label`), not numeric offsets. Labels are resolved to PC-relative offsets during compilation, making bytecode position-independent. PUSH_TRY/PUSH_FINALLY use absolute addresses and can accept either labels or numeric offsets.
|
**Relative jumps**: All JUMP instructions use PC-relative offsets (not absolute addresses), making bytecode position-independent. PUSH_TRY/PUSH_FINALLY use absolute addresses.
|
||||||
|
|
||||||
**Truthiness semantics**: Only `null` and `false` are falsy. Unlike JavaScript, `0`, `""`, empty arrays, and empty dicts are truthy.
|
**Truthiness semantics**: Only `null` and `false` are falsy. Unlike JavaScript, `0`, `""`, empty arrays, and empty dicts are truthy.
|
||||||
|
|
||||||
|
|
@ -137,100 +137,40 @@ Array format features:
|
||||||
- Function params as string arrays: `["MAKE_FUNCTION", ["x", "y=10"], ".body"]`
|
- Function params as string arrays: `["MAKE_FUNCTION", ["x", "y=10"], ".body"]`
|
||||||
- See `tests/programmatic.test.ts` and `examples/programmatic.ts` for examples
|
- See `tests/programmatic.test.ts` and `examples/programmatic.ts` for examples
|
||||||
|
|
||||||
### Native Function Registration and Global Values
|
### Native Function Registration
|
||||||
|
|
||||||
**Option 1**: Pass to `run()` or `VM` constructor (convenience)
|
**Option 1**: Pass to `run()` or `VM` constructor (convenience)
|
||||||
```typescript
|
```typescript
|
||||||
const result = await run(bytecode, {
|
const result = await run(bytecode, {
|
||||||
add: (a: number, b: number) => a + b,
|
add: (a: number, b: number) => a + b,
|
||||||
greet: (name: string) => `Hello, ${name}!`,
|
greet: (name: string) => `Hello, ${name}!`
|
||||||
pi: 3.14159,
|
|
||||||
config: { debug: true, port: 8080 }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Or with VM constructor
|
// Or with VM constructor
|
||||||
const vm = new VM(bytecode, { add, greet, pi, config })
|
const vm = new VM(bytecode, { add, greet })
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option 2**: Set values with `vm.set()` (manual)
|
**Option 2**: Register with `vm.registerFunction()` (manual)
|
||||||
```typescript
|
```typescript
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
vm.registerFunction('add', (a: number, b: number) => a + b)
|
||||||
// Set functions (auto-wrapped to native functions)
|
|
||||||
vm.set('add', (a: number, b: number) => a + b)
|
|
||||||
|
|
||||||
// Set any other values (auto-converted to ReefVM Values)
|
|
||||||
vm.set('pi', 3.14159)
|
|
||||||
vm.set('config', { debug: true, port: 8080 })
|
|
||||||
|
|
||||||
await vm.run()
|
await vm.run()
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option 3**: Set Value-based functions with `vm.setValueFunction()` (advanced)
|
**Option 3**: Register Value-based functions (for direct Value access)
|
||||||
|
|
||||||
For functions that work directly with ReefVM Value types:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const vm = new VM(bytecode)
|
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
|
|
||||||
// Set Value-based function (no wrapping, works directly with Values)
|
|
||||||
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
|
|
||||||
return toValue(toNumber(a) + toNumber(b))
|
return toValue(toNumber(a) + toNumber(b))
|
||||||
})
|
})
|
||||||
|
|
||||||
await vm.run()
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Auto-wrapping handles:
|
Auto-wrapping handles:
|
||||||
- Functions: wrapped as native functions with Value ↔ native type conversion
|
- Value ↔ native type conversion (`fromValue`/`toValue`)
|
||||||
- Sync and async functions
|
- Sync and async functions
|
||||||
- Arrays, objects, primitives, null, RegExp
|
- Arrays, objects, primitives, null, RegExp
|
||||||
- All values converted via `toValue()`
|
|
||||||
|
|
||||||
### Calling Functions from TypeScript
|
### Label Usage (Preferred)
|
||||||
|
Use labels instead of numeric offsets for readability:
|
||||||
Use `vm.call()` to invoke Reef or native functions from TypeScript:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
MAKE_FUNCTION (x y=10) .add
|
|
||||||
STORE add
|
|
||||||
HALT
|
|
||||||
|
|
||||||
.add:
|
|
||||||
LOAD x
|
|
||||||
LOAD y
|
|
||||||
ADD
|
|
||||||
RETURN
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode, {
|
|
||||||
log: (msg: string) => console.log(msg) // Native function
|
|
||||||
})
|
|
||||||
await vm.run()
|
|
||||||
|
|
||||||
// Call Reef function with positional arguments
|
|
||||||
const result1 = await vm.call('add', 5, 3) // → 8
|
|
||||||
|
|
||||||
// Call Reef function with named arguments (pass final object)
|
|
||||||
const result2 = await vm.call('add', 5, { y: 20 }) // → 25
|
|
||||||
|
|
||||||
// Call Reef function with all named arguments
|
|
||||||
const result3 = await vm.call('add', { x: 10, y: 15 }) // → 25
|
|
||||||
|
|
||||||
// Call native function
|
|
||||||
await vm.call('log', 'Hello!')
|
|
||||||
```
|
|
||||||
|
|
||||||
**How it works**:
|
|
||||||
- Looks up function (Reef or native) in VM scope
|
|
||||||
- For Reef functions: converts to callable JavaScript function using `fnFromValue`
|
|
||||||
- For native functions: calls directly
|
|
||||||
- Automatically converts arguments to ReefVM Values
|
|
||||||
- Converts result back to JavaScript types
|
|
||||||
|
|
||||||
### Label Usage (Required for JUMP instructions)
|
|
||||||
All JUMP instructions must use labels:
|
|
||||||
```
|
```
|
||||||
JUMP .skip
|
JUMP .skip
|
||||||
PUSH 42
|
PUSH 42
|
||||||
|
|
@ -240,67 +180,6 @@ HALT
|
||||||
HALT
|
HALT
|
||||||
```
|
```
|
||||||
|
|
||||||
### Function Definition Patterns
|
|
||||||
|
|
||||||
When defining functions, you MUST prevent the PC from falling through into function bodies. Two patterns:
|
|
||||||
|
|
||||||
**Pattern 1: JUMP over function bodies (Recommended)**
|
|
||||||
```
|
|
||||||
MAKE_FUNCTION (params) .body
|
|
||||||
STORE function_name
|
|
||||||
JUMP .end ; Skip over function body
|
|
||||||
.body:
|
|
||||||
<function code>
|
|
||||||
RETURN
|
|
||||||
.end:
|
|
||||||
<continue with program>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pattern 2: Function bodies after HALT**
|
|
||||||
```
|
|
||||||
MAKE_FUNCTION (params) .body
|
|
||||||
STORE function_name
|
|
||||||
<use the function>
|
|
||||||
HALT ; Stop before function bodies
|
|
||||||
.body:
|
|
||||||
<function code>
|
|
||||||
RETURN
|
|
||||||
```
|
|
||||||
|
|
||||||
Pattern 1 is required for:
|
|
||||||
- Defining multiple functions before using them
|
|
||||||
- REPL mode
|
|
||||||
- Any case where execution continues after defining a function
|
|
||||||
|
|
||||||
Pattern 2 only works if you HALT before reaching function bodies.
|
|
||||||
|
|
||||||
### REPL Mode (Incremental Execution)
|
|
||||||
|
|
||||||
For building REPLs (like the Shrimp REPL), use `vm.continue()` and `vm.appendBytecode()`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const vm = new VM(toBytecode([]), natives)
|
|
||||||
await vm.run() // Initialize (empty bytecode)
|
|
||||||
|
|
||||||
// User enters: x = 42
|
|
||||||
const line1 = compileLine("x = 42") // No HALT!
|
|
||||||
vm.appendBytecode(line1)
|
|
||||||
await vm.continue() // Execute only line 1
|
|
||||||
|
|
||||||
// User enters: x + 10
|
|
||||||
const line2 = compileLine("x + 10") // No HALT!
|
|
||||||
vm.appendBytecode(line2)
|
|
||||||
await vm.continue() // Execute only line 2, result is 52
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key points**:
|
|
||||||
- `vm.run()` resets PC to 0 (re-executes everything) - use for initial setup only
|
|
||||||
- `vm.continue()` resumes from current PC (executes only new bytecode)
|
|
||||||
- `vm.appendBytecode(bytecode)` properly handles constant index remapping
|
|
||||||
- Don't use HALT in REPL lines - let VM stop naturally
|
|
||||||
- Scope and variables persist across all lines
|
|
||||||
- Side effects only run once
|
|
||||||
|
|
||||||
## TypeScript Configuration
|
## TypeScript Configuration
|
||||||
|
|
||||||
- Import alias: `#reef` maps to `./src/index.ts`
|
- Import alias: `#reef` maps to `./src/index.ts`
|
||||||
|
|
@ -486,7 +365,7 @@ Run `bun test` to verify all tests pass before committing.
|
||||||
|
|
||||||
## Common Gotchas
|
## Common Gotchas
|
||||||
|
|
||||||
**Label requirements**: JUMP/JUMP_IF_FALSE/JUMP_IF_TRUE require label operands (`.label`), not numeric offsets. The bytecode compiler resolves labels to PC-relative offsets internally. PUSH_TRY/PUSH_FINALLY can use either labels or absolute instruction indices (`#N`).
|
**Jump offsets**: JUMP/JUMP_IF_FALSE/JUMP_IF_TRUE use relative offsets from the next instruction (PC + 1). PUSH_TRY/PUSH_FINALLY use absolute instruction indices.
|
||||||
|
|
||||||
**Stack operations**: Most binary operations pop in reverse order (second operand is popped first, then first operand).
|
**Stack operations**: Most binary operations pop in reverse order (second operand is popped first, then first operand).
|
||||||
|
|
||||||
|
|
|
||||||
340
GUIDE.md
340
GUIDE.md
|
|
@ -179,25 +179,12 @@ PUSH 1 ; Named count
|
||||||
CALL
|
CALL
|
||||||
```
|
```
|
||||||
|
|
||||||
**Null triggers defaults**: Pass `null` to use default values:
|
|
||||||
```
|
|
||||||
; Function: greet(name='Guest', msg='Hello')
|
|
||||||
LOAD greet
|
|
||||||
PUSH null ; Use default for 'name'
|
|
||||||
PUSH "Hi" ; Provide 'msg'
|
|
||||||
PUSH 2
|
|
||||||
PUSH 0
|
|
||||||
CALL ; → "Hi, Guest"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Opcodes
|
## Opcodes
|
||||||
|
|
||||||
### Stack
|
### Stack
|
||||||
- `PUSH <const>` - Push constant
|
- `PUSH <const>` - Push constant
|
||||||
- `POP` - Remove top
|
- `POP` - Remove top
|
||||||
- `DUP` - Duplicate top
|
- `DUP` - Duplicate top
|
||||||
- `SWAP` - Swap top two values
|
|
||||||
- `TYPE` - Pop value, push its type as string
|
|
||||||
|
|
||||||
### Variables
|
### Variables
|
||||||
- `LOAD <name>` - Push variable value (throws if not found)
|
- `LOAD <name>` - Push variable value (throws if not found)
|
||||||
|
|
@ -207,10 +194,6 @@ CALL ; → "Hi, Guest"
|
||||||
### Arithmetic
|
### Arithmetic
|
||||||
- `ADD`, `SUB`, `MUL`, `DIV`, `MOD` - Binary ops (pop 2, push result)
|
- `ADD`, `SUB`, `MUL`, `DIV`, `MOD` - Binary ops (pop 2, push result)
|
||||||
|
|
||||||
### Bitwise
|
|
||||||
- `BIT_AND`, `BIT_OR`, `BIT_XOR` - Bitwise logical ops (pop 2, push result)
|
|
||||||
- `BIT_SHL`, `BIT_SHR`, `BIT_USHR` - Bitwise shift ops (pop 2, push result)
|
|
||||||
|
|
||||||
### Comparison
|
### Comparison
|
||||||
- `EQ`, `NEQ`, `LT`, `GT`, `LTE`, `GTE` - Pop 2, push boolean
|
- `EQ`, `NEQ`, `LT`, `GT`, `LTE`, `GTE` - Pop 2, push boolean
|
||||||
|
|
||||||
|
|
@ -258,40 +241,6 @@ CALL ; → "Hi, Guest"
|
||||||
|
|
||||||
## Compiler Patterns
|
## Compiler Patterns
|
||||||
|
|
||||||
### Function Definitions
|
|
||||||
|
|
||||||
When defining functions, you must prevent the PC from "falling through" into the function body during sequential execution. There are two standard patterns:
|
|
||||||
|
|
||||||
**Pattern 1: JUMP over function bodies (Recommended)**
|
|
||||||
```
|
|
||||||
MAKE_FUNCTION (params) .body
|
|
||||||
STORE function_name
|
|
||||||
JUMP .end ; Skip over function body
|
|
||||||
.body:
|
|
||||||
<function code>
|
|
||||||
RETURN
|
|
||||||
.end:
|
|
||||||
<continue with program>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pattern 2: Function bodies after HALT**
|
|
||||||
```
|
|
||||||
MAKE_FUNCTION (params) .body
|
|
||||||
STORE function_name
|
|
||||||
<use the function>
|
|
||||||
HALT ; Stop execution before function bodies
|
|
||||||
.body:
|
|
||||||
<function code>
|
|
||||||
RETURN
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important**: Pattern 2 only works if you HALT before reaching function bodies. Pattern 1 is more flexible and required for:
|
|
||||||
- Defining multiple functions before using them
|
|
||||||
- REPL mode (incremental execution)
|
|
||||||
- Any case where execution continues after defining a function
|
|
||||||
|
|
||||||
**Why?** `MAKE_FUNCTION` creates a function value but doesn't jump to the body—it just stores the body's address. Without JUMP or HALT, the PC increments into the function body and executes it as top-level code.
|
|
||||||
|
|
||||||
### If-Else
|
### If-Else
|
||||||
```
|
```
|
||||||
<condition>
|
<condition>
|
||||||
|
|
@ -377,125 +326,6 @@ POP
|
||||||
.end: ; Result on stack
|
.end: ; Result on stack
|
||||||
```
|
```
|
||||||
|
|
||||||
### Reversing Operand Order
|
|
||||||
Use SWAP to reverse operand order for non-commutative operations:
|
|
||||||
|
|
||||||
```
|
|
||||||
; Compute 10 / 2 when values are in reverse order
|
|
||||||
PUSH 2
|
|
||||||
PUSH 10
|
|
||||||
SWAP ; Now: [10, 2]
|
|
||||||
DIV ; 10 / 2 = 5
|
|
||||||
```
|
|
||||||
|
|
||||||
```
|
|
||||||
; Compute "hello" - "world" (subtraction with strings coerced to numbers)
|
|
||||||
PUSH "world"
|
|
||||||
PUSH "hello"
|
|
||||||
SWAP ; Now: ["hello", "world"]
|
|
||||||
SUB ; Result based on operand order
|
|
||||||
```
|
|
||||||
|
|
||||||
**Common Use Cases**:
|
|
||||||
- Division and subtraction when operands are in wrong order
|
|
||||||
- String concatenation with specific order
|
|
||||||
- Preparing arguments for functions that care about position
|
|
||||||
|
|
||||||
### Bitwise Operations
|
|
||||||
All bitwise operations work with 32-bit signed integers:
|
|
||||||
|
|
||||||
```
|
|
||||||
; Bitwise AND (masking)
|
|
||||||
PUSH 5
|
|
||||||
PUSH 3
|
|
||||||
BIT_AND ; → 1 (0101 & 0011 = 0001)
|
|
||||||
|
|
||||||
; Bitwise OR (combining flags)
|
|
||||||
PUSH 5
|
|
||||||
PUSH 3
|
|
||||||
BIT_OR ; → 7 (0101 | 0011 = 0111)
|
|
||||||
|
|
||||||
; Bitwise XOR (toggling bits)
|
|
||||||
PUSH 5
|
|
||||||
PUSH 3
|
|
||||||
BIT_XOR ; → 6 (0101 ^ 0011 = 0110)
|
|
||||||
|
|
||||||
; Left shift (multiply by power of 2)
|
|
||||||
PUSH 5
|
|
||||||
PUSH 2
|
|
||||||
BIT_SHL ; → 20 (5 << 2 = 5 * 4)
|
|
||||||
|
|
||||||
; Arithmetic right shift (divide by power of 2, preserves sign)
|
|
||||||
PUSH 20
|
|
||||||
PUSH 2
|
|
||||||
BIT_SHR ; → 5 (20 >> 2 = 20 / 4)
|
|
||||||
|
|
||||||
PUSH -20
|
|
||||||
PUSH 2
|
|
||||||
BIT_SHR ; → -5 (sign preserved)
|
|
||||||
|
|
||||||
; Logical right shift (zero-fill)
|
|
||||||
PUSH -1
|
|
||||||
PUSH 1
|
|
||||||
BIT_USHR ; → 2147483647 (unsigned shift)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Common Use Cases**:
|
|
||||||
- Flags and bit masks: `flags band MASK` to test, `flags bor FLAG` to set
|
|
||||||
- Fast multiplication/division by powers of 2
|
|
||||||
- Color manipulation: extract RGB components
|
|
||||||
- Low-level bit manipulation for protocols or file formats
|
|
||||||
|
|
||||||
### Runtime Type Checking (TYPE)
|
|
||||||
Get the type of a value as a string for runtime introspection:
|
|
||||||
|
|
||||||
```
|
|
||||||
; Basic type check
|
|
||||||
PUSH 42
|
|
||||||
TYPE ; → "number"
|
|
||||||
|
|
||||||
PUSH "hello"
|
|
||||||
TYPE ; → "string"
|
|
||||||
|
|
||||||
MAKE_ARRAY #3
|
|
||||||
TYPE ; → "array"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Type Guard Pattern** (check type before operation):
|
|
||||||
```
|
|
||||||
; Safe addition - only add if both are numbers
|
|
||||||
LOAD x
|
|
||||||
DUP
|
|
||||||
TYPE
|
|
||||||
PUSH "number"
|
|
||||||
EQ
|
|
||||||
JUMP_IF_FALSE .not_number
|
|
||||||
|
|
||||||
LOAD y
|
|
||||||
DUP
|
|
||||||
TYPE
|
|
||||||
PUSH "number"
|
|
||||||
EQ
|
|
||||||
JUMP_IF_FALSE .cleanup_not_number
|
|
||||||
|
|
||||||
ADD ; Safe to add
|
|
||||||
JUMP .end
|
|
||||||
|
|
||||||
.cleanup_not_number:
|
|
||||||
POP ; Remove y
|
|
||||||
.not_number:
|
|
||||||
POP ; Remove x
|
|
||||||
PUSH null
|
|
||||||
.end:
|
|
||||||
```
|
|
||||||
|
|
||||||
**Common Use Cases**:
|
|
||||||
- Type validation before operations
|
|
||||||
- Polymorphic functions that handle multiple types
|
|
||||||
- Debugging and introspection
|
|
||||||
- Dynamic dispatch in DSLs
|
|
||||||
- Safe coercion with fallbacks
|
|
||||||
|
|
||||||
### Try-Catch
|
### Try-Catch
|
||||||
```
|
```
|
||||||
PUSH_TRY .catch
|
PUSH_TRY .catch
|
||||||
|
|
@ -532,8 +362,7 @@ Functions automatically capture current scope:
|
||||||
PUSH 0
|
PUSH 0
|
||||||
STORE counter
|
STORE counter
|
||||||
MAKE_FUNCTION () .increment
|
MAKE_FUNCTION () .increment
|
||||||
STORE increment_fn
|
RETURN
|
||||||
JUMP .main
|
|
||||||
|
|
||||||
.increment:
|
.increment:
|
||||||
LOAD counter ; Captured variable
|
LOAD counter ; Captured variable
|
||||||
|
|
@ -542,18 +371,6 @@ JUMP .main
|
||||||
STORE counter
|
STORE counter
|
||||||
LOAD counter
|
LOAD counter
|
||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
.main:
|
|
||||||
LOAD increment_fn
|
|
||||||
PUSH 0
|
|
||||||
PUSH 0
|
|
||||||
CALL ; Returns 1
|
|
||||||
POP
|
|
||||||
LOAD increment_fn
|
|
||||||
PUSH 0
|
|
||||||
PUSH 0
|
|
||||||
CALL ; Returns 2 (counter persists!)
|
|
||||||
HALT
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tail Recursion
|
### Tail Recursion
|
||||||
|
|
@ -561,7 +378,7 @@ Use TAIL_CALL instead of CALL for last call:
|
||||||
```
|
```
|
||||||
MAKE_FUNCTION (n acc) .factorial
|
MAKE_FUNCTION (n acc) .factorial
|
||||||
STORE factorial
|
STORE factorial
|
||||||
JUMP .main
|
<...>
|
||||||
|
|
||||||
.factorial:
|
.factorial:
|
||||||
LOAD n
|
LOAD n
|
||||||
|
|
@ -581,15 +398,6 @@ JUMP .main
|
||||||
PUSH 2
|
PUSH 2
|
||||||
PUSH 0
|
PUSH 0
|
||||||
TAIL_CALL ; Reuses stack frame
|
TAIL_CALL ; Reuses stack frame
|
||||||
|
|
||||||
.main:
|
|
||||||
LOAD factorial
|
|
||||||
PUSH 5
|
|
||||||
PUSH 1
|
|
||||||
PUSH 2
|
|
||||||
PUSH 0
|
|
||||||
CALL ; factorial(5, 1) = 120
|
|
||||||
HALT
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Optional Function Calls (TRY_CALL)
|
### Optional Function Calls (TRY_CALL)
|
||||||
|
|
@ -733,8 +541,6 @@ Only `null` and `false` are falsy. Everything else (including `0`, `""`, empty a
|
||||||
|
|
||||||
**Arithmetic ops** (ADD, SUB, MUL, DIV, MOD) coerce both operands to numbers.
|
**Arithmetic ops** (ADD, SUB, MUL, DIV, MOD) coerce both operands to numbers.
|
||||||
|
|
||||||
**Bitwise ops** (BIT_AND, BIT_OR, BIT_XOR, BIT_SHL, BIT_SHR, BIT_USHR) coerce both operands to 32-bit signed integers.
|
|
||||||
|
|
||||||
**Comparison ops** (LT, GT, LTE, GTE) coerce both operands to numbers.
|
**Comparison ops** (LT, GT, LTE, GTE) coerce both operands to numbers.
|
||||||
|
|
||||||
**Equality ops** (EQ, NEQ) use type-aware comparison with deep equality for arrays/dicts.
|
**Equality ops** (EQ, NEQ) use type-aware comparison with deep equality for arrays/dicts.
|
||||||
|
|
@ -759,24 +565,11 @@ Variable and function parameter names support Unicode and emoji:
|
||||||
|
|
||||||
### Parameter Binding Priority
|
### Parameter Binding Priority
|
||||||
For function calls, parameters bound in order:
|
For function calls, parameters bound in order:
|
||||||
1. Named argument (if provided and matches param name)
|
1. Positional argument (if provided)
|
||||||
2. Positional argument (if provided)
|
2. Named argument (if provided and matches param name)
|
||||||
3. Default value (if defined)
|
3. Default value (if defined)
|
||||||
4. Null
|
4. Null
|
||||||
|
|
||||||
**Null Triggering Defaults**: Passing `null` as an argument (positional or named) triggers the default value if one exists. This allows callers to explicitly "opt-in" to defaults:
|
|
||||||
```
|
|
||||||
# Function with defaults: greet(name='Guest', greeting='Hello')
|
|
||||||
LOAD greet
|
|
||||||
PUSH null # Triggers default: name='Guest'
|
|
||||||
PUSH 'Hi' # Provided: greeting='Hi'
|
|
||||||
PUSH 2
|
|
||||||
PUSH 0
|
|
||||||
CALL # Returns "Hi, Guest"
|
|
||||||
```
|
|
||||||
|
|
||||||
This works for both ReefVM functions and native TypeScript functions. If no default exists, `null` is bound as-is.
|
|
||||||
|
|
||||||
### Exception Handlers
|
### Exception Handlers
|
||||||
- PUSH_TRY uses absolute addresses for catch blocks
|
- PUSH_TRY uses absolute addresses for catch blocks
|
||||||
- Nested try blocks form a stack
|
- Nested try blocks form a stack
|
||||||
|
|
@ -813,18 +606,18 @@ const vm = new VM(bytecode, { add, greet })
|
||||||
**Method 2**: Register after construction
|
**Method 2**: Register after construction
|
||||||
```typescript
|
```typescript
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
vm.set('add', (a: number, b: number) => a + b)
|
vm.registerFunction('add', (a: number, b: number) => a + b)
|
||||||
await vm.run()
|
await vm.run()
|
||||||
```
|
```
|
||||||
|
|
||||||
**Method 3**: Value-based functions (for full control)
|
**Method 3**: Value-based functions (for full control)
|
||||||
```typescript
|
```typescript
|
||||||
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
|
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
**Auto-wrapping**: `vm.set()` automatically converts between native TypeScript types and ReefVM Value types. Both sync and async functions work.
|
**Auto-wrapping**: `registerFunction` automatically converts between native TypeScript types and ReefVM Value types. Both sync and async functions work.
|
||||||
|
|
||||||
**Usage in bytecode**:
|
**Usage in bytecode**:
|
||||||
```
|
```
|
||||||
|
|
@ -853,12 +646,12 @@ CALL ; → "Hi, Alice!"
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Basic @named - collects all named args
|
// Basic @named - collects all named args
|
||||||
vm.set('greet', (atNamed: any = {}) => {
|
vm.registerFunction('greet', (atNamed: any = {}) => {
|
||||||
return `Hello, ${atNamed.name || 'World'}!`
|
return `Hello, ${atNamed.name || 'World'}!`
|
||||||
})
|
})
|
||||||
|
|
||||||
// Mixed positional and @named
|
// Mixed positional and @named
|
||||||
vm.set('configure', (name: string, atOptions: any = {}) => {
|
vm.registerFunction('configure', (name: string, atOptions: any = {}) => {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
debug: atOptions.debug || false,
|
debug: atOptions.debug || false,
|
||||||
|
|
@ -883,121 +676,6 @@ CALL ; atOptions receives {debug: true, port: 8080}
|
||||||
|
|
||||||
Named arguments that match fixed parameter names are bound to those parameters. Remaining unmatched named arguments are collected into the `atXxx` parameter as a plain JavaScript object.
|
Named arguments that match fixed parameter names are bound to those parameters. Remaining unmatched named arguments are collected into the `atXxx` parameter as a plain JavaScript object.
|
||||||
|
|
||||||
### Calling Functions from TypeScript
|
|
||||||
|
|
||||||
You can call both Reef and native functions from TypeScript using `vm.call()`:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const bytecode = toBytecode(`
|
|
||||||
MAKE_FUNCTION (name greeting="Hello") .greet
|
|
||||||
STORE greet
|
|
||||||
HALT
|
|
||||||
|
|
||||||
.greet:
|
|
||||||
LOAD greeting
|
|
||||||
PUSH " "
|
|
||||||
LOAD name
|
|
||||||
PUSH "!"
|
|
||||||
STR_CONCAT #4
|
|
||||||
RETURN
|
|
||||||
`)
|
|
||||||
|
|
||||||
const vm = new VM(bytecode, {
|
|
||||||
log: (msg: string) => console.log(msg) // Native function
|
|
||||||
})
|
|
||||||
await vm.run()
|
|
||||||
|
|
||||||
// Call Reef function with positional arguments
|
|
||||||
const result1 = await vm.call('greet', 'Alice')
|
|
||||||
// Returns: "Hello Alice!"
|
|
||||||
|
|
||||||
// Call Reef function with named arguments (pass as final object)
|
|
||||||
const result2 = await vm.call('greet', 'Bob', { greeting: 'Hi' })
|
|
||||||
// Returns: "Hi Bob!"
|
|
||||||
|
|
||||||
// Call Reef function with only named arguments
|
|
||||||
const result3 = await vm.call('greet', { name: 'Carol', greeting: 'Hey' })
|
|
||||||
// Returns: "Hey Carol!"
|
|
||||||
|
|
||||||
// Call native function
|
|
||||||
await vm.call('log', 'Hello from TypeScript!')
|
|
||||||
```
|
|
||||||
|
|
||||||
**How it works**:
|
|
||||||
- `vm.call(functionName, ...args)` looks up the function (Reef or native) in the VM's scope
|
|
||||||
- For Reef functions: converts to callable JavaScript function
|
|
||||||
- For native functions: calls directly
|
|
||||||
- Arguments are automatically converted to ReefVM Values
|
|
||||||
- Returns the result (automatically converted back to JavaScript types)
|
|
||||||
|
|
||||||
**Named arguments**: Pass a plain object as the final argument to provide named arguments. If the last argument is a non-array object, it's treated as named arguments. All preceding arguments are treated as positional.
|
|
||||||
|
|
||||||
**Type conversion**: Arguments and return values are automatically converted between JavaScript types and ReefVM Values:
|
|
||||||
- Primitives: `number`, `string`, `boolean`, `null`
|
|
||||||
- Arrays: converted recursively
|
|
||||||
- Objects: converted to ReefVM dicts
|
|
||||||
- Functions: Reef functions are converted to callable JavaScript functions
|
|
||||||
|
|
||||||
### REPL Mode (Incremental Compilation)
|
|
||||||
|
|
||||||
ReefVM supports incremental bytecode execution for building REPLs. This allows you to execute code line-by-line while preserving scope and avoiding re-execution of side effects.
|
|
||||||
|
|
||||||
**The Problem**: By default, `vm.run()` resets the program counter (PC) to 0, re-executing all previous bytecode. This makes it impossible to implement a REPL where each line executes only once.
|
|
||||||
|
|
||||||
**The Solution**: Use `vm.continue()` to resume execution from where you left off:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Line 1: Define variable
|
|
||||||
const line1 = toBytecode([
|
|
||||||
["PUSH", 42],
|
|
||||||
["STORE", "x"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const vm = new VM(line1)
|
|
||||||
await vm.run() // Execute first line
|
|
||||||
|
|
||||||
// Line 2: Use the variable
|
|
||||||
const line2 = toBytecode([
|
|
||||||
["LOAD", "x"],
|
|
||||||
["PUSH", 10],
|
|
||||||
["ADD"]
|
|
||||||
])
|
|
||||||
|
|
||||||
vm.appendBytecode(line2) // Append new bytecode with proper constant remapping
|
|
||||||
await vm.continue() // Execute ONLY the new bytecode
|
|
||||||
|
|
||||||
// Result: 52 (42 + 10)
|
|
||||||
// The first line never re-executed!
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key methods**:
|
|
||||||
- `vm.run()`: Resets PC to 0 and runs from the beginning (normal execution)
|
|
||||||
- `vm.continue()`: Continues from current PC (REPL mode)
|
|
||||||
- `vm.appendBytecode(bytecode)`: Helper that properly appends bytecode with constant index remapping
|
|
||||||
|
|
||||||
**Important**: Don't use `HALT` in REPL mode! The VM naturally stops when it runs out of instructions. Using `HALT` sets `vm.stopped = true`, which prevents `continue()` from resuming.
|
|
||||||
|
|
||||||
**Example REPL pattern**:
|
|
||||||
```typescript
|
|
||||||
const vm = new VM(toBytecode([]), { /* native functions */ })
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
const input = await getUserInput() // Get next line from user
|
|
||||||
const bytecode = compileLine(input) // Compile to bytecode (no HALT!)
|
|
||||||
|
|
||||||
vm.appendBytecode(bytecode) // Append to VM
|
|
||||||
const result = await vm.continue() // Execute only the new code
|
|
||||||
|
|
||||||
console.log(fromValue(result)) // Show result to user
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
This pattern ensures:
|
|
||||||
- Variables persist between lines
|
|
||||||
- Side effects (like `echo` or function calls) only run once
|
|
||||||
- Previous bytecode never re-executes
|
|
||||||
- Scope accumulates across all lines
|
|
||||||
|
|
||||||
### Empty Stack
|
### Empty Stack
|
||||||
- RETURN with empty stack returns null
|
- RETURN with empty stack returns null
|
||||||
- HALT with empty stack returns null
|
- HALT with empty stack returns null
|
||||||
|
|
|
||||||
500
IDEAS.md
500
IDEAS.md
|
|
@ -1,500 +0,0 @@
|
||||||
# ReefVM Architectural Improvement Ideas
|
|
||||||
|
|
||||||
This document contains architectural ideas for improving ReefVM. These focus on enhancing the VM's capabilities through structural improvements rather than just adding new opcodes.
|
|
||||||
|
|
||||||
## 1. Scope Resolution Optimization
|
|
||||||
|
|
||||||
**Current Issue**: Variable lookups are O(n) through the scope chain on every `LOAD`. This becomes expensive in deeply nested closures.
|
|
||||||
|
|
||||||
**Architectural Solution**: Implement **static scope analysis** with **lexical addressing**:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Instead of: LOAD x (runtime scope chain walk)
|
|
||||||
// Compile to: LOAD_FAST 2 1 (scope depth 2, slot 1 - O(1) lookup)
|
|
||||||
|
|
||||||
class Scope {
|
|
||||||
locals: Map<string, Value>
|
|
||||||
parent?: Scope
|
|
||||||
|
|
||||||
// NEW: Add indexed slots for fast access
|
|
||||||
slots: Value[] // Direct array access
|
|
||||||
nameToSlot: Map<string, number> // Compile-time mapping
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**:
|
|
||||||
- O(1) variable access instead of O(n)
|
|
||||||
- Critical for hot loops and deeply nested functions
|
|
||||||
- Compiler can still fall back to named lookup for dynamic cases
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Module System Architecture
|
|
||||||
|
|
||||||
**Current Gap**: No way to organize code across multiple files or create reusable libraries.
|
|
||||||
|
|
||||||
**Architectural Solution**: Add first-class module support:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// New opcodes: IMPORT, EXPORT, MAKE_MODULE
|
|
||||||
// New bytecode structure:
|
|
||||||
type Bytecode = {
|
|
||||||
instructions: Instruction[]
|
|
||||||
constants: Constant[]
|
|
||||||
exports?: Map<string, number> // Exported symbols
|
|
||||||
imports?: Import[] // Import declarations
|
|
||||||
}
|
|
||||||
|
|
||||||
type Import = {
|
|
||||||
modulePath: string
|
|
||||||
symbols: string[] // [] means import all
|
|
||||||
alias?: string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Pattern**:
|
|
||||||
```
|
|
||||||
MAKE_MODULE .module_body
|
|
||||||
EXPORT add
|
|
||||||
EXPORT subtract
|
|
||||||
HALT
|
|
||||||
|
|
||||||
.module_body:
|
|
||||||
MAKE_FUNCTION (x y) .add_impl
|
|
||||||
RETURN
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**:
|
|
||||||
- Code organization and reusability
|
|
||||||
- Circular dependency detection at load time
|
|
||||||
- Natural namespace isolation
|
|
||||||
- Enables standard library architecture
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Source Map Integration
|
|
||||||
|
|
||||||
**Current Issue**: Runtime errors show bytecode addresses, not source locations.
|
|
||||||
|
|
||||||
**Architectural Solution**: Add source mapping layer:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
type Bytecode = {
|
|
||||||
instructions: Instruction[]
|
|
||||||
constants: Constant[]
|
|
||||||
sourceMap?: SourceMap // NEW
|
|
||||||
}
|
|
||||||
|
|
||||||
type SourceMap = {
|
|
||||||
file?: string
|
|
||||||
mappings: SourceMapping[] // Instruction index → source location
|
|
||||||
}
|
|
||||||
|
|
||||||
type SourceMapping = {
|
|
||||||
instruction: number
|
|
||||||
line: number
|
|
||||||
column: number
|
|
||||||
source?: string // Original source text
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**:
|
|
||||||
- Meaningful error messages with line/column
|
|
||||||
- Debugger can show original source
|
|
||||||
- Stack traces map to source code
|
|
||||||
- Critical for production debugging
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Debugger Hook Architecture
|
|
||||||
|
|
||||||
**Current Gap**: No way to pause execution, inspect state, or step through code.
|
|
||||||
|
|
||||||
**Architectural Solution**: Add debug event system:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class VM {
|
|
||||||
debugger?: Debugger
|
|
||||||
|
|
||||||
async execute(instruction: Instruction) {
|
|
||||||
// Before execution
|
|
||||||
await this.debugger?.onInstruction(this.pc, instruction, this)
|
|
||||||
|
|
||||||
// Execute
|
|
||||||
switch (instruction.op) { ... }
|
|
||||||
|
|
||||||
// After execution
|
|
||||||
await this.debugger?.afterInstruction(this.pc, this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Debugger {
|
|
||||||
breakpoints: Set<number>
|
|
||||||
onInstruction(pc: number, instruction: Instruction, vm: VM): Promise<void>
|
|
||||||
afterInstruction(pc: number, vm: VM): Promise<void>
|
|
||||||
onCall(fn: Value, args: Value[]): Promise<void>
|
|
||||||
onReturn(value: Value): Promise<void>
|
|
||||||
onException(error: Value): Promise<void>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**:
|
|
||||||
- Step-through debugging
|
|
||||||
- Breakpoints at any instruction
|
|
||||||
- State inspection at any point
|
|
||||||
- Non-invasive (no bytecode modification)
|
|
||||||
- Can build IDE integrations
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Bytecode Optimization Pass Framework
|
|
||||||
|
|
||||||
**Current Gap**: Bytecode is emitted directly, no optimization.
|
|
||||||
|
|
||||||
**Architectural Solution**: Add optimization pipeline:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
type Optimizer = (bytecode: Bytecode) => Bytecode
|
|
||||||
|
|
||||||
// Framework for composable optimization passes
|
|
||||||
class BytecodeOptimizer {
|
|
||||||
passes: Optimizer[] = []
|
|
||||||
|
|
||||||
add(pass: Optimizer): this {
|
|
||||||
this.passes.push(pass)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
optimize(bytecode: Bytecode): Bytecode {
|
|
||||||
return this.passes.reduce((bc, pass) => pass(bc), bytecode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example passes:
|
|
||||||
const optimizer = new BytecodeOptimizer()
|
|
||||||
.add(constantFolding) // PUSH 2; PUSH 3; ADD → PUSH 5
|
|
||||||
.add(deadCodeElimination) // Remove unreachable code after HALT/RETURN
|
|
||||||
.add(jumpChaining) // JUMP .a → .a: JUMP .b → JUMP .b directly
|
|
||||||
.add(peepholeOptimization) // DUP; POP → (nothing)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**:
|
|
||||||
- Faster execution without changing compiler
|
|
||||||
- Can add passes without modifying VM
|
|
||||||
- Composable and testable
|
|
||||||
- Enables aggressive optimizations (inlining, constant folding, etc.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Value Memory Management Architecture
|
|
||||||
|
|
||||||
**Current Issue**: No tracking of memory usage, no GC hooks, unbounded growth.
|
|
||||||
|
|
||||||
**Architectural Solution**: Add memory management layer:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class MemoryManager {
|
|
||||||
allocatedBytes: number = 0
|
|
||||||
maxBytes?: number
|
|
||||||
|
|
||||||
allocateValue(value: Value): Value {
|
|
||||||
const size = this.sizeOf(value)
|
|
||||||
if (this.maxBytes && this.allocatedBytes + size > this.maxBytes) {
|
|
||||||
throw new Error('Out of memory')
|
|
||||||
}
|
|
||||||
this.allocatedBytes += size
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
sizeOf(value: Value): number {
|
|
||||||
// Estimate memory footprint
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hook for custom GC
|
|
||||||
gc?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
class VM {
|
|
||||||
memory: MemoryManager
|
|
||||||
|
|
||||||
// All value-creating operations check memory
|
|
||||||
push(value: Value) {
|
|
||||||
this.memory.allocateValue(value)
|
|
||||||
this.stack.push(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**:
|
|
||||||
- Memory limits for sandboxing
|
|
||||||
- Memory profiling
|
|
||||||
- Custom GC strategies
|
|
||||||
- Prevents runaway memory usage
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Instruction Profiler Architecture
|
|
||||||
|
|
||||||
**Current Gap**: No way to identify performance bottlenecks in bytecode.
|
|
||||||
|
|
||||||
**Architectural Solution**: Add instrumentation layer:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class Profiler {
|
|
||||||
instructionCounts: Map<number, number> = new Map()
|
|
||||||
instructionTime: Map<number, number> = new Map()
|
|
||||||
hotFunctions: Map<number, FunctionProfile> = new Map()
|
|
||||||
|
|
||||||
recordInstruction(pc: number, duration: number) {
|
|
||||||
this.instructionCounts.set(pc, (this.instructionCounts.get(pc) || 0) + 1)
|
|
||||||
this.instructionTime.set(pc, (this.instructionTime.get(pc) || 0) + duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
getHotSpots(): HotSpot[] {
|
|
||||||
// Identify most-executed instructions
|
|
||||||
}
|
|
||||||
|
|
||||||
generateReport(): ProfileReport {
|
|
||||||
// Human-readable performance report
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class VM {
|
|
||||||
profiler?: Profiler
|
|
||||||
|
|
||||||
async execute(instruction: Instruction) {
|
|
||||||
const start = performance.now()
|
|
||||||
// ... execute ...
|
|
||||||
const duration = performance.now() - start
|
|
||||||
this.profiler?.recordInstruction(this.pc, duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**:
|
|
||||||
- Identify hot loops and functions
|
|
||||||
- Guide optimization efforts
|
|
||||||
- Measure impact of changes
|
|
||||||
- Can feed into JIT compiler (future)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Standard Library Plugin Architecture
|
|
||||||
|
|
||||||
**Current Issue**: Native functions registered manually, no standard library structure.
|
|
||||||
|
|
||||||
**Architectural Solution**: Module-based native libraries:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
interface NativeModule {
|
|
||||||
name: string
|
|
||||||
exports: Record<string, any>
|
|
||||||
init?(vm: VM): void
|
|
||||||
}
|
|
||||||
|
|
||||||
class VM {
|
|
||||||
modules: Map<string, NativeModule> = new Map()
|
|
||||||
|
|
||||||
registerModule(module: NativeModule) {
|
|
||||||
this.modules.set(module.name, module)
|
|
||||||
module.init?.(this)
|
|
||||||
|
|
||||||
// Auto-register exports to global scope
|
|
||||||
for (const [name, value] of Object.entries(module.exports)) {
|
|
||||||
this.set(name, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadModule(name: string): NativeModule {
|
|
||||||
return this.modules.get(name) || throw new Error(`Module ${name} not found`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example usage:
|
|
||||||
const mathModule: NativeModule = {
|
|
||||||
name: 'math',
|
|
||||||
exports: {
|
|
||||||
sin: Math.sin,
|
|
||||||
cos: Math.cos,
|
|
||||||
sqrt: Math.sqrt,
|
|
||||||
PI: Math.PI
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vm.registerModule(mathModule)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**:
|
|
||||||
- Organized standard library
|
|
||||||
- Lazy loading of modules
|
|
||||||
- Third-party plugin system
|
|
||||||
- Clear namespace boundaries
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. Streaming Bytecode Execution
|
|
||||||
|
|
||||||
**Current Limitation**: Must load entire bytecode before execution.
|
|
||||||
|
|
||||||
**Architectural Solution**: Incremental bytecode loading:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class StreamingBytecode {
|
|
||||||
chunks: BytecodeChunk[] = []
|
|
||||||
|
|
||||||
append(chunk: BytecodeChunk) {
|
|
||||||
// Remap addresses, merge constants
|
|
||||||
this.chunks.push(chunk)
|
|
||||||
}
|
|
||||||
|
|
||||||
getInstruction(pc: number): Instruction | undefined {
|
|
||||||
// Resolve across chunks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class VM {
|
|
||||||
async runStreaming(stream: ReadableStream<BytecodeChunk>) {
|
|
||||||
for await (const chunk of stream) {
|
|
||||||
this.bytecode.append(chunk)
|
|
||||||
await this.continue() // Execute new chunk
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**:
|
|
||||||
- Execute before full load (faster startup)
|
|
||||||
- Network streaming of bytecode
|
|
||||||
- Incremental compilation
|
|
||||||
- Better REPL experience
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. Type Annotation System (Optional Runtime Types)
|
|
||||||
|
|
||||||
**Current Gap**: All values dynamically typed, no way to enforce types.
|
|
||||||
|
|
||||||
**Architectural Solution**: Optional type metadata:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
type TypedValue = Value & {
|
|
||||||
typeAnnotation?: TypeAnnotation
|
|
||||||
}
|
|
||||||
|
|
||||||
type TypeAnnotation =
|
|
||||||
| { kind: 'number' }
|
|
||||||
| { kind: 'string' }
|
|
||||||
| { kind: 'array', elementType?: TypeAnnotation }
|
|
||||||
| { kind: 'dict', valueType?: TypeAnnotation }
|
|
||||||
| { kind: 'function', params: TypeAnnotation[], return: TypeAnnotation }
|
|
||||||
|
|
||||||
// New opcodes: TYPE_CHECK, TYPE_ASSERT
|
|
||||||
// Functions can declare parameter types:
|
|
||||||
MAKE_FUNCTION (x:number y:string) .body
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**:
|
|
||||||
- Catch type errors earlier
|
|
||||||
- Self-documenting code
|
|
||||||
- Enables static analysis tools
|
|
||||||
- Optional (doesn't break existing code)
|
|
||||||
- Can enable optimizations (known number type → skip toNumber())
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. VM State Serialization
|
|
||||||
|
|
||||||
**Current Gap**: Can't save/restore VM execution state.
|
|
||||||
|
|
||||||
**Architectural Solution**: Serializable VM state:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
class VM {
|
|
||||||
serialize(): SerializedState {
|
|
||||||
return {
|
|
||||||
instructions: this.instructions,
|
|
||||||
constants: this.constants,
|
|
||||||
pc: this.pc,
|
|
||||||
stack: this.stack.map(serializeValue),
|
|
||||||
callStack: this.callStack.map(serializeFrame),
|
|
||||||
scope: serializeScope(this.scope),
|
|
||||||
handlers: this.handlers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static deserialize(state: SerializedState): VM {
|
|
||||||
const vm = new VM(/* ... */)
|
|
||||||
vm.restore(state)
|
|
||||||
return vm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**:
|
|
||||||
- Save/restore execution state
|
|
||||||
- Distributed computing (send state to workers)
|
|
||||||
- Crash recovery
|
|
||||||
- Time-travel debugging
|
|
||||||
- Checkpoint/restart
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 12. Async Iterator Support
|
|
||||||
|
|
||||||
**Current Gap**: Iterators work via break, but no async iteration.
|
|
||||||
|
|
||||||
**Architectural Solution**: First-class async iteration:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// New value type:
|
|
||||||
type Value = ... | { type: 'async_iterator', value: AsyncIterableIterator<Value> }
|
|
||||||
|
|
||||||
// New opcodes: MAKE_ASYNC_ITERATOR, AWAIT_NEXT, YIELD_ASYNC
|
|
||||||
|
|
||||||
// Pattern:
|
|
||||||
for_await (item in asyncIterable) {
|
|
||||||
// Compiles to AWAIT_NEXT loop
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits**:
|
|
||||||
- Stream processing
|
|
||||||
- Async I/O without blocking
|
|
||||||
- Natural async patterns
|
|
||||||
- Matches JavaScript async iterators
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Priority Recommendations
|
|
||||||
|
|
||||||
### Tier 1 (Highest Impact):
|
|
||||||
1. **Source Map Integration** - Critical for usability
|
|
||||||
2. **Module System** - Essential for scaling beyond toy programs
|
|
||||||
3. **Scope Resolution Optimization** - Performance multiplier
|
|
||||||
|
|
||||||
### Tier 2 (High Value):
|
|
||||||
4. **Debugger Hook Architecture** - Developer experience game-changer
|
|
||||||
5. **Standard Library Plugin Architecture** - Enables ecosystem
|
|
||||||
6. **Bytecode Optimization Framework** - Performance without complexity
|
|
||||||
|
|
||||||
### Tier 3 (Nice to Have):
|
|
||||||
7. **Instruction Profiler** - Guides future optimization
|
|
||||||
8. **Memory Management** - Important for production use
|
|
||||||
9. **VM State Serialization** - Enables advanced use cases
|
|
||||||
|
|
||||||
### Tier 4 (Future/Experimental):
|
|
||||||
10. **Type Annotations** - Optional, doesn't break existing code
|
|
||||||
11. **Streaming Bytecode** - Mostly useful for large programs
|
|
||||||
12. **Async Iterators** - Specialized use case
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Design Principles
|
|
||||||
|
|
||||||
These improvements focus on:
|
|
||||||
- **Performance** (scope optimization, bytecode optimization)
|
|
||||||
- **Developer Experience** (source maps, debugger, profiler)
|
|
||||||
- **Scalability** (modules, standard library architecture)
|
|
||||||
- **Production Readiness** (memory management, serialization)
|
|
||||||
|
|
||||||
All ideas maintain ReefVM's core design philosophy of simplicity, orthogonality, and explicit behavior.
|
|
||||||
|
|
@ -44,14 +44,11 @@ Commands: `clear`, `reset`, `exit`.
|
||||||
- Variadic functions with positional rest parameters (`...rest`)
|
- Variadic functions with positional rest parameters (`...rest`)
|
||||||
- Named arguments (named) that collect unmatched named args into a dict (`@named`)
|
- Named arguments (named) that collect unmatched named args into a dict (`@named`)
|
||||||
- Mixed positional and named arguments with proper priority binding
|
- Mixed positional and named arguments with proper priority binding
|
||||||
- Default parameter values with null-triggering: passing `null` explicitly uses the default value
|
|
||||||
- Tail call optimization with unbounded recursion (10,000+ iterations without stack overflow)
|
- Tail call optimization with unbounded recursion (10,000+ iterations without stack overflow)
|
||||||
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
||||||
- Native function interop with auto-wrapping for native TypeScript types
|
- Native function interop with auto-wrapping for native TypeScript types
|
||||||
- Native functions stored in scope, called via LOAD + CALL
|
- Native functions stored in scope, called via LOAD + CALL
|
||||||
- Native functions support `atXxx` parameters (e.g., `atOptions`) to collect unmatched named args
|
|
||||||
- Pass functions directly to `run(bytecode, { fnName: fn })` or `new VM(bytecode, { fnName: fn })`
|
- Pass functions directly to `run(bytecode, { fnName: fn })` or `new VM(bytecode, { fnName: fn })`
|
||||||
- Call Reef functions from TypeScript with `vm.call(name, ...args)` with automatic type conversion
|
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|
||||||
|
|
@ -61,4 +58,3 @@ Commands: `clear`, `reset`, `exit`.
|
||||||
- Variadic parameters: Functions can collect remaining positional arguments into an array using `...rest` syntax
|
- Variadic parameters: Functions can collect remaining positional arguments into an array using `...rest` syntax
|
||||||
- Named parameters: Functions can collect unmatched named arguments into a dict using `@named` syntax
|
- Named parameters: Functions can collect unmatched named arguments into a dict using `@named` syntax
|
||||||
- Argument binding priority: Named args bind to regular params first, with unmatched ones going to `@named`
|
- Argument binding priority: Named args bind to regular params first, with unmatched ones going to `@named`
|
||||||
- Null triggers defaults: Passing `null` to a parameter with a default value explicitly uses that default (applies to both ReefVM and native functions)
|
|
||||||
196
SPEC.md
196
SPEC.md
|
|
@ -138,24 +138,6 @@ type ExceptionHandler = {
|
||||||
**Effect**: Duplicate top of stack
|
**Effect**: Duplicate top of stack
|
||||||
**Stack**: [value] → [value, value]
|
**Stack**: [value] → [value, value]
|
||||||
|
|
||||||
#### SWAP
|
|
||||||
**Operand**: None
|
|
||||||
**Effect**: Swap the top two values on the stack
|
|
||||||
**Stack**: [value1, value2] → [value2, value1]
|
|
||||||
|
|
||||||
#### TYPE
|
|
||||||
**Operand**: None
|
|
||||||
**Effect**: Pop value from stack, push its type as a string
|
|
||||||
**Stack**: [value] → [typeString]
|
|
||||||
|
|
||||||
Returns the type of a value as a string.
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
```
|
|
||||||
PUSH 42
|
|
||||||
TYPE ; Pushes "number"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Variable Operations
|
### Variable Operations
|
||||||
|
|
||||||
#### LOAD
|
#### LOAD
|
||||||
|
|
@ -197,29 +179,7 @@ All arithmetic operations pop two values, perform operation, push result as numb
|
||||||
|
|
||||||
#### ADD
|
#### ADD
|
||||||
**Stack**: [a, b] → [a + b]
|
**Stack**: [a, b] → [a + b]
|
||||||
|
**Note**: Only for numbers (use separate string concat if needed)
|
||||||
Performs different operations depending on operand types:
|
|
||||||
- If either operand is a string, converts both to strings and concatenates
|
|
||||||
- Else if both operands are arrays, concatenates the arrays
|
|
||||||
- Else if both operands are dicts, merges them (b's keys overwrite a's keys on conflict)
|
|
||||||
- Else if both operands are numbers, performs numeric addition
|
|
||||||
- Otherwise, throws an error
|
|
||||||
|
|
||||||
**Examples**:
|
|
||||||
- `5 + 3` → `8` (numeric addition)
|
|
||||||
- `"hello" + " world"` → `"hello world"` (string concatenation)
|
|
||||||
- `"count: " + 42` → `"count: 42"` (string concatenation)
|
|
||||||
- `100 + " items"` → `"100 items"` (string concatenation)
|
|
||||||
- `[1, 2, 3] + [4]` → `[1, 2, 3, 4]` (array concatenation)
|
|
||||||
- `[1, 2] + [3, 4]` → `[1, 2, 3, 4]` (array concatenation)
|
|
||||||
- `{a: 1} + {b: 2}` → `{a: 1, b: 2}` (dict merge)
|
|
||||||
- `{a: 1, b: 2} + {b: 99}` → `{a: 1, b: 99}` (dict merge, b overwrites)
|
|
||||||
|
|
||||||
**Invalid operations** (throw errors):
|
|
||||||
- `true + false` → Error
|
|
||||||
- `null + 5` → Error
|
|
||||||
- `[1] + 5` → Error
|
|
||||||
- `{a: 1} + 5` → Error
|
|
||||||
|
|
||||||
#### SUB
|
#### SUB
|
||||||
**Stack**: [a, b] → [a - b]
|
**Stack**: [a, b] → [a - b]
|
||||||
|
|
@ -233,62 +193,6 @@ Performs different operations depending on operand types:
|
||||||
#### MOD
|
#### MOD
|
||||||
**Stack**: [a, b] → [a % b]
|
**Stack**: [a, b] → [a % b]
|
||||||
|
|
||||||
### Bitwise Operations
|
|
||||||
|
|
||||||
All bitwise operations coerce operands to 32-bit signed integers, perform the operation, and push the result as a number.
|
|
||||||
|
|
||||||
#### BIT_AND
|
|
||||||
**Operand**: None
|
|
||||||
**Stack**: [a, b] → [a & b]
|
|
||||||
|
|
||||||
Performs bitwise AND operation. Both operands are coerced to 32-bit signed integers.
|
|
||||||
|
|
||||||
**Example**: `5 & 3` → `1` (binary: `0101 & 0011` → `0001`)
|
|
||||||
|
|
||||||
#### BIT_OR
|
|
||||||
**Operand**: None
|
|
||||||
**Stack**: [a, b] → [a | b]
|
|
||||||
|
|
||||||
Performs bitwise OR operation. Both operands are coerced to 32-bit signed integers.
|
|
||||||
|
|
||||||
**Example**: `5 | 3` → `7` (binary: `0101 | 0011` → `0111`)
|
|
||||||
|
|
||||||
#### BIT_XOR
|
|
||||||
**Operand**: None
|
|
||||||
**Stack**: [a, b] → [a ^ b]
|
|
||||||
|
|
||||||
Performs bitwise XOR (exclusive OR) operation. Both operands are coerced to 32-bit signed integers.
|
|
||||||
|
|
||||||
**Example**: `5 ^ 3` → `6` (binary: `0101 ^ 0011` → `0110`)
|
|
||||||
|
|
||||||
#### BIT_SHL
|
|
||||||
**Operand**: None
|
|
||||||
**Stack**: [a, b] → [a << b]
|
|
||||||
|
|
||||||
Performs left shift operation. Left operand is coerced to 32-bit signed integer, right operand determines shift amount (masked to 0-31).
|
|
||||||
|
|
||||||
**Example**: `5 << 2` → `20` (binary: `0101` shifted left 2 positions → `10100`)
|
|
||||||
|
|
||||||
#### BIT_SHR
|
|
||||||
**Operand**: None
|
|
||||||
**Stack**: [a, b] → [a >> b]
|
|
||||||
|
|
||||||
Performs sign-preserving right shift operation. Left operand is coerced to 32-bit signed integer, right operand determines shift amount (masked to 0-31). The sign bit is preserved (arithmetic shift).
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
- `20 >> 2` → `5` (binary: `10100` shifted right 2 positions → `0101`)
|
|
||||||
- `-20 >> 2` → `-5` (sign bit preserved)
|
|
||||||
|
|
||||||
#### BIT_USHR
|
|
||||||
**Operand**: None
|
|
||||||
**Stack**: [a, b] → [a >>> b]
|
|
||||||
|
|
||||||
Performs zero-fill right shift operation. Left operand is coerced to 32-bit signed integer, right operand determines shift amount (masked to 0-31). Zeros are shifted in from the left (logical shift).
|
|
||||||
|
|
||||||
**Example**:
|
|
||||||
- `-1 >>> 1` → `2147483647` (all bits shift right, zero fills from left)
|
|
||||||
- `-8 >>> 1` → `2147483644`
|
|
||||||
|
|
||||||
### Comparison Operations
|
### Comparison Operations
|
||||||
|
|
||||||
All comparison operations pop two values, compare, push boolean result.
|
All comparison operations pop two values, compare, push boolean result.
|
||||||
|
|
@ -327,45 +231,39 @@ All comparison operations pop two values, compare, push boolean result.
|
||||||
```
|
```
|
||||||
<evaluate left>
|
<evaluate left>
|
||||||
DUP
|
DUP
|
||||||
JUMP_IF_FALSE .end
|
JUMP_IF_FALSE #2 # skip POP and <evaluate right>
|
||||||
POP
|
POP
|
||||||
<evaluate right>
|
<evaluate right>
|
||||||
.end:
|
end:
|
||||||
```
|
```
|
||||||
|
|
||||||
**OR pattern** (short-circuits if left side is true):
|
**OR pattern** (short-circuits if left side is true):
|
||||||
```
|
```
|
||||||
<evaluate left>
|
<evaluate left>
|
||||||
DUP
|
DUP
|
||||||
JUMP_IF_TRUE .end
|
JUMP_IF_TRUE #2 # skip POP and <evaluate right>
|
||||||
POP
|
POP
|
||||||
<evaluate right>
|
<evaluate right>
|
||||||
.end:
|
end:
|
||||||
```
|
```
|
||||||
|
|
||||||
### Control Flow
|
### Control Flow
|
||||||
|
|
||||||
#### JUMP
|
#### JUMP
|
||||||
**Operand**: Label (string)
|
**Operand**: Offset (number)
|
||||||
**Effect**: Jump to the specified label
|
**Effect**: Add offset to PC (relative jump)
|
||||||
**Stack**: No change
|
**Stack**: No change
|
||||||
|
|
||||||
**Note**: JUMP only accepts label operands (`.label`), not numeric offsets. The VM resolves labels to relative offsets internally.
|
|
||||||
|
|
||||||
#### JUMP_IF_FALSE
|
#### JUMP_IF_FALSE
|
||||||
**Operand**: Label (string)
|
**Operand**: Offset (number)
|
||||||
**Effect**: If top of stack is falsy, jump to the specified label
|
**Effect**: If top of stack is falsy, add offset to PC (relative jump)
|
||||||
**Stack**: [condition] → []
|
**Stack**: [condition] → []
|
||||||
|
|
||||||
**Note**: JUMP_IF_FALSE only accepts label operands (`.label`), not numeric offsets.
|
|
||||||
|
|
||||||
#### JUMP_IF_TRUE
|
#### JUMP_IF_TRUE
|
||||||
**Operand**: Label (string)
|
**Operand**: Offset (number)
|
||||||
**Effect**: If top of stack is truthy, jump to the specified label
|
**Effect**: If top of stack is truthy, add offset to PC (relative jump)
|
||||||
**Stack**: [condition] → []
|
**Stack**: [condition] → []
|
||||||
|
|
||||||
**Note**: JUMP_IF_TRUE only accepts label operands (`.label`), not numeric offsets.
|
|
||||||
|
|
||||||
#### BREAK
|
#### BREAK
|
||||||
**Operand**: None
|
**Operand**: None
|
||||||
**Effect**: Unwind call stack until frame with `isBreakTarget = true`, resume there
|
**Effect**: Unwind call stack until frame with `isBreakTarget = true`, resume there
|
||||||
|
|
@ -480,19 +378,11 @@ The created function captures `currentScope` as its `parentScope`.
|
||||||
3. Default value (if defined)
|
3. Default value (if defined)
|
||||||
4. Null
|
4. Null
|
||||||
|
|
||||||
**Null Value Semantics**:
|
|
||||||
- Passing `null` as an argument explicitly triggers the default value (if one exists)
|
|
||||||
- This allows callers to "opt-in" to defaults even when providing arguments positionally
|
|
||||||
- If no default exists, `null` is bound as-is
|
|
||||||
- This applies to both ReefVM functions and native TypeScript functions
|
|
||||||
- Example: `fn(null, 20)` where `fn(x=10, y)` binds `x=10` (default triggered), `y=20`
|
|
||||||
|
|
||||||
**Named Args Handling**:
|
**Named Args Handling**:
|
||||||
- Named args that match fixed parameter names are bound to those params
|
- Named args that match fixed parameter names are bound to those params
|
||||||
- If the function has `named: true`, remaining named args (that don't match any fixed param) are collected into the last parameter as a dict
|
- If the function has `named: true`, remaining named args (that don't match any fixed param) are collected into the last parameter as a dict
|
||||||
- This allows flexible calling: `fn(x=10, y=20, extra=30)` where `extra` goes to the named args dict
|
- This allows flexible calling: `fn(x=10, y=20, extra=30)` where `extra` goes to the named args dict
|
||||||
- **Native functions support named arguments** - parameter names are extracted from the function signature at call time
|
- **Native functions support named arguments** - parameter names are extracted from the function signature at call time
|
||||||
- Passing `null` via named args also triggers defaults: `fn(x=null)` triggers `x`'s default
|
|
||||||
|
|
||||||
**Errors**: Throws if top of stack is not a function (or native function)
|
**Errors**: Throws if top of stack is not a function (or native function)
|
||||||
|
|
||||||
|
|
@ -732,7 +622,7 @@ const vm = new VM(bytecode, {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Or after construction:
|
// Or after construction:
|
||||||
vm.set('multiply', (a: number, b: number) => a * b)
|
vm.registerFunction('multiply', (a: number, b: number) => a * b)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Usage in Bytecode**:
|
**Usage in Bytecode**:
|
||||||
|
|
@ -747,9 +637,9 @@ CALL ; Call it like any other function
|
||||||
|
|
||||||
**Native Function Types**:
|
**Native Function Types**:
|
||||||
|
|
||||||
1. **Auto-wrapped functions** (via `vm.set()`): Accept and return native TypeScript types (number, string, boolean, array, object, etc.). The VM automatically converts between Value types and native types.
|
1. **Auto-wrapped functions** (via `registerFunction`): Accept and return native TypeScript types (number, string, boolean, array, object, etc.). The VM automatically converts between Value types and native types.
|
||||||
|
|
||||||
2. **Value-based functions** (via `vm.setValueFunction()`): Accept and return `Value` types directly for full control over type handling.
|
2. **Value-based functions** (via `registerValueFunction`): Accept and return `Value` types directly for full control over type handling.
|
||||||
|
|
||||||
**Auto-Wrapping Behavior**:
|
**Auto-Wrapping Behavior**:
|
||||||
- Parameters: `Value` → native type (number, string, boolean, array, object, null, RegExp)
|
- Parameters: `Value` → native type (number, string, boolean, array, object, null, RegExp)
|
||||||
|
|
@ -765,27 +655,27 @@ CALL ; Call it like any other function
|
||||||
**Examples**:
|
**Examples**:
|
||||||
```typescript
|
```typescript
|
||||||
// Auto-wrapped native types
|
// Auto-wrapped native types
|
||||||
vm.set('add', (a: number, b: number) => a + b)
|
vm.registerFunction('add', (a: number, b: number) => a + b)
|
||||||
vm.set('greet', (name: string) => `Hello, ${name}!`)
|
vm.registerFunction('greet', (name: string) => `Hello, ${name}!`)
|
||||||
vm.set('range', (n: number) => Array.from({ length: n }, (_, i) => i))
|
vm.registerFunction('range', (n: number) => Array.from({ length: n }, (_, i) => i))
|
||||||
|
|
||||||
// With defaults
|
// With defaults
|
||||||
vm.set('greet', (name: string, greeting = 'Hello') => {
|
vm.registerFunction('greet', (name: string, greeting = 'Hello') => {
|
||||||
return `${greeting}, ${name}!`
|
return `${greeting}, ${name}!`
|
||||||
})
|
})
|
||||||
|
|
||||||
// Variadic functions
|
// Variadic functions
|
||||||
vm.set('sum', (...nums: number[]) => {
|
vm.registerFunction('sum', (...nums: number[]) => {
|
||||||
return nums.reduce((acc, n) => acc + n, 0)
|
return nums.reduce((acc, n) => acc + n, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Value-based for custom logic
|
// Value-based for custom logic
|
||||||
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
|
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
||||||
})
|
})
|
||||||
|
|
||||||
// Async functions
|
// Async functions
|
||||||
vm.set('fetchData', async (url: string) => {
|
vm.registerFunction('fetchData', async (url: string) => {
|
||||||
const response = await fetch(url)
|
const response = await fetch(url)
|
||||||
return response.json()
|
return response.json()
|
||||||
})
|
})
|
||||||
|
|
@ -820,16 +710,14 @@ CALL ; → "Hi, Bob!"
|
||||||
|
|
||||||
## Label Syntax
|
## Label Syntax
|
||||||
|
|
||||||
The bytecode format requires labels for control flow jumps:
|
The bytecode format supports labels for improved readability:
|
||||||
|
|
||||||
**Label Definition**: `.label_name:` marks an instruction position
|
**Label Definition**: `.label_name:` marks an instruction position
|
||||||
**Label Reference**: `.label_name` in operands (e.g., `JUMP .loop_start`)
|
**Label Reference**: `.label_name` in operands (e.g., `JUMP .loop_start`)
|
||||||
|
|
||||||
Labels are resolved to relative PC offsets during bytecode compilation. All JUMP instructions (`JUMP`, `JUMP_IF_FALSE`, `JUMP_IF_TRUE`) require label operands.
|
Labels are resolved to numeric offsets during parsing. The original numeric offset syntax (`#N`) is still supported for backwards compatibility.
|
||||||
|
|
||||||
**Note**: Exception handling instructions (`PUSH_TRY`, `PUSH_FINALLY`) and function definitions (`MAKE_FUNCTION`) can use either labels or absolute instruction indices (`#N`).
|
Example with labels:
|
||||||
|
|
||||||
Example:
|
|
||||||
```
|
```
|
||||||
JUMP .skip
|
JUMP .skip
|
||||||
.middle:
|
.middle:
|
||||||
|
|
@ -840,6 +728,15 @@ JUMP .skip
|
||||||
HALT
|
HALT
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Equivalent with numeric offsets:
|
||||||
|
```
|
||||||
|
JUMP #2
|
||||||
|
PUSH 999
|
||||||
|
HALT
|
||||||
|
PUSH 42
|
||||||
|
HALT
|
||||||
|
```
|
||||||
|
|
||||||
## Common Bytecode Patterns
|
## Common Bytecode Patterns
|
||||||
|
|
||||||
### If-Else Statement
|
### If-Else Statement
|
||||||
|
|
@ -915,29 +812,6 @@ PUSH 1 # namedCount
|
||||||
CALL
|
CALL
|
||||||
```
|
```
|
||||||
|
|
||||||
### Null Triggering Default Values
|
|
||||||
```
|
|
||||||
# Function: greet(name='Guest', greeting='Hello')
|
|
||||||
MAKE_FUNCTION (name='Guest' greeting='Hello') .greet_body
|
|
||||||
STORE 'greet'
|
|
||||||
JUMP .main
|
|
||||||
.greet_body:
|
|
||||||
LOAD 'greeting'
|
|
||||||
PUSH ', '
|
|
||||||
ADD
|
|
||||||
LOAD 'name'
|
|
||||||
ADD
|
|
||||||
RETURN
|
|
||||||
.main:
|
|
||||||
# Call with null for first param - triggers default
|
|
||||||
LOAD 'greet'
|
|
||||||
PUSH null # name will use default 'Guest'
|
|
||||||
PUSH 'Hi' # greeting='Hi' (provided)
|
|
||||||
PUSH 2 # positionalCount
|
|
||||||
PUSH 0 # namedCount
|
|
||||||
CALL # Returns "Hi, Guest"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Tail Recursive Function
|
### Tail Recursive Function
|
||||||
```
|
```
|
||||||
MAKE_FUNCTION (n acc) .factorial_body
|
MAKE_FUNCTION (n acc) .factorial_body
|
||||||
|
|
@ -1038,10 +912,10 @@ const vm = new VM(bytecode, {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Or register after construction
|
// Or register after construction
|
||||||
vm.set('multiply', (a: number, b: number) => a * b)
|
vm.registerFunction('multiply', (a: number, b: number) => a * b)
|
||||||
|
|
||||||
// Or use Value-based functions
|
// Or use Value-based functions
|
||||||
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
|
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,8 +104,6 @@ function formatValue(value: Value): string {
|
||||||
} else if (value.type === 'function') {
|
} else if (value.type === 'function') {
|
||||||
const params = value.params.join(', ')
|
const params = value.params.join(', ')
|
||||||
return `${colors.dim}<fn(${params})>${colors.reset}`
|
return `${colors.dim}<fn(${params})>${colors.reset}`
|
||||||
} else if (value.type === 'native') {
|
|
||||||
return `${colors.dim}<native>${colors.reset}`
|
|
||||||
}
|
}
|
||||||
return String(value)
|
return String(value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
bin/repl
2
bin/repl
|
|
@ -38,8 +38,6 @@ function formatValue(value: Value): string {
|
||||||
} else if (value.type === 'function') {
|
} else if (value.type === 'function') {
|
||||||
const params = value.params.join(', ')
|
const params = value.params.join(', ')
|
||||||
return `${colors.dim}<fn(${params})>${colors.reset}`
|
return `${colors.dim}<fn(${params})>${colors.reset}`
|
||||||
} else if (value.type === 'native') {
|
|
||||||
return `${colors.dim}<native>${colors.reset}`
|
|
||||||
}
|
}
|
||||||
return String(value)
|
return String(value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
[test]
|
|
||||||
preload = ["./tests/setup.ts"]
|
|
||||||
|
|
@ -1,116 +0,0 @@
|
||||||
/**
|
|
||||||
* Demonstrates the ADD opcode working with arrays
|
|
||||||
*
|
|
||||||
* ADD now handles array concatenation:
|
|
||||||
* - [1, 2, 3] + [4] === [1, 2, 3, 4]
|
|
||||||
* - If both operands are arrays, they are concatenated
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { toBytecode, run } from "#reef"
|
|
||||||
|
|
||||||
// Basic array concatenation
|
|
||||||
const basicConcat = toBytecode([
|
|
||||||
["PUSH", 1],
|
|
||||||
["PUSH", 2],
|
|
||||||
["PUSH", 3],
|
|
||||||
["MAKE_ARRAY", 3],
|
|
||||||
["PUSH", 4],
|
|
||||||
["MAKE_ARRAY", 1],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
console.log('Basic array concatenation ([1, 2, 3] + [4]):')
|
|
||||||
const result1 = await run(basicConcat)
|
|
||||||
console.log(result1)
|
|
||||||
// Output: { type: 'array', value: [1, 2, 3, 4] }
|
|
||||||
|
|
||||||
// Concatenate two multi-element arrays
|
|
||||||
const multiConcat = toBytecode([
|
|
||||||
["PUSH", 1],
|
|
||||||
["PUSH", 2],
|
|
||||||
["MAKE_ARRAY", 2],
|
|
||||||
["PUSH", 3],
|
|
||||||
["PUSH", 4],
|
|
||||||
["MAKE_ARRAY", 2],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
console.log('\nConcatenate two arrays ([1, 2] + [3, 4]):')
|
|
||||||
const result2 = await run(multiConcat)
|
|
||||||
console.log(result2)
|
|
||||||
// Output: { type: 'array', value: [1, 2, 3, 4] }
|
|
||||||
|
|
||||||
// Concatenate multiple arrays in sequence
|
|
||||||
const multipleConcat = toBytecode([
|
|
||||||
["PUSH", 1],
|
|
||||||
["MAKE_ARRAY", 1],
|
|
||||||
["PUSH", 2],
|
|
||||||
["MAKE_ARRAY", 1],
|
|
||||||
["ADD"],
|
|
||||||
["PUSH", 3],
|
|
||||||
["MAKE_ARRAY", 1],
|
|
||||||
["ADD"],
|
|
||||||
["PUSH", 4],
|
|
||||||
["MAKE_ARRAY", 1],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
console.log('\nMultiple concatenations ([1] + [2] + [3] + [4]):')
|
|
||||||
const result3 = await run(multipleConcat)
|
|
||||||
console.log(result3)
|
|
||||||
// Output: { type: 'array', value: [1, 2, 3, 4] }
|
|
||||||
|
|
||||||
// Concatenate arrays with mixed types
|
|
||||||
const mixedTypes = toBytecode([
|
|
||||||
["PUSH", 1],
|
|
||||||
["PUSH", "hello"],
|
|
||||||
["MAKE_ARRAY", 2],
|
|
||||||
["PUSH", true],
|
|
||||||
["PUSH", null],
|
|
||||||
["MAKE_ARRAY", 2],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
console.log('\nConcatenate arrays with mixed types ([1, "hello"] + [true, null]):')
|
|
||||||
const result4 = await run(mixedTypes)
|
|
||||||
console.log(result4)
|
|
||||||
// Output: { type: 'array', value: [1, "hello", true, null] }
|
|
||||||
|
|
||||||
// Concatenate empty array with non-empty
|
|
||||||
const emptyConcat = toBytecode([
|
|
||||||
["MAKE_ARRAY", 0],
|
|
||||||
["PUSH", 1],
|
|
||||||
["PUSH", 2],
|
|
||||||
["PUSH", 3],
|
|
||||||
["MAKE_ARRAY", 3],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
console.log('\nConcatenate empty array with [1, 2, 3] ([] + [1, 2, 3]):')
|
|
||||||
const result5 = await run(emptyConcat)
|
|
||||||
console.log(result5)
|
|
||||||
// Output: { type: 'array', value: [1, 2, 3] }
|
|
||||||
|
|
||||||
// Nested arrays
|
|
||||||
const nestedConcat = toBytecode([
|
|
||||||
["PUSH", 1],
|
|
||||||
["PUSH", 2],
|
|
||||||
["MAKE_ARRAY", 2],
|
|
||||||
["MAKE_ARRAY", 1],
|
|
||||||
["PUSH", 3],
|
|
||||||
["PUSH", 4],
|
|
||||||
["MAKE_ARRAY", 2],
|
|
||||||
["MAKE_ARRAY", 1],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
console.log('\nConcatenate nested arrays ([[1, 2]] + [[3, 4]]):')
|
|
||||||
const result6 = await run(nestedConcat)
|
|
||||||
console.log(result6)
|
|
||||||
// Output: { type: 'array', value: [[1, 2], [3, 4]] }
|
|
||||||
|
|
@ -1,158 +0,0 @@
|
||||||
/**
|
|
||||||
* Demonstrates the ADD opcode working with dicts
|
|
||||||
*
|
|
||||||
* ADD now handles dict merging:
|
|
||||||
* - {a: 1} + {b: 2} === {a: 1, b: 2}
|
|
||||||
* - If both operands are dicts, they are merged
|
|
||||||
* - Keys from the second dict overwrite keys from the first on conflict
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { toBytecode, run } from "#reef"
|
|
||||||
|
|
||||||
// Basic dict merge
|
|
||||||
const basicMerge = toBytecode([
|
|
||||||
["PUSH", "a"],
|
|
||||||
["PUSH", 1],
|
|
||||||
["MAKE_DICT", 1],
|
|
||||||
["PUSH", "b"],
|
|
||||||
["PUSH", 2],
|
|
||||||
["MAKE_DICT", 1],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
console.log('Basic dict merge ({a: 1} + {b: 2}):')
|
|
||||||
const result1 = await run(basicMerge)
|
|
||||||
console.log(result1)
|
|
||||||
// Output: { type: 'dict', value: Map { a: 1, b: 2 } }
|
|
||||||
|
|
||||||
// Merge with overlapping keys
|
|
||||||
const overlapMerge = toBytecode([
|
|
||||||
["PUSH", "a"],
|
|
||||||
["PUSH", 1],
|
|
||||||
["PUSH", "b"],
|
|
||||||
["PUSH", 2],
|
|
||||||
["MAKE_DICT", 2],
|
|
||||||
["PUSH", "b"],
|
|
||||||
["PUSH", 99],
|
|
||||||
["PUSH", "c"],
|
|
||||||
["PUSH", 3],
|
|
||||||
["MAKE_DICT", 2],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
console.log('\nMerge with overlapping keys ({a: 1, b: 2} + {b: 99, c: 3}):')
|
|
||||||
const result2 = await run(overlapMerge)
|
|
||||||
console.log(result2)
|
|
||||||
console.log('Note: b is overwritten from 2 to 99')
|
|
||||||
// Output: { type: 'dict', value: Map { a: 1, b: 99, c: 3 } }
|
|
||||||
|
|
||||||
// Merge multiple dicts in sequence
|
|
||||||
const multipleMerge = toBytecode([
|
|
||||||
["PUSH", "a"],
|
|
||||||
["PUSH", 1],
|
|
||||||
["MAKE_DICT", 1],
|
|
||||||
["PUSH", "b"],
|
|
||||||
["PUSH", 2],
|
|
||||||
["MAKE_DICT", 1],
|
|
||||||
["ADD"],
|
|
||||||
["PUSH", "c"],
|
|
||||||
["PUSH", 3],
|
|
||||||
["MAKE_DICT", 1],
|
|
||||||
["ADD"],
|
|
||||||
["PUSH", "d"],
|
|
||||||
["PUSH", 4],
|
|
||||||
["MAKE_DICT", 1],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
console.log('\nMultiple merges ({a: 1} + {b: 2} + {c: 3} + {d: 4}):')
|
|
||||||
const result3 = await run(multipleMerge)
|
|
||||||
console.log(result3)
|
|
||||||
// Output: { type: 'dict', value: Map { a: 1, b: 2, c: 3, d: 4 } }
|
|
||||||
|
|
||||||
// Merge dicts with different value types
|
|
||||||
const mixedTypes = toBytecode([
|
|
||||||
["PUSH", "num"],
|
|
||||||
["PUSH", 42],
|
|
||||||
["PUSH", "str"],
|
|
||||||
["PUSH", "hello"],
|
|
||||||
["MAKE_DICT", 2],
|
|
||||||
["PUSH", "bool"],
|
|
||||||
["PUSH", true],
|
|
||||||
["PUSH", "null"],
|
|
||||||
["PUSH", null],
|
|
||||||
["MAKE_DICT", 2],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
console.log('\nMerge dicts with different types ({num: 42, str: "hello"} + {bool: true, null: null}):')
|
|
||||||
const result4 = await run(mixedTypes)
|
|
||||||
console.log(result4)
|
|
||||||
// Output: { type: 'dict', value: Map { num: 42, str: 'hello', bool: true, null: null } }
|
|
||||||
|
|
||||||
// Merge empty dict with non-empty
|
|
||||||
const emptyMerge = toBytecode([
|
|
||||||
["MAKE_DICT", 0],
|
|
||||||
["PUSH", "x"],
|
|
||||||
["PUSH", 100],
|
|
||||||
["PUSH", "y"],
|
|
||||||
["PUSH", 200],
|
|
||||||
["MAKE_DICT", 2],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
console.log('\nMerge empty dict with {x: 100, y: 200} ({} + {x: 100, y: 200}):')
|
|
||||||
const result5 = await run(emptyMerge)
|
|
||||||
console.log(result5)
|
|
||||||
// Output: { type: 'dict', value: Map { x: 100, y: 200 } }
|
|
||||||
|
|
||||||
// Merge dicts with nested structures
|
|
||||||
const nestedMerge = toBytecode([
|
|
||||||
["PUSH", "data"],
|
|
||||||
["PUSH", 1],
|
|
||||||
["PUSH", 2],
|
|
||||||
["MAKE_ARRAY", 2],
|
|
||||||
["MAKE_DICT", 1],
|
|
||||||
["PUSH", "config"],
|
|
||||||
["PUSH", "debug"],
|
|
||||||
["PUSH", true],
|
|
||||||
["MAKE_DICT", 1],
|
|
||||||
["MAKE_DICT", 1],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
console.log('\nMerge dicts with nested structures:')
|
|
||||||
const result6 = await run(nestedMerge)
|
|
||||||
console.log(result6)
|
|
||||||
// Output: { type: 'dict', value: Map { data: [1, 2], config: { debug: true } } }
|
|
||||||
|
|
||||||
// Building configuration objects
|
|
||||||
const configBuild = toBytecode([
|
|
||||||
// Default config
|
|
||||||
["PUSH", "debug"],
|
|
||||||
["PUSH", false],
|
|
||||||
["PUSH", "port"],
|
|
||||||
["PUSH", 3000],
|
|
||||||
["PUSH", "host"],
|
|
||||||
["PUSH", "localhost"],
|
|
||||||
["MAKE_DICT", 3],
|
|
||||||
// Override with user config
|
|
||||||
["PUSH", "debug"],
|
|
||||||
["PUSH", true],
|
|
||||||
["PUSH", "port"],
|
|
||||||
["PUSH", 8080],
|
|
||||||
["MAKE_DICT", 2],
|
|
||||||
["ADD"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
console.log('\nBuilding config (defaults + overrides):')
|
|
||||||
const result7 = await run(configBuild)
|
|
||||||
console.log(result7)
|
|
||||||
// Output: { type: 'dict', value: Map { debug: true, port: 8080, host: 'localhost' } }
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
||||||
/**
|
|
||||||
* Demonstrates the ADD opcode working with both numbers and strings
|
|
||||||
*
|
|
||||||
* ADD now behaves like JavaScript's + operator:
|
|
||||||
* - If either operand is a string, it does string concatenation
|
|
||||||
* - Otherwise, it does numeric addition
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { toBytecode, run } from "#reef"
|
|
||||||
|
|
||||||
// Numeric addition
|
|
||||||
const numericAdd = toBytecode(`
|
|
||||||
PUSH 10
|
|
||||||
PUSH 5
|
|
||||||
ADD
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
console.log('Numeric addition (10 + 5):')
|
|
||||||
console.log(await run(numericAdd))
|
|
||||||
// Output: { type: 'number', value: 15 }
|
|
||||||
|
|
||||||
// String concatenation
|
|
||||||
const stringConcat = toBytecode(`
|
|
||||||
PUSH "hello"
|
|
||||||
PUSH " world"
|
|
||||||
ADD
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
console.log('\nString concatenation ("hello" + " world"):')
|
|
||||||
console.log(await run(stringConcat))
|
|
||||||
// Output: { type: 'string', value: 'hello world' }
|
|
||||||
|
|
||||||
// Mixed: string + number
|
|
||||||
const mixedConcat = toBytecode(`
|
|
||||||
PUSH "count: "
|
|
||||||
PUSH 42
|
|
||||||
ADD
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
console.log('\nMixed concatenation ("count: " + 42):')
|
|
||||||
console.log(await run(mixedConcat))
|
|
||||||
// Output: { type: 'string', value: 'count: 42' }
|
|
||||||
|
|
||||||
// Building a message
|
|
||||||
const buildMessage = toBytecode(`
|
|
||||||
PUSH "You have "
|
|
||||||
PUSH 3
|
|
||||||
ADD
|
|
||||||
PUSH " new messages"
|
|
||||||
ADD
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
console.log('\nBuilding a message:')
|
|
||||||
console.log(await run(buildMessage))
|
|
||||||
// Output: { type: 'string', value: 'You have 3 new messages' }
|
|
||||||
|
|
||||||
// Computing then concatenating
|
|
||||||
const computeAndConcat = toBytecode(`
|
|
||||||
PUSH "Result: "
|
|
||||||
PUSH 10
|
|
||||||
PUSH 5
|
|
||||||
ADD
|
|
||||||
ADD
|
|
||||||
HALT
|
|
||||||
`)
|
|
||||||
|
|
||||||
console.log('\nComputing then concatenating ("Result: " + (10 + 5)):')
|
|
||||||
console.log(await run(computeAndConcat))
|
|
||||||
// Output: { type: 'string', value: 'Result: 15' }
|
|
||||||
|
|
@ -9,7 +9,7 @@ const bytecode = toBytecode(`
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
vm.set('print', (...args: Value[]): Value => {
|
vm.registerFunction('print', (...args: Value[]): Value => {
|
||||||
console.log(...args.map(toString))
|
console.log(...args.map(toString))
|
||||||
return toNull()
|
return toNull()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -16,15 +16,13 @@ export type Constant =
|
||||||
| Value
|
| Value
|
||||||
| FunctionDef
|
| FunctionDef
|
||||||
|
|
||||||
type Atom = RegExp | number | string | boolean | null
|
type Atom = number | string | boolean | null
|
||||||
|
|
||||||
type InstructionTuple =
|
type InstructionTuple =
|
||||||
// Stack
|
// Stack
|
||||||
| ["PUSH", Atom]
|
| ["PUSH", Atom]
|
||||||
| ["POP"]
|
| ["POP"]
|
||||||
| ["DUP"]
|
| ["DUP"]
|
||||||
| ["SWAP"]
|
|
||||||
| ["TYPE"]
|
|
||||||
|
|
||||||
// Variables
|
// Variables
|
||||||
| ["LOAD", string]
|
| ["LOAD", string]
|
||||||
|
|
@ -34,9 +32,6 @@ type InstructionTuple =
|
||||||
// Arithmetic
|
// Arithmetic
|
||||||
| ["ADD"] | ["SUB"] | ["MUL"] | ["DIV"] | ["MOD"]
|
| ["ADD"] | ["SUB"] | ["MUL"] | ["DIV"] | ["MOD"]
|
||||||
|
|
||||||
// Bitwise
|
|
||||||
| ["BIT_AND"] | ["BIT_OR"] | ["BIT_XOR"] | ["BIT_SHL"] | ["BIT_SHR"] | ["BIT_USHR"]
|
|
||||||
|
|
||||||
// Comparison
|
// Comparison
|
||||||
| ["EQ"] | ["NEQ"] | ["LT"] | ["GT"] | ["LTE"] | ["GTE"]
|
| ["EQ"] | ["NEQ"] | ["LT"] | ["GT"] | ["LTE"] | ["GTE"]
|
||||||
|
|
||||||
|
|
@ -44,9 +39,9 @@ type InstructionTuple =
|
||||||
| ["NOT"]
|
| ["NOT"]
|
||||||
|
|
||||||
// Control flow
|
// Control flow
|
||||||
| ["JUMP", string]
|
| ["JUMP", string | number]
|
||||||
| ["JUMP_IF_FALSE", string]
|
| ["JUMP_IF_FALSE", string | number]
|
||||||
| ["JUMP_IF_TRUE", string]
|
| ["JUMP_IF_TRUE", string | number]
|
||||||
| ["BREAK"]
|
| ["BREAK"]
|
||||||
|
|
||||||
// Exception handling
|
// Exception handling
|
||||||
|
|
@ -56,7 +51,7 @@ type InstructionTuple =
|
||||||
| ["THROW"]
|
| ["THROW"]
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
| ["MAKE_FUNCTION", string[], string]
|
| ["MAKE_FUNCTION", string[], string | number]
|
||||||
| ["CALL"]
|
| ["CALL"]
|
||||||
| ["TAIL_CALL"]
|
| ["TAIL_CALL"]
|
||||||
| ["RETURN"]
|
| ["RETURN"]
|
||||||
|
|
@ -88,6 +83,30 @@ type LabelDefinition = [string] // Just ".label_name:"
|
||||||
|
|
||||||
export type ProgramItem = InstructionTuple | LabelDefinition
|
export type ProgramItem = InstructionTuple | LabelDefinition
|
||||||
|
|
||||||
|
//
|
||||||
|
// Parse bytecode from human-readable string format.
|
||||||
|
// Operand types are determined by prefix/literal:
|
||||||
|
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
|
||||||
|
// .label -> label reference (e.g., JUMP .loop_start, MAKE_FUNCTION (x y) .body)
|
||||||
|
// name -> variable/function name (e.g., LOAD x, LOAD_NATIVE add)
|
||||||
|
// 42 -> number constant (e.g., PUSH 42)
|
||||||
|
// "str" -> string constant (e.g., PUSH "hello")
|
||||||
|
// 'str' -> string constant (e.g., PUSH 'hello')
|
||||||
|
// true -> boolean constant (e.g., PUSH true)
|
||||||
|
// false -> boolean constant (e.g., PUSH false)
|
||||||
|
// null -> null constant (e.g., PUSH null)
|
||||||
|
//
|
||||||
|
// Labels:
|
||||||
|
// .label_name: -> label definition (marks current instruction position)
|
||||||
|
//
|
||||||
|
// Function definitions:
|
||||||
|
// MAKE_FUNCTION (x y) #7 -> basic function (numeric offset)
|
||||||
|
// MAKE_FUNCTION (x y) .body -> basic function (label reference)
|
||||||
|
// MAKE_FUNCTION (x y=42) #7 -> with defaults
|
||||||
|
// MAKE_FUNCTION (x ...rest) #7 -> variadic
|
||||||
|
// MAKE_FUNCTION (x @named) #7 -> named
|
||||||
|
//
|
||||||
|
|
||||||
function parseFunctionParams(paramStr: string, constants: Constant[]): {
|
function parseFunctionParams(paramStr: string, constants: Constant[]): {
|
||||||
params: string[]
|
params: string[]
|
||||||
defaults: Record<string, number>
|
defaults: Record<string, number>
|
||||||
|
|
@ -349,29 +368,6 @@ function toBytecodeFromArray(program: ProgramItem[]): Bytecode /* throws */ {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
////
|
|
||||||
// Parse bytecode from human-readable string format.
|
|
||||||
// Operand types are determined by prefix/literal:
|
|
||||||
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
|
|
||||||
// .label -> label reference (e.g., JUMP .loop_start, MAKE_FUNCTION (x y) .body)
|
|
||||||
// name -> variable/function name (e.g., LOAD x, LOAD_NATIVE add)
|
|
||||||
// 42 -> number constant (e.g., PUSH 42)
|
|
||||||
// "str" -> string constant (e.g., PUSH "hello")
|
|
||||||
// 'str' -> string constant (e.g., PUSH 'hello')
|
|
||||||
// true -> boolean constant (e.g., PUSH true)
|
|
||||||
// false -> boolean constant (e.g., PUSH false)
|
|
||||||
// null -> null constant (e.g., PUSH null)
|
|
||||||
//
|
|
||||||
// Labels:
|
|
||||||
// .label_name: -> label definition (marks current instruction position)
|
|
||||||
//
|
|
||||||
// Function definitions:
|
|
||||||
// MAKE_FUNCTION (x y) #7 -> basic function (numeric offset)
|
|
||||||
// MAKE_FUNCTION (x y) .body -> basic function (label reference)
|
|
||||||
// MAKE_FUNCTION (x y=42) #7 -> with defaults
|
|
||||||
// MAKE_FUNCTION (x ...rest) #7 -> variadic
|
|
||||||
// MAKE_FUNCTION (x @named) #7 -> named
|
|
||||||
function toBytecodeFromString(str: string): Bytecode /* throws */ {
|
function toBytecodeFromString(str: string): Bytecode /* throws */ {
|
||||||
const lines = str.trim().split("\n")
|
const lines = str.trim().split("\n")
|
||||||
|
|
||||||
|
|
@ -390,7 +386,7 @@ function toBytecodeFromString(str: string): Bytecode /* throws */ {
|
||||||
if (!trimmed) continue
|
if (!trimmed) continue
|
||||||
|
|
||||||
// Check for label definition (.label_name:)
|
// Check for label definition (.label_name:)
|
||||||
if (/^\.[a-zA-Z_][a-zA-Z0-9_.]*:$/.test(trimmed)) {
|
if (/^\.[a-zA-Z_][a-zA-Z0-9_]*:$/.test(trimmed)) {
|
||||||
const labelName = trimmed.slice(1, -1)
|
const labelName = trimmed.slice(1, -1)
|
||||||
labels.set(labelName, cleanLines.length)
|
labels.set(labelName, cleanLines.length)
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { type Value, type NativeFunction, fromValue, toValue } from "./value"
|
import { type Value, type NativeFunction, fromValue, toValue } from "./value"
|
||||||
import { VM } from "./vm"
|
|
||||||
|
|
||||||
export type ParamInfo = {
|
export type ParamInfo = {
|
||||||
params: string[]
|
params: string[]
|
||||||
|
|
@ -10,13 +9,11 @@ export type ParamInfo = {
|
||||||
|
|
||||||
const WRAPPED_MARKER = Symbol('reef-wrapped')
|
const WRAPPED_MARKER = Symbol('reef-wrapped')
|
||||||
|
|
||||||
export function wrapNative(vm: VM, fn: Function): (this: VM, ...args: Value[]) => Promise<Value> {
|
export function wrapNative(fn: Function): (...args: Value[]) => Promise<Value> {
|
||||||
if ((fn as any).raw) return fn as (this: VM, ...args: Value[]) => Promise<Value>
|
const wrapped = async (...values: Value[]) => {
|
||||||
|
const nativeArgs = values.map(fromValue)
|
||||||
const wrapped = async function (this: VM, ...values: Value[]) {
|
const result = await fn(...nativeArgs)
|
||||||
const nativeArgs = values.map(arg => fromValue(arg, vm))
|
return toValue(result)
|
||||||
const result = await fn.call(this, ...nativeArgs)
|
|
||||||
return toValue(result, this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrappedObj = wrapped as any
|
const wrappedObj = wrapped as any
|
||||||
|
|
|
||||||
15
src/index.ts
15
src/index.ts
|
|
@ -1,17 +1,14 @@
|
||||||
import type { Bytecode } from "./bytecode"
|
import type { Bytecode } from "./bytecode"
|
||||||
import type { Value } from "./value"
|
import { type Value } from "./value"
|
||||||
import { VM } from "./vm"
|
import { VM } from "./vm"
|
||||||
|
|
||||||
export async function run(bytecode: Bytecode, globals?: Record<string, any>): Promise<Value> {
|
export async function run(bytecode: Bytecode, functions?: Record<string, Function>): Promise<Value> {
|
||||||
const vm = new VM(bytecode, globals)
|
const vm = new VM(bytecode, functions)
|
||||||
return await vm.run()
|
return await vm.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
export { type Bytecode, toBytecode, type ProgramItem } from "./bytecode"
|
export { type Bytecode, toBytecode, type ProgramItem } from "./bytecode"
|
||||||
export { bytecodeToString } from "./format"
|
export { wrapNative } from "./function"
|
||||||
export { wrapNative, isWrapped, type ParamInfo, extractParamInfo, getOriginalFunction } from "./function"
|
export { type Value, toValue, toString, toNumber, fromValue, toNull } from "./value"
|
||||||
export { OpCode } from "./opcode"
|
|
||||||
export { Scope } from "./scope"
|
|
||||||
export type { Value, TypeScriptFunction, NativeFunction } from "./value"
|
|
||||||
export { isValue, toValue, toString, toNumber, fromValue, toNull, fnFromValue } from "./value"
|
|
||||||
export { VM } from "./vm"
|
export { VM } from "./vm"
|
||||||
|
export { bytecodeToString } from "./format"
|
||||||
|
|
@ -3,16 +3,12 @@ export enum OpCode {
|
||||||
PUSH, // operand: constant index (number) | stack: [] → [value]
|
PUSH, // operand: constant index (number) | stack: [] → [value]
|
||||||
POP, // operand: none | stack: [value] → []
|
POP, // operand: none | stack: [value] → []
|
||||||
DUP, // operand: none | stack: [value] → [value, value]
|
DUP, // operand: none | stack: [value] → [value, value]
|
||||||
SWAP, // operand: none | stack: [value1, value2] → [value2, value1]
|
|
||||||
|
|
||||||
// variables
|
// variables
|
||||||
LOAD, // operand: variable name (identifier) | stack: [] → [value]
|
LOAD, // operand: variable name (identifier) | stack: [] → [value]
|
||||||
STORE, // operand: variable name (identifier) | stack: [value] → []
|
STORE, // operand: variable name (identifier) | stack: [value] → []
|
||||||
TRY_LOAD, // operand: variable name (identifier) | stack: [] → [value] | load a variable (if found) or a string
|
TRY_LOAD, // operand: variable name (identifier) | stack: [] → [value] | load a variable (if found) or a string
|
||||||
|
|
||||||
// information
|
|
||||||
TYPE, // operand: none | stack: [a] → []
|
|
||||||
|
|
||||||
// math (coerce to number, pop 2, push result)
|
// math (coerce to number, pop 2, push result)
|
||||||
ADD, // operand: none | stack: [a, b] → [a + b]
|
ADD, // operand: none | stack: [a, b] → [a + b]
|
||||||
SUB, // operand: none | stack: [a, b] → [a - b]
|
SUB, // operand: none | stack: [a, b] → [a - b]
|
||||||
|
|
@ -20,14 +16,6 @@ export enum OpCode {
|
||||||
DIV, // operand: none | stack: [a, b] → [a / b]
|
DIV, // operand: none | stack: [a, b] → [a / b]
|
||||||
MOD, // operand: none | stack: [a, b] → [a % b]
|
MOD, // operand: none | stack: [a, b] → [a % b]
|
||||||
|
|
||||||
// bitwise operations (coerce to 32-bit integers, pop 2, push result)
|
|
||||||
BIT_AND, // operand: none | stack: [a, b] → [a & b]
|
|
||||||
BIT_OR, // operand: none | stack: [a, b] → [a | b]
|
|
||||||
BIT_XOR, // operand: none | stack: [a, b] → [a ^ b]
|
|
||||||
BIT_SHL, // operand: none | stack: [a, b] → [a << b]
|
|
||||||
BIT_SHR, // operand: none | stack: [a, b] → [a >> b] (sign-preserving)
|
|
||||||
BIT_USHR, // operand: none | stack: [a, b] → [a >>> b] (zero-fill)
|
|
||||||
|
|
||||||
// comparison (pop 2, push boolean)
|
// comparison (pop 2, push boolean)
|
||||||
EQ, // operand: none | stack: [a, b] → [a == b] (deep equality)
|
EQ, // operand: none | stack: [a, b] → [a == b] (deep equality)
|
||||||
NEQ, // operand: none | stack: [a, b] → [a != b]
|
NEQ, // operand: none | stack: [a, b] → [a != b]
|
||||||
|
|
|
||||||
|
|
@ -30,15 +30,6 @@ export class Scope {
|
||||||
has(name: string): boolean {
|
has(name: string): boolean {
|
||||||
return this.locals.has(name) || this.parent?.has(name) || false
|
return this.locals.has(name) || this.parent?.has(name) || false
|
||||||
}
|
}
|
||||||
|
|
||||||
vars(): string[] {
|
|
||||||
const vars = new Set(this.parent?.vars())
|
|
||||||
|
|
||||||
for (const name of this.locals.keys())
|
|
||||||
vars.add(name)
|
|
||||||
|
|
||||||
return [...vars].sort()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,19 +50,11 @@ const OPCODES_WITH_OPERANDS = new Set([
|
||||||
const OPCODES_WITHOUT_OPERANDS = new Set([
|
const OPCODES_WITHOUT_OPERANDS = new Set([
|
||||||
OpCode.POP,
|
OpCode.POP,
|
||||||
OpCode.DUP,
|
OpCode.DUP,
|
||||||
OpCode.SWAP,
|
|
||||||
OpCode.TYPE,
|
|
||||||
OpCode.ADD,
|
OpCode.ADD,
|
||||||
OpCode.SUB,
|
OpCode.SUB,
|
||||||
OpCode.MUL,
|
OpCode.MUL,
|
||||||
OpCode.DIV,
|
OpCode.DIV,
|
||||||
OpCode.MOD,
|
OpCode.MOD,
|
||||||
OpCode.BIT_AND,
|
|
||||||
OpCode.BIT_OR,
|
|
||||||
OpCode.BIT_XOR,
|
|
||||||
OpCode.BIT_SHL,
|
|
||||||
OpCode.BIT_SHR,
|
|
||||||
OpCode.BIT_USHR,
|
|
||||||
OpCode.EQ,
|
OpCode.EQ,
|
||||||
OpCode.NEQ,
|
OpCode.NEQ,
|
||||||
OpCode.LT,
|
OpCode.LT,
|
||||||
|
|
@ -87,15 +79,11 @@ const OPCODES_WITHOUT_OPERANDS = new Set([
|
||||||
OpCode.DOT_GET,
|
OpCode.DOT_GET,
|
||||||
])
|
])
|
||||||
|
|
||||||
// JUMP* instructions require labels only (no numeric immediates)
|
// immediate = immediate number, eg #5
|
||||||
const OPCODES_REQUIRING_LABEL = new Set([
|
const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
|
||||||
OpCode.JUMP,
|
OpCode.JUMP,
|
||||||
OpCode.JUMP_IF_FALSE,
|
OpCode.JUMP_IF_FALSE,
|
||||||
OpCode.JUMP_IF_TRUE,
|
OpCode.JUMP_IF_TRUE,
|
||||||
])
|
|
||||||
|
|
||||||
// PUSH_TRY/PUSH_FINALLY still allow immediate or label
|
|
||||||
const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
|
|
||||||
OpCode.PUSH_TRY,
|
OpCode.PUSH_TRY,
|
||||||
OpCode.PUSH_FINALLY,
|
OpCode.PUSH_FINALLY,
|
||||||
])
|
])
|
||||||
|
|
@ -201,16 +189,6 @@ export function validateBytecode(source: string): ValidationResult {
|
||||||
|
|
||||||
// Validate specific operand formats
|
// Validate specific operand formats
|
||||||
if (operand) {
|
if (operand) {
|
||||||
if (OPCODES_REQUIRING_LABEL.has(opCode)) {
|
|
||||||
if (!operand.startsWith('.')) {
|
|
||||||
errors.push({
|
|
||||||
line: lineNum,
|
|
||||||
message: `${opName} requires label (.label), got: ${operand}`,
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OPCODES_REQUIRING_IMMEDIATE_OR_LABEL.has(opCode)) {
|
if (OPCODES_REQUIRING_IMMEDIATE_OR_LABEL.has(opCode)) {
|
||||||
if (!operand.startsWith('#') && !operand.startsWith('.')) {
|
if (!operand.startsWith('#') && !operand.startsWith('.')) {
|
||||||
errors.push({
|
errors.push({
|
||||||
|
|
@ -324,11 +302,11 @@ export function validateBytecode(source: string): ValidationResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate body address (must be a label)
|
// Validate body address
|
||||||
if (!bodyAddr!.startsWith('.')) {
|
if (!bodyAddr!.startsWith('.') && !bodyAddr!.startsWith('#')) {
|
||||||
errors.push({
|
errors.push({
|
||||||
line: lineNum,
|
line: lineNum,
|
||||||
message: `Invalid body address: expected .label, got: ${bodyAddr}`,
|
message: `Invalid body address: expected .label or #offset`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
118
src/value.ts
118
src/value.ts
|
|
@ -1,12 +1,6 @@
|
||||||
import { wrapNative, getOriginalFunction } from "./function"
|
|
||||||
import { OpCode } from "./opcode"
|
|
||||||
import { Scope } from "./scope"
|
import { Scope } from "./scope"
|
||||||
import { VM } from "./vm"
|
|
||||||
|
|
||||||
export type NativeFunction = (...args: Value[]) => Promise<Value> | Value
|
export type NativeFunction = (...args: Value[]) => Promise<Value> | Value
|
||||||
export type TypeScriptFunction = (this: VM, ...args: any[]) => any
|
|
||||||
const REEF_FUNCTION = Symbol('__reefFunction')
|
|
||||||
const VALUE_TYPES = new Set(['null', 'boolean', 'number', 'string', 'array', 'dict', 'regex', 'native', 'function'])
|
|
||||||
|
|
||||||
export type Value =
|
export type Value =
|
||||||
| { type: 'null', value: null }
|
| { type: 'null', value: null }
|
||||||
|
|
@ -39,19 +33,15 @@ export type FunctionDef = {
|
||||||
named: boolean
|
named: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValue(v: any): boolean {
|
export function toValue(v: any): Value /* throws */ {
|
||||||
return !!(v && typeof v === 'object' && VALUE_TYPES.has(v.type) && 'value' in v)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toValue(v: any, vm?: VM): Value /* throws */ {
|
|
||||||
if (v === null || v === undefined)
|
if (v === null || v === undefined)
|
||||||
return { type: 'null', value: null }
|
return { type: 'null', value: null }
|
||||||
|
|
||||||
if (isValue(v))
|
if (v && typeof v === 'object' && 'type' in v && 'value' in v)
|
||||||
return v as Value
|
return v as Value
|
||||||
|
|
||||||
if (Array.isArray(v))
|
if (Array.isArray(v))
|
||||||
return { type: 'array', value: v.map(x => toValue(x, vm)) }
|
return { type: 'array', value: v.map(toValue) }
|
||||||
|
|
||||||
if (v instanceof RegExp)
|
if (v instanceof RegExp)
|
||||||
return { type: 'regex', value: v }
|
return { type: 'regex', value: v }
|
||||||
|
|
@ -64,38 +54,15 @@ export function toValue(v: any, vm?: VM): Value /* throws */ {
|
||||||
case 'string':
|
case 'string':
|
||||||
return { type: 'string', value: v }
|
return { type: 'string', value: v }
|
||||||
case 'function':
|
case 'function':
|
||||||
if ((v as any)[REEF_FUNCTION])
|
throw "can't toValue() a js function yet"
|
||||||
return (v as any)[REEF_FUNCTION]
|
|
||||||
|
|
||||||
let fn = vm ? wrapNative(vm, v) : cantCallFunctionWithoutVM(v)
|
|
||||||
return { type: 'native', fn, value: '<function>' }
|
|
||||||
case 'object':
|
case 'object':
|
||||||
const dict: Dict = new Map()
|
const dict: Dict = new Map()
|
||||||
|
|
||||||
for (const key of Object.keys(v)) dict.set(key, toValue(v[key], vm))
|
for (const key of Object.keys(v)) dict.set(key, toValue(v[key]))
|
||||||
|
|
||||||
return { type: 'dict', value: dict }
|
return { type: 'dict', value: dict }
|
||||||
default:
|
default:
|
||||||
throw new Error(`can't toValue this: ${v}`)
|
throw `can't toValue this: ${v}`
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cantCallFunctionWithoutVM(fn: Function) {
|
|
||||||
const name = fn.name || '<anonymous>'
|
|
||||||
const str = fn.toString().slice(0, 100)
|
|
||||||
return (...args: Value[]) => {
|
|
||||||
const stack = new Error().stack || ''
|
|
||||||
const stackLines = stack.split('\n')
|
|
||||||
.slice(1)
|
|
||||||
.filter(line => !line.includes('toValue'))
|
|
||||||
.map(line => ' ' + line.trim())
|
|
||||||
.join('\n')
|
|
||||||
throw new Error(
|
|
||||||
`can't call function that was converted without a vm\n` +
|
|
||||||
` Function: ${name}\n` +
|
|
||||||
` Source: ${str}${str.length > 100 ? '...' : ''}\n` +
|
|
||||||
` Called from:\n${stackLines}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -187,87 +154,28 @@ export function isEqual(a: Value, b: Value): boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fromValue(v: Value, vm?: VM): any {
|
export function fromValue(v: Value): any {
|
||||||
switch (v.type) {
|
switch (v.type) {
|
||||||
case 'null':
|
case 'null':
|
||||||
|
return null
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
|
return v.value
|
||||||
case 'number':
|
case 'number':
|
||||||
|
return v.value
|
||||||
case 'string':
|
case 'string':
|
||||||
return v.value
|
return v.value
|
||||||
case 'array':
|
case 'array':
|
||||||
return v.value.map(x => fromValue(x, vm))
|
return v.value.map(fromValue)
|
||||||
case 'dict':
|
case 'dict':
|
||||||
return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v, vm)]))
|
return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v)]))
|
||||||
case 'regex':
|
case 'regex':
|
||||||
return v.value
|
return v.value
|
||||||
case 'function':
|
case 'function':
|
||||||
if (!vm || !(vm instanceof VM)) {
|
|
||||||
const stack = new Error().stack || ''
|
|
||||||
const stackLines = stack.split('\n')
|
|
||||||
.slice(1)
|
|
||||||
.filter(line => !line.includes('fromValue'))
|
|
||||||
.map(line => ' ' + line.trim())
|
|
||||||
.join('\n')
|
|
||||||
throw new Error(
|
|
||||||
`VM is required for function conversion\n` +
|
|
||||||
` Function params: [${v.params.join(', ')}]\n` +
|
|
||||||
` Function body at instruction: ${v.body}\n` +
|
|
||||||
` Called from:\n${stackLines}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return fnFromValue(v, vm)
|
|
||||||
case 'native':
|
case 'native':
|
||||||
return getOriginalFunction(v.fn)
|
return '<function>'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toNull(): Value {
|
export function toNull(): Value {
|
||||||
return toValue(null)
|
return toValue(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fnFromValue(fn: Value, vm: VM): Function {
|
|
||||||
if (fn.type !== 'function')
|
|
||||||
throw new Error('Value is not a function')
|
|
||||||
|
|
||||||
const wrapper = async function (...args: any[]) {
|
|
||||||
let positional: any[] = args
|
|
||||||
let named: Record<string, any> = {}
|
|
||||||
|
|
||||||
if (args.length > 0 && !Array.isArray(args[args.length - 1]) && args[args.length - 1].constructor === Object) {
|
|
||||||
named = args[args.length - 1]
|
|
||||||
positional = args.slice(0, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const newVM = new VM({
|
|
||||||
instructions: vm.instructions,
|
|
||||||
constants: vm.constants,
|
|
||||||
labels: vm.labels
|
|
||||||
})
|
|
||||||
newVM.scope = fn.parentScope
|
|
||||||
|
|
||||||
newVM.stack.push(fn)
|
|
||||||
newVM.stack.push(...positional.map(x => toValue(x, vm)))
|
|
||||||
for (const [key, val] of Object.entries(named)) {
|
|
||||||
newVM.stack.push(toValue(key))
|
|
||||||
newVM.stack.push(toValue(val, vm))
|
|
||||||
}
|
|
||||||
newVM.stack.push(toValue(positional.length))
|
|
||||||
newVM.stack.push(toValue(Object.keys(named).length))
|
|
||||||
|
|
||||||
const targetDepth = newVM.callStack.length
|
|
||||||
await newVM.execute({ op: OpCode.CALL })
|
|
||||||
newVM.pc++
|
|
||||||
|
|
||||||
while (newVM.callStack.length > targetDepth && newVM.pc < newVM.instructions.length) {
|
|
||||||
await newVM.execute(newVM.instructions[newVM.pc]!)
|
|
||||||
newVM.pc++
|
|
||||||
}
|
|
||||||
|
|
||||||
return fromValue(newVM.stack.pop() || toNull(), vm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// support roundtrips, eg fromValue(toValue(fn))
|
|
||||||
; (wrapper as any)[REEF_FUNCTION] = fn
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
}
|
|
||||||
271
src/vm.ts
271
src/vm.ts
|
|
@ -3,9 +3,8 @@ import type { ExceptionHandler } from "./exception"
|
||||||
import { type Frame } from "./frame"
|
import { type Frame } from "./frame"
|
||||||
import { OpCode } from "./opcode"
|
import { OpCode } from "./opcode"
|
||||||
import { Scope } from "./scope"
|
import { Scope } from "./scope"
|
||||||
import type { Value, NativeFunction, TypeScriptFunction } from "./value"
|
import { type Value, type NativeFunction, toValue, toNumber, isTrue, isEqual, toString, fromValue } from "./value"
|
||||||
import { toValue, toNumber, isTrue, isEqual, toString, fromValue, fnFromValue } from "./value"
|
import { extractParamInfo, wrapNative, isWrapped, getOriginalFunction } from "./function"
|
||||||
import { extractParamInfo, getOriginalFunction } from "./function"
|
|
||||||
|
|
||||||
export class VM {
|
export class VM {
|
||||||
pc = 0
|
pc = 0
|
||||||
|
|
@ -19,71 +18,26 @@ export class VM {
|
||||||
labels: Map<number, string> = new Map()
|
labels: Map<number, string> = new Map()
|
||||||
nativeFunctions: Map<string, NativeFunction> = new Map()
|
nativeFunctions: Map<string, NativeFunction> = new Map()
|
||||||
|
|
||||||
constructor(bytecode: Bytecode, globals?: Record<string, any>) {
|
constructor(bytecode: Bytecode, functions?: Record<string, Function>) {
|
||||||
this.instructions = bytecode.instructions
|
this.instructions = bytecode.instructions
|
||||||
this.constants = bytecode.constants
|
this.constants = bytecode.constants
|
||||||
this.labels = bytecode.labels || new Map()
|
this.labels = bytecode.labels || new Map()
|
||||||
this.scope = new Scope()
|
this.scope = new Scope()
|
||||||
|
|
||||||
if (globals) {
|
if (functions)
|
||||||
for (const name of Object.keys(globals ?? {}))
|
for (const name of Object.keys(functions))
|
||||||
this.set(name, globals[name])
|
this.registerFunction(name, functions[name]!)
|
||||||
|
|
||||||
this.scope = new Scope(this.scope)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async call(name: string, ...args: any) {
|
registerFunction(name: string, fn: Function) {
|
||||||
const value = this.scope.get(name)
|
const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(fn)
|
||||||
|
this.scope.set(name, { type: 'native', fn: wrapped, value: '<function>' })
|
||||||
if (!value) throw new Error(`Can't find ${name}`)
|
|
||||||
if (value.type !== 'function' && value.type !== 'native')
|
|
||||||
throw new Error(`Can't call ${name}`)
|
|
||||||
|
|
||||||
if (value.type === 'native') {
|
|
||||||
return await this.callNative(value.fn, args)
|
|
||||||
} else {
|
|
||||||
const fn = fnFromValue(value, this)
|
|
||||||
return await fn(...args)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
has(name: string): boolean {
|
registerValueFunction(name: string, fn: NativeFunction) {
|
||||||
return this.scope.has(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
vars(): string[] {
|
|
||||||
return this.scope.vars()
|
|
||||||
}
|
|
||||||
|
|
||||||
get(name: string) {
|
|
||||||
return this.scope.get(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
set(name: string, value: any) {
|
|
||||||
this.scope.set(name, toValue(value, this))
|
|
||||||
}
|
|
||||||
|
|
||||||
setFunction(name: string, fn: TypeScriptFunction) {
|
|
||||||
this.scope.set(name, toValue(fn, this))
|
|
||||||
}
|
|
||||||
|
|
||||||
setValueFunction(name: string, fn: NativeFunction) {
|
|
||||||
this.scope.set(name, { type: 'native', fn, value: '<function>' })
|
this.scope.set(name, { type: 'native', fn, value: '<function>' })
|
||||||
}
|
}
|
||||||
|
|
||||||
pushScope(locals?: Record<string, any>) {
|
|
||||||
this.scope = new Scope(this.scope)
|
|
||||||
|
|
||||||
if (locals)
|
|
||||||
for (const [name, value] of Object.entries(locals))
|
|
||||||
this.set(name, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
popScope() {
|
|
||||||
this.scope = this.scope.parent!
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(): Promise<Value> {
|
async run(): Promise<Value> {
|
||||||
this.pc = 0
|
this.pc = 0
|
||||||
this.stopped = false
|
this.stopped = false
|
||||||
|
|
@ -97,57 +51,6 @@ export class VM {
|
||||||
return this.stack[this.stack.length - 1] || toValue(null)
|
return this.stack[this.stack.length - 1] || toValue(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resume execution from current PC without resetting
|
|
||||||
// Useful for REPL mode where you append bytecode incrementally
|
|
||||||
async continue(): Promise<Value> {
|
|
||||||
this.stopped = false
|
|
||||||
|
|
||||||
while (!this.stopped && this.pc < this.instructions.length) {
|
|
||||||
const instruction = this.instructions[this.pc]!
|
|
||||||
await this.execute(instruction)
|
|
||||||
this.pc++
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.stack[this.stack.length - 1] || toValue(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper for REPL mode: append new bytecode with proper constant index remapping
|
|
||||||
appendBytecode(bytecode: Bytecode): void {
|
|
||||||
const constantOffset = this.constants.length
|
|
||||||
const instructionOffset = this.instructions.length
|
|
||||||
|
|
||||||
// Remap function body addresses in constants before adding them
|
|
||||||
for (const constant of bytecode.constants) {
|
|
||||||
if (constant.type === 'function_def') {
|
|
||||||
this.constants.push({ ...constant, body: constant.body + instructionOffset })
|
|
||||||
} else {
|
|
||||||
this.constants.push(constant)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const instruction of bytecode.instructions) {
|
|
||||||
if (instruction.operand !== undefined && typeof instruction.operand === 'number') {
|
|
||||||
// Opcodes that reference constants need their operand adjusted
|
|
||||||
if (instruction.op === OpCode.PUSH || instruction.op === OpCode.MAKE_FUNCTION) {
|
|
||||||
this.instructions.push({
|
|
||||||
op: instruction.op,
|
|
||||||
operand: instruction.operand + constantOffset
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.instructions.push(instruction)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.instructions.push(instruction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bytecode.labels) {
|
|
||||||
for (const [addr, label] of bytecode.labels.entries()) {
|
|
||||||
this.labels.set(addr + instructionOffset, label)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(instruction: Instruction) /* throws */ {
|
async execute(instruction: Instruction) /* throws */ {
|
||||||
switch (instruction.op) {
|
switch (instruction.op) {
|
||||||
case OpCode.PUSH:
|
case OpCode.PUSH:
|
||||||
|
|
@ -168,32 +71,8 @@ export class VM {
|
||||||
this.stack.push(this.stack[this.stack.length - 1]!)
|
this.stack.push(this.stack[this.stack.length - 1]!)
|
||||||
break
|
break
|
||||||
|
|
||||||
case OpCode.SWAP:
|
|
||||||
const first = this.stack.pop()!
|
|
||||||
const second = this.stack.pop()!
|
|
||||||
this.stack.push(first)
|
|
||||||
this.stack.push(second)
|
|
||||||
break
|
|
||||||
|
|
||||||
case OpCode.ADD:
|
case OpCode.ADD:
|
||||||
const b = this.stack.pop()!
|
this.binaryOp((a, b) => toNumber(a) + toNumber(b))
|
||||||
const a = this.stack.pop()!
|
|
||||||
|
|
||||||
if (a.type === 'string' || b.type === 'string') {
|
|
||||||
this.stack.push(toValue(toString(a) + toString(b)))
|
|
||||||
} else if (a.type === 'array' && b.type === 'array') {
|
|
||||||
this.stack.push({ type: 'array', value: [...a.value, ...b.value] })
|
|
||||||
} else if (a.type === 'dict' && b.type === 'dict') {
|
|
||||||
const merged = new Map(a.value)
|
|
||||||
for (const [key, value] of b.value) {
|
|
||||||
merged.set(key, value)
|
|
||||||
}
|
|
||||||
this.stack.push({ type: 'dict', value: merged })
|
|
||||||
} else if (a.type === 'number' && b.type === 'number') {
|
|
||||||
this.stack.push(toValue(a.value + b.value))
|
|
||||||
} else {
|
|
||||||
throw new Error(`ADD: Cannot add ${a.type} and ${b.type}`)
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case OpCode.SUB:
|
case OpCode.SUB:
|
||||||
|
|
@ -241,31 +120,6 @@ export class VM {
|
||||||
this.stack.push({ type: 'boolean', value: !isTrue(val) })
|
this.stack.push({ type: 'boolean', value: !isTrue(val) })
|
||||||
break
|
break
|
||||||
|
|
||||||
// Bitwise operations
|
|
||||||
case OpCode.BIT_AND:
|
|
||||||
this.binaryOp((a, b) => (toNumber(a) | 0) & (toNumber(b) | 0))
|
|
||||||
break
|
|
||||||
|
|
||||||
case OpCode.BIT_OR:
|
|
||||||
this.binaryOp((a, b) => (toNumber(a) | 0) | (toNumber(b) | 0))
|
|
||||||
break
|
|
||||||
|
|
||||||
case OpCode.BIT_XOR:
|
|
||||||
this.binaryOp((a, b) => (toNumber(a) | 0) ^ (toNumber(b) | 0))
|
|
||||||
break
|
|
||||||
|
|
||||||
case OpCode.BIT_SHL:
|
|
||||||
this.binaryOp((a, b) => (toNumber(a) | 0) << (toNumber(b) | 0))
|
|
||||||
break
|
|
||||||
|
|
||||||
case OpCode.BIT_SHR:
|
|
||||||
this.binaryOp((a, b) => (toNumber(a) | 0) >> (toNumber(b) | 0))
|
|
||||||
break
|
|
||||||
|
|
||||||
case OpCode.BIT_USHR:
|
|
||||||
this.binaryOp((a, b) => (toNumber(a) | 0) >>> (toNumber(b) | 0))
|
|
||||||
break
|
|
||||||
|
|
||||||
case OpCode.HALT:
|
case OpCode.HALT:
|
||||||
this.stopped = true
|
this.stopped = true
|
||||||
break
|
break
|
||||||
|
|
@ -286,18 +140,13 @@ export class VM {
|
||||||
const value = this.scope.get(varName)
|
const value = this.scope.get(varName)
|
||||||
|
|
||||||
if (value === undefined)
|
if (value === undefined)
|
||||||
this.stack.push(toValue(varName, this))
|
this.stack.push(toValue(varName))
|
||||||
else
|
else
|
||||||
this.stack.push(value)
|
this.stack.push(value)
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case OpCode.TYPE:
|
|
||||||
const value = this.stack.pop()!
|
|
||||||
this.stack.push(toValue(value.type))
|
|
||||||
break
|
|
||||||
|
|
||||||
case OpCode.STORE:
|
case OpCode.STORE:
|
||||||
const name = instruction.operand as string
|
const name = instruction.operand as string
|
||||||
const toStore = this.stack.pop()!
|
const toStore = this.stack.pop()!
|
||||||
|
|
@ -493,10 +342,10 @@ export class VM {
|
||||||
const target = this.stack.pop()!
|
const target = this.stack.pop()!
|
||||||
|
|
||||||
if (target.type === 'array')
|
if (target.type === 'array')
|
||||||
this.stack.push(toValue(target.value?.[Number(index.value)], this))
|
this.stack.push(toValue(target.value?.[Number(index.value)]))
|
||||||
|
|
||||||
else if (target.type === 'dict')
|
else if (target.type === 'dict')
|
||||||
this.stack.push(toValue(target.value?.get(String(index.value)), this))
|
this.stack.push(toValue(target.value?.get(String(index.value))))
|
||||||
|
|
||||||
else
|
else
|
||||||
throw new Error(`DOT_GET: ${target.type} not supported`)
|
throw new Error(`DOT_GET: ${target.type} not supported`)
|
||||||
|
|
@ -602,25 +451,16 @@ export class VM {
|
||||||
let nativePositionalArgIndex = 0
|
let nativePositionalArgIndex = 0
|
||||||
|
|
||||||
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
||||||
// Note: null values trigger defaults (null acts as "use default")
|
|
||||||
for (let i = 0; i < nativeFixedParamCount; i++) {
|
for (let i = 0; i < nativeFixedParamCount; i++) {
|
||||||
const paramName = paramInfo.params[i]!
|
const paramName = paramInfo.params[i]!
|
||||||
let paramValue: Value | undefined
|
|
||||||
|
|
||||||
// Check if named argument was provided for this param
|
// Check if named argument was provided for this param
|
||||||
if (namedArgs.has(paramName)) {
|
if (namedArgs.has(paramName)) {
|
||||||
paramValue = namedArgs.get(paramName)!
|
nativeArgs.push(namedArgs.get(paramName)!)
|
||||||
namedArgs.delete(paramName) // Remove from named args so it won't go to @named
|
namedArgs.delete(paramName) // Remove from named args so it won't go to @named
|
||||||
} else if (nativePositionalArgIndex < positionalArgs.length) {
|
} else if (nativePositionalArgIndex < positionalArgs.length) {
|
||||||
paramValue = positionalArgs[nativePositionalArgIndex]!
|
nativeArgs.push(positionalArgs[nativePositionalArgIndex]!)
|
||||||
nativePositionalArgIndex++
|
nativePositionalArgIndex++
|
||||||
}
|
|
||||||
|
|
||||||
// If the parameter value is null and a default exists, use the default
|
|
||||||
if (paramValue?.type === 'null' && paramInfo.defaults[paramName] !== undefined) {
|
|
||||||
nativeArgs.push(paramInfo.defaults[paramName]!)
|
|
||||||
} else if (paramValue) {
|
|
||||||
nativeArgs.push(paramValue)
|
|
||||||
} else if (paramInfo.defaults[paramName] !== undefined) {
|
} else if (paramInfo.defaults[paramName] !== undefined) {
|
||||||
nativeArgs.push(paramInfo.defaults[paramName]!)
|
nativeArgs.push(paramInfo.defaults[paramName]!)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -636,8 +476,8 @@ export class VM {
|
||||||
namedDict.set(key, value)
|
namedDict.set(key, value)
|
||||||
}
|
}
|
||||||
// Convert dict to plain JavaScript object for the native function
|
// Convert dict to plain JavaScript object for the native function
|
||||||
const namedObj = fromValue({ type: 'dict', value: namedDict }, this)
|
const namedObj = fromValue({ type: 'dict', value: namedDict })
|
||||||
nativeArgs.push(toValue(namedObj, this))
|
nativeArgs.push(toValue(namedObj))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle variadic parameter (TypeScript rest parameters)
|
// Handle variadic parameter (TypeScript rest parameters)
|
||||||
|
|
@ -649,41 +489,13 @@ export class VM {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the native function with bound args
|
// Call the native function with bound args
|
||||||
try {
|
const result = await fn.fn(...nativeArgs)
|
||||||
const result = await fn.fn.call(this, ...nativeArgs)
|
|
||||||
this.stack.push(result)
|
this.stack.push(result)
|
||||||
break
|
break
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
||||||
const errorValue = toValue(errorMessage)
|
|
||||||
|
|
||||||
// no exception handlers, let it crash
|
|
||||||
if (this.exceptionHandlers.length === 0) {
|
|
||||||
throw new Error(errorMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// use existing THROW logic
|
|
||||||
const throwHandler = this.exceptionHandlers.pop()!
|
|
||||||
|
|
||||||
while (this.callStack.length > throwHandler.callStackDepth)
|
|
||||||
this.callStack.pop()
|
|
||||||
|
|
||||||
this.scope = throwHandler.scope
|
|
||||||
this.stack.push(errorValue)
|
|
||||||
|
|
||||||
// Jump to `finally` if present, otherwise jump to `catch`
|
|
||||||
const targetAddress = throwHandler.finallyAddress !== undefined
|
|
||||||
? throwHandler.finallyAddress
|
|
||||||
: throwHandler.catchAddress
|
|
||||||
|
|
||||||
// subtract 1 because pc will be incremented
|
|
||||||
this.pc = targetAddress - 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fn.type !== 'function')
|
if (fn.type !== 'function')
|
||||||
throw new Error(`CALL: ${fn.value} is not a function`)
|
throw new Error('CALL: not a function')
|
||||||
|
|
||||||
if (this.callStack.length > 0)
|
if (this.callStack.length > 0)
|
||||||
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
|
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
|
||||||
|
|
@ -705,29 +517,16 @@ export class VM {
|
||||||
let positionalArgIndex = 0
|
let positionalArgIndex = 0
|
||||||
|
|
||||||
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
||||||
// Note: null values trigger defaults (null acts as "use default")
|
|
||||||
for (let i = 0; i < fixedParamCount; i++) {
|
for (let i = 0; i < fixedParamCount; i++) {
|
||||||
const paramName = fn.params[i]!
|
const paramName = fn.params[i]!
|
||||||
let paramValue: Value | undefined
|
|
||||||
|
|
||||||
// Check if named argument was provided for this param
|
// Check if named argument was provided for this param
|
||||||
if (namedArgs.has(paramName)) {
|
if (namedArgs.has(paramName)) {
|
||||||
paramValue = namedArgs.get(paramName)!
|
this.scope.set(paramName, namedArgs.get(paramName)!)
|
||||||
namedArgs.delete(paramName) // Remove from named args so it won't go to named
|
namedArgs.delete(paramName) // Remove from named args so it won't go to named
|
||||||
} else if (positionalArgIndex < positionalArgs.length) {
|
} else if (positionalArgIndex < positionalArgs.length) {
|
||||||
paramValue = positionalArgs[positionalArgIndex]!
|
this.scope.set(paramName, positionalArgs[positionalArgIndex]!)
|
||||||
positionalArgIndex++
|
positionalArgIndex++
|
||||||
}
|
|
||||||
|
|
||||||
// If the parameter value is null and a default exists, use the default
|
|
||||||
if (paramValue && paramValue.type === 'null' && fn.defaults[paramName] !== undefined) {
|
|
||||||
const defaultIdx = fn.defaults[paramName]!
|
|
||||||
const defaultValue = this.constants[defaultIdx]!
|
|
||||||
if (defaultValue.type === 'function_def')
|
|
||||||
throw new Error('Default value cannot be a function definition')
|
|
||||||
this.scope.set(paramName, defaultValue)
|
|
||||||
} else if (paramValue) {
|
|
||||||
this.scope.set(paramName, paramValue)
|
|
||||||
} else if (fn.defaults[paramName] !== undefined) {
|
} else if (fn.defaults[paramName] !== undefined) {
|
||||||
const defaultIdx = fn.defaults[paramName]!
|
const defaultIdx = fn.defaults[paramName]!
|
||||||
const defaultValue = this.constants[defaultIdx]!
|
const defaultValue = this.constants[defaultIdx]!
|
||||||
|
|
@ -788,7 +587,7 @@ export class VM {
|
||||||
const tailFn = this.stack.pop()!
|
const tailFn = this.stack.pop()!
|
||||||
|
|
||||||
if (tailFn.type !== 'function')
|
if (tailFn.type !== 'function')
|
||||||
throw new Error(`TAIL_CALL: ${tailFn.value} is not a function`)
|
throw new Error('TAIL_CALL: not a function')
|
||||||
|
|
||||||
this.scope = new Scope(tailFn.parentScope)
|
this.scope = new Scope(tailFn.parentScope)
|
||||||
|
|
||||||
|
|
@ -857,7 +656,7 @@ export class VM {
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown op: ${instruction.op}`)
|
throw `Unknown op: ${instruction.op}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -874,26 +673,4 @@ export class VM {
|
||||||
const result = fn(a, b)
|
const result = fn(a, b)
|
||||||
this.stack.push({ type: 'boolean', value: result })
|
this.stack.push({ type: 'boolean', value: result })
|
||||||
}
|
}
|
||||||
|
|
||||||
async callNative(nativeFn: NativeFunction, args: any[]): Promise<Value> {
|
|
||||||
const originalFn = getOriginalFunction(nativeFn)
|
|
||||||
|
|
||||||
const lastArg = args[args.length - 1]
|
|
||||||
if (lastArg && !Array.isArray(lastArg) && typeof lastArg === 'object') {
|
|
||||||
const paramInfo = extractParamInfo(originalFn)
|
|
||||||
const positional = args.slice(0, -1)
|
|
||||||
const named = lastArg
|
|
||||||
|
|
||||||
args = [...positional]
|
|
||||||
for (let i = positional.length; i < paramInfo.params.length; i++) {
|
|
||||||
const paramName = paramInfo.params[i]!
|
|
||||||
if (named[paramName] !== undefined) {
|
|
||||||
args[i] = named[paramName]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await originalFn.call(this, ...args)
|
|
||||||
return toValue(result, this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
import { expect, describe, test } from 'bun:test'
|
|
||||||
import { toBytecode, run } from '#reef'
|
|
||||||
|
|
||||||
describe('bitwise operations', () => {
|
|
||||||
test('BIT_AND', async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", 5], ["PUSH", 3], ["BIT_AND"], ["HALT"]
|
|
||||||
])
|
|
||||||
await expect(bytecode).toBeNumber(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('BIT_AND with zero', async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", 5], ["PUSH", 0], ["BIT_AND"], ["HALT"]
|
|
||||||
])
|
|
||||||
await expect(bytecode).toBeNumber(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('BIT_OR', async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", 5], ["PUSH", 3], ["BIT_OR"], ["HALT"]
|
|
||||||
])
|
|
||||||
await expect(bytecode).toBeNumber(7)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('BIT_OR with zero', async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", 5], ["PUSH", 0], ["BIT_OR"], ["HALT"]
|
|
||||||
])
|
|
||||||
await expect(bytecode).toBeNumber(5)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('BIT_XOR', async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", 5], ["PUSH", 3], ["BIT_XOR"], ["HALT"]
|
|
||||||
])
|
|
||||||
await expect(bytecode).toBeNumber(6)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('BIT_XOR with itself returns zero', async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", 5], ["PUSH", 5], ["BIT_XOR"], ["HALT"]
|
|
||||||
])
|
|
||||||
await expect(bytecode).toBeNumber(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('BIT_SHL', async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", 5], ["PUSH", 2], ["BIT_SHL"], ["HALT"]
|
|
||||||
])
|
|
||||||
await expect(bytecode).toBeNumber(20)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('BIT_SHL by zero', async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", 5], ["PUSH", 0], ["BIT_SHL"], ["HALT"]
|
|
||||||
])
|
|
||||||
await expect(bytecode).toBeNumber(5)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('BIT_SHR', async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", 20], ["PUSH", 2], ["BIT_SHR"], ["HALT"]
|
|
||||||
])
|
|
||||||
await expect(bytecode).toBeNumber(5)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('BIT_SHR preserves sign for negative numbers', async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", -20], ["PUSH", 2], ["BIT_SHR"], ["HALT"]
|
|
||||||
])
|
|
||||||
await expect(bytecode).toBeNumber(-5)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('BIT_USHR', async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", -1], ["PUSH", 1], ["BIT_USHR"], ["HALT"]
|
|
||||||
])
|
|
||||||
await expect(bytecode).toBeNumber(2147483647)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('BIT_USHR does not preserve sign', async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", -8], ["PUSH", 1], ["BIT_USHR"], ["HALT"]
|
|
||||||
])
|
|
||||||
await expect(bytecode).toBeNumber(2147483644)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('compound bitwise operations', async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
// (5 & 3) | (8 ^ 12)
|
|
||||||
["PUSH", 5], ["PUSH", 3], ["BIT_AND"], // stack: [1]
|
|
||||||
["PUSH", 8], ["PUSH", 12], ["BIT_XOR"], // stack: [1, 4]
|
|
||||||
["BIT_OR"], // stack: [5]
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
await expect(bytecode).toBeNumber(5)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shift with large shift amounts', async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["PUSH", 1], ["PUSH", 31], ["BIT_SHL"], ["HALT"]
|
|
||||||
])
|
|
||||||
// 1 << 31 = -2147483648 (most significant bit set)
|
|
||||||
await expect(bytecode).toBeNumber(-2147483648)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -15,7 +15,7 @@ test("PUSH_TRY and POP_TRY - no exception thrown", async () => {
|
||||||
PUSH 999
|
PUSH 999
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
await expect(str).toBeNumber(52)
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("THROW - catch exception with error value", async () => {
|
test("THROW - catch exception with error value", async () => {
|
||||||
|
|
@ -29,7 +29,7 @@ test("THROW - catch exception with error value", async () => {
|
||||||
.catch:
|
.catch:
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
await expect(str).toBeString('error occurred')
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error occurred' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("THROW - uncaught exception throws JS error", async () => {
|
test("THROW - uncaught exception throws JS error", async () => {
|
||||||
|
|
@ -58,7 +58,7 @@ test("THROW - exception with nested try blocks", async () => {
|
||||||
PUSH "outer error"
|
PUSH "outer error"
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
await expect(str).toBeString('inner error')
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'inner error' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("THROW - exception skips outer handler", async () => {
|
test("THROW - exception skips outer handler", async () => {
|
||||||
|
|
@ -75,7 +75,7 @@ test("THROW - exception skips outer handler", async () => {
|
||||||
.outer_catch:
|
.outer_catch:
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
await expect(str).toBeString('error message')
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error message' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("THROW - exception unwinds call stack", async () => {
|
test("THROW - exception unwinds call stack", async () => {
|
||||||
|
|
@ -150,7 +150,7 @@ test("PUSH_FINALLY - finally executes after successful try", async () => {
|
||||||
ADD
|
ADD
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
await expect(str).toBeNumber(110)
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 110 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("PUSH_FINALLY - finally executes after exception", async () => {
|
test("PUSH_FINALLY - finally executes after exception", async () => {
|
||||||
|
|
@ -169,7 +169,7 @@ test("PUSH_FINALLY - finally executes after exception", async () => {
|
||||||
PUSH "finally ran"
|
PUSH "finally ran"
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
await expect(str).toBeString('finally ran')
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'finally ran' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("PUSH_FINALLY - finally without catch", async () => {
|
test("PUSH_FINALLY - finally without catch", async () => {
|
||||||
|
|
@ -189,7 +189,7 @@ test("PUSH_FINALLY - finally without catch", async () => {
|
||||||
ADD
|
ADD
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
await expect(str).toBeNumber(52)
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("PUSH_FINALLY - nested try-finally blocks", async () => {
|
test("PUSH_FINALLY - nested try-finally blocks", async () => {
|
||||||
|
|
@ -214,7 +214,7 @@ test("PUSH_FINALLY - nested try-finally blocks", async () => {
|
||||||
ADD
|
ADD
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
await expect(str).toBeNumber(11)
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 11 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("PUSH_FINALLY - error when no handler", async () => {
|
test("PUSH_FINALLY - error when no handler", async () => {
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ describe("functions parameter", () => {
|
||||||
expect(result).toEqual({ type: 'number', value: 200 })
|
expect(result).toEqual({ type: 'number', value: 200 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("can combine with manual vm.set", async () => {
|
test("can combine with manual registerFunction", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
LOAD add
|
LOAD add
|
||||||
PUSH 5
|
PUSH 5
|
||||||
|
|
@ -127,7 +127,7 @@ describe("functions parameter", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Register another function manually
|
// Register another function manually
|
||||||
vm.set('subtract', (a: number, b: number) => a - b)
|
vm.registerFunction('subtract', (a: number, b: number) => a - b)
|
||||||
|
|
||||||
const result = await vm.run()
|
const result = await vm.run()
|
||||||
expect(result).toEqual({ type: 'number', value: 6 })
|
expect(result).toEqual({ type: 'number', value: 6 })
|
||||||
|
|
@ -224,7 +224,7 @@ describe("functions parameter", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Override with manual registration
|
// Override with manual registration
|
||||||
vm.set('getValue', () => 200)
|
vm.registerFunction('getValue', () => 200)
|
||||||
|
|
||||||
const result = await vm.run()
|
const result = await vm.run()
|
||||||
expect(result).toEqual({ type: 'number', value: 200 })
|
expect(result).toEqual({ type: 'number', value: 200 })
|
||||||
|
|
|
||||||
|
|
@ -487,7 +487,7 @@ test("TRY_CALL - handles null values", async () => {
|
||||||
|
|
||||||
test("TRY_CALL - function can access its parameters", async () => {
|
test("TRY_CALL - function can access its parameters", async () => {
|
||||||
const bytecode = toBytecode([
|
const bytecode = toBytecode([
|
||||||
["MAKE_FUNCTION", ["x=0"], ".body"],
|
["MAKE_FUNCTION", ["x"], ".body"],
|
||||||
["STORE", "addFive"],
|
["STORE", "addFive"],
|
||||||
["PUSH", 10],
|
["PUSH", 10],
|
||||||
["STORE", "x"],
|
["STORE", "x"],
|
||||||
|
|
@ -501,8 +501,8 @@ test("TRY_CALL - function can access its parameters", async () => {
|
||||||
])
|
])
|
||||||
|
|
||||||
const result = await run(bytecode)
|
const result = await run(bytecode)
|
||||||
// Function is called with 0 args, so x defaults to 0
|
// Function is called with 0 args, so x inside function should be null
|
||||||
// Then we add 5 to 0
|
// Then we add 5 to null (which coerces to 0)
|
||||||
expect(result).toEqual({ type: 'number', value: 5 })
|
expect(result).toEqual({ type: 'number', value: 5 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -519,206 +519,3 @@ test("TRY_CALL - with string format", async () => {
|
||||||
const result = await run(bytecode)
|
const result = await run(bytecode)
|
||||||
expect(result).toEqual({ type: 'number', value: 100 })
|
expect(result).toEqual({ type: 'number', value: 100 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("CALL - passing null triggers default value for single parameter", async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["MAKE_FUNCTION", ["x=42"], ".body"],
|
|
||||||
["STORE", "func"],
|
|
||||||
["JUMP", ".end"],
|
|
||||||
[".body:"],
|
|
||||||
["LOAD", "x"],
|
|
||||||
["RETURN"],
|
|
||||||
[".end:"],
|
|
||||||
["LOAD", "func"],
|
|
||||||
["PUSH", null],
|
|
||||||
["PUSH", 1],
|
|
||||||
["PUSH", 0],
|
|
||||||
["CALL"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
// Passing null should trigger the default value of 42
|
|
||||||
expect(result).toEqual({ type: 'number', value: 42 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("CALL - passing null triggers default value for multiple parameters", async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["MAKE_FUNCTION", ["a=10", "b=20", "c=30"], ".body"],
|
|
||||||
["STORE", "func"],
|
|
||||||
["JUMP", ".end"],
|
|
||||||
[".body:"],
|
|
||||||
["LOAD", "a"],
|
|
||||||
["LOAD", "b"],
|
|
||||||
["ADD"],
|
|
||||||
["LOAD", "c"],
|
|
||||||
["ADD"],
|
|
||||||
["RETURN"],
|
|
||||||
[".end:"],
|
|
||||||
["LOAD", "func"],
|
|
||||||
["PUSH", 5],
|
|
||||||
["PUSH", null],
|
|
||||||
["PUSH", null],
|
|
||||||
["PUSH", 3],
|
|
||||||
["PUSH", 0],
|
|
||||||
["CALL"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
// a=5 (provided), b=20 (null triggers default), c=30 (null triggers default)
|
|
||||||
// Result: 5 + 20 + 30 = 55
|
|
||||||
expect(result).toEqual({ type: 'number', value: 55 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("CALL - null in middle parameter triggers default", async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["MAKE_FUNCTION", ["x=100", "y=200", "z=300"], ".body"],
|
|
||||||
["STORE", "func"],
|
|
||||||
["JUMP", ".end"],
|
|
||||||
[".body:"],
|
|
||||||
["LOAD", "x"],
|
|
||||||
["LOAD", "y"],
|
|
||||||
["ADD"],
|
|
||||||
["LOAD", "z"],
|
|
||||||
["ADD"],
|
|
||||||
["RETURN"],
|
|
||||||
[".end:"],
|
|
||||||
["LOAD", "func"],
|
|
||||||
["PUSH", 1],
|
|
||||||
["PUSH", null],
|
|
||||||
["PUSH", 3],
|
|
||||||
["PUSH", 3],
|
|
||||||
["PUSH", 0],
|
|
||||||
["CALL"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
// x=1, y=200 (null triggers default), z=3
|
|
||||||
// Result: 1 + 200 + 3 = 204
|
|
||||||
expect(result).toEqual({ type: 'number', value: 204 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("CALL - null with named arguments triggers default", async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["MAKE_FUNCTION", ["x=50", "y=75"], ".body"],
|
|
||||||
["STORE", "func"],
|
|
||||||
["JUMP", ".end"],
|
|
||||||
[".body:"],
|
|
||||||
["LOAD", "x"],
|
|
||||||
["LOAD", "y"],
|
|
||||||
["ADD"],
|
|
||||||
["RETURN"],
|
|
||||||
[".end:"],
|
|
||||||
["LOAD", "func"],
|
|
||||||
["PUSH", "x"],
|
|
||||||
["PUSH", null],
|
|
||||||
["PUSH", "y"],
|
|
||||||
["PUSH", 25],
|
|
||||||
["PUSH", 0],
|
|
||||||
["PUSH", 2],
|
|
||||||
["CALL"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
// x=50 (null triggers default), y=25 (provided via named arg)
|
|
||||||
// Result: 50 + 25 = 75
|
|
||||||
expect(result).toEqual({ type: 'number', value: 75 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("CALL - null with string default value", async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["MAKE_FUNCTION", ["name='Guest'"], ".body"],
|
|
||||||
["STORE", "greet"],
|
|
||||||
["JUMP", ".end"],
|
|
||||||
[".body:"],
|
|
||||||
["PUSH", "Hello, "],
|
|
||||||
["LOAD", "name"],
|
|
||||||
["ADD"],
|
|
||||||
["RETURN"],
|
|
||||||
[".end:"],
|
|
||||||
["LOAD", "greet"],
|
|
||||||
["PUSH", null],
|
|
||||||
["PUSH", 1],
|
|
||||||
["PUSH", 0],
|
|
||||||
["CALL"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
// Passing null should trigger the default value 'Guest'
|
|
||||||
expect(result).toEqual({ type: 'string', value: 'Hello, Guest' })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("CALL - null with no default still results in null", async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["MAKE_FUNCTION", ["x"], ".body"],
|
|
||||||
["STORE", "func"],
|
|
||||||
["JUMP", ".end"],
|
|
||||||
[".body:"],
|
|
||||||
["LOAD", "x"],
|
|
||||||
["RETURN"],
|
|
||||||
[".end:"],
|
|
||||||
["LOAD", "func"],
|
|
||||||
["PUSH", null],
|
|
||||||
["PUSH", 1],
|
|
||||||
["PUSH", 0],
|
|
||||||
["CALL"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
// No default value, so null should be returned
|
|
||||||
expect(result).toEqual({ type: 'null', value: null })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("CALL - null triggers default with variadic parameters", async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["MAKE_FUNCTION", ["x=99", "...rest"], ".body"],
|
|
||||||
["STORE", "func"],
|
|
||||||
["JUMP", ".end"],
|
|
||||||
[".body:"],
|
|
||||||
["LOAD", "x"],
|
|
||||||
["RETURN"],
|
|
||||||
[".end:"],
|
|
||||||
["LOAD", "func"],
|
|
||||||
["PUSH", null],
|
|
||||||
["PUSH", 1],
|
|
||||||
["PUSH", 2],
|
|
||||||
["PUSH", 3],
|
|
||||||
["PUSH", 4],
|
|
||||||
["PUSH", 0],
|
|
||||||
["CALL"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
// x should be 99 (null triggers default), rest gets [1, 2, 3]
|
|
||||||
expect(result).toEqual({ type: 'number', value: 99 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("CALL - null triggers default with @named parameter", async () => {
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["MAKE_FUNCTION", ["x=777", "@named"], ".body"],
|
|
||||||
["STORE", "func"],
|
|
||||||
["JUMP", ".end"],
|
|
||||||
[".body:"],
|
|
||||||
["LOAD", "x"],
|
|
||||||
["RETURN"],
|
|
||||||
[".end:"],
|
|
||||||
["LOAD", "func"],
|
|
||||||
["PUSH", null],
|
|
||||||
["PUSH", "foo"],
|
|
||||||
["PUSH", "bar"],
|
|
||||||
["PUSH", 1],
|
|
||||||
["PUSH", 1],
|
|
||||||
["CALL"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const result = await run(bytecode)
|
|
||||||
// x should be 777 (null triggers default)
|
|
||||||
expect(result).toEqual({ type: 'number', value: 777 })
|
|
||||||
})
|
|
||||||
|
|
|
||||||
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
|
||||||
`
|
`
|
||||||
await expect(str).toBeBoolean(true)
|
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||||
|
|
||||||
const str2 = `
|
const str2 = `
|
||||||
PUSH /test/i
|
PUSH /test/i
|
||||||
PUSH /test/i
|
PUSH /test/i
|
||||||
NEQ
|
NEQ
|
||||||
`
|
`
|
||||||
await expect(str2).toBeBoolean(false)
|
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("is truthy", async () => {
|
test("is truthy", async () => {
|
||||||
|
|
@ -177,7 +177,7 @@ describe("RegExp", () => {
|
||||||
PUSH 42
|
PUSH 42
|
||||||
.end:
|
.end:
|
||||||
`
|
`
|
||||||
await expect(str).toBeNumber(42)
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("NOT returns false (regex is truthy)", async () => {
|
test("NOT returns false (regex is truthy)", async () => {
|
||||||
|
|
@ -185,7 +185,7 @@ describe("RegExp", () => {
|
||||||
PUSH /pattern/
|
PUSH /pattern/
|
||||||
NOT
|
NOT
|
||||||
`
|
`
|
||||||
await expect(str).toBeBoolean(false)
|
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("in arrays", async () => {
|
test("in arrays", async () => {
|
||||||
|
|
@ -301,7 +301,7 @@ describe("RegExp", () => {
|
||||||
PUSH /bar/i
|
PUSH /bar/i
|
||||||
STR_CONCAT #3
|
STR_CONCAT #3
|
||||||
`
|
`
|
||||||
await expect(str).toBeString('/foo/ and /bar/i')
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: '/foo/ and /bar/i' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("DUP with regex", async () => {
|
test("DUP with regex", async () => {
|
||||||
|
|
@ -311,7 +311,7 @@ describe("RegExp", () => {
|
||||||
EQ
|
EQ
|
||||||
`
|
`
|
||||||
// Same regex duplicated should be equal
|
// Same regex duplicated should be equal
|
||||||
await expect(str).toBeBoolean(true)
|
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("empty pattern", async () => {
|
test("empty pattern", async () => {
|
||||||
|
|
@ -365,7 +365,7 @@ describe("RegExp", () => {
|
||||||
PUSH /xyz/
|
PUSH /xyz/
|
||||||
EQ
|
EQ
|
||||||
`
|
`
|
||||||
await expect(str1).toBeBoolean(false)
|
expect(await run(toBytecode(str1))).toEqual({ type: 'boolean', value: false })
|
||||||
|
|
||||||
// Same pattern, different flags
|
// Same pattern, different flags
|
||||||
const str2 = `
|
const str2 = `
|
||||||
|
|
@ -373,7 +373,7 @@ describe("RegExp", () => {
|
||||||
PUSH /test/i
|
PUSH /test/i
|
||||||
EQ
|
EQ
|
||||||
`
|
`
|
||||||
await expect(str2).toBeBoolean(false)
|
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
||||||
|
|
||||||
// Different order of flags (should be equal)
|
// Different order of flags (should be equal)
|
||||||
const str3 = `
|
const str3 = `
|
||||||
|
|
@ -381,7 +381,7 @@ describe("RegExp", () => {
|
||||||
PUSH /test/gi
|
PUSH /test/gi
|
||||||
EQ
|
EQ
|
||||||
`
|
`
|
||||||
await expect(str3).toBeBoolean(true)
|
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("with native functions", async () => {
|
test("with native functions", async () => {
|
||||||
|
|
@ -399,7 +399,7 @@ describe("RegExp", () => {
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
// Register a native function that takes a string and regex
|
// Register a native function that takes a string and regex
|
||||||
vm.set('match', (str: string, pattern: RegExp) => {
|
vm.registerFunction('match', (str: string, pattern: RegExp) => {
|
||||||
return pattern.test(str)
|
return pattern.test(str)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -422,7 +422,7 @@ describe("RegExp", () => {
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
vm.set('replace', (str: string, pattern: RegExp, replacement: string) => {
|
vm.registerFunction('replace', (str: string, pattern: RegExp, replacement: string) => {
|
||||||
return str.replace(pattern, replacement)
|
return str.replace(pattern, replacement)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -444,7 +444,7 @@ describe("RegExp", () => {
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
vm.set('extractNumbers', (str: string, pattern: RegExp) => {
|
vm.registerFunction('extractNumbers', (str: string, pattern: RegExp) => {
|
||||||
return str.match(pattern) || []
|
return str.match(pattern) || []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,149 +0,0 @@
|
||||||
import { test, expect } from "bun:test"
|
|
||||||
import { VM, toBytecode } from "#reef"
|
|
||||||
|
|
||||||
test("REPL mode - demonstrates PC reset problem", async () => {
|
|
||||||
// Track how many times each line executes
|
|
||||||
let line1Count = 0
|
|
||||||
let line2Count = 0
|
|
||||||
|
|
||||||
// Line 1: Set x = 5, track execution
|
|
||||||
const line1 = toBytecode([
|
|
||||||
["LOAD", "trackLine1"],
|
|
||||||
["PUSH", 0],
|
|
||||||
["PUSH", 0],
|
|
||||||
["CALL"],
|
|
||||||
["POP"],
|
|
||||||
["PUSH", 5],
|
|
||||||
["STORE", "x"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const vm = new VM(line1, {
|
|
||||||
trackLine1: () => { line1Count++; return null },
|
|
||||||
trackLine2: () => { line2Count++; return null }
|
|
||||||
})
|
|
||||||
|
|
||||||
await vm.run()
|
|
||||||
expect(vm.scope.get("x")).toEqual({ type: "number", value: 5 })
|
|
||||||
expect(line1Count).toBe(1)
|
|
||||||
expect(line2Count).toBe(0)
|
|
||||||
|
|
||||||
// Line 2: Track execution, load x
|
|
||||||
const line2 = toBytecode([
|
|
||||||
["LOAD", "trackLine2"],
|
|
||||||
["PUSH", 0],
|
|
||||||
["PUSH", 0],
|
|
||||||
["CALL"],
|
|
||||||
["POP"],
|
|
||||||
["LOAD", "x"]
|
|
||||||
])
|
|
||||||
|
|
||||||
// Append line2 bytecode to VM (what a REPL would do)
|
|
||||||
vm.instructions.push(...line2.instructions)
|
|
||||||
for (const constant of line2.constants) {
|
|
||||||
if (!vm.constants.includes(constant)) {
|
|
||||||
vm.constants.push(constant)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current behavior: run() resets PC to 0, re-executing everything
|
|
||||||
await vm.run()
|
|
||||||
|
|
||||||
// PROBLEM: Line 1 ran AGAIN (count is now 2, not 1)
|
|
||||||
// This is the issue for REPL - side effects run multiple times
|
|
||||||
expect(line1Count).toBe(2) // Ran twice! Should still be 1
|
|
||||||
expect(line2Count).toBe(1) // This is correct
|
|
||||||
|
|
||||||
// What we WANT for REPL:
|
|
||||||
// - Only execute the NEW bytecode (line 2)
|
|
||||||
// - line1Count should stay at 1
|
|
||||||
// - line2Count should be 1
|
|
||||||
})
|
|
||||||
|
|
||||||
test("REPL mode - continue() executes only new bytecode", async () => {
|
|
||||||
// Track how many times each line executes
|
|
||||||
let line1Count = 0
|
|
||||||
let line2Count = 0
|
|
||||||
|
|
||||||
// Line 1: Set x = 5, track execution
|
|
||||||
const line1 = toBytecode([
|
|
||||||
["LOAD", "trackLine1"],
|
|
||||||
["PUSH", 0],
|
|
||||||
["PUSH", 0],
|
|
||||||
["CALL"],
|
|
||||||
["POP"],
|
|
||||||
["PUSH", 5],
|
|
||||||
["STORE", "x"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const vm = new VM(line1, {
|
|
||||||
trackLine1: () => { line1Count++; return null },
|
|
||||||
trackLine2: () => { line2Count++; return null }
|
|
||||||
})
|
|
||||||
|
|
||||||
await vm.run()
|
|
||||||
expect(vm.scope.get("x")).toEqual({ type: "number", value: 5 })
|
|
||||||
expect(line1Count).toBe(1)
|
|
||||||
expect(line2Count).toBe(0)
|
|
||||||
|
|
||||||
// Line 2: Track execution, load x and add 10
|
|
||||||
const line2 = toBytecode([
|
|
||||||
["LOAD", "trackLine2"],
|
|
||||||
["PUSH", 0],
|
|
||||||
["PUSH", 0],
|
|
||||||
["CALL"],
|
|
||||||
["POP"],
|
|
||||||
["LOAD", "x"],
|
|
||||||
["PUSH", 10],
|
|
||||||
["ADD"]
|
|
||||||
])
|
|
||||||
|
|
||||||
// Append line2 bytecode to VM using the helper method
|
|
||||||
vm.appendBytecode(line2)
|
|
||||||
|
|
||||||
// SOLUTION: Use continue() instead of run() to resume from current PC
|
|
||||||
await vm.continue()
|
|
||||||
|
|
||||||
const result = vm.stack[vm.stack.length - 1]
|
|
||||||
|
|
||||||
// SUCCESS: Line 1 only ran once, line 2 ran once
|
|
||||||
expect(line1Count).toBe(1) // Still 1! Side effect didn't re-run
|
|
||||||
expect(line2Count).toBe(1) // Ran once as expected
|
|
||||||
expect(result).toEqual({ type: "number", value: 15 }) // 5 + 10
|
|
||||||
})
|
|
||||||
|
|
||||||
test("REPL mode - function calls work across chunks", async () => {
|
|
||||||
const vm = new VM(toBytecode([]))
|
|
||||||
await vm.run()
|
|
||||||
|
|
||||||
// Line 1: Define a function
|
|
||||||
const line1 = toBytecode([
|
|
||||||
["MAKE_FUNCTION", ["x"], ".body"],
|
|
||||||
["STORE", "add1"],
|
|
||||||
["JUMP", ".end"],
|
|
||||||
[".body:"],
|
|
||||||
["LOAD", "x"],
|
|
||||||
["PUSH", 1],
|
|
||||||
["ADD"],
|
|
||||||
["RETURN"],
|
|
||||||
[".end:"]
|
|
||||||
])
|
|
||||||
|
|
||||||
vm.appendBytecode(line1)
|
|
||||||
await vm.continue()
|
|
||||||
|
|
||||||
expect(vm.scope.get("add1")?.type).toBe("function")
|
|
||||||
|
|
||||||
// Line 2: Call the function
|
|
||||||
const line2 = toBytecode([
|
|
||||||
["LOAD", "add1"],
|
|
||||||
["PUSH", 10],
|
|
||||||
["PUSH", 1],
|
|
||||||
["PUSH", 0],
|
|
||||||
["CALL"]
|
|
||||||
])
|
|
||||||
|
|
||||||
vm.appendBytecode(line2)
|
|
||||||
const result = await vm.continue()
|
|
||||||
|
|
||||||
expect(result).toEqual({ type: "number", value: 11 })
|
|
||||||
})
|
|
||||||
|
|
@ -1,243 +0,0 @@
|
||||||
import { test, expect } from "bun:test"
|
|
||||||
import { Scope, toValue } from "#reef"
|
|
||||||
|
|
||||||
test("Scope - create empty scope", () => {
|
|
||||||
const scope = new Scope()
|
|
||||||
expect(scope.parent).toBeUndefined()
|
|
||||||
expect(scope.locals.size).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope - create child scope with parent", () => {
|
|
||||||
const parent = new Scope()
|
|
||||||
const child = new Scope(parent)
|
|
||||||
|
|
||||||
expect(child.parent).toBe(parent)
|
|
||||||
expect(child.locals.size).toBe(0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope - set and get variable in same scope", () => {
|
|
||||||
const scope = new Scope()
|
|
||||||
const value = toValue(42)
|
|
||||||
|
|
||||||
scope.set("x", value)
|
|
||||||
expect(scope.get("x")).toBe(value)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope - get returns undefined for non-existent variable", () => {
|
|
||||||
const scope = new Scope()
|
|
||||||
expect(scope.get("x")).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope - set updates existing variable in same scope", () => {
|
|
||||||
const scope = new Scope()
|
|
||||||
|
|
||||||
scope.set("x", toValue(10))
|
|
||||||
expect(scope.get("x")).toEqual({ type: "number", value: 10 })
|
|
||||||
|
|
||||||
scope.set("x", toValue(20))
|
|
||||||
expect(scope.get("x")).toEqual({ type: "number", value: 20 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope - get searches parent scope", () => {
|
|
||||||
const parent = new Scope()
|
|
||||||
const child = new Scope(parent)
|
|
||||||
|
|
||||||
parent.set("x", toValue(42))
|
|
||||||
expect(child.get("x")).toEqual({ type: "number", value: 42 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope - get searches multiple levels up", () => {
|
|
||||||
const grandparent = new Scope()
|
|
||||||
const parent = new Scope(grandparent)
|
|
||||||
const child = new Scope(parent)
|
|
||||||
|
|
||||||
grandparent.set("x", toValue(100))
|
|
||||||
expect(child.get("x")).toEqual({ type: "number", value: 100 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope - child updates parent variable when it exists (no shadowing)", () => {
|
|
||||||
const parent = new Scope()
|
|
||||||
const child = new Scope(parent)
|
|
||||||
|
|
||||||
parent.set("x", toValue(10))
|
|
||||||
child.set("x", toValue(20))
|
|
||||||
|
|
||||||
// set() updates parent's variable, not shadow it
|
|
||||||
expect(parent.get("x")).toEqual({ type: "number", value: 20 })
|
|
||||||
expect(child.get("x")).toEqual({ type: "number", value: 20 })
|
|
||||||
expect(child.locals.has("x")).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope - set updates parent scope variable when it exists", () => {
|
|
||||||
const parent = new Scope()
|
|
||||||
const child = new Scope(parent)
|
|
||||||
|
|
||||||
parent.set("x", toValue(10))
|
|
||||||
child.set("x", toValue(20))
|
|
||||||
|
|
||||||
// Should update parent's variable, not create new local one
|
|
||||||
expect(parent.get("x")).toEqual({ type: "number", value: 20 })
|
|
||||||
expect(child.get("x")).toEqual({ type: "number", value: 20 })
|
|
||||||
expect(child.locals.has("x")).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope - set creates new local variable when not found in parent", () => {
|
|
||||||
const parent = new Scope()
|
|
||||||
const child = new Scope(parent)
|
|
||||||
|
|
||||||
parent.set("x", toValue(10))
|
|
||||||
child.set("y", toValue(20))
|
|
||||||
|
|
||||||
expect(parent.get("x")).toEqual({ type: "number", value: 10 })
|
|
||||||
expect(parent.get("y")).toBeUndefined()
|
|
||||||
expect(child.get("x")).toEqual({ type: "number", value: 10 })
|
|
||||||
expect(child.get("y")).toEqual({ type: "number", value: 20 })
|
|
||||||
expect(child.locals.has("y")).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope - has returns true for variable in current scope", () => {
|
|
||||||
const scope = new Scope()
|
|
||||||
scope.set("x", toValue(42))
|
|
||||||
|
|
||||||
expect(scope.has("x")).toBe(true)
|
|
||||||
expect(scope.has("y")).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope - has returns true for variable in parent scope", () => {
|
|
||||||
const parent = new Scope()
|
|
||||||
const child = new Scope(parent)
|
|
||||||
|
|
||||||
parent.set("x", toValue(42))
|
|
||||||
|
|
||||||
expect(child.has("x")).toBe(true)
|
|
||||||
expect(child.has("y")).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope - has searches multiple levels up", () => {
|
|
||||||
const grandparent = new Scope()
|
|
||||||
const parent = new Scope(grandparent)
|
|
||||||
const child = new Scope(parent)
|
|
||||||
|
|
||||||
grandparent.set("x", toValue(100))
|
|
||||||
|
|
||||||
expect(child.has("x")).toBe(true)
|
|
||||||
expect(parent.has("x")).toBe(true)
|
|
||||||
expect(grandparent.has("x")).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope.vars() - returns empty array for empty scope", () => {
|
|
||||||
const scope = new Scope()
|
|
||||||
expect(scope.vars()).toEqual([])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope.vars() - returns single variable from current scope", () => {
|
|
||||||
const scope = new Scope()
|
|
||||||
scope.set("x", toValue(42))
|
|
||||||
|
|
||||||
expect(scope.vars()).toEqual(["x"])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope.vars() - returns multiple variables from current scope", () => {
|
|
||||||
const scope = new Scope()
|
|
||||||
scope.set("x", toValue(1))
|
|
||||||
scope.set("y", toValue(2))
|
|
||||||
scope.set("z", toValue(3))
|
|
||||||
|
|
||||||
const vars = scope.vars()
|
|
||||||
expect(vars.length).toBe(3)
|
|
||||||
expect(vars).toContain("x")
|
|
||||||
expect(vars).toContain("y")
|
|
||||||
expect(vars).toContain("z")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope.vars() - includes variables from parent scope", () => {
|
|
||||||
const parent = new Scope()
|
|
||||||
const child = new Scope(parent)
|
|
||||||
|
|
||||||
parent.set("x", toValue(1))
|
|
||||||
child.set("y", toValue(2))
|
|
||||||
|
|
||||||
const vars = child.vars()
|
|
||||||
expect(vars.length).toBe(2)
|
|
||||||
expect(vars).toContain("x")
|
|
||||||
expect(vars).toContain("y")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope.vars() - includes variables from multiple parent scopes", () => {
|
|
||||||
const grandparent = new Scope()
|
|
||||||
const parent = new Scope(grandparent)
|
|
||||||
const child = new Scope(parent)
|
|
||||||
|
|
||||||
grandparent.set("x", toValue(1))
|
|
||||||
parent.set("y", toValue(2))
|
|
||||||
child.set("z", toValue(3))
|
|
||||||
|
|
||||||
const vars = child.vars()
|
|
||||||
expect(vars.length).toBe(3)
|
|
||||||
expect(vars).toContain("x")
|
|
||||||
expect(vars).toContain("y")
|
|
||||||
expect(vars).toContain("z")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope.vars() - no duplicates when child updates parent variable", () => {
|
|
||||||
const parent = new Scope()
|
|
||||||
const child = new Scope(parent)
|
|
||||||
|
|
||||||
parent.set("x", toValue(10))
|
|
||||||
child.set("x", toValue(20)) // Updates parent, doesn't create local
|
|
||||||
|
|
||||||
// Only one "x" since child doesn't have its own local x
|
|
||||||
const vars = child.vars()
|
|
||||||
expect(vars).toEqual(["x"])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope.vars() - can't have duplicates if manually set in locals", () => {
|
|
||||||
const parent = new Scope()
|
|
||||||
const child = new Scope(parent)
|
|
||||||
|
|
||||||
parent.set("x", toValue(10))
|
|
||||||
child.locals.set("x", toValue(20))
|
|
||||||
const vars = child.vars()
|
|
||||||
expect(vars).toEqual(["x"])
|
|
||||||
|
|
||||||
expect(child.get("x")).toEqual({ type: "number", value: 20 })
|
|
||||||
expect(parent.get("x")).toEqual({ type: "number", value: 10 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope.vars() - handles deep scope chains", () => {
|
|
||||||
let scope = new Scope()
|
|
||||||
|
|
||||||
// Create a deep chain: level0 -> level1 -> ... -> level5
|
|
||||||
for (let i = 0; i < 5; i++) {
|
|
||||||
scope.set(`var${i}`, toValue(i))
|
|
||||||
scope = new Scope(scope)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final scope should have all variables from the chain
|
|
||||||
const vars = scope.vars()
|
|
||||||
expect(vars.length).toBe(5)
|
|
||||||
expect(vars).toContain("var0")
|
|
||||||
expect(vars).toContain("var1")
|
|
||||||
expect(vars).toContain("var2")
|
|
||||||
expect(vars).toContain("var3")
|
|
||||||
expect(vars).toContain("var4")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("Scope - can store and retrieve functions", () => {
|
|
||||||
const scope = new Scope()
|
|
||||||
|
|
||||||
const fnValue = {
|
|
||||||
type: 'function' as const,
|
|
||||||
value: '<function>' as const,
|
|
||||||
params: ['x'],
|
|
||||||
defaults: {},
|
|
||||||
body: 0,
|
|
||||||
variadic: false,
|
|
||||||
named: false,
|
|
||||||
parentScope: scope
|
|
||||||
}
|
|
||||||
|
|
||||||
scope.set("myFunc", fnValue)
|
|
||||||
expect(scope.get("myFunc")).toBe(fnValue)
|
|
||||||
expect(scope.get("myFunc")?.type).toBe("function")
|
|
||||||
})
|
|
||||||
249
tests/setup.ts
249
tests/setup.ts
|
|
@ -1,249 +0,0 @@
|
||||||
import { expect } from "bun:test"
|
|
||||||
import { toValue, fromValue, type Value, toBytecode, run, type Bytecode } from "#reef"
|
|
||||||
import { isEqual } from "../src/value"
|
|
||||||
|
|
||||||
declare module "bun:test" {
|
|
||||||
interface Matchers<T> {
|
|
||||||
/**
|
|
||||||
* Run bytecode and assert that the result equals a JavaScript value after conversion via toValue()
|
|
||||||
* @example expect(bytecode).toEqualValue(42)
|
|
||||||
* @example expect("PUSH 5\nPUSH 3\nADD").toEqualValue(8)
|
|
||||||
* @example expect([["PUSH", 42]]).toEqualValue(42)
|
|
||||||
*/
|
|
||||||
toEqualValue(expected: any): Promise<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run bytecode and assert that the result is null
|
|
||||||
* @example expect(bytecode).toBeNull()
|
|
||||||
* @example expect("PUSH null").toBeNull()
|
|
||||||
*/
|
|
||||||
toBeNull(): Promise<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run bytecode and assert that the result is a boolean with the expected value
|
|
||||||
* @example expect(bytecode).toBeBoolean(true)
|
|
||||||
* @example expect("PUSH true").toBeBoolean(true)
|
|
||||||
*/
|
|
||||||
toBeBoolean(expected: boolean): Promise<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run bytecode and assert that the result is a number with the expected value
|
|
||||||
* @example expect(bytecode).toBeNumber(42)
|
|
||||||
* @example expect("PUSH 42").toBeNumber(42)
|
|
||||||
*/
|
|
||||||
toBeNumber(expected: number): Promise<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run bytecode and assert that the result is a string with the expected value
|
|
||||||
* @example expect(bytecode).toBeString("hello")
|
|
||||||
* @example expect("PUSH \"hello\"").toBeString("hello")
|
|
||||||
*/
|
|
||||||
toBeString(expected: string): Promise<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run bytecode and assert that the result is an array with the expected values
|
|
||||||
* @example expect(bytecode).toBeArray([1, 2, 3])
|
|
||||||
*/
|
|
||||||
toBeArray(expected: any[]): Promise<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run bytecode and assert that the result is a dict with the expected key-value pairs
|
|
||||||
* @example expect(bytecode).toBeDict({ x: 10, y: 20 })
|
|
||||||
*/
|
|
||||||
toBeDict(expected: Record<string, any>): Promise<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run bytecode and assert that the result is a function (Reef or native)
|
|
||||||
* @example expect(bytecode).toBeFunction()
|
|
||||||
*/
|
|
||||||
toBeFunction(): Promise<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run bytecode and assert that the result is truthy according to ReefVM semantics
|
|
||||||
* (only null and false are falsy)
|
|
||||||
* @example expect(bytecode).toBeTruthy()
|
|
||||||
*/
|
|
||||||
toBeTruthy(): Promise<void>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run bytecode and assert that the result is falsy according to ReefVM semantics
|
|
||||||
* (only null and false are falsy)
|
|
||||||
* @example expect(bytecode).toBeFalsy()
|
|
||||||
*/
|
|
||||||
toBeFalsy(): Promise<void>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
expect.extend({
|
|
||||||
async toEqualValue(this: void, received: unknown, expected: any) {
|
|
||||||
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
|
||||||
const result = await run(bytecode)
|
|
||||||
const expectedValue = toValue(expected)
|
|
||||||
const pass = isEqual(result, expectedValue)
|
|
||||||
|
|
||||||
return {
|
|
||||||
pass,
|
|
||||||
message: () =>
|
|
||||||
pass
|
|
||||||
? `Expected value NOT to equal ${formatValue(expectedValue)}, but it did`
|
|
||||||
: `Expected value to equal ${formatValue(expectedValue)}, but received ${formatValue(result)}`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async toBeNull(this: void, received: unknown) {
|
|
||||||
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
|
||||||
const result = await run(bytecode)
|
|
||||||
const pass = result.type === "null"
|
|
||||||
|
|
||||||
return {
|
|
||||||
pass,
|
|
||||||
message: () =>
|
|
||||||
pass
|
|
||||||
? `Expected value NOT to be null, but it was`
|
|
||||||
: `Expected value to be null, but received ${formatValue(result)}`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async toBeBoolean(this: void, received: unknown, expected: boolean) {
|
|
||||||
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
|
||||||
const result = await run(bytecode)
|
|
||||||
const pass = result.type === "boolean" && result.value === expected
|
|
||||||
|
|
||||||
return {
|
|
||||||
pass,
|
|
||||||
message: () =>
|
|
||||||
pass
|
|
||||||
? `Expected value NOT to be boolean ${expected}, but it was`
|
|
||||||
: `Expected value to be boolean ${expected}, but received ${formatValue(result)}`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async toBeNumber(this: void, received: unknown, expected: number) {
|
|
||||||
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
|
||||||
const result = await run(bytecode)
|
|
||||||
const pass = result.type === "number" && result.value === expected
|
|
||||||
|
|
||||||
return {
|
|
||||||
pass,
|
|
||||||
message: () =>
|
|
||||||
pass
|
|
||||||
? `Expected value NOT to be number ${expected}, but it was`
|
|
||||||
: `Expected value to be number ${expected}, but received ${formatValue(result)}`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async toBeString(this: void, received: unknown, expected: string) {
|
|
||||||
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
|
||||||
const result = await run(bytecode)
|
|
||||||
const pass = result.type === "string" && result.value === expected
|
|
||||||
|
|
||||||
return {
|
|
||||||
pass,
|
|
||||||
message: () =>
|
|
||||||
pass
|
|
||||||
? `Expected value NOT to be string "${expected}", but it was`
|
|
||||||
: `Expected value to be string "${expected}", but received ${formatValue(result)}`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async toBeArray(this: void, received: unknown, expected: any[]) {
|
|
||||||
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
|
||||||
const result = await run(bytecode)
|
|
||||||
const expectedValue = toValue(expected)
|
|
||||||
const pass = result.type === "array" && isEqual(result, expectedValue)
|
|
||||||
|
|
||||||
return {
|
|
||||||
pass,
|
|
||||||
message: () =>
|
|
||||||
pass
|
|
||||||
? `Expected value NOT to be array ${formatValue(expectedValue)}, but it was`
|
|
||||||
: `Expected value to be array ${formatValue(expectedValue)}, but received ${formatValue(result)}`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async toBeDict(this: void, received: unknown, expected: Record<string, any>) {
|
|
||||||
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
|
||||||
const result = await run(bytecode)
|
|
||||||
const expectedValue = toValue(expected)
|
|
||||||
const pass = result.type === "dict" && isEqual(result, expectedValue)
|
|
||||||
|
|
||||||
return {
|
|
||||||
pass,
|
|
||||||
message: () =>
|
|
||||||
pass
|
|
||||||
? `Expected value NOT to be dict ${formatValue(expectedValue)}, but it was`
|
|
||||||
: `Expected value to be dict ${formatValue(expectedValue)}, but received ${formatValue(result)}`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async toBeFunction(this: void, received: unknown) {
|
|
||||||
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
|
||||||
const result = await run(bytecode)
|
|
||||||
const pass = result.type === "function" || result.type === "native"
|
|
||||||
|
|
||||||
return {
|
|
||||||
pass,
|
|
||||||
message: () =>
|
|
||||||
pass
|
|
||||||
? `Expected value NOT to be a function, but it was`
|
|
||||||
: `Expected value to be a function, but received ${formatValue(result)}`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async toBeTruthy(this: void, received: unknown) {
|
|
||||||
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
|
||||||
const result = await run(bytecode)
|
|
||||||
// ReefVM semantics: only null and false are falsy
|
|
||||||
const pass = !(result.type === "null" || (result.type === "boolean" && !result.value))
|
|
||||||
|
|
||||||
return {
|
|
||||||
pass,
|
|
||||||
message: () =>
|
|
||||||
pass
|
|
||||||
? `Expected value NOT to be truthy, but it was: ${formatValue(result)}`
|
|
||||||
: `Expected value to be truthy, but received ${formatValue(result)}`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
async toBeFalsy(this: void, received: unknown) {
|
|
||||||
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
|
||||||
const result = await run(bytecode)
|
|
||||||
// ReefVM semantics: only null and false are falsy
|
|
||||||
const pass = result.type === "null" || (result.type === "boolean" && !result.value)
|
|
||||||
|
|
||||||
return {
|
|
||||||
pass,
|
|
||||||
message: () =>
|
|
||||||
pass
|
|
||||||
? `Expected value NOT to be falsy, but it was: ${formatValue(result)}`
|
|
||||||
: `Expected value to be falsy, but received ${formatValue(result)}`,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
function formatValue(value: Value): string {
|
|
||||||
switch (value.type) {
|
|
||||||
case "null":
|
|
||||||
return "null"
|
|
||||||
case "boolean":
|
|
||||||
case "number":
|
|
||||||
return String(value.value)
|
|
||||||
case "string":
|
|
||||||
return `"${value.value}"`
|
|
||||||
case "array":
|
|
||||||
return `[${value.value.map(formatValue).join(", ")}]`
|
|
||||||
case "dict": {
|
|
||||||
const entries = Array.from(value.value.entries())
|
|
||||||
.map(([k, v]) => `${k}: ${formatValue(v)}`)
|
|
||||||
.join(", ")
|
|
||||||
return `{${entries}}`
|
|
||||||
}
|
|
||||||
case "regex":
|
|
||||||
return String(value.value)
|
|
||||||
case "function":
|
|
||||||
case "native":
|
|
||||||
return "<function>"
|
|
||||||
default:
|
|
||||||
return String(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -201,17 +201,17 @@ test("formatValidationErrors produces readable output", () => {
|
||||||
expect(formatted).toContain("UNKNOWN")
|
expect(formatted).toContain("UNKNOWN")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("detects JUMP without .label", () => {
|
test("detects JUMP without # or .label", () => {
|
||||||
const source = `
|
const source = `
|
||||||
JUMP 5
|
JUMP 5
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
const result = validateBytecode(source)
|
const result = validateBytecode(source)
|
||||||
expect(result.valid).toBe(false)
|
expect(result.valid).toBe(false)
|
||||||
expect(result.errors[0]!.message).toContain("JUMP requires label (.label)")
|
expect(result.errors[0]!.message).toContain("JUMP requires immediate (#number) or label (.label)")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("detects JUMP_IF_TRUE without .label", () => {
|
test("detects JUMP_IF_TRUE without # or .label", () => {
|
||||||
const source = `
|
const source = `
|
||||||
PUSH true
|
PUSH true
|
||||||
JUMP_IF_TRUE 2
|
JUMP_IF_TRUE 2
|
||||||
|
|
@ -219,10 +219,10 @@ test("detects JUMP_IF_TRUE without .label", () => {
|
||||||
`
|
`
|
||||||
const result = validateBytecode(source)
|
const result = validateBytecode(source)
|
||||||
expect(result.valid).toBe(false)
|
expect(result.valid).toBe(false)
|
||||||
expect(result.errors[0]!.message).toContain("JUMP_IF_TRUE requires label (.label)")
|
expect(result.errors[0]!.message).toContain("JUMP_IF_TRUE requires immediate (#number) or label (.label)")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("detects JUMP_IF_FALSE without .label", () => {
|
test("detects JUMP_IF_FALSE without # or .label", () => {
|
||||||
const source = `
|
const source = `
|
||||||
PUSH false
|
PUSH false
|
||||||
JUMP_IF_FALSE 2
|
JUMP_IF_FALSE 2
|
||||||
|
|
@ -230,18 +230,17 @@ test("detects JUMP_IF_FALSE without .label", () => {
|
||||||
`
|
`
|
||||||
const result = validateBytecode(source)
|
const result = validateBytecode(source)
|
||||||
expect(result.valid).toBe(false)
|
expect(result.valid).toBe(false)
|
||||||
expect(result.errors[0]!.message).toContain("JUMP_IF_FALSE requires label (.label)")
|
expect(result.errors[0]!.message).toContain("JUMP_IF_FALSE requires immediate (#number) or label (.label)")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("rejects JUMP with immediate number", () => {
|
test("allows JUMP with immediate number", () => {
|
||||||
const source = `
|
const source = `
|
||||||
JUMP #2
|
JUMP #2
|
||||||
PUSH 999
|
PUSH 999
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
const result = validateBytecode(source)
|
const result = validateBytecode(source)
|
||||||
expect(result.valid).toBe(false)
|
expect(result.valid).toBe(true)
|
||||||
expect(result.errors[0]!.message).toContain("JUMP requires label (.label)")
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("detects MAKE_ARRAY without #", () => {
|
test("detects MAKE_ARRAY without #", () => {
|
||||||
|
|
|
||||||
|
|
@ -1,302 +0,0 @@
|
||||||
import { test, expect } from "bun:test"
|
|
||||||
import { isValue, toValue } from "#reef"
|
|
||||||
|
|
||||||
test("isValue - recognizes valid Value objects", () => {
|
|
||||||
expect(isValue({ type: 'number', value: 42 })).toBe(true)
|
|
||||||
expect(isValue({ type: 'number', value: 0 })).toBe(true)
|
|
||||||
expect(isValue({ type: 'string', value: 'hello' })).toBe(true)
|
|
||||||
expect(isValue({ type: 'string', value: '' })).toBe(true)
|
|
||||||
expect(isValue({ type: 'boolean', value: true })).toBe(true)
|
|
||||||
expect(isValue({ type: 'boolean', value: false })).toBe(true)
|
|
||||||
expect(isValue({ type: 'null', value: null })).toBe(true)
|
|
||||||
expect(isValue({ type: 'array', value: [] })).toBe(true)
|
|
||||||
expect(isValue({ type: 'array', value: [toValue(1), toValue(2)] })).toBe(true)
|
|
||||||
expect(isValue({ type: 'dict', value: new Map() })).toBe(true)
|
|
||||||
expect(isValue({ type: 'regex', value: /test/ })).toBe(true)
|
|
||||||
expect(isValue({
|
|
||||||
type: 'function',
|
|
||||||
value: '<function>',
|
|
||||||
params: [],
|
|
||||||
defaults: {},
|
|
||||||
body: 0,
|
|
||||||
variadic: false,
|
|
||||||
named: false,
|
|
||||||
parentScope: null as any
|
|
||||||
})).toBe(true)
|
|
||||||
expect(isValue({
|
|
||||||
type: 'native',
|
|
||||||
value: '<function>',
|
|
||||||
fn: (() => { }) as any
|
|
||||||
})).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("isValue - rejects primitives", () => {
|
|
||||||
expect(isValue(42)).toBe(false)
|
|
||||||
expect(isValue(0)).toBe(false)
|
|
||||||
expect(isValue('hello')).toBe(false)
|
|
||||||
expect(isValue('')).toBe(false)
|
|
||||||
expect(isValue(true)).toBe(false)
|
|
||||||
expect(isValue(false)).toBe(false)
|
|
||||||
expect(isValue(null)).toBe(false)
|
|
||||||
expect(isValue(undefined)).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("isValue - rejects plain objects", () => {
|
|
||||||
expect(isValue({})).toBe(false)
|
|
||||||
expect(isValue({ foo: 'bar' })).toBe(false)
|
|
||||||
expect(isValue({ type: 'number' })).toBe(false)
|
|
||||||
expect(isValue({ value: 42 })).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("isValue - rejects arrays and functions", () => {
|
|
||||||
expect(isValue([])).toBe(false)
|
|
||||||
expect(isValue([1, 2, 3])).toBe(false)
|
|
||||||
expect(isValue(() => { })).toBe(false)
|
|
||||||
expect(isValue(function () { })).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("isValue - rejects other object types", () => {
|
|
||||||
expect(isValue(new Date())).toBe(false)
|
|
||||||
expect(isValue(/regex/)).toBe(false)
|
|
||||||
expect(isValue(new Map())).toBe(false)
|
|
||||||
expect(isValue(new Set())).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("isValue - used by toValue to detect already-converted values", () => {
|
|
||||||
const value = toValue(42)
|
|
||||||
expect(isValue(value)).toBe(true)
|
|
||||||
|
|
||||||
const result = toValue(value)
|
|
||||||
expect(result).toBe(value)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("isValue - edge cases with type and value properties", () => {
|
|
||||||
expect(isValue({ type: 'number', value: 42, extra: 'data' })).toBe(true)
|
|
||||||
|
|
||||||
expect(isValue({ type: null, value: 42 })).toBe(false)
|
|
||||||
expect(isValue({ type: 'number', value: undefined })).toBe(true)
|
|
||||||
|
|
||||||
expect(isValue({ type: 'number', val: 42 })).toBe(false)
|
|
||||||
expect(isValue({ typ: 'number', value: 42 })).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("isValue - rejects objects with invalid type values", () => {
|
|
||||||
expect(isValue({ type: 'text', value: 'Bob' })).toBe(false)
|
|
||||||
expect(isValue({ type: 'email', value: 'test@example.com' })).toBe(false)
|
|
||||||
expect(isValue({ type: 'password', value: 'secret' })).toBe(false)
|
|
||||||
expect(isValue({ type: 'checkbox', value: true })).toBe(false)
|
|
||||||
expect(isValue({ type: 'custom', value: 123 })).toBe(false)
|
|
||||||
expect(isValue({ type: 'unknown', value: null })).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("toValue - correctly handles HTML input props", async () => {
|
|
||||||
const { VM, toBytecode, toValue } = await import("#reef")
|
|
||||||
|
|
||||||
const bytecode = toBytecode([["HALT"]])
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
const inputProps = { type: 'text', value: 'Bob' }
|
|
||||||
const converted = toValue(inputProps, vm)
|
|
||||||
|
|
||||||
expect(converted.type).toBe('dict')
|
|
||||||
expect(converted.value.get('type')).toEqual({ type: 'string', value: 'text' })
|
|
||||||
expect(converted.value.get('value')).toEqual({ type: 'string', value: 'Bob' })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("toValue - converts wrapped Reef functions back to original Value", async () => {
|
|
||||||
const { VM, toBytecode, fnFromValue } = await import("#reef")
|
|
||||||
|
|
||||||
// Create a Reef function
|
|
||||||
const bytecode = toBytecode([
|
|
||||||
["MAKE_FUNCTION", ["x"], ".body"],
|
|
||||||
["STORE", "add1"],
|
|
||||||
["JUMP", ".end"],
|
|
||||||
[".body:"],
|
|
||||||
["LOAD", "x"],
|
|
||||||
["PUSH", 1],
|
|
||||||
["ADD"],
|
|
||||||
["RETURN"],
|
|
||||||
[".end:"],
|
|
||||||
["HALT"]
|
|
||||||
])
|
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
await vm.run()
|
|
||||||
|
|
||||||
const reefFunction = vm.scope.get("add1")!
|
|
||||||
expect(reefFunction.type).toBe("function")
|
|
||||||
|
|
||||||
// Convert to JS function
|
|
||||||
const jsFunction = fnFromValue(reefFunction, vm)
|
|
||||||
expect(typeof jsFunction).toBe("function")
|
|
||||||
|
|
||||||
// Convert back to Value - should return the original Reef function
|
|
||||||
const backToValue = toValue(jsFunction)
|
|
||||||
expect(backToValue).toBe(reefFunction) // Same reference
|
|
||||||
expect(backToValue.type).toBe("function")
|
|
||||||
})
|
|
||||||
|
|
||||||
test("fromValue - converts native function back to original JS function", async () => {
|
|
||||||
const { VM, toBytecode, toValue, fromValue } = await import("#reef")
|
|
||||||
|
|
||||||
const bytecode = toBytecode([["HALT"]])
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
// Create a native JS function
|
|
||||||
const originalFn = (x: number, y: number) => x * y
|
|
||||||
|
|
||||||
// Convert to Value (wraps it as a native function)
|
|
||||||
const nativeValue = toValue(originalFn, vm)
|
|
||||||
expect(nativeValue.type).toBe("native")
|
|
||||||
|
|
||||||
// Convert back to JS - should get the original function
|
|
||||||
const convertedFn = fromValue(nativeValue, vm)
|
|
||||||
expect(typeof convertedFn).toBe("function")
|
|
||||||
|
|
||||||
// Verify it's the same function
|
|
||||||
expect(convertedFn).toBe(originalFn)
|
|
||||||
|
|
||||||
// Verify it works correctly
|
|
||||||
expect(convertedFn(3, 4)).toBe(12)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("fromValue - native function roundtrip preserves functionality", async () => {
|
|
||||||
const { VM, toBytecode, toValue, fromValue } = await import("#reef")
|
|
||||||
|
|
||||||
const bytecode = toBytecode([["HALT"]])
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
// Create a native function with state
|
|
||||||
let callCount = 0
|
|
||||||
const countingFn = (n: number) => {
|
|
||||||
callCount++
|
|
||||||
return n * callCount
|
|
||||||
}
|
|
||||||
|
|
||||||
// Roundtrip through Value
|
|
||||||
const nativeValue = toValue(countingFn, vm)
|
|
||||||
const roundtrippedFn = fromValue(nativeValue, vm)
|
|
||||||
|
|
||||||
// Verify it maintains state across calls
|
|
||||||
expect(roundtrippedFn(10)).toBe(10) // 10 * 1
|
|
||||||
expect(roundtrippedFn(10)).toBe(20) // 10 * 2
|
|
||||||
expect(roundtrippedFn(10)).toBe(30) // 10 * 3
|
|
||||||
expect(callCount).toBe(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("fromValue - async native function roundtrip", async () => {
|
|
||||||
const { VM, toBytecode, toValue, fromValue } = await import("#reef")
|
|
||||||
|
|
||||||
const bytecode = toBytecode([["HALT"]])
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
const asyncFn = async (x: number, y: number) => {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1))
|
|
||||||
return x + y
|
|
||||||
}
|
|
||||||
|
|
||||||
const nativeValue = toValue(asyncFn, vm)
|
|
||||||
expect(nativeValue.type).toBe("native")
|
|
||||||
|
|
||||||
const roundtrippedFn = fromValue(nativeValue, vm)
|
|
||||||
|
|
||||||
const result = await roundtrippedFn(5, 7)
|
|
||||||
expect(result).toBe(12)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("toValue - throws helpful error when calling function converted without VM", async () => {
|
|
||||||
function myFunction(x: number) {
|
|
||||||
return x * 2
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = toValue(myFunction)
|
|
||||||
expect(value.type).toBe('native')
|
|
||||||
|
|
||||||
// Error is thrown when calling the function, not when converting
|
|
||||||
await expect(async () => {
|
|
||||||
await value.fn({ type: 'number', value: 5 })
|
|
||||||
}).toThrow(/can't call function that was converted without a vm/)
|
|
||||||
|
|
||||||
await expect(async () => {
|
|
||||||
await value.fn({ type: 'number', value: 5 })
|
|
||||||
}).toThrow(/Function: myFunction/)
|
|
||||||
|
|
||||||
await expect(async () => {
|
|
||||||
await value.fn({ type: 'number', value: 5 })
|
|
||||||
}).toThrow(/Source:/)
|
|
||||||
|
|
||||||
await expect(async () => {
|
|
||||||
await value.fn({ type: 'number', value: 5 })
|
|
||||||
}).toThrow(/Called from:/)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("toValue - error message shows function info for arrow functions", async () => {
|
|
||||||
const anonymousFn = (x: number) => x * 2
|
|
||||||
|
|
||||||
const value = toValue(anonymousFn)
|
|
||||||
expect(value.type).toBe('native')
|
|
||||||
|
|
||||||
// Arrow functions show as <anonymous>
|
|
||||||
await expect(async () => {
|
|
||||||
await value.fn({ type: 'number', value: 5 })
|
|
||||||
}).toThrow(/Function: <anonymous>/)
|
|
||||||
|
|
||||||
await expect(async () => {
|
|
||||||
await value.fn({ type: 'number', value: 5 })
|
|
||||||
}).toThrow(/Source:/)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("toValue - error when function is nested in object without VM", async () => {
|
|
||||||
const obj = {
|
|
||||||
name: "test",
|
|
||||||
handler: (x: number) => x * 2
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = toValue(obj)
|
|
||||||
expect(value.type).toBe('dict')
|
|
||||||
|
|
||||||
const handlerValue = value.value.get('handler')!
|
|
||||||
expect(handlerValue.type).toBe('native')
|
|
||||||
|
|
||||||
await expect(async () => {
|
|
||||||
await (handlerValue as any).fn({ type: 'number', value: 5 })
|
|
||||||
}).toThrow(/can't call function that was converted without a vm/)
|
|
||||||
|
|
||||||
await expect(async () => {
|
|
||||||
await (handlerValue as any).fn({ type: 'number', value: 5 })
|
|
||||||
}).toThrow(/Function: handler/)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("toValue - error when function is nested in array without VM", async () => {
|
|
||||||
const arr = [1, 2, (x: number) => x * 2]
|
|
||||||
|
|
||||||
const value = toValue(arr)
|
|
||||||
expect(value.type).toBe('array')
|
|
||||||
|
|
||||||
const fnValue = value.value[2]!
|
|
||||||
expect(fnValue.type).toBe('native')
|
|
||||||
|
|
||||||
await expect(async () => {
|
|
||||||
await (fnValue as any).fn({ type: 'number', value: 5 })
|
|
||||||
}).toThrow(/can't call function that was converted without a vm/)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("fromValue - throws helpful error when converting function without VM", async () => {
|
|
||||||
const { Scope, fromValue } = await import("#reef")
|
|
||||||
|
|
||||||
const reefFunction = {
|
|
||||||
type: 'function' as const,
|
|
||||||
params: ['x', 'y'],
|
|
||||||
defaults: {},
|
|
||||||
body: 10,
|
|
||||||
parentScope: new Scope(),
|
|
||||||
variadic: false,
|
|
||||||
named: false,
|
|
||||||
value: '<function>' as const
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(() => fromValue(reefFunction)).toThrow(/VM is required for function conversion/)
|
|
||||||
expect(() => fromValue(reefFunction)).toThrow(/Function params: \[x, y\]/)
|
|
||||||
expect(() => fromValue(reefFunction)).toThrow(/Function body at instruction: 10/)
|
|
||||||
expect(() => fromValue(reefFunction)).toThrow(/Called from:/)
|
|
||||||
})
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
import { test, expect, describe } from "bun:test"
|
|
||||||
import { VM } from "#vm"
|
|
||||||
import { toBytecode } from "#bytecode"
|
|
||||||
import { toValue } from "#value"
|
|
||||||
|
|
||||||
describe("VM scope methods", () => {
|
|
||||||
test("pushScope creates isolated child scope", async () => {
|
|
||||||
const bytecode = toBytecode([["HALT"]])
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.set("x", 42)
|
|
||||||
vm.pushScope()
|
|
||||||
|
|
||||||
const xValue = vm.scope.get("x")
|
|
||||||
expect(xValue).toEqual({ type: "number", value: 42 })
|
|
||||||
|
|
||||||
vm.set("y", 100)
|
|
||||||
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
|
|
||||||
expect(vm.scope.get("y")).toEqual({ type: "number", value: 100 })
|
|
||||||
})
|
|
||||||
|
|
||||||
test("popScope returns to parent scope and child variables are not accessible", async () => {
|
|
||||||
const bytecode = toBytecode([["HALT"]])
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.set("x", 42)
|
|
||||||
vm.pushScope()
|
|
||||||
vm.set("y", 100)
|
|
||||||
|
|
||||||
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
|
|
||||||
expect(vm.scope.get("y")).toEqual({ type: "number", value: 100 })
|
|
||||||
|
|
||||||
vm.popScope()
|
|
||||||
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
|
|
||||||
expect(vm.scope.get("y")).toBeUndefined()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("pushScope with locals initializes child scope with variables", async () => {
|
|
||||||
const bytecode = toBytecode([["HALT"]])
|
|
||||||
const vm = new VM(bytecode)
|
|
||||||
|
|
||||||
vm.set("x", 42)
|
|
||||||
vm.pushScope({ y: 100, z: "hello" })
|
|
||||||
|
|
||||||
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
|
|
||||||
expect(vm.scope.get("y")).toEqual({ type: "number", value: 100 })
|
|
||||||
expect(vm.scope.get("z")).toEqual({ type: "string", value: "hello" })
|
|
||||||
|
|
||||||
vm.popScope()
|
|
||||||
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
|
|
||||||
expect(vm.scope.get("y")).toBeUndefined()
|
|
||||||
expect(vm.scope.get("z")).toBeUndefined()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Loading…
Reference in New Issue
Block a user