Compare commits
No commits in common. "main" and "tweaks" have entirely different histories.
178
CLAUDE.md
178
CLAUDE.md
|
|
@ -42,7 +42,8 @@ No build step required - Bun runs TypeScript directly.
|
|||
- Stack-based execution with program counter (PC)
|
||||
- Call stack for function frames
|
||||
- Exception handler stack for try/catch/finally
|
||||
- Lexical scope chain with parent references (includes native functions)
|
||||
- Lexical scope chain with parent references
|
||||
- Native function registry for TypeScript interop
|
||||
|
||||
**Key subsystems**:
|
||||
- **bytecode.ts**: Compiler that converts both string and array formats to executable bytecode. Handles label resolution, constant pool management, and function definition parsing. The `toBytecode()` function accepts either a string (human-readable) or typed array format (programmatic).
|
||||
|
|
@ -55,7 +56,7 @@ No build step required - Bun runs TypeScript directly.
|
|||
|
||||
### Critical Design Decisions
|
||||
|
||||
**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.
|
||||
|
||||
|
|
@ -69,22 +70,19 @@ No build step required - Bun runs TypeScript directly.
|
|||
|
||||
**Parameter binding priority**: Named args bind to fixed params first. Unmatched named args go to `@named` dict parameter. Fixed params bind in order: named arg > positional arg > default > null.
|
||||
|
||||
**Native function calling**: Native functions are stored in scope and called via LOAD + CALL, using the same calling convention as Reef functions. Named arguments are supported by extracting parameter names from the function signature at call time.
|
||||
**Native function calling**: CALL_NATIVE consumes the entire stack as arguments (different from CALL which pops specific argument counts).
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Tests are organized by feature area:
|
||||
- **opcodes.test.ts**: Stack ops, arithmetic, comparisons, variables, control flow
|
||||
- **basic.test.ts**: Stack ops, arithmetic, comparisons, variables, control flow
|
||||
- **functions.test.ts**: Function creation, calls, closures, defaults, variadic, named args
|
||||
- **tail-call.test.ts**: Tail call optimization and unbounded recursion
|
||||
- **exceptions.test.ts**: Try/catch/finally, exception unwinding, nested handlers
|
||||
- **native.test.ts**: Native function interop (sync and async)
|
||||
- **functions-parameter.test.ts**: Convenience parameter for passing functions to run() and VM
|
||||
- **bytecode.test.ts**: Bytecode string parser, label resolution, constants
|
||||
- **programmatic.test.ts**: Array format API, typed tuples, labels, functions
|
||||
- **validator.test.ts**: Bytecode validation rules
|
||||
- **unicode.test.ts**: Unicode and emoji identifiers
|
||||
- **regex.test.ts**: RegExp support
|
||||
- **examples.test.ts**: Integration tests for example programs
|
||||
|
||||
When adding features:
|
||||
|
|
@ -137,100 +135,53 @@ Array format features:
|
|||
- Function params as string arrays: `["MAKE_FUNCTION", ["x", "y=10"], ".body"]`
|
||||
- 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)
|
||||
```typescript
|
||||
const result = await run(bytecode, {
|
||||
add: (a: number, b: number) => a + b,
|
||||
greet: (name: string) => `Hello, ${name}!`,
|
||||
pi: 3.14159,
|
||||
config: { debug: true, port: 8080 }
|
||||
})
|
||||
ReefVM supports two ways to register native functions:
|
||||
|
||||
// Or with VM constructor
|
||||
const vm = new VM(bytecode, { add, greet, pi, config })
|
||||
```
|
||||
|
||||
**Option 2**: Set values with `vm.set()` (manual)
|
||||
**1. Native TypeScript functions (recommended)** - Auto-converts between native TS and ReefVM types:
|
||||
```typescript
|
||||
const vm = new VM(bytecode)
|
||||
|
||||
// Set functions (auto-wrapped to native functions)
|
||||
vm.set('add', (a: number, b: number) => a + b)
|
||||
// Works with native TypeScript types!
|
||||
vm.registerFunction('add', (a: number, b: number) => {
|
||||
return a + b
|
||||
})
|
||||
|
||||
// Set any other values (auto-converted to ReefVM Values)
|
||||
vm.set('pi', 3.14159)
|
||||
vm.set('config', { debug: true, port: 8080 })
|
||||
// Supports defaults (like NOSE commands)
|
||||
vm.registerFunction('ls', (path: string, link = false) => {
|
||||
return link ? `listing ${path} with links` : `listing ${path}`
|
||||
})
|
||||
|
||||
// Async functions work too
|
||||
vm.registerFunction('fetch', async (url: string) => {
|
||||
const response = await fetch(url)
|
||||
return await response.text()
|
||||
})
|
||||
|
||||
await vm.run()
|
||||
```
|
||||
|
||||
**Option 3**: Set Value-based functions with `vm.setValueFunction()` (advanced)
|
||||
|
||||
For functions that work directly with ReefVM Value types:
|
||||
|
||||
**2. Value-based functions (manual)** - For functions that need direct Value access:
|
||||
```typescript
|
||||
const vm = new VM(bytecode)
|
||||
|
||||
// Set Value-based function (no wrapping, works directly with Values)
|
||||
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||
// Direct access to Value types
|
||||
return toValue(toNumber(a) + toNumber(b))
|
||||
})
|
||||
|
||||
await vm.run()
|
||||
```
|
||||
|
||||
Auto-wrapping handles:
|
||||
- Functions: wrapped as native functions with Value ↔ native type conversion
|
||||
- Sync and async functions
|
||||
- Arrays, objects, primitives, null, RegExp
|
||||
- All values converted via `toValue()`
|
||||
The auto-wrapping handles:
|
||||
- Converting Value → native types on input (using `fromValue`)
|
||||
- Converting native types → Value on output (using `toValue`)
|
||||
- Both sync and async functions
|
||||
- Arrays, objects, primitives, and null
|
||||
|
||||
### Calling Functions from TypeScript
|
||||
|
||||
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:
|
||||
### Label Usage (Preferred)
|
||||
Use labels instead of numeric offsets for readability:
|
||||
```
|
||||
JUMP .skip
|
||||
PUSH 42
|
||||
|
|
@ -240,67 +191,6 @@ 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
|
||||
|
||||
- Import alias: `#reef` maps to `./src/index.ts`
|
||||
|
|
@ -486,12 +376,14 @@ Run `bun test` to verify all tests pass before committing.
|
|||
|
||||
## 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).
|
||||
|
||||
**MAKE_ARRAY operand**: Specifies count, not a stack index. `MAKE_ARRAY #3` pops 3 items.
|
||||
|
||||
**CALL_NATIVE stack behavior**: Unlike CALL, it consumes all stack values as arguments and clears the stack.
|
||||
|
||||
**Finally blocks**: The compiler must generate explicit JUMPs to finally blocks for successful try/catch completion. The VM only auto-jumps to finally on THROW.
|
||||
|
||||
**Variable scoping**: STORE updates existing variables in parent scopes or creates in current scope. It does NOT shadow by default.
|
||||
|
|
|
|||
530
GUIDE.md
530
GUIDE.md
|
|
@ -42,6 +42,9 @@ OPCODE operand ; comment
|
|||
- Booleans: `PUSH true`, `PUSH false`
|
||||
- Null: `PUSH null`
|
||||
|
||||
**Native function names**: Registered TypeScript functions
|
||||
- `CALL_NATIVE print`
|
||||
|
||||
## Array Format
|
||||
|
||||
The programmatic array format uses TypeScript tuples for type safety:
|
||||
|
|
@ -96,6 +99,11 @@ const result = await run(bytecode)
|
|||
["MAKE_DICT", 2] // Pop 2 key-value pairs
|
||||
```
|
||||
|
||||
**Native function names**: Strings for registered functions
|
||||
```typescript
|
||||
["CALL_NATIVE", "print"]
|
||||
```
|
||||
|
||||
### Functions in Array Format
|
||||
|
||||
```typescript
|
||||
|
|
@ -179,25 +187,12 @@ PUSH 1 ; Named count
|
|||
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
|
||||
|
||||
### Stack
|
||||
- `PUSH <const>` - Push constant
|
||||
- `POP` - Remove top
|
||||
- `DUP` - Duplicate top
|
||||
- `SWAP` - Swap top two values
|
||||
- `TYPE` - Pop value, push its type as string
|
||||
|
||||
### Variables
|
||||
- `LOAD <name>` - Push variable value (throws if not found)
|
||||
|
|
@ -207,10 +202,6 @@ CALL ; → "Hi, Guest"
|
|||
### Arithmetic
|
||||
- `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
|
||||
- `EQ`, `NEQ`, `LT`, `GT`, `LTE`, `GTE` - Pop 2, push boolean
|
||||
|
||||
|
|
@ -244,54 +235,17 @@ CALL ; → "Hi, Guest"
|
|||
- `DICT_SET` - Pop value, key, dict; mutate dict
|
||||
- `DICT_HAS` - Pop key and dict, push boolean
|
||||
|
||||
### Unified Access
|
||||
- `DOT_GET` - Pop index/key and array/dict, push value (null if missing)
|
||||
|
||||
### Strings
|
||||
- `STR_CONCAT #N` - Pop N values, convert to strings, concatenate, push result
|
||||
|
||||
### Exceptions
|
||||
- `PUSH_TRY .catch` - Register exception handler
|
||||
- `PUSH_FINALLY .finally` - Add finally to current handler
|
||||
- `POP_TRY` - Remove handler (try succeeded)
|
||||
- `THROW` - Throw exception (pops error value)
|
||||
|
||||
### Native
|
||||
- `CALL_NATIVE <name>` - Call registered TypeScript function (consumes entire stack as args)
|
||||
|
||||
## 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
|
||||
```
|
||||
<condition>
|
||||
|
|
@ -377,125 +331,6 @@ POP
|
|||
.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
|
||||
```
|
||||
PUSH_TRY .catch
|
||||
|
|
@ -532,8 +367,7 @@ Functions automatically capture current scope:
|
|||
PUSH 0
|
||||
STORE counter
|
||||
MAKE_FUNCTION () .increment
|
||||
STORE increment_fn
|
||||
JUMP .main
|
||||
RETURN
|
||||
|
||||
.increment:
|
||||
LOAD counter ; Captured variable
|
||||
|
|
@ -542,18 +376,6 @@ JUMP .main
|
|||
STORE counter
|
||||
LOAD counter
|
||||
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
|
||||
|
|
@ -561,7 +383,7 @@ Use TAIL_CALL instead of CALL for last call:
|
|||
```
|
||||
MAKE_FUNCTION (n acc) .factorial
|
||||
STORE factorial
|
||||
JUMP .main
|
||||
<...>
|
||||
|
||||
.factorial:
|
||||
LOAD n
|
||||
|
|
@ -581,15 +403,6 @@ JUMP .main
|
|||
PUSH 2
|
||||
PUSH 0
|
||||
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)
|
||||
|
|
@ -618,96 +431,6 @@ TRY_CALL unknown ; Pushes "unknown" as string
|
|||
- Shell-like languages where unknown identifiers become strings
|
||||
- Templating systems with optional transformers
|
||||
|
||||
### String Concatenation
|
||||
Build strings from multiple values:
|
||||
```
|
||||
; Simple concatenation
|
||||
PUSH "Hello"
|
||||
PUSH " "
|
||||
PUSH "World"
|
||||
STR_CONCAT #3 ; → "Hello World"
|
||||
|
||||
; With variables
|
||||
PUSH "Name: "
|
||||
LOAD userName
|
||||
STR_CONCAT #2 ; → "Name: Alice"
|
||||
|
||||
; With expressions and type coercion
|
||||
PUSH "Result: "
|
||||
PUSH 10
|
||||
PUSH 5
|
||||
ADD
|
||||
STR_CONCAT #2 ; → "Result: 15"
|
||||
|
||||
; Template-like interpolation
|
||||
PUSH "User "
|
||||
LOAD userId
|
||||
PUSH " has "
|
||||
LOAD count
|
||||
PUSH " items"
|
||||
STR_CONCAT #5 ; → "User 42 has 3 items"
|
||||
```
|
||||
|
||||
**Composability**: Results can be concatenated again
|
||||
```
|
||||
PUSH "Hello"
|
||||
PUSH " "
|
||||
PUSH "World"
|
||||
STR_CONCAT #3
|
||||
PUSH "!"
|
||||
STR_CONCAT #2 ; → "Hello World!"
|
||||
```
|
||||
|
||||
### Unified Access (DOT_GET)
|
||||
DOT_GET provides a single opcode for accessing both arrays and dicts:
|
||||
|
||||
```
|
||||
; Array access
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
MAKE_ARRAY #3
|
||||
PUSH 1
|
||||
DOT_GET ; → 20
|
||||
|
||||
; Dict access
|
||||
PUSH 'name'
|
||||
PUSH 'Alice'
|
||||
MAKE_DICT #1
|
||||
PUSH 'name'
|
||||
DOT_GET ; → 'Alice'
|
||||
```
|
||||
|
||||
**Chained access**:
|
||||
```
|
||||
; Access dict['users'][0]['name']
|
||||
LOAD dict
|
||||
PUSH 'users'
|
||||
DOT_GET ; Get users array
|
||||
PUSH 0
|
||||
DOT_GET ; Get first user
|
||||
PUSH 'name'
|
||||
DOT_GET ; Get name field
|
||||
```
|
||||
|
||||
**With variables**:
|
||||
```
|
||||
LOAD data
|
||||
LOAD key ; Key can be string or number
|
||||
DOT_GET ; Works for both array and dict
|
||||
```
|
||||
|
||||
**Null safety**: Returns null for missing keys or out-of-bounds indices
|
||||
```
|
||||
MAKE_ARRAY #0
|
||||
PUSH 0
|
||||
DOT_GET ; → null (empty array)
|
||||
|
||||
MAKE_DICT #0
|
||||
PUSH 'key'
|
||||
DOT_GET ; → null (missing key)
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
### Truthiness
|
||||
|
|
@ -733,8 +456,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.
|
||||
|
||||
**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.
|
||||
|
||||
**Equality ops** (EQ, NEQ) use type-aware comparison with deep equality for arrays/dicts.
|
||||
|
|
@ -759,24 +480,11 @@ Variable and function parameter names support Unicode and emoji:
|
|||
|
||||
### Parameter Binding Priority
|
||||
For function calls, parameters bound in order:
|
||||
1. Named argument (if provided and matches param name)
|
||||
2. Positional argument (if provided)
|
||||
1. Positional argument (if provided)
|
||||
2. Named argument (if provided and matches param name)
|
||||
3. Default value (if defined)
|
||||
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
|
||||
- PUSH_TRY uses absolute addresses for catch blocks
|
||||
- Nested try blocks form a stack
|
||||
|
|
@ -785,7 +493,7 @@ This works for both ReefVM functions and native TypeScript functions. If no defa
|
|||
- Finally execution in all cases is compiler's responsibility, not VM's
|
||||
|
||||
### Calling Convention
|
||||
All calls (including native functions) push arguments in order:
|
||||
All calls push arguments in order:
|
||||
1. Function
|
||||
2. Positional args (in order)
|
||||
3. Named args (key1, val1, key2, val2, ...)
|
||||
|
|
@ -793,210 +501,8 @@ All calls (including native functions) push arguments in order:
|
|||
5. Named count (as number)
|
||||
6. CALL or TAIL_CALL
|
||||
|
||||
Native functions use the same calling convention as Reef functions. They are registered into scope and called via LOAD + CALL.
|
||||
|
||||
### Registering Native Functions
|
||||
|
||||
Native TypeScript functions are registered into the VM's scope and accessed like regular variables.
|
||||
|
||||
**Method 1**: Pass to `run()` or `VM` constructor
|
||||
```typescript
|
||||
const result = await run(bytecode, {
|
||||
add: (a: number, b: number) => a + b,
|
||||
greet: (name: string) => `Hello, ${name}!`
|
||||
})
|
||||
|
||||
// Or with VM
|
||||
const vm = new VM(bytecode, { add, greet })
|
||||
```
|
||||
|
||||
**Method 2**: Register after construction
|
||||
```typescript
|
||||
const vm = new VM(bytecode)
|
||||
vm.set('add', (a: number, b: number) => a + b)
|
||||
await vm.run()
|
||||
```
|
||||
|
||||
**Method 3**: Value-based functions (for full control)
|
||||
```typescript
|
||||
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
||||
})
|
||||
```
|
||||
|
||||
**Auto-wrapping**: `vm.set()` automatically converts between native TypeScript types and ReefVM Value types. Both sync and async functions work.
|
||||
|
||||
**Usage in bytecode**:
|
||||
```
|
||||
; Positional arguments
|
||||
LOAD add ; Load native function from scope
|
||||
PUSH 5
|
||||
PUSH 10
|
||||
PUSH 2 ; positionalCount
|
||||
PUSH 0 ; namedCount
|
||||
CALL ; Call like any other function
|
||||
|
||||
; Named arguments
|
||||
LOAD greet
|
||||
PUSH "name"
|
||||
PUSH "Alice"
|
||||
PUSH "greeting"
|
||||
PUSH "Hi"
|
||||
PUSH 0 ; positionalCount
|
||||
PUSH 2 ; namedCount
|
||||
CALL ; → "Hi, Alice!"
|
||||
```
|
||||
|
||||
**Named Arguments**: Native functions support named arguments. Parameter names are extracted from the function signature at call time, and arguments are bound using the same priority as Reef functions (named arg > positional arg > default > null).
|
||||
|
||||
**@named Pattern**: Parameters starting with `at` followed by an uppercase letter (e.g., `atOptions`, `atNamed`) collect unmatched named arguments:
|
||||
|
||||
```typescript
|
||||
// Basic @named - collects all named args
|
||||
vm.set('greet', (atNamed: any = {}) => {
|
||||
return `Hello, ${atNamed.name || 'World'}!`
|
||||
})
|
||||
|
||||
// Mixed positional and @named
|
||||
vm.set('configure', (name: string, atOptions: any = {}) => {
|
||||
return {
|
||||
name,
|
||||
debug: atOptions.debug || false,
|
||||
port: atOptions.port || 3000
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Bytecode example:
|
||||
```
|
||||
; Call with mixed positional and named args
|
||||
LOAD configure
|
||||
PUSH "myApp" ; positional arg → name
|
||||
PUSH "debug"
|
||||
PUSH true
|
||||
PUSH "port"
|
||||
PUSH 8080
|
||||
PUSH 1 ; 1 positional arg
|
||||
PUSH 2 ; 2 named args (debug, port)
|
||||
CALL ; atOptions receives {debug: true, port: 8080}
|
||||
```
|
||||
|
||||
Named arguments that match fixed parameter names are bound to those parameters. Remaining unmatched named arguments are collected into the `atXxx` parameter as a plain JavaScript object.
|
||||
|
||||
### Calling Functions from TypeScript
|
||||
|
||||
You can call both Reef and native functions from TypeScript using `vm.call()`:
|
||||
|
||||
```typescript
|
||||
const bytecode = toBytecode(`
|
||||
MAKE_FUNCTION (name greeting="Hello") .greet
|
||||
STORE greet
|
||||
HALT
|
||||
|
||||
.greet:
|
||||
LOAD greeting
|
||||
PUSH " "
|
||||
LOAD name
|
||||
PUSH "!"
|
||||
STR_CONCAT #4
|
||||
RETURN
|
||||
`)
|
||||
|
||||
const vm = new VM(bytecode, {
|
||||
log: (msg: string) => console.log(msg) // Native function
|
||||
})
|
||||
await vm.run()
|
||||
|
||||
// Call Reef function with positional arguments
|
||||
const result1 = await vm.call('greet', 'Alice')
|
||||
// Returns: "Hello Alice!"
|
||||
|
||||
// Call Reef function with named arguments (pass as final object)
|
||||
const result2 = await vm.call('greet', 'Bob', { greeting: 'Hi' })
|
||||
// Returns: "Hi Bob!"
|
||||
|
||||
// Call Reef function with only named arguments
|
||||
const result3 = await vm.call('greet', { name: 'Carol', greeting: 'Hey' })
|
||||
// Returns: "Hey Carol!"
|
||||
|
||||
// Call native function
|
||||
await vm.call('log', 'Hello from TypeScript!')
|
||||
```
|
||||
|
||||
**How it works**:
|
||||
- `vm.call(functionName, ...args)` looks up the function (Reef or native) in the VM's scope
|
||||
- For Reef functions: converts to callable JavaScript function
|
||||
- For native functions: calls directly
|
||||
- Arguments are automatically converted to ReefVM Values
|
||||
- Returns the result (automatically converted back to JavaScript types)
|
||||
|
||||
**Named arguments**: Pass a plain object as the final argument to provide named arguments. If the last argument is a non-array object, it's treated as named arguments. All preceding arguments are treated as positional.
|
||||
|
||||
**Type conversion**: Arguments and return values are automatically converted between JavaScript types and ReefVM Values:
|
||||
- Primitives: `number`, `string`, `boolean`, `null`
|
||||
- Arrays: converted recursively
|
||||
- Objects: converted to ReefVM dicts
|
||||
- Functions: Reef functions are converted to callable JavaScript functions
|
||||
|
||||
### REPL Mode (Incremental Compilation)
|
||||
|
||||
ReefVM supports incremental bytecode execution for building REPLs. This allows you to execute code line-by-line while preserving scope and avoiding re-execution of side effects.
|
||||
|
||||
**The Problem**: By default, `vm.run()` resets the program counter (PC) to 0, re-executing all previous bytecode. This makes it impossible to implement a REPL where each line executes only once.
|
||||
|
||||
**The Solution**: Use `vm.continue()` to resume execution from where you left off:
|
||||
|
||||
```typescript
|
||||
// Line 1: Define variable
|
||||
const line1 = toBytecode([
|
||||
["PUSH", 42],
|
||||
["STORE", "x"]
|
||||
])
|
||||
|
||||
const vm = new VM(line1)
|
||||
await vm.run() // Execute first line
|
||||
|
||||
// Line 2: Use the variable
|
||||
const line2 = toBytecode([
|
||||
["LOAD", "x"],
|
||||
["PUSH", 10],
|
||||
["ADD"]
|
||||
])
|
||||
|
||||
vm.appendBytecode(line2) // Append new bytecode with proper constant remapping
|
||||
await vm.continue() // Execute ONLY the new bytecode
|
||||
|
||||
// Result: 52 (42 + 10)
|
||||
// The first line never re-executed!
|
||||
```
|
||||
|
||||
**Key methods**:
|
||||
- `vm.run()`: Resets PC to 0 and runs from the beginning (normal execution)
|
||||
- `vm.continue()`: Continues from current PC (REPL mode)
|
||||
- `vm.appendBytecode(bytecode)`: Helper that properly appends bytecode with constant index remapping
|
||||
|
||||
**Important**: Don't use `HALT` in REPL mode! The VM naturally stops when it runs out of instructions. Using `HALT` sets `vm.stopped = true`, which prevents `continue()` from resuming.
|
||||
|
||||
**Example REPL pattern**:
|
||||
```typescript
|
||||
const vm = new VM(toBytecode([]), { /* native functions */ })
|
||||
|
||||
while (true) {
|
||||
const input = await getUserInput() // Get next line from user
|
||||
const bytecode = compileLine(input) // Compile to bytecode (no HALT!)
|
||||
|
||||
vm.appendBytecode(bytecode) // Append to VM
|
||||
const result = await vm.continue() // Execute only the new code
|
||||
|
||||
console.log(fromValue(result)) // Show result to user
|
||||
}
|
||||
```
|
||||
|
||||
This pattern ensures:
|
||||
- Variables persist between lines
|
||||
- Side effects (like `echo` or function calls) only run once
|
||||
- Previous bytecode never re-executes
|
||||
- Scope accumulates across all lines
|
||||
### CALL_NATIVE Behavior
|
||||
Unlike CALL, CALL_NATIVE consumes the **entire stack** as arguments and clears the stack. The native function receives all values that were on the stack at the time of the call.
|
||||
|
||||
### Empty Stack
|
||||
- RETURN 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,10 @@ Commands: `clear`, `reset`, `exit`.
|
|||
- Variadic functions with positional rest parameters (`...rest`)
|
||||
- Named arguments (named) that collect unmatched named args into a dict (`@named`)
|
||||
- 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)
|
||||
- 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 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 })`
|
||||
- Call Reef functions from TypeScript with `vm.call(name, ...args)` with automatic type conversion
|
||||
- Native function interop (CALL_NATIVE) with sync and async functions
|
||||
- Write native functions with regular TypeScript types instead of Shrimp's internal Value types
|
||||
|
||||
## Design Decisions
|
||||
|
||||
|
|
@ -61,4 +57,3 @@ Commands: `clear`, `reset`, `exit`.
|
|||
- 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
|
||||
- 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)
|
||||
419
SPEC.md
419
SPEC.md
|
|
@ -13,9 +13,10 @@ The ReefVM is a stack-based bytecode virtual machine designed for the Shrimp pro
|
|||
- **Value Stack**: Operand stack for computation
|
||||
- **Call Stack**: Call frames for function invocations
|
||||
- **Exception Handlers**: Stack of try/catch handlers
|
||||
- **Scope Chain**: Linked scopes for lexical variable resolution (includes native functions)
|
||||
- **Scope Chain**: Linked scopes for lexical variable resolution
|
||||
- **Program Counter (PC)**: Current instruction index
|
||||
- **Constants Pool**: Immutable values and function metadata
|
||||
- **Native Function Registry**: External functions callable from Shrimp
|
||||
|
||||
### Execution Model
|
||||
|
||||
|
|
@ -39,7 +40,6 @@ type Value =
|
|||
| { type: 'dict', value: Map<string, Value> }
|
||||
| { type: 'function', params: string[], defaults: Record<string, number>,
|
||||
body: number, parentScope: Scope, variadic: boolean, named: boolean }
|
||||
| { type: 'native', fn: NativeFunction, value: '<function>' }
|
||||
```
|
||||
|
||||
### Type Coercion
|
||||
|
|
@ -138,24 +138,6 @@ type ExceptionHandler = {
|
|||
**Effect**: Duplicate top of stack
|
||||
**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
|
||||
|
||||
#### LOAD
|
||||
|
|
@ -197,29 +179,7 @@ All arithmetic operations pop two values, perform operation, push result as numb
|
|||
|
||||
#### ADD
|
||||
**Stack**: [a, b] → [a + b]
|
||||
|
||||
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
|
||||
**Note**: Only for numbers (use separate string concat if needed)
|
||||
|
||||
#### SUB
|
||||
**Stack**: [a, b] → [a - b]
|
||||
|
|
@ -233,62 +193,6 @@ Performs different operations depending on operand types:
|
|||
#### MOD
|
||||
**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
|
||||
|
||||
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>
|
||||
DUP
|
||||
JUMP_IF_FALSE .end
|
||||
JUMP_IF_FALSE #2 # skip POP and <evaluate right>
|
||||
POP
|
||||
<evaluate right>
|
||||
.end:
|
||||
end:
|
||||
```
|
||||
|
||||
**OR pattern** (short-circuits if left side is true):
|
||||
```
|
||||
<evaluate left>
|
||||
DUP
|
||||
JUMP_IF_TRUE .end
|
||||
JUMP_IF_TRUE #2 # skip POP and <evaluate right>
|
||||
POP
|
||||
<evaluate right>
|
||||
.end:
|
||||
end:
|
||||
```
|
||||
|
||||
### Control Flow
|
||||
|
||||
#### JUMP
|
||||
**Operand**: Label (string)
|
||||
**Effect**: Jump to the specified label
|
||||
**Operand**: Offset (number)
|
||||
**Effect**: Add offset to PC (relative jump)
|
||||
**Stack**: No change
|
||||
|
||||
**Note**: JUMP only accepts label operands (`.label`), not numeric offsets. The VM resolves labels to relative offsets internally.
|
||||
|
||||
#### JUMP_IF_FALSE
|
||||
**Operand**: Label (string)
|
||||
**Effect**: If top of stack is falsy, jump to the specified label
|
||||
**Operand**: Offset (number)
|
||||
**Effect**: If top of stack is falsy, add offset to PC (relative jump)
|
||||
**Stack**: [condition] → []
|
||||
|
||||
**Note**: JUMP_IF_FALSE only accepts label operands (`.label`), not numeric offsets.
|
||||
|
||||
#### JUMP_IF_TRUE
|
||||
**Operand**: Label (string)
|
||||
**Effect**: If top of stack is truthy, jump to the specified label
|
||||
**Operand**: Offset (number)
|
||||
**Effect**: If top of stack is truthy, add offset to PC (relative jump)
|
||||
**Stack**: [condition] → []
|
||||
|
||||
**Note**: JUMP_IF_TRUE only accepts label operands (`.label`), not numeric offsets.
|
||||
|
||||
#### BREAK
|
||||
**Operand**: None
|
||||
**Effect**: Unwind call stack until frame with `isBreakTarget = true`, resume there
|
||||
|
|
@ -459,20 +357,15 @@ The created function captures `currentScope` as its `parentScope`.
|
|||
3. Pop named arguments (name/value pairs) from stack
|
||||
4. Pop positional arguments from stack
|
||||
5. Pop function from stack
|
||||
6. **If function is native**:
|
||||
- Mark current frame (if exists) as break target
|
||||
- Call native function with positional args
|
||||
- Push return value onto stack
|
||||
- Done (skip steps 7-11)
|
||||
7. Mark current frame (if exists) as break target (`isBreakTarget = true`)
|
||||
8. Push new call frame with current PC and scope
|
||||
9. Create new scope with function's parentScope as parent
|
||||
10. Bind parameters:
|
||||
6. Mark current frame (if exists) as break target (`isBreakTarget = true`)
|
||||
7. Push new call frame with current PC and scope
|
||||
8. Create new scope with function's parentScope as parent
|
||||
9. Bind parameters:
|
||||
- For regular functions: bind params by position, then by name, then defaults, then null
|
||||
- For variadic functions: bind fixed params, collect rest into array
|
||||
- For functions with `named: true`: bind fixed params by position/name, collect unmatched named args into dict
|
||||
11. Set currentScope to new scope
|
||||
12. Jump to function body
|
||||
10. Set currentScope to new scope
|
||||
11. Jump to function body
|
||||
|
||||
**Parameter Binding Priority** (for fixed params):
|
||||
1. Named argument (if provided and matches param name)
|
||||
|
|
@ -480,21 +373,12 @@ The created function captures `currentScope` as its `parentScope`.
|
|||
3. Default value (if defined)
|
||||
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 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
|
||||
- This allows flexible calling: `fn(x=10, y=20, extra=30)` where `extra` goes to the named args dict
|
||||
- **Native functions support named arguments** - parameter names are extracted from the function signature at call time
|
||||
- Passing `null` via named args also triggers defaults: `fn(x=null)` triggers `x`'s default
|
||||
|
||||
**Errors**: Throws if top of stack is not a function (or native function)
|
||||
**Errors**: Throws if top of stack is not a function
|
||||
|
||||
#### TAIL_CALL
|
||||
**Operand**: None
|
||||
|
|
@ -625,190 +509,30 @@ Key is coerced to string.
|
|||
Key is coerced to string.
|
||||
**Errors**: Throws if not dict
|
||||
|
||||
### Unified Access
|
||||
|
||||
#### DOT_GET
|
||||
**Operand**: None
|
||||
**Effect**: Get value from array or dict
|
||||
**Stack**: [array|dict, index|key] → [value]
|
||||
|
||||
**Behavior**:
|
||||
- If target is array: coerce index to number and access `array[index]`
|
||||
- If target is dict: coerce key to string and access `dict.get(key)`
|
||||
- Returns null if index out of bounds or key not found
|
||||
|
||||
**Errors**: Throws if target is not array or dict
|
||||
|
||||
**Use Cases**:
|
||||
- Unified syntax for accessing both arrays and dicts
|
||||
- Chaining access operations: `obj.users.0.name`
|
||||
- Generic accessor that works with any indexable type
|
||||
|
||||
**Example**:
|
||||
```
|
||||
; Array access
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
MAKE_ARRAY #3
|
||||
PUSH 1
|
||||
DOT_GET ; → 20
|
||||
|
||||
; Dict access
|
||||
PUSH 'name'
|
||||
PUSH 'Alice'
|
||||
MAKE_DICT #1
|
||||
PUSH 'name'
|
||||
DOT_GET ; → 'Alice'
|
||||
|
||||
; Chained access
|
||||
; dict['users'][0]
|
||||
LOAD dict
|
||||
PUSH 'users'
|
||||
DOT_GET
|
||||
PUSH 0
|
||||
DOT_GET
|
||||
```
|
||||
|
||||
### String Operations
|
||||
|
||||
#### STR_CONCAT
|
||||
**Operand**: Number of values to concatenate (number)
|
||||
**Effect**: Concatenate N values from stack into a single string
|
||||
**Stack**: [val1, val2, ..., valN] → [string]
|
||||
|
||||
**Behavior**:
|
||||
1. Pop N values from stack (in reverse order)
|
||||
2. Convert each value to string using `toString()`
|
||||
3. Concatenate all strings in order (val1 + val2 + ... + valN)
|
||||
4. Push resulting string onto stack
|
||||
|
||||
**Type Coercion**:
|
||||
- Numbers → string representation (e.g., `42` → `"42"`)
|
||||
- Booleans → `"true"` or `"false"`
|
||||
- Null → `"null"`
|
||||
- Strings → identity
|
||||
- Arrays → `"[item, item]"` format
|
||||
- Dicts → `"{key: value, ...}"` format
|
||||
- Functions → `"<function>"`
|
||||
|
||||
**Use Cases**:
|
||||
- Building dynamic strings from multiple parts
|
||||
- Template string interpolation
|
||||
- String formatting with mixed types
|
||||
|
||||
**Composability**:
|
||||
- Results can be concatenated again with additional STR_CONCAT operations
|
||||
- Can leave values on stack (only consumes specified count)
|
||||
|
||||
**Example**:
|
||||
```
|
||||
PUSH "Hello"
|
||||
PUSH " "
|
||||
PUSH "World"
|
||||
STR_CONCAT #3 ; → "Hello World"
|
||||
|
||||
PUSH "Count: "
|
||||
PUSH 42
|
||||
PUSH ", Active: "
|
||||
PUSH true
|
||||
STR_CONCAT #4 ; → "Count: 42, Active: true"
|
||||
```
|
||||
|
||||
**Edge Cases**:
|
||||
- `STR_CONCAT #0` produces empty string `""`
|
||||
- `STR_CONCAT #1` converts single value to string
|
||||
- If stack has fewer values than count, behavior depends on implementation (may use empty strings or throw)
|
||||
|
||||
### TypeScript Interop
|
||||
|
||||
Native TypeScript functions are registered into the VM's scope and accessed via regular LOAD/CALL operations. They behave identically to Reef functions from the bytecode perspective.
|
||||
#### CALL_NATIVE
|
||||
**Operand**: Function name (string)
|
||||
**Effect**: Call registered TypeScript function
|
||||
**Stack**: [...args] → [returnValue]
|
||||
|
||||
**Registration**:
|
||||
**Behavior**:
|
||||
1. Look up function by name in registry
|
||||
2. Mark current frame (if exists) as break target
|
||||
3. Await function call (native function receives arguments and returns a Value)
|
||||
4. Push return value onto stack
|
||||
|
||||
**Notes**:
|
||||
- TypeScript functions are passed the raw stack values as arguments
|
||||
- They must return a valid Value
|
||||
- They can be async (VM awaits them)
|
||||
- Like CALL, but function is from TypeScript registry instead of stack
|
||||
|
||||
**Errors**: Throws if function not found
|
||||
|
||||
**TypeScript Function Signature**:
|
||||
```typescript
|
||||
const vm = new VM(bytecode, {
|
||||
add: (a: number, b: number) => a + b,
|
||||
greet: (name: string) => `Hello, ${name}!`
|
||||
})
|
||||
|
||||
// Or after construction:
|
||||
vm.set('multiply', (a: number, b: number) => a * b)
|
||||
```
|
||||
|
||||
**Usage in Bytecode**:
|
||||
```
|
||||
LOAD add ; Load native function from scope
|
||||
PUSH 5
|
||||
PUSH 10
|
||||
PUSH 2 ; positionalCount
|
||||
PUSH 0 ; namedCount
|
||||
CALL ; Call it like any other function
|
||||
```
|
||||
|
||||
**Native Function Types**:
|
||||
|
||||
1. **Auto-wrapped functions** (via `vm.set()`): Accept and return native TypeScript types (number, string, boolean, array, object, etc.). The VM automatically converts between Value types and native types.
|
||||
|
||||
2. **Value-based functions** (via `vm.setValueFunction()`): Accept and return `Value` types directly for full control over type handling.
|
||||
|
||||
**Auto-Wrapping Behavior**:
|
||||
- Parameters: `Value` → native type (number, string, boolean, array, object, null, RegExp)
|
||||
- Return value: native type → `Value`
|
||||
- Supports sync and async functions
|
||||
- Objects convert to dicts, arrays convert to Value arrays
|
||||
|
||||
**Named Arguments**:
|
||||
- Native functions support named arguments by extracting parameter names from the function signature
|
||||
- Parameter binding follows the same priority as Reef functions: named arg > positional arg > default > null
|
||||
- TypeScript rest parameters (`...args`) are supported and behave like Reef variadic parameters
|
||||
|
||||
**Examples**:
|
||||
```typescript
|
||||
// Auto-wrapped native types
|
||||
vm.set('add', (a: number, b: number) => a + b)
|
||||
vm.set('greet', (name: string) => `Hello, ${name}!`)
|
||||
vm.set('range', (n: number) => Array.from({ length: n }, (_, i) => i))
|
||||
|
||||
// With defaults
|
||||
vm.set('greet', (name: string, greeting = 'Hello') => {
|
||||
return `${greeting}, ${name}!`
|
||||
})
|
||||
|
||||
// Variadic functions
|
||||
vm.set('sum', (...nums: number[]) => {
|
||||
return nums.reduce((acc, n) => acc + n, 0)
|
||||
})
|
||||
|
||||
// Value-based for custom logic
|
||||
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
||||
})
|
||||
|
||||
// Async functions
|
||||
vm.set('fetchData', async (url: string) => {
|
||||
const response = await fetch(url)
|
||||
return response.json()
|
||||
})
|
||||
```
|
||||
|
||||
**Calling with Named Arguments**:
|
||||
```
|
||||
; Call with positional args
|
||||
LOAD greet
|
||||
PUSH "Alice"
|
||||
PUSH 1
|
||||
PUSH 0
|
||||
CALL ; → "Hello, Alice!"
|
||||
|
||||
; Call with named args
|
||||
LOAD greet
|
||||
PUSH "name"
|
||||
PUSH "Bob"
|
||||
PUSH "greeting"
|
||||
PUSH "Hi"
|
||||
PUSH 0
|
||||
PUSH 2
|
||||
CALL ; → "Hi, Bob!"
|
||||
type TypeScriptFunction = (...args: Value[]) => Promise<Value> | Value;
|
||||
```
|
||||
|
||||
### Special
|
||||
|
|
@ -820,16 +544,14 @@ CALL ; → "Hi, Bob!"
|
|||
|
||||
## 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 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:
|
||||
Example with labels:
|
||||
```
|
||||
JUMP .skip
|
||||
.middle:
|
||||
|
|
@ -840,6 +562,15 @@ JUMP .skip
|
|||
HALT
|
||||
```
|
||||
|
||||
Equivalent with numeric offsets:
|
||||
```
|
||||
JUMP #2
|
||||
PUSH 999
|
||||
HALT
|
||||
PUSH 42
|
||||
HALT
|
||||
```
|
||||
|
||||
## Common Bytecode Patterns
|
||||
|
||||
### If-Else Statement
|
||||
|
|
@ -915,29 +646,6 @@ PUSH 1 # namedCount
|
|||
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
|
||||
```
|
||||
MAKE_FUNCTION (n acc) .factorial_body
|
||||
|
|
@ -984,9 +692,10 @@ All of these should throw errors:
|
|||
6. **Break Outside Loop**: BREAK with no break target
|
||||
7. **Continue Outside Loop**: CONTINUE with no continue target
|
||||
8. **Return Outside Function**: RETURN with no call frame
|
||||
9. **Mismatched Handler**: POP_TRY with no handler
|
||||
10. **Invalid Constant**: PUSH with invalid constant index
|
||||
11. **Invalid Function Definition**: MAKE_FUNCTION with non-function_def constant
|
||||
9. **Unknown Function**: CALL_NATIVE with unregistered function
|
||||
10. **Mismatched Handler**: POP_TRY with no handler
|
||||
11. **Invalid Constant**: PUSH with invalid constant index
|
||||
12. **Invalid Function Definition**: MAKE_FUNCTION with non-function_def constant
|
||||
|
||||
## Edge Cases
|
||||
|
||||
|
|
@ -1031,21 +740,11 @@ All of these should throw errors:
|
|||
## VM Initialization
|
||||
|
||||
```typescript
|
||||
// Register native functions during construction
|
||||
const vm = new VM(bytecode, {
|
||||
add: (a: number, b: number) => a + b,
|
||||
greet: (name: string) => `Hello, ${name}!`
|
||||
})
|
||||
|
||||
// Or register after construction
|
||||
vm.set('multiply', (a: number, b: number) => a * b)
|
||||
|
||||
// Or use Value-based functions
|
||||
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||
const vm = new VM(bytecode);
|
||||
vm.registerFunction('add', (a, b) => {
|
||||
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
||||
})
|
||||
|
||||
const result = await vm.run()
|
||||
const result = await vm.execute()
|
||||
```
|
||||
|
||||
## Testing Considerations
|
||||
|
|
|
|||
|
|
@ -104,8 +104,6 @@ function formatValue(value: Value): string {
|
|||
} else if (value.type === 'function') {
|
||||
const params = value.params.join(', ')
|
||||
return `${colors.dim}<fn(${params})>${colors.reset}`
|
||||
} else if (value.type === 'native') {
|
||||
return `${colors.dim}<native>${colors.reset}`
|
||||
}
|
||||
return String(value)
|
||||
}
|
||||
|
|
|
|||
2
bin/repl
2
bin/repl
|
|
@ -38,8 +38,6 @@ function formatValue(value: Value): string {
|
|||
} else if (value.type === 'function') {
|
||||
const params = value.params.join(', ')
|
||||
return `${colors.dim}<fn(${params})>${colors.reset}`
|
||||
} else if (value.type === 'native') {
|
||||
return `${colors.dim}<native>${colors.reset}`
|
||||
}
|
||||
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)
|
||||
|
||||
vm.set('print', (...args: Value[]): Value => {
|
||||
vm.registerFunction('print', (...args: Value[]): Value => {
|
||||
console.log(...args.map(toString))
|
||||
return toNull()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -16,15 +16,13 @@ export type Constant =
|
|||
| Value
|
||||
| FunctionDef
|
||||
|
||||
type Atom = RegExp | number | string | boolean | null
|
||||
type Atom = number | string | boolean | null
|
||||
|
||||
type InstructionTuple =
|
||||
// Stack
|
||||
| ["PUSH", Atom]
|
||||
| ["POP"]
|
||||
| ["DUP"]
|
||||
| ["SWAP"]
|
||||
| ["TYPE"]
|
||||
|
||||
// Variables
|
||||
| ["LOAD", string]
|
||||
|
|
@ -34,9 +32,6 @@ type InstructionTuple =
|
|||
// Arithmetic
|
||||
| ["ADD"] | ["SUB"] | ["MUL"] | ["DIV"] | ["MOD"]
|
||||
|
||||
// Bitwise
|
||||
| ["BIT_AND"] | ["BIT_OR"] | ["BIT_XOR"] | ["BIT_SHL"] | ["BIT_SHR"] | ["BIT_USHR"]
|
||||
|
||||
// Comparison
|
||||
| ["EQ"] | ["NEQ"] | ["LT"] | ["GT"] | ["LTE"] | ["GTE"]
|
||||
|
||||
|
|
@ -44,9 +39,9 @@ type InstructionTuple =
|
|||
| ["NOT"]
|
||||
|
||||
// Control flow
|
||||
| ["JUMP", string]
|
||||
| ["JUMP_IF_FALSE", string]
|
||||
| ["JUMP_IF_TRUE", string]
|
||||
| ["JUMP", string | number]
|
||||
| ["JUMP_IF_FALSE", string | number]
|
||||
| ["JUMP_IF_TRUE", string | number]
|
||||
| ["BREAK"]
|
||||
|
||||
// Exception handling
|
||||
|
|
@ -56,7 +51,7 @@ type InstructionTuple =
|
|||
| ["THROW"]
|
||||
|
||||
// Functions
|
||||
| ["MAKE_FUNCTION", string[], string]
|
||||
| ["MAKE_FUNCTION", string[], string | number]
|
||||
| ["CALL"]
|
||||
| ["TAIL_CALL"]
|
||||
| ["RETURN"]
|
||||
|
|
@ -75,11 +70,8 @@ type InstructionTuple =
|
|||
| ["DICT_SET"]
|
||||
| ["DICT_HAS"]
|
||||
|
||||
// Strings
|
||||
| ["STR_CONCAT", number]
|
||||
|
||||
// Arrays and dicts
|
||||
| ["DOT_GET"]
|
||||
// Native
|
||||
| ["CALL_NATIVE", string]
|
||||
|
||||
// Special
|
||||
| ["HALT"]
|
||||
|
|
@ -88,6 +80,30 @@ type LabelDefinition = [string] // Just ".label_name:"
|
|||
|
||||
export type ProgramItem = InstructionTuple | LabelDefinition
|
||||
|
||||
//
|
||||
// Parse bytecode from human-readable string format.
|
||||
// Operand types are determined by prefix/literal:
|
||||
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
|
||||
// .label -> label reference (e.g., JUMP .loop_start, MAKE_FUNCTION (x y) .body)
|
||||
// name -> variable/function name (e.g., LOAD x, CALL_NATIVE add)
|
||||
// 42 -> number constant (e.g., PUSH 42)
|
||||
// "str" -> string constant (e.g., PUSH "hello")
|
||||
// 'str' -> string constant (e.g., PUSH 'hello')
|
||||
// true -> boolean constant (e.g., PUSH true)
|
||||
// false -> boolean constant (e.g., PUSH false)
|
||||
// null -> null constant (e.g., PUSH null)
|
||||
//
|
||||
// Labels:
|
||||
// .label_name: -> label definition (marks current instruction position)
|
||||
//
|
||||
// Function definitions:
|
||||
// MAKE_FUNCTION (x y) #7 -> basic function (numeric offset)
|
||||
// MAKE_FUNCTION (x y) .body -> basic function (label reference)
|
||||
// MAKE_FUNCTION (x y=42) #7 -> with defaults
|
||||
// MAKE_FUNCTION (x ...rest) #7 -> variadic
|
||||
// MAKE_FUNCTION (x @named) #7 -> named
|
||||
//
|
||||
|
||||
function parseFunctionParams(paramStr: string, constants: Constant[]): {
|
||||
params: string[]
|
||||
defaults: Record<string, number>
|
||||
|
|
@ -317,12 +333,12 @@ function toBytecodeFromArray(program: ProgramItem[]): Bytecode /* throws */ {
|
|||
case "STORE":
|
||||
case "TRY_LOAD":
|
||||
case "TRY_CALL":
|
||||
case "CALL_NATIVE":
|
||||
operandValue = operand as string
|
||||
break
|
||||
|
||||
case "MAKE_ARRAY":
|
||||
case "MAKE_DICT":
|
||||
case "STR_CONCAT":
|
||||
operandValue = operand as number
|
||||
break
|
||||
|
||||
|
|
@ -349,29 +365,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 */ {
|
||||
const lines = str.trim().split("\n")
|
||||
|
||||
|
|
@ -390,7 +383,7 @@ function toBytecodeFromString(str: string): Bytecode /* throws */ {
|
|||
if (!trimmed) continue
|
||||
|
||||
// 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)
|
||||
labels.set(labelName, cleanLines.length)
|
||||
continue
|
||||
|
|
@ -498,18 +491,6 @@ function toBytecodeFromString(str: string): Bytecode /* throws */ {
|
|||
bytecode.constants.push(toValue(null))
|
||||
operandValue = bytecode.constants.length - 1
|
||||
|
||||
} else if (/^\/.*\/[a-z]*$/.test(operand)) {
|
||||
// regex literal (/pattern/flags)
|
||||
const lastSlash = operand.lastIndexOf('/')
|
||||
const pattern = operand.slice(1, lastSlash)
|
||||
const flags = operand.slice(lastSlash + 1)
|
||||
try {
|
||||
const regex = new RegExp(pattern, flags)
|
||||
bytecode.constants.push(toValue(regex))
|
||||
operandValue = bytecode.constants.length - 1
|
||||
} catch (e) {
|
||||
throw new Error(`Invalid regex literal: ${operand}`)
|
||||
}
|
||||
} else {
|
||||
// Assume it's a variable name if it doesn't match any other pattern
|
||||
// This allows emoji, Unicode, and other creative identifiers
|
||||
|
|
|
|||
202
src/format.ts
202
src/format.ts
|
|
@ -1,202 +0,0 @@
|
|||
import type { Bytecode, Constant } from "./bytecode"
|
||||
import { OpCode } from "./opcode"
|
||||
import type { Value, FunctionDef } from "./value"
|
||||
|
||||
/**
|
||||
* Converts a Bytecode object back to human-readable string format.
|
||||
* This is the inverse of toBytecode().
|
||||
*/
|
||||
export const bytecodeToString = (bytecode: Bytecode): string => {
|
||||
const lines: string[] = []
|
||||
const { instructions, constants, labels } = bytecode
|
||||
|
||||
for (let i = 0; i < instructions.length; i++) {
|
||||
// Check if there's a label at this position
|
||||
if (labels?.has(i)) {
|
||||
lines.push(`.${labels.get(i)}:`)
|
||||
}
|
||||
|
||||
const instr = instructions[i]!
|
||||
const opName = OpCode[instr.op] // Get string name from enum
|
||||
|
||||
// Format based on whether operand exists
|
||||
if (instr.operand === undefined) {
|
||||
lines.push(opName)
|
||||
} else {
|
||||
const operandStr = formatOperand(instr.op, instr.operand, constants, labels, i)
|
||||
lines.push(`${opName} ${operandStr}`)
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an operand based on the opcode type
|
||||
*/
|
||||
const formatOperand = (
|
||||
op: OpCode,
|
||||
operand: number | string,
|
||||
constants: Constant[],
|
||||
labels: Map<number, string> | undefined,
|
||||
currentIndex: number
|
||||
): string => {
|
||||
// Handle string operands (variable names)
|
||||
if (typeof operand === 'string') {
|
||||
return operand
|
||||
}
|
||||
|
||||
// Handle numeric operands based on opcode
|
||||
switch (op) {
|
||||
case OpCode.PUSH: {
|
||||
// Look up constant value
|
||||
const value = constants[operand]
|
||||
if (!value) {
|
||||
throw new Error(`Invalid constant index: ${operand}`)
|
||||
}
|
||||
return formatConstant(value)
|
||||
}
|
||||
|
||||
case OpCode.MAKE_FUNCTION: {
|
||||
// Look up function definition and format as (params) .label
|
||||
const funcDef = constants[operand]
|
||||
if (!funcDef || !('type' in funcDef) || funcDef.type !== 'function_def') {
|
||||
throw new Error(`Invalid function definition at constant index: ${operand}`)
|
||||
}
|
||||
return formatFunctionDef(funcDef as FunctionDef, labels, constants)
|
||||
}
|
||||
|
||||
case OpCode.JUMP:
|
||||
case OpCode.JUMP_IF_FALSE:
|
||||
case OpCode.JUMP_IF_TRUE: {
|
||||
// Convert relative offset to absolute position
|
||||
const targetIndex = currentIndex + 1 + operand
|
||||
const labelName = labels?.get(targetIndex)
|
||||
return labelName ? `.${labelName}` : `#${operand}`
|
||||
}
|
||||
|
||||
case OpCode.PUSH_TRY:
|
||||
case OpCode.PUSH_FINALLY: {
|
||||
// These use absolute positions
|
||||
const labelName = labels?.get(operand)
|
||||
return labelName ? `.${labelName}` : `#${operand}`
|
||||
}
|
||||
|
||||
case OpCode.MAKE_ARRAY:
|
||||
case OpCode.MAKE_DICT:
|
||||
case OpCode.STR_CONCAT:
|
||||
// These are just counts
|
||||
return `#${operand}`
|
||||
|
||||
default:
|
||||
return `#${operand}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a constant value (from constants pool)
|
||||
*/
|
||||
const formatConstant = (constant: Constant): string => {
|
||||
// Handle function definitions (shouldn't happen in PUSH, but be safe)
|
||||
if ('type' in constant && constant.type === 'function_def') {
|
||||
return '<function_def>'
|
||||
}
|
||||
|
||||
// Handle Value types
|
||||
const value = constant as Value
|
||||
switch (value.type) {
|
||||
case 'null':
|
||||
return 'null'
|
||||
|
||||
case 'boolean':
|
||||
return value.value.toString()
|
||||
|
||||
case 'number':
|
||||
return value.value.toString()
|
||||
|
||||
case 'string':
|
||||
// Use single quotes and escape special characters
|
||||
return `'${escapeString(value.value)}'`
|
||||
|
||||
case 'regex': {
|
||||
// Format as /pattern/flags
|
||||
const pattern = value.value.source
|
||||
const flags = value.value.flags
|
||||
return `/${pattern}/${flags}`
|
||||
}
|
||||
|
||||
case 'array': {
|
||||
// Format as [item1, item2, ...]
|
||||
const items = value.value.map(formatConstant).join(', ')
|
||||
return `[${items}]`
|
||||
}
|
||||
|
||||
case 'dict': {
|
||||
// Format as {key1: value1, key2: value2}
|
||||
const entries = Array.from(value.value.entries())
|
||||
.map(([k, v]) => `${k}: ${formatConstant(v)}`)
|
||||
.join(', ')
|
||||
return `{${entries}}`
|
||||
}
|
||||
|
||||
case 'function':
|
||||
case 'native':
|
||||
return '<function>'
|
||||
|
||||
default:
|
||||
return '<unknown>'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape special characters in strings for output
|
||||
*/
|
||||
const escapeString = (str: string): string => {
|
||||
return str
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/'/g, "\\'")
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\t/g, '\\t')
|
||||
.replace(/\r/g, '\\r')
|
||||
.replace(/\$/g, '\\$')
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a function definition as (params) .label
|
||||
*/
|
||||
const formatFunctionDef = (
|
||||
funcDef: FunctionDef,
|
||||
labels: Map<number, string> | undefined,
|
||||
constants: Constant[]
|
||||
): string => {
|
||||
const params: string[] = []
|
||||
|
||||
for (let i = 0; i < funcDef.params.length; i++) {
|
||||
const paramName = funcDef.params[i]!
|
||||
const defaultIndex = funcDef.defaults[paramName]
|
||||
|
||||
if (defaultIndex !== undefined) {
|
||||
// Parameter has a default value
|
||||
const defaultValue = constants[defaultIndex]
|
||||
if (!defaultValue) {
|
||||
throw new Error(`Invalid default value index: ${defaultIndex}`)
|
||||
}
|
||||
params.push(`${paramName}=${formatConstant(defaultValue)}`)
|
||||
} else if (i === funcDef.params.length - 1 && funcDef.variadic) {
|
||||
// Last parameter and function is variadic
|
||||
params.push(`...${paramName}`)
|
||||
} else if (i === funcDef.params.length - 1 && funcDef.named) {
|
||||
// Last parameter and function accepts named args
|
||||
params.push(`@${paramName}`)
|
||||
} else {
|
||||
// Regular parameter
|
||||
params.push(paramName)
|
||||
}
|
||||
}
|
||||
|
||||
// Format body address (prefer label name if available)
|
||||
const bodyLabel = labels?.get(funcDef.body)
|
||||
const bodyStr = bodyLabel ? `.${bodyLabel}` : `#${funcDef.body}`
|
||||
|
||||
return `(${params.join(' ')}) ${bodyStr}`
|
||||
}
|
||||
112
src/function.ts
112
src/function.ts
|
|
@ -1,112 +0,0 @@
|
|||
import { type Value, type NativeFunction, fromValue, toValue } from "./value"
|
||||
import { VM } from "./vm"
|
||||
|
||||
export type ParamInfo = {
|
||||
params: string[]
|
||||
defaults: Record<string, Value>
|
||||
variadic: boolean
|
||||
named: boolean
|
||||
}
|
||||
|
||||
const WRAPPED_MARKER = Symbol('reef-wrapped')
|
||||
|
||||
export function wrapNative(vm: VM, fn: Function): (this: VM, ...args: Value[]) => Promise<Value> {
|
||||
if ((fn as any).raw) return fn as (this: VM, ...args: Value[]) => Promise<Value>
|
||||
|
||||
const wrapped = async function (this: VM, ...values: Value[]) {
|
||||
const nativeArgs = values.map(arg => fromValue(arg, vm))
|
||||
const result = await fn.call(this, ...nativeArgs)
|
||||
return toValue(result, this)
|
||||
}
|
||||
|
||||
const wrappedObj = wrapped as any
|
||||
wrappedObj[WRAPPED_MARKER] = true
|
||||
|
||||
// Store the original function for param extraction
|
||||
wrappedObj.originalFn = fn
|
||||
|
||||
return wrapped
|
||||
}
|
||||
|
||||
export function isWrapped(fn: Function): boolean {
|
||||
return !!(fn as any)[WRAPPED_MARKER]
|
||||
}
|
||||
|
||||
export function getOriginalFunction(fn: NativeFunction): Function {
|
||||
return (fn as any).originalFn || fn
|
||||
}
|
||||
|
||||
export function extractParamInfo(fn: Function): ParamInfo {
|
||||
const params: string[] = []
|
||||
const defaults: Record<string, Value> = {}
|
||||
let variadic = false
|
||||
let named = false
|
||||
|
||||
const fnStr = fn.toString()
|
||||
|
||||
// Match function signature: function(a, b) or (a, b) => or async (a, b) =>
|
||||
const match = fnStr.match(/(?:function\s*.*?\(|^\s*\(|^\s*async\s*\(|^\s*async\s+function\s*.*?\()([^)]*)\)/)
|
||||
|
||||
if (!match || !match[1]) {
|
||||
return { params, defaults, variadic, named }
|
||||
}
|
||||
|
||||
const paramStr = match[1].trim()
|
||||
if (!paramStr) {
|
||||
return { params, defaults, variadic, named }
|
||||
}
|
||||
|
||||
// Split parameters by comma (naive - doesn't handle nested objects/arrays)
|
||||
const paramParts = paramStr.split(',').map(p => p.trim())
|
||||
|
||||
for (const part of paramParts) {
|
||||
// Check for rest parameters (...rest)
|
||||
if (part.startsWith('...')) {
|
||||
variadic = true
|
||||
const paramName = part.slice(3).trim()
|
||||
params.push(paramName)
|
||||
}
|
||||
// Check for default values (name = value)
|
||||
else if (part.includes('=')) {
|
||||
const eqIndex = part.indexOf('=')
|
||||
const paramName = part.slice(0, eqIndex).trim()
|
||||
const defaultStr = part.slice(eqIndex + 1).trim()
|
||||
|
||||
params.push(paramName)
|
||||
|
||||
// Check if this is a named parameter (atXxx pattern)
|
||||
if (/^at[A-Z]/.test(paramName)) {
|
||||
named = true
|
||||
}
|
||||
|
||||
// Try to parse the default value (only simple literals)
|
||||
try {
|
||||
if (defaultStr === 'null') {
|
||||
defaults[paramName] = toValue(null)
|
||||
} else if (defaultStr === 'true') {
|
||||
defaults[paramName] = toValue(true)
|
||||
} else if (defaultStr === 'false') {
|
||||
defaults[paramName] = toValue(false)
|
||||
} else if (/^-?\d+(\.\d+)?$/.test(defaultStr)) {
|
||||
defaults[paramName] = toValue(parseFloat(defaultStr))
|
||||
} else if (/^['"].*['"]$/.test(defaultStr)) {
|
||||
defaults[paramName] = toValue(defaultStr.slice(1, -1))
|
||||
}
|
||||
// For complex defaults (like {}), we skip them and let the function's own default be used
|
||||
} catch {
|
||||
// If parsing fails, ignore the default
|
||||
}
|
||||
} else {
|
||||
// Regular parameter
|
||||
const paramName = part.trim()
|
||||
params.push(paramName)
|
||||
|
||||
// Check if this is a named parameter (atXxx pattern)
|
||||
if (/^at[A-Z]/.test(paramName)) {
|
||||
named = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { params, defaults, variadic, named }
|
||||
}
|
||||
15
src/index.ts
15
src/index.ts
|
|
@ -1,17 +1,12 @@
|
|||
import type { Bytecode } from "./bytecode"
|
||||
import type { Value } from "./value"
|
||||
import { type Value } from "./value"
|
||||
import { VM } from "./vm"
|
||||
|
||||
export async function run(bytecode: Bytecode, globals?: Record<string, any>): Promise<Value> {
|
||||
const vm = new VM(bytecode, globals)
|
||||
export async function run(bytecode: Bytecode): Promise<Value> {
|
||||
const vm = new VM(bytecode)
|
||||
return await vm.run()
|
||||
}
|
||||
|
||||
export { type Bytecode, toBytecode, type ProgramItem } from "./bytecode"
|
||||
export { bytecodeToString } from "./format"
|
||||
export { wrapNative, isWrapped, type ParamInfo, extractParamInfo, getOriginalFunction } from "./function"
|
||||
export { OpCode } from "./opcode"
|
||||
export { Scope } from "./scope"
|
||||
export type { Value, TypeScriptFunction, NativeFunction } from "./value"
|
||||
export { isValue, toValue, toString, toNumber, fromValue, toNull, fnFromValue } from "./value"
|
||||
export { type Bytecode, toBytecode } from "./bytecode"
|
||||
export { type Value, toValue, toString, toNumber, fromValue, toNull, wrapNative } from "./value"
|
||||
export { VM } from "./vm"
|
||||
|
|
@ -3,16 +3,12 @@ export enum OpCode {
|
|||
PUSH, // operand: constant index (number) | stack: [] → [value]
|
||||
POP, // operand: none | stack: [value] → []
|
||||
DUP, // operand: none | stack: [value] → [value, value]
|
||||
SWAP, // operand: none | stack: [value1, value2] → [value2, value1]
|
||||
|
||||
// variables
|
||||
LOAD, // 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
|
||||
|
||||
// information
|
||||
TYPE, // operand: none | stack: [a] → []
|
||||
|
||||
// math (coerce to number, pop 2, push result)
|
||||
ADD, // 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]
|
||||
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)
|
||||
EQ, // operand: none | stack: [a, b] → [a == b] (deep equality)
|
||||
NEQ, // operand: none | stack: [a, b] → [a != b]
|
||||
|
|
@ -71,11 +59,8 @@ export enum OpCode {
|
|||
DICT_SET, // operand: none | stack: [dict, key, value] → [] | mutates dict
|
||||
DICT_HAS, // operand: none | stack: [dict, key] → [boolean]
|
||||
|
||||
// arrays and dicts
|
||||
DOT_GET, // operand: none | stack: [array|dict, index|key] → [value] | unified accessor, returns null if missing
|
||||
|
||||
// strings
|
||||
STR_CONCAT, // operand: value count (number) | stack: [val1, ..., valN] → [string] | concatenate N values
|
||||
// typescript interop
|
||||
CALL_NATIVE, // operand: function name (identifier) | stack: [...args] → [result] | consumes entire stack
|
||||
|
||||
// special
|
||||
HALT // operand: none | stop execution
|
||||
|
|
|
|||
|
|
@ -30,15 +30,6 @@ export class Scope {
|
|||
has(name: string): boolean {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -43,26 +43,18 @@ const OPCODES_WITH_OPERANDS = new Set([
|
|||
OpCode.PUSH_FINALLY,
|
||||
OpCode.MAKE_ARRAY,
|
||||
OpCode.MAKE_DICT,
|
||||
OpCode.STR_CONCAT,
|
||||
OpCode.MAKE_FUNCTION,
|
||||
OpCode.CALL_NATIVE,
|
||||
])
|
||||
|
||||
const OPCODES_WITHOUT_OPERANDS = new Set([
|
||||
OpCode.POP,
|
||||
OpCode.DUP,
|
||||
OpCode.SWAP,
|
||||
OpCode.TYPE,
|
||||
OpCode.ADD,
|
||||
OpCode.SUB,
|
||||
OpCode.MUL,
|
||||
OpCode.DIV,
|
||||
OpCode.MOD,
|
||||
OpCode.BIT_AND,
|
||||
OpCode.BIT_OR,
|
||||
OpCode.BIT_XOR,
|
||||
OpCode.BIT_SHL,
|
||||
OpCode.BIT_SHR,
|
||||
OpCode.BIT_USHR,
|
||||
OpCode.EQ,
|
||||
OpCode.NEQ,
|
||||
OpCode.LT,
|
||||
|
|
@ -84,18 +76,13 @@ const OPCODES_WITHOUT_OPERANDS = new Set([
|
|||
OpCode.DICT_GET,
|
||||
OpCode.DICT_SET,
|
||||
OpCode.DICT_HAS,
|
||||
OpCode.DOT_GET,
|
||||
])
|
||||
|
||||
// JUMP* instructions require labels only (no numeric immediates)
|
||||
const OPCODES_REQUIRING_LABEL = new Set([
|
||||
// immediate = immediate number, eg #5
|
||||
const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
|
||||
OpCode.JUMP,
|
||||
OpCode.JUMP_IF_FALSE,
|
||||
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_FINALLY,
|
||||
])
|
||||
|
|
@ -104,7 +91,6 @@ const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
|
|||
const OPCODES_REQUIRING_IMMEDIATE = new Set([
|
||||
OpCode.MAKE_ARRAY,
|
||||
OpCode.MAKE_DICT,
|
||||
OpCode.STR_CONCAT,
|
||||
])
|
||||
|
||||
export function validateBytecode(source: string): ValidationResult {
|
||||
|
|
@ -201,16 +187,6 @@ export function validateBytecode(source: string): ValidationResult {
|
|||
|
||||
// Validate specific operand formats
|
||||
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 (!operand.startsWith('#') && !operand.startsWith('.')) {
|
||||
errors.push({
|
||||
|
|
@ -324,11 +300,11 @@ export function validateBytecode(source: string): ValidationResult {
|
|||
}
|
||||
}
|
||||
|
||||
// Validate body address (must be a label)
|
||||
if (!bodyAddr!.startsWith('.')) {
|
||||
// Validate body address
|
||||
if (!bodyAddr!.startsWith('.') && !bodyAddr!.startsWith('#')) {
|
||||
errors.push({
|
||||
line: lineNum,
|
||||
message: `Invalid body address: expected .label, got: ${bodyAddr}`,
|
||||
message: `Invalid body address: expected .label or #offset`,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
200
src/value.ts
200
src/value.ts
|
|
@ -1,12 +1,4 @@
|
|||
import { wrapNative, getOriginalFunction } from "./function"
|
||||
import { OpCode } from "./opcode"
|
||||
import { Scope } from "./scope"
|
||||
import { VM } from "./vm"
|
||||
|
||||
export type NativeFunction = (...args: Value[]) => Promise<Value> | Value
|
||||
export type TypeScriptFunction = (this: VM, ...args: any[]) => any
|
||||
const REEF_FUNCTION = Symbol('__reefFunction')
|
||||
const VALUE_TYPES = new Set(['null', 'boolean', 'number', 'string', 'array', 'dict', 'regex', 'native', 'function'])
|
||||
|
||||
export type Value =
|
||||
| { type: 'null', value: null }
|
||||
|
|
@ -15,8 +7,6 @@ export type Value =
|
|||
| { type: 'string', value: string }
|
||||
| { type: 'array', value: Value[] }
|
||||
| { type: 'dict', value: Dict }
|
||||
| { type: 'regex', value: RegExp }
|
||||
| { type: 'native', fn: NativeFunction, value: '<function>' }
|
||||
| {
|
||||
type: 'function',
|
||||
params: string[],
|
||||
|
|
@ -39,22 +29,15 @@ export type FunctionDef = {
|
|||
named: boolean
|
||||
}
|
||||
|
||||
export function isValue(v: any): boolean {
|
||||
return !!(v && typeof v === 'object' && VALUE_TYPES.has(v.type) && 'value' in v)
|
||||
}
|
||||
|
||||
export function toValue(v: any, vm?: VM): Value /* throws */ {
|
||||
export function toValue(v: any): Value /* throws */ {
|
||||
if (v === null || v === undefined)
|
||||
return { type: 'null', value: null }
|
||||
|
||||
if (isValue(v))
|
||||
if (v && typeof v === 'object' && 'type' in v && 'value' in v)
|
||||
return v as Value
|
||||
|
||||
if (Array.isArray(v))
|
||||
return { type: 'array', value: v.map(x => toValue(x, vm)) }
|
||||
|
||||
if (v instanceof RegExp)
|
||||
return { type: 'regex', value: v }
|
||||
return { type: 'array', value: v.map(toValue) }
|
||||
|
||||
switch (typeof v) {
|
||||
case 'boolean':
|
||||
|
|
@ -64,53 +47,28 @@ export function toValue(v: any, vm?: VM): Value /* throws */ {
|
|||
case 'string':
|
||||
return { type: 'string', value: v }
|
||||
case 'function':
|
||||
if ((v as any)[REEF_FUNCTION])
|
||||
return (v as any)[REEF_FUNCTION]
|
||||
|
||||
let fn = vm ? wrapNative(vm, v) : cantCallFunctionWithoutVM(v)
|
||||
return { type: 'native', fn, value: '<function>' }
|
||||
throw "can't toValue() a js function yet"
|
||||
case 'object':
|
||||
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 }
|
||||
default:
|
||||
throw new Error(`can't toValue this: ${v}`)
|
||||
}
|
||||
}
|
||||
|
||||
function cantCallFunctionWithoutVM(fn: Function) {
|
||||
const name = fn.name || '<anonymous>'
|
||||
const str = fn.toString().slice(0, 100)
|
||||
return (...args: Value[]) => {
|
||||
const stack = new Error().stack || ''
|
||||
const stackLines = stack.split('\n')
|
||||
.slice(1)
|
||||
.filter(line => !line.includes('toValue'))
|
||||
.map(line => ' ' + line.trim())
|
||||
.join('\n')
|
||||
throw new Error(
|
||||
`can't call function that was converted without a vm\n` +
|
||||
` Function: ${name}\n` +
|
||||
` Source: ${str}${str.length > 100 ? '...' : ''}\n` +
|
||||
` Called from:\n${stackLines}`
|
||||
)
|
||||
throw `can't toValue this: ${v}`
|
||||
}
|
||||
}
|
||||
|
||||
export function toNumber(v: Value): number {
|
||||
switch (v.type) {
|
||||
case 'number':
|
||||
return v.value
|
||||
case 'boolean':
|
||||
return v.value ? 1 : 0
|
||||
case 'number': return v.value
|
||||
case 'boolean': return v.value ? 1 : 0
|
||||
case 'string': {
|
||||
const parsed = parseFloat(v.value)
|
||||
return isNaN(parsed) ? 0 : parsed
|
||||
}
|
||||
default:
|
||||
return 0
|
||||
default: return 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,28 +85,17 @@ export function isTrue(v: Value): boolean {
|
|||
|
||||
export function toString(v: Value): string {
|
||||
switch (v.type) {
|
||||
case 'string':
|
||||
return v.value
|
||||
case 'number':
|
||||
return String(v.value)
|
||||
case 'boolean':
|
||||
return String(v.value)
|
||||
case 'null':
|
||||
return 'null'
|
||||
case 'function':
|
||||
return '<function>'
|
||||
case 'native':
|
||||
return '<function>'
|
||||
case 'array':
|
||||
return `[${v.value.map(toString).join(', ')}]`
|
||||
case 'string': return v.value
|
||||
case 'number': return String(v.value)
|
||||
case 'boolean': return String(v.value)
|
||||
case 'null': return 'null'
|
||||
case 'function': return '<function>'
|
||||
case 'array': return `[${v.value.map(toString).join(', ')}]`
|
||||
case 'dict': {
|
||||
const pairs = Array.from(v.value.entries()).map(([k, v]) => `${k}: ${toString(v)}`)
|
||||
const pairs = Array.from(v.value.entries())
|
||||
.map(([k, v]) => `${k}: ${toString(v)}`)
|
||||
return `{${pairs.join(', ')}}`
|
||||
}
|
||||
case 'regex':
|
||||
return String(v.value)
|
||||
default:
|
||||
return String(v)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,12 +103,10 @@ export function isEqual(a: Value, b: Value): boolean {
|
|||
if (a.type !== b.type) return false
|
||||
|
||||
switch (a.type) {
|
||||
case 'null':
|
||||
return true
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'string':
|
||||
return a.value === b.value
|
||||
case 'null': return true
|
||||
case 'boolean': return a.value === (b as typeof a).value
|
||||
case 'number': return a.value === (b as typeof a).value
|
||||
case 'string': return a.value === (b as typeof a).value
|
||||
case 'array': {
|
||||
const bArr = b as typeof a
|
||||
if (a.value.length !== bArr.value.length) return false
|
||||
|
|
@ -176,48 +121,19 @@ export function isEqual(a: Value, b: Value): boolean {
|
|||
}
|
||||
return true
|
||||
}
|
||||
case 'regex': {
|
||||
return String(a.value) === String(b.value)
|
||||
}
|
||||
case 'function':
|
||||
case 'native':
|
||||
return false // functions never equal
|
||||
default:
|
||||
return false
|
||||
case 'function': return false // functions never equal
|
||||
}
|
||||
}
|
||||
|
||||
export function fromValue(v: Value, vm?: VM): any {
|
||||
export function fromValue(v: Value): any {
|
||||
switch (v.type) {
|
||||
case 'null':
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'string':
|
||||
return v.value
|
||||
case 'array':
|
||||
return v.value.map(x => fromValue(x, vm))
|
||||
case 'dict':
|
||||
return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v, vm)]))
|
||||
case 'regex':
|
||||
return v.value
|
||||
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':
|
||||
return getOriginalFunction(v.fn)
|
||||
case 'null': return null
|
||||
case 'boolean': return v.value
|
||||
case 'number': return v.value
|
||||
case 'string': return v.value
|
||||
case 'array': return v.value.map(fromValue)
|
||||
case 'dict': return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v)]))
|
||||
case 'function': return '<function>'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -225,49 +141,21 @@ export function toNull(): Value {
|
|||
return toValue(null)
|
||||
}
|
||||
|
||||
export function fnFromValue(fn: Value, vm: VM): Function {
|
||||
if (fn.type !== 'function')
|
||||
throw new Error('Value is not a function')
|
||||
const WRAPPED_MARKER = Symbol('reef-wrapped')
|
||||
|
||||
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)
|
||||
export function wrapNative(fn: Function): (...args: Value[]) => Promise<Value> {
|
||||
const wrapped = async (...values: Value[]) => {
|
||||
const nativeArgs = values.map(fromValue)
|
||||
const result = await fn(...nativeArgs)
|
||||
return toValue(result)
|
||||
}
|
||||
|
||||
// support roundtrips, eg fromValue(toValue(fn))
|
||||
; (wrapper as any)[REEF_FUNCTION] = fn
|
||||
const wrappedObj = wrapped as any
|
||||
wrappedObj[WRAPPED_MARKER] = true
|
||||
|
||||
return wrapper
|
||||
return wrapped
|
||||
}
|
||||
|
||||
export function isWrapped(fn: Function): boolean {
|
||||
return !!(fn as any)[WRAPPED_MARKER]
|
||||
}
|
||||
401
src/vm.ts
401
src/vm.ts
|
|
@ -3,9 +3,9 @@ import type { ExceptionHandler } from "./exception"
|
|||
import { type Frame } from "./frame"
|
||||
import { OpCode } from "./opcode"
|
||||
import { Scope } from "./scope"
|
||||
import type { Value, NativeFunction, TypeScriptFunction } from "./value"
|
||||
import { toValue, toNumber, isTrue, isEqual, toString, fromValue, fnFromValue } from "./value"
|
||||
import { extractParamInfo, getOriginalFunction } from "./function"
|
||||
import { type Value, toValue, toNumber, isTrue, isEqual, toString, wrapNative, isWrapped } from "./value"
|
||||
|
||||
type NativeFunction = (...args: Value[]) => Promise<Value> | Value
|
||||
|
||||
export class VM {
|
||||
pc = 0
|
||||
|
|
@ -19,69 +19,20 @@ export class VM {
|
|||
labels: Map<number, string> = new Map()
|
||||
nativeFunctions: Map<string, NativeFunction> = new Map()
|
||||
|
||||
constructor(bytecode: Bytecode, globals?: Record<string, any>) {
|
||||
constructor(bytecode: Bytecode) {
|
||||
this.instructions = bytecode.instructions
|
||||
this.constants = bytecode.constants
|
||||
this.labels = bytecode.labels || new Map()
|
||||
this.scope = new Scope()
|
||||
|
||||
if (globals) {
|
||||
for (const name of Object.keys(globals ?? {}))
|
||||
this.set(name, globals[name])
|
||||
|
||||
this.scope = new Scope(this.scope)
|
||||
}
|
||||
}
|
||||
|
||||
async call(name: string, ...args: any) {
|
||||
const value = this.scope.get(name)
|
||||
|
||||
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)
|
||||
}
|
||||
registerFunction(name: string, fn: Function) {
|
||||
const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(fn)
|
||||
this.nativeFunctions.set(name, wrapped)
|
||||
}
|
||||
|
||||
has(name: string): boolean {
|
||||
return this.scope.has(name)
|
||||
}
|
||||
|
||||
vars(): string[] {
|
||||
return this.scope.vars()
|
||||
}
|
||||
|
||||
get(name: string) {
|
||||
return this.scope.get(name)
|
||||
}
|
||||
|
||||
set(name: string, value: any) {
|
||||
this.scope.set(name, toValue(value, this))
|
||||
}
|
||||
|
||||
setFunction(name: string, fn: TypeScriptFunction) {
|
||||
this.scope.set(name, toValue(fn, this))
|
||||
}
|
||||
|
||||
setValueFunction(name: string, fn: NativeFunction) {
|
||||
this.scope.set(name, { type: 'native', fn, value: '<function>' })
|
||||
}
|
||||
|
||||
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!
|
||||
registerValueFunction(name: string, fn: NativeFunction) {
|
||||
this.nativeFunctions.set(name, fn)
|
||||
}
|
||||
|
||||
async run(): Promise<Value> {
|
||||
|
|
@ -97,57 +48,6 @@ export class VM {
|
|||
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 */ {
|
||||
switch (instruction.op) {
|
||||
case OpCode.PUSH:
|
||||
|
|
@ -168,32 +68,8 @@ export class VM {
|
|||
this.stack.push(this.stack[this.stack.length - 1]!)
|
||||
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:
|
||||
const b = this.stack.pop()!
|
||||
const a = this.stack.pop()!
|
||||
|
||||
if (a.type === 'string' || b.type === 'string') {
|
||||
this.stack.push(toValue(toString(a) + toString(b)))
|
||||
} else if (a.type === 'array' && b.type === 'array') {
|
||||
this.stack.push({ type: 'array', value: [...a.value, ...b.value] })
|
||||
} else if (a.type === 'dict' && b.type === 'dict') {
|
||||
const merged = new Map(a.value)
|
||||
for (const [key, value] of b.value) {
|
||||
merged.set(key, value)
|
||||
}
|
||||
this.stack.push({ type: 'dict', value: merged })
|
||||
} else if (a.type === 'number' && b.type === 'number') {
|
||||
this.stack.push(toValue(a.value + b.value))
|
||||
} else {
|
||||
throw new Error(`ADD: Cannot add ${a.type} and ${b.type}`)
|
||||
}
|
||||
this.binaryOp((a, b) => toNumber(a) + toNumber(b))
|
||||
break
|
||||
|
||||
case OpCode.SUB:
|
||||
|
|
@ -241,31 +117,6 @@ export class VM {
|
|||
this.stack.push({ type: 'boolean', value: !isTrue(val) })
|
||||
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:
|
||||
this.stopped = true
|
||||
break
|
||||
|
|
@ -286,18 +137,13 @@ export class VM {
|
|||
const value = this.scope.get(varName)
|
||||
|
||||
if (value === undefined)
|
||||
this.stack.push(toValue(varName, this))
|
||||
this.stack.push(toValue(varName))
|
||||
else
|
||||
this.stack.push(value)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case OpCode.TYPE:
|
||||
const value = this.stack.pop()!
|
||||
this.stack.push(toValue(value.type))
|
||||
break
|
||||
|
||||
case OpCode.STORE:
|
||||
const name = instruction.operand as string
|
||||
const toStore = this.stack.pop()!
|
||||
|
|
@ -488,32 +334,6 @@ export class VM {
|
|||
this.stack.push({ type: 'boolean', value: hasDict.value.has(toString(hasKey)) })
|
||||
break
|
||||
|
||||
case OpCode.DOT_GET: {
|
||||
const index = this.stack.pop()!
|
||||
const target = this.stack.pop()!
|
||||
|
||||
if (target.type === 'array')
|
||||
this.stack.push(toValue(target.value?.[Number(index.value)], this))
|
||||
|
||||
else if (target.type === 'dict')
|
||||
this.stack.push(toValue(target.value?.get(String(index.value)), this))
|
||||
|
||||
else
|
||||
throw new Error(`DOT_GET: ${target.type} not supported`)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case OpCode.STR_CONCAT:
|
||||
let count = instruction.operand as number
|
||||
let parts = []
|
||||
|
||||
while (count-- > 0 && this.stack.length)
|
||||
parts.unshift(toString(this.stack.pop()!))
|
||||
|
||||
this.stack.push(toValue(parts.join('')))
|
||||
break
|
||||
|
||||
case OpCode.MAKE_FUNCTION:
|
||||
const fnDefIdx = instruction.operand as number
|
||||
const fnDef = this.constants[fnDefIdx]
|
||||
|
|
@ -533,17 +353,16 @@ export class VM {
|
|||
})
|
||||
break
|
||||
|
||||
// @ts-ignore
|
||||
case OpCode.TRY_CALL: {
|
||||
const varName = instruction.operand as string
|
||||
const value = this.scope.get(varName)
|
||||
|
||||
if (value?.type === 'function' || value?.type === 'native') {
|
||||
if (value?.type === 'function') {
|
||||
this.stack.push(value)
|
||||
this.stack.push(toValue(0))
|
||||
this.stack.push(toValue(0))
|
||||
this.instructions[this.pc] = { op: OpCode.CALL }
|
||||
this.pc--
|
||||
break
|
||||
// No `break` here -- we want to fall through to OpCode.CALL!
|
||||
} else if (value) {
|
||||
this.stack.push(value)
|
||||
break
|
||||
|
|
@ -553,6 +372,8 @@ export class VM {
|
|||
}
|
||||
}
|
||||
|
||||
// don't put any `case` statement here - `TRY_CALL` MUST go before `CALL!`
|
||||
|
||||
case OpCode.CALL: {
|
||||
// Pop named count from stack (top)
|
||||
const namedCount = toNumber(this.stack.pop()!)
|
||||
|
|
@ -570,8 +391,9 @@ export class VM {
|
|||
const key = this.stack.pop()!
|
||||
namedPairs.unshift({ key: toString(key), value })
|
||||
}
|
||||
for (const pair of namedPairs)
|
||||
for (const pair of namedPairs) {
|
||||
namedArgs.set(pair.key, pair.value)
|
||||
}
|
||||
|
||||
// Pop positional arguments from stack
|
||||
const positionalArgs: Value[] = []
|
||||
|
|
@ -580,110 +402,8 @@ export class VM {
|
|||
|
||||
const fn = this.stack.pop()!
|
||||
|
||||
// Handle native functions
|
||||
if (fn.type === 'native') {
|
||||
// Mark current frame as break target (like regular CALL does)
|
||||
if (this.callStack.length > 0)
|
||||
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
|
||||
|
||||
// Extract parameter info on-demand
|
||||
const originalFn = getOriginalFunction(fn.fn)
|
||||
const paramInfo = extractParamInfo(originalFn)
|
||||
|
||||
// Bind parameters using the same priority as Reef functions
|
||||
const nativeArgs: Value[] = []
|
||||
|
||||
// Determine how many params are fixed (excluding variadic and named)
|
||||
let nativeFixedParamCount = paramInfo.params.length
|
||||
if (paramInfo.variadic) nativeFixedParamCount--
|
||||
if (paramInfo.named) nativeFixedParamCount--
|
||||
|
||||
// Track which positional args have been consumed
|
||||
let nativePositionalArgIndex = 0
|
||||
|
||||
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
||||
// Note: null values trigger defaults (null acts as "use default")
|
||||
for (let i = 0; i < nativeFixedParamCount; i++) {
|
||||
const paramName = paramInfo.params[i]!
|
||||
let paramValue: Value | undefined
|
||||
|
||||
// Check if named argument was provided for this param
|
||||
if (namedArgs.has(paramName)) {
|
||||
paramValue = namedArgs.get(paramName)!
|
||||
namedArgs.delete(paramName) // Remove from named args so it won't go to @named
|
||||
} else if (nativePositionalArgIndex < positionalArgs.length) {
|
||||
paramValue = positionalArgs[nativePositionalArgIndex]!
|
||||
nativePositionalArgIndex++
|
||||
}
|
||||
|
||||
// If the parameter value is null and a default exists, use the default
|
||||
if (paramValue?.type === 'null' && paramInfo.defaults[paramName] !== undefined) {
|
||||
nativeArgs.push(paramInfo.defaults[paramName]!)
|
||||
} else if (paramValue) {
|
||||
nativeArgs.push(paramValue)
|
||||
} else if (paramInfo.defaults[paramName] !== undefined) {
|
||||
nativeArgs.push(paramInfo.defaults[paramName]!)
|
||||
} else {
|
||||
nativeArgs.push(toValue(null))
|
||||
}
|
||||
}
|
||||
|
||||
// Handle named parameter (collect remaining unmatched named args)
|
||||
// Parameter names matching atXxx pattern (e.g., atOptions, atNamed) collect extra named args
|
||||
if (paramInfo.named) {
|
||||
const namedDict = new Map<string, Value>()
|
||||
for (const [key, value] of namedArgs) {
|
||||
namedDict.set(key, value)
|
||||
}
|
||||
// Convert dict to plain JavaScript object for the native function
|
||||
const namedObj = fromValue({ type: 'dict', value: namedDict }, this)
|
||||
nativeArgs.push(toValue(namedObj, this))
|
||||
}
|
||||
|
||||
// Handle variadic parameter (TypeScript rest parameters)
|
||||
// For TypeScript functions with ...rest, we spread the remaining args
|
||||
// rather than wrapping them in an array
|
||||
if (paramInfo.variadic) {
|
||||
const remainingArgs = positionalArgs.slice(nativePositionalArgIndex)
|
||||
nativeArgs.push(...remainingArgs)
|
||||
}
|
||||
|
||||
// Call the native function with bound args
|
||||
try {
|
||||
const result = await fn.fn.call(this, ...nativeArgs)
|
||||
this.stack.push(result)
|
||||
break
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const errorValue = toValue(errorMessage)
|
||||
|
||||
// no exception handlers, let it crash
|
||||
if (this.exceptionHandlers.length === 0) {
|
||||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
// use existing THROW logic
|
||||
const throwHandler = this.exceptionHandlers.pop()!
|
||||
|
||||
while (this.callStack.length > throwHandler.callStackDepth)
|
||||
this.callStack.pop()
|
||||
|
||||
this.scope = throwHandler.scope
|
||||
this.stack.push(errorValue)
|
||||
|
||||
// Jump to `finally` if present, otherwise jump to `catch`
|
||||
const targetAddress = throwHandler.finallyAddress !== undefined
|
||||
? throwHandler.finallyAddress
|
||||
: throwHandler.catchAddress
|
||||
|
||||
// subtract 1 because pc will be incremented
|
||||
this.pc = targetAddress - 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (fn.type !== 'function')
|
||||
throw new Error(`CALL: ${fn.value} is not a function`)
|
||||
throw new Error('CALL: not a function')
|
||||
|
||||
if (this.callStack.length > 0)
|
||||
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
|
||||
|
|
@ -701,33 +421,16 @@ export class VM {
|
|||
if (fn.variadic) fixedParamCount--
|
||||
if (fn.named) fixedParamCount--
|
||||
|
||||
// Track which positional args have been consumed
|
||||
let positionalArgIndex = 0
|
||||
|
||||
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
||||
// Note: null values trigger defaults (null acts as "use default")
|
||||
for (let i = 0; i < fixedParamCount; i++) {
|
||||
const paramName = fn.params[i]!
|
||||
let paramValue: Value | undefined
|
||||
|
||||
// Check if named argument was provided for this param
|
||||
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
|
||||
} else if (positionalArgIndex < positionalArgs.length) {
|
||||
paramValue = positionalArgs[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 (positionalArgs[i] !== undefined) {
|
||||
this.scope.set(paramName, positionalArgs[i]!)
|
||||
} else if (fn.defaults[paramName] !== undefined) {
|
||||
const defaultIdx = fn.defaults[paramName]!
|
||||
const defaultValue = this.constants[defaultIdx]!
|
||||
|
|
@ -742,7 +445,7 @@ export class VM {
|
|||
// Handle variadic parameter (collect remaining positional args)
|
||||
if (fn.variadic) {
|
||||
const variadicParamName = fn.params[fn.params.length - (fn.named ? 2 : 1)]!
|
||||
const remainingArgs = positionalArgs.slice(positionalArgIndex)
|
||||
const remainingArgs = positionalArgs.slice(fixedParamCount)
|
||||
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs })
|
||||
}
|
||||
|
||||
|
|
@ -788,7 +491,7 @@ export class VM {
|
|||
const tailFn = this.stack.pop()!
|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -797,9 +500,6 @@ export class VM {
|
|||
if (tailFn.variadic) tailFixedParamCount--
|
||||
if (tailFn.named) tailFixedParamCount--
|
||||
|
||||
// Track which positional args have been consumed
|
||||
let tailPositionalArgIndex = 0
|
||||
|
||||
// Bind fixed parameters
|
||||
for (let i = 0; i < tailFixedParamCount; i++) {
|
||||
const paramName = tailFn.params[i]!
|
||||
|
|
@ -807,9 +507,8 @@ export class VM {
|
|||
if (tailNamedArgs.has(paramName)) {
|
||||
this.scope.set(paramName, tailNamedArgs.get(paramName)!)
|
||||
tailNamedArgs.delete(paramName)
|
||||
} else if (tailPositionalArgIndex < tailPositionalArgs.length) {
|
||||
this.scope.set(paramName, tailPositionalArgs[tailPositionalArgIndex]!)
|
||||
tailPositionalArgIndex++
|
||||
} else if (tailPositionalArgs[i] !== undefined) {
|
||||
this.scope.set(paramName, tailPositionalArgs[i]!)
|
||||
} else if (tailFn.defaults[paramName] !== undefined) {
|
||||
const defaultIdx = tailFn.defaults[paramName]!
|
||||
const defaultValue = this.constants[defaultIdx]!
|
||||
|
|
@ -824,7 +523,7 @@ export class VM {
|
|||
// Handle variadic parameter
|
||||
if (tailFn.variadic) {
|
||||
const variadicParamName = tailFn.params[tailFn.params.length - (tailFn.named ? 2 : 1)]!
|
||||
const remainingArgs = tailPositionalArgs.slice(tailPositionalArgIndex)
|
||||
const remainingArgs = tailPositionalArgs.slice(tailFixedParamCount)
|
||||
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs })
|
||||
}
|
||||
|
||||
|
|
@ -856,8 +555,30 @@ export class VM {
|
|||
this.stack.push(returnValue)
|
||||
break
|
||||
|
||||
case OpCode.CALL_NATIVE:
|
||||
const functionName = instruction.operand as string
|
||||
const tsFunction = this.nativeFunctions.get(functionName)
|
||||
|
||||
if (!tsFunction)
|
||||
throw new Error(`CALL_NATIVE: function not found: ${functionName}`)
|
||||
|
||||
// Mark current frame as break target (like CALL does)
|
||||
if (this.callStack.length > 0)
|
||||
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
|
||||
|
||||
// Pop all arguments from stack (TypeScript function consumes entire stack)
|
||||
const tsArgs = [...this.stack]
|
||||
this.stack = []
|
||||
|
||||
// Call the TypeScript function and await if necessary
|
||||
const tsResult = await tsFunction(...tsArgs)
|
||||
|
||||
// Push result back onto stack
|
||||
this.stack.push(tsResult)
|
||||
break
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown op: ${instruction.op}`)
|
||||
throw `Unknown op: ${instruction.op}`
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -874,26 +595,4 @@ export class VM {
|
|||
const result = fn(a, b)
|
||||
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)
|
||||
}
|
||||
}
|
||||
861
tests/basic.test.ts
Normal file
861
tests/basic.test.ts
Normal file
|
|
@ -0,0 +1,861 @@
|
|||
import { test, expect } from "bun:test"
|
||||
import { run } from "#index"
|
||||
import { toBytecode } from "#bytecode"
|
||||
|
||||
test("ADD - add two numbers", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
PUSH 5
|
||||
ADD
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 6 })
|
||||
|
||||
const str2 = `
|
||||
PUSH 100
|
||||
PUSH 500
|
||||
ADD
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 600 })
|
||||
})
|
||||
|
||||
test("SUB - subtract two numbers", async () => {
|
||||
const str = `
|
||||
PUSH 5
|
||||
PUSH 2
|
||||
SUB
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
|
||||
})
|
||||
|
||||
test("MUL - multiply two numbers", async () => {
|
||||
const str = `
|
||||
PUSH 5
|
||||
PUSH 2
|
||||
MUL
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
|
||||
})
|
||||
|
||||
test("DIV - divide two numbers", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 2
|
||||
DIV
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 5 })
|
||||
|
||||
const str2 = `
|
||||
PUSH 10
|
||||
PUSH 0
|
||||
DIV
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: Infinity })
|
||||
})
|
||||
|
||||
test("MOD - modulo two numbers", async () => {
|
||||
const str = `
|
||||
PUSH 17
|
||||
PUSH 5
|
||||
MOD
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
|
||||
})
|
||||
|
||||
test("PUSH - pushes value onto stack", async () => {
|
||||
const str = `
|
||||
PUSH 42
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("POP - removes top value", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
POP
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
|
||||
})
|
||||
|
||||
test("DUP - duplicates top value", async () => {
|
||||
const str = `
|
||||
PUSH 5
|
||||
DUP
|
||||
ADD
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
|
||||
})
|
||||
|
||||
test("EQ - equality comparison", async () => {
|
||||
const str = `
|
||||
PUSH 5
|
||||
PUSH 5
|
||||
EQ
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||
|
||||
const str2 = `
|
||||
PUSH 5
|
||||
PUSH 10
|
||||
EQ
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
||||
})
|
||||
|
||||
test("NEQ - not equal comparison", async () => {
|
||||
const str = `
|
||||
PUSH 5
|
||||
PUSH 10
|
||||
NEQ
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||
})
|
||||
|
||||
test("LT - less than", async () => {
|
||||
const str = `
|
||||
PUSH 5
|
||||
PUSH 10
|
||||
LT
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||
})
|
||||
|
||||
test("GT - greater than", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 5
|
||||
GT
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||
})
|
||||
|
||||
test("LTE - less than or equal", async () => {
|
||||
// equal case
|
||||
const str = `
|
||||
PUSH 5
|
||||
PUSH 5
|
||||
LTE
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||
|
||||
// less than case
|
||||
const str2 = `
|
||||
PUSH 3
|
||||
PUSH 5
|
||||
LTE
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true })
|
||||
|
||||
// greater than case (false)
|
||||
const str3 = `
|
||||
PUSH 10
|
||||
PUSH 5
|
||||
LTE
|
||||
`
|
||||
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false })
|
||||
})
|
||||
|
||||
test("GTE - greater than or equal", async () => {
|
||||
// equal case
|
||||
const str = `
|
||||
PUSH 5
|
||||
PUSH 5
|
||||
GTE
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||
|
||||
// greater than case
|
||||
const str2 = `
|
||||
PUSH 10
|
||||
PUSH 5
|
||||
GTE
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true })
|
||||
|
||||
// less than case (false)
|
||||
const str3 = `
|
||||
PUSH 3
|
||||
PUSH 5
|
||||
GTE
|
||||
`
|
||||
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false })
|
||||
})
|
||||
|
||||
test("AND pattern - short circuits when false", async () => {
|
||||
// false && <anything> should short-circuit and return false
|
||||
const str = `
|
||||
PUSH 1
|
||||
PUSH 0
|
||||
EQ
|
||||
DUP
|
||||
JUMP_IF_FALSE .end
|
||||
POP
|
||||
PUSH 999
|
||||
.end:
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('boolean')
|
||||
if (result.type === 'boolean') {
|
||||
expect(result.value).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
test("AND pattern - evaluates both when true", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
DUP
|
||||
JUMP_IF_FALSE .end
|
||||
POP
|
||||
PUSH 2
|
||||
.end:
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
|
||||
})
|
||||
|
||||
test("OR pattern - short circuits when true", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
DUP
|
||||
JUMP_IF_TRUE .end
|
||||
POP
|
||||
PUSH 2
|
||||
.end:
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 1 })
|
||||
})
|
||||
|
||||
test("OR pattern - evaluates second when false", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
PUSH 0
|
||||
EQ
|
||||
DUP
|
||||
JUMP_IF_TRUE .end
|
||||
POP
|
||||
PUSH 2
|
||||
.end:
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
|
||||
})
|
||||
|
||||
test("NOT - logical not", async () => {
|
||||
// number is truthy, so NOT returns false
|
||||
const str = `
|
||||
PUSH 1
|
||||
NOT
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
|
||||
|
||||
// 0 is truthy in this language, so NOT returns false
|
||||
const str2 = `
|
||||
PUSH 0
|
||||
NOT
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
||||
|
||||
// boolean false is falsy, so NOT returns true
|
||||
const str3 = `
|
||||
PUSH 1
|
||||
PUSH 0
|
||||
EQ
|
||||
NOT
|
||||
`
|
||||
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true })
|
||||
})
|
||||
|
||||
test("isTruthy - only null and false are falsy", async () => {
|
||||
// 0 is truthy (unlike JS)
|
||||
const str1 = `
|
||||
PUSH 0
|
||||
JUMP_IF_FALSE .end
|
||||
PUSH 1
|
||||
.end:
|
||||
`
|
||||
expect(await run(toBytecode(str1))).toEqual({ type: 'number', value: 1 })
|
||||
|
||||
// empty string is truthy (unlike JS)
|
||||
const str2 = `
|
||||
PUSH ''
|
||||
JUMP_IF_FALSE .end
|
||||
PUSH 1
|
||||
.end:
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 1 })
|
||||
|
||||
// false is falsy
|
||||
const str3 = `
|
||||
PUSH 0
|
||||
PUSH 0
|
||||
EQ
|
||||
JUMP_IF_FALSE .end
|
||||
PUSH 999
|
||||
.end:
|
||||
`
|
||||
expect(await run(toBytecode(str3))).toEqual({ type: 'number', value: 999 })
|
||||
})
|
||||
|
||||
test("HALT - stops execution", async () => {
|
||||
const str = `
|
||||
PUSH 42
|
||||
HALT
|
||||
PUSH 100
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("STORE and LOAD - variables", async () => {
|
||||
const str = `
|
||||
PUSH 42
|
||||
STORE x
|
||||
PUSH 21
|
||||
LOAD x
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("STORE and LOAD - multiple variables", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
STORE a
|
||||
PUSH 20
|
||||
STORE b
|
||||
PUSH 44
|
||||
LOAD a
|
||||
LOAD b
|
||||
ADD
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 30 })
|
||||
})
|
||||
|
||||
test("TRY_LOAD - variable found", async () => {
|
||||
const str = `
|
||||
PUSH 100
|
||||
STORE count
|
||||
TRY_LOAD count
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 })
|
||||
|
||||
const str2 = `
|
||||
PUSH 'Bobby'
|
||||
STORE name
|
||||
TRY_LOAD name
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'Bobby' })
|
||||
})
|
||||
|
||||
test("TRY_LOAD - variable missing", async () => {
|
||||
const str = `
|
||||
PUSH 100
|
||||
STORE count
|
||||
TRY_LOAD count1
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'count1' })
|
||||
|
||||
const str2 = `
|
||||
PUSH 'Bobby'
|
||||
STORE name
|
||||
TRY_LOAD full-name
|
||||
`
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'full-name' })
|
||||
})
|
||||
|
||||
test("TRY_LOAD - with different value types", async () => {
|
||||
// Array
|
||||
const str1 = `
|
||||
PUSH 1
|
||||
PUSH 2
|
||||
PUSH 3
|
||||
MAKE_ARRAY #3
|
||||
STORE arr
|
||||
TRY_LOAD arr
|
||||
`
|
||||
const result1 = await run(toBytecode(str1))
|
||||
expect(result1.type).toBe('array')
|
||||
|
||||
// Dict
|
||||
const str2 = `
|
||||
PUSH 'key'
|
||||
PUSH 'value'
|
||||
MAKE_DICT #1
|
||||
STORE dict
|
||||
TRY_LOAD dict
|
||||
`
|
||||
const result2 = await run(toBytecode(str2))
|
||||
expect(result2.type).toBe('dict')
|
||||
|
||||
// Boolean
|
||||
const str3 = `
|
||||
PUSH true
|
||||
STORE flag
|
||||
TRY_LOAD flag
|
||||
`
|
||||
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true })
|
||||
|
||||
// Null
|
||||
const str4 = `
|
||||
PUSH null
|
||||
STORE empty
|
||||
TRY_LOAD empty
|
||||
`
|
||||
expect(await run(toBytecode(str4))).toEqual({ type: 'null', value: null })
|
||||
})
|
||||
|
||||
test("TRY_LOAD - in nested scope", async () => {
|
||||
// Function should be able to TRY_LOAD variable from parent scope
|
||||
const str = `
|
||||
PUSH 42
|
||||
STORE outer
|
||||
MAKE_FUNCTION () .fn
|
||||
PUSH 0
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
.fn:
|
||||
TRY_LOAD outer
|
||||
RETURN
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("TRY_LOAD - missing variable in nested scope returns name", async () => {
|
||||
// If variable doesn't exist in any scope, should return name as string
|
||||
const str = `
|
||||
PUSH 42
|
||||
STORE outer
|
||||
MAKE_FUNCTION () .fn
|
||||
PUSH 0
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
.fn:
|
||||
TRY_LOAD inner
|
||||
RETURN
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'inner' })
|
||||
})
|
||||
|
||||
test("TRY_LOAD - used for conditional variable existence check", async () => {
|
||||
// Pattern: use TRY_LOAD to check if variable exists and get its value or name
|
||||
const str = `
|
||||
PUSH 100
|
||||
STORE count
|
||||
TRY_LOAD count
|
||||
PUSH 'count'
|
||||
EQ
|
||||
`
|
||||
// Variable exists, so TRY_LOAD returns 100, which != 'count'
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
|
||||
|
||||
const str2 = `
|
||||
PUSH 100
|
||||
STORE count
|
||||
TRY_LOAD missing
|
||||
PUSH 'missing'
|
||||
EQ
|
||||
`
|
||||
// Variable missing, so TRY_LOAD returns 'missing', which == 'missing'
|
||||
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true })
|
||||
})
|
||||
|
||||
test("TRY_LOAD - with function value", async () => {
|
||||
const str = `
|
||||
MAKE_FUNCTION () .fn
|
||||
STORE myFunc
|
||||
JUMP .skip
|
||||
.fn:
|
||||
PUSH 99
|
||||
RETURN
|
||||
.skip:
|
||||
TRY_LOAD myFunc
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('function')
|
||||
})
|
||||
|
||||
test("JUMP - relative jump forward", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
JUMP .skip
|
||||
PUSH 100
|
||||
.skip:
|
||||
PUSH 2
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
|
||||
})
|
||||
|
||||
test("JUMP - forward jump skips instructions", async () => {
|
||||
// Use forward jump to skip, demonstrating relative addressing
|
||||
const str = `
|
||||
PUSH 100
|
||||
JUMP .end
|
||||
PUSH 200
|
||||
PUSH 300
|
||||
.end:
|
||||
PUSH 400
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 400 })
|
||||
})
|
||||
|
||||
test("JUMP_IF_FALSE - conditional jump when false", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
PUSH 0
|
||||
EQ
|
||||
JUMP_IF_FALSE .skip
|
||||
PUSH 100
|
||||
.skip:
|
||||
PUSH 42
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("JUMP_IF_FALSE - no jump when true", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
JUMP_IF_FALSE .skip
|
||||
PUSH 100
|
||||
.skip:
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 })
|
||||
})
|
||||
|
||||
test("JUMP_IF_TRUE - conditional jump when true", async () => {
|
||||
const str = `
|
||||
PUSH 1
|
||||
JUMP_IF_TRUE .skip
|
||||
PUSH 100
|
||||
.skip:
|
||||
PUSH 42
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("MAKE_ARRAY - creates array", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
MAKE_ARRAY #3
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('array')
|
||||
if (result.type === 'array') {
|
||||
expect(result.value).toHaveLength(3)
|
||||
expect(result.value[0]).toEqual({ type: 'number', value: 10 })
|
||||
expect(result.value[1]).toEqual({ type: 'number', value: 20 })
|
||||
expect(result.value[2]).toEqual({ type: 'number', value: 30 })
|
||||
}
|
||||
})
|
||||
|
||||
test("ARRAY_GET - gets element", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
MAKE_ARRAY #3
|
||||
PUSH 1
|
||||
ARRAY_GET
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 20 })
|
||||
})
|
||||
|
||||
test("ARRAY_SET - sets element", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
MAKE_ARRAY #3
|
||||
DUP
|
||||
PUSH 1
|
||||
PUSH 99
|
||||
ARRAY_SET
|
||||
PUSH 1
|
||||
ARRAY_GET
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 99 })
|
||||
})
|
||||
|
||||
test("ARRAY_PUSH - appends to array", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
MAKE_ARRAY #2
|
||||
DUP
|
||||
PUSH 30
|
||||
ARRAY_PUSH
|
||||
ARRAY_LEN
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
|
||||
})
|
||||
|
||||
test("ARRAY_PUSH - mutates original array", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
MAKE_ARRAY #2
|
||||
DUP
|
||||
PUSH 30
|
||||
ARRAY_PUSH
|
||||
PUSH 2
|
||||
ARRAY_GET
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 30 })
|
||||
})
|
||||
|
||||
test("ARRAY_LEN - gets length", async () => {
|
||||
const str = `
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 30
|
||||
MAKE_ARRAY #3
|
||||
ARRAY_LEN
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
|
||||
})
|
||||
|
||||
test("MAKE_DICT - creates dict", async () => {
|
||||
const str = `
|
||||
PUSH 'name'
|
||||
PUSH 'Alice'
|
||||
PUSH 'age'
|
||||
PUSH 30
|
||||
MAKE_DICT #2
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('dict')
|
||||
if (result.type === 'dict') {
|
||||
expect(result.value.size).toBe(2)
|
||||
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Alice' })
|
||||
expect(result.value.get('age')).toEqual({ type: 'number', value: 30 })
|
||||
}
|
||||
})
|
||||
|
||||
test("DICT_GET - gets value", async () => {
|
||||
const str = `
|
||||
PUSH 'name'
|
||||
PUSH 'Bob'
|
||||
MAKE_DICT #1
|
||||
PUSH 'name'
|
||||
DICT_GET
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'Bob' })
|
||||
})
|
||||
|
||||
test("DICT_SET - sets value", async () => {
|
||||
const str = `
|
||||
MAKE_DICT #0
|
||||
DUP
|
||||
PUSH 'key'
|
||||
PUSH 'value'
|
||||
DICT_SET
|
||||
PUSH 'key'
|
||||
DICT_GET
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'value' })
|
||||
})
|
||||
|
||||
test("DICT_HAS - checks key exists", async () => {
|
||||
const str = `
|
||||
PUSH 'key'
|
||||
PUSH 'value'
|
||||
MAKE_DICT #1
|
||||
PUSH 'key'
|
||||
DICT_HAS
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
||||
})
|
||||
|
||||
test("DICT_HAS - checks key missing", async () => {
|
||||
const str = `
|
||||
MAKE_DICT #0
|
||||
PUSH 'missing'
|
||||
DICT_HAS
|
||||
`
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
|
||||
})
|
||||
|
||||
test("BREAK - throws error when no break target", async () => {
|
||||
// BREAK requires a break target frame on the call stack
|
||||
// A single function call has no previous frame to mark as break target
|
||||
const bytecode = toBytecode(`
|
||||
MAKE_FUNCTION () .fn
|
||||
PUSH 0
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
.fn:
|
||||
BREAK
|
||||
`)
|
||||
|
||||
try {
|
||||
await run(bytecode)
|
||||
expect(true).toBe(false) // Should not reach here
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain('no break target found')
|
||||
}
|
||||
})
|
||||
|
||||
test("BREAK - exits from nested function call", async () => {
|
||||
// BREAK unwinds to the break target (the outer function's frame)
|
||||
// Main calls outer, outer calls inner, inner BREAKs back to outer's caller (main)
|
||||
const bytecode = toBytecode(`
|
||||
MAKE_FUNCTION () .outer
|
||||
PUSH 0
|
||||
PUSH 0
|
||||
CALL
|
||||
PUSH 42
|
||||
HALT
|
||||
.outer:
|
||||
MAKE_FUNCTION () .inner
|
||||
PUSH 0
|
||||
PUSH 0
|
||||
CALL
|
||||
PUSH 99
|
||||
RETURN
|
||||
.inner:
|
||||
BREAK
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("JUMP backward - simple loop", async () => {
|
||||
// Very simple: counter starts at 0, loops 3 times incrementing
|
||||
// On 3rd iteration (counter==3), exits and returns counter
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 0
|
||||
STORE counter
|
||||
.loop:
|
||||
LOAD counter
|
||||
PUSH 3
|
||||
EQ
|
||||
JUMP_IF_FALSE .body
|
||||
LOAD counter
|
||||
HALT
|
||||
.body:
|
||||
LOAD counter
|
||||
PUSH 1
|
||||
ADD
|
||||
STORE counter
|
||||
JUMP .loop
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 3 })
|
||||
})
|
||||
|
||||
test("emoji variable names - string format", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 5
|
||||
STORE 💎
|
||||
LOAD 💎
|
||||
HALT
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 5 })
|
||||
})
|
||||
|
||||
test("emoji variable names - array format", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["PUSH", 100],
|
||||
["STORE", "💰"],
|
||||
["LOAD", "💰"],
|
||||
["PUSH", 50],
|
||||
["ADD"],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 150 })
|
||||
})
|
||||
|
||||
test("unicode variable names - Japanese", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 42
|
||||
STORE 変数
|
||||
LOAD 変数
|
||||
HALT
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("unicode variable names - Chinese", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["PUSH", 888],
|
||||
["STORE", "数字"],
|
||||
["LOAD", "数字"],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 888 })
|
||||
})
|
||||
|
||||
test("emoji in function parameters", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
MAKE_FUNCTION (💎 🌟) .add
|
||||
STORE add
|
||||
JUMP .after
|
||||
.add:
|
||||
LOAD 💎
|
||||
LOAD 🌟
|
||||
ADD
|
||||
RETURN
|
||||
.after:
|
||||
LOAD add
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 2
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 30 })
|
||||
})
|
||||
|
||||
test("emoji with defaults and variadic", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["MAKE_FUNCTION", ["🎯=100", "...🎨"], ".fn"],
|
||||
["STORE", "fn"],
|
||||
["JUMP", ".after"],
|
||||
[".fn:"],
|
||||
["LOAD", "🎯"],
|
||||
["RETURN"],
|
||||
[".after:"],
|
||||
["LOAD", "fn"],
|
||||
["PUSH", 0],
|
||||
["PUSH", 0],
|
||||
["CALL"],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 100 })
|
||||
})
|
||||
|
||||
test("mixed emoji and regular names", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["PUSH", 10],
|
||||
["STORE", "💎"],
|
||||
["PUSH", 20],
|
||||
["STORE", "value"],
|
||||
["PUSH", 30],
|
||||
["STORE", "🌟"],
|
||||
["LOAD", "💎"],
|
||||
["LOAD", "value"],
|
||||
["ADD"],
|
||||
["LOAD", "🌟"],
|
||||
["ADD"],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 60 })
|
||||
})
|
||||
|
||||
|
|
@ -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
|
||||
HALT
|
||||
`
|
||||
await expect(str).toBeNumber(52)
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 })
|
||||
})
|
||||
|
||||
test("THROW - catch exception with error value", async () => {
|
||||
|
|
@ -29,7 +29,7 @@ test("THROW - catch exception with error value", async () => {
|
|||
.catch:
|
||||
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 () => {
|
||||
|
|
@ -58,7 +58,7 @@ test("THROW - exception with nested try blocks", async () => {
|
|||
PUSH "outer error"
|
||||
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 () => {
|
||||
|
|
@ -75,7 +75,7 @@ test("THROW - exception skips outer handler", async () => {
|
|||
.outer_catch:
|
||||
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 () => {
|
||||
|
|
@ -150,7 +150,7 @@ test("PUSH_FINALLY - finally executes after successful try", async () => {
|
|||
ADD
|
||||
HALT
|
||||
`
|
||||
await expect(str).toBeNumber(110)
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 110 })
|
||||
})
|
||||
|
||||
test("PUSH_FINALLY - finally executes after exception", async () => {
|
||||
|
|
@ -169,7 +169,7 @@ test("PUSH_FINALLY - finally executes after exception", async () => {
|
|||
PUSH "finally ran"
|
||||
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 () => {
|
||||
|
|
@ -189,7 +189,7 @@ test("PUSH_FINALLY - finally without catch", async () => {
|
|||
ADD
|
||||
HALT
|
||||
`
|
||||
await expect(str).toBeNumber(52)
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 })
|
||||
})
|
||||
|
||||
test("PUSH_FINALLY - nested try-finally blocks", async () => {
|
||||
|
|
@ -214,7 +214,7 @@ test("PUSH_FINALLY - nested try-finally blocks", async () => {
|
|||
ADD
|
||||
HALT
|
||||
`
|
||||
await expect(str).toBeNumber(11)
|
||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 11 })
|
||||
})
|
||||
|
||||
test("PUSH_FINALLY - error when no handler", async () => {
|
||||
|
|
|
|||
|
|
@ -1,232 +0,0 @@
|
|||
import { test, expect, describe } from "bun:test"
|
||||
import { run, VM } from "#index"
|
||||
import { toBytecode } from "#bytecode"
|
||||
|
||||
describe("functions parameter", () => {
|
||||
test("pass functions to run()", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
LOAD add
|
||||
PUSH 5
|
||||
PUSH 3
|
||||
PUSH 2
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
`)
|
||||
|
||||
const result = await run(bytecode, {
|
||||
add: (a: number, b: number) => a + b
|
||||
})
|
||||
|
||||
expect(result).toEqual({ type: 'number', value: 8 })
|
||||
})
|
||||
|
||||
test("pass functions to VM constructor", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
LOAD multiply
|
||||
PUSH 10
|
||||
PUSH 2
|
||||
PUSH 2
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
`)
|
||||
|
||||
const vm = new VM(bytecode, {
|
||||
multiply: (a: number, b: number) => a * b
|
||||
})
|
||||
|
||||
const result = await vm.run()
|
||||
expect(result).toEqual({ type: 'number', value: 20 })
|
||||
})
|
||||
|
||||
test("pass multiple functions", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
LOAD add
|
||||
PUSH 10
|
||||
PUSH 5
|
||||
PUSH 2
|
||||
PUSH 0
|
||||
CALL
|
||||
STORE sum
|
||||
LOAD multiply
|
||||
LOAD sum
|
||||
PUSH 3
|
||||
PUSH 2
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
`)
|
||||
|
||||
const result = await run(bytecode, {
|
||||
add: (a: number, b: number) => a + b,
|
||||
multiply: (a: number, b: number) => a * b
|
||||
})
|
||||
|
||||
expect(result).toEqual({ type: 'number', value: 45 })
|
||||
})
|
||||
|
||||
test("auto-wraps native functions", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
LOAD concat
|
||||
PUSH "hello"
|
||||
PUSH "world"
|
||||
PUSH 2
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
`)
|
||||
|
||||
const result = await run(bytecode, {
|
||||
concat: (a: string, b: string) => a + " " + b
|
||||
})
|
||||
|
||||
expect(result).toEqual({ type: 'string', value: 'hello world' })
|
||||
})
|
||||
|
||||
test("works with async functions", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
LOAD delay
|
||||
PUSH 100
|
||||
PUSH 1
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
`)
|
||||
|
||||
const result = await run(bytecode, {
|
||||
delay: async (n: number) => {
|
||||
await new Promise(resolve => setTimeout(resolve, 1))
|
||||
return n * 2
|
||||
}
|
||||
})
|
||||
|
||||
expect(result).toEqual({ type: 'number', value: 200 })
|
||||
})
|
||||
|
||||
test("can combine with manual vm.set", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
LOAD add
|
||||
PUSH 5
|
||||
PUSH 3
|
||||
PUSH 2
|
||||
PUSH 0
|
||||
CALL
|
||||
STORE sum
|
||||
LOAD subtract
|
||||
LOAD sum
|
||||
PUSH 2
|
||||
PUSH 2
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
`)
|
||||
|
||||
const vm = new VM(bytecode, {
|
||||
add: (a: number, b: number) => a + b
|
||||
})
|
||||
|
||||
// Register another function manually
|
||||
vm.set('subtract', (a: number, b: number) => a - b)
|
||||
|
||||
const result = await vm.run()
|
||||
expect(result).toEqual({ type: 'number', value: 6 })
|
||||
})
|
||||
|
||||
test("no functions parameter (undefined)", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 42
|
||||
HALT
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("empty functions object", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 99
|
||||
HALT
|
||||
`)
|
||||
|
||||
const result = await run(bytecode, {})
|
||||
expect(result).toEqual({ type: 'number', value: 99 })
|
||||
})
|
||||
|
||||
test("function throws error", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
LOAD divide
|
||||
PUSH 0
|
||||
PUSH 1
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
`)
|
||||
|
||||
try {
|
||||
await run(bytecode, {
|
||||
divide: (n: number) => {
|
||||
if (n === 0) throw new Error("Cannot divide by zero")
|
||||
return 100 / n
|
||||
}
|
||||
})
|
||||
expect(true).toBe(false) // Should not reach here
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain("Cannot divide by zero")
|
||||
}
|
||||
})
|
||||
|
||||
test("complex workflow with multiple function calls", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
LOAD add
|
||||
PUSH 5
|
||||
PUSH 3
|
||||
PUSH 2
|
||||
PUSH 0
|
||||
CALL
|
||||
STORE result
|
||||
LOAD multiply
|
||||
LOAD result
|
||||
PUSH 2
|
||||
PUSH 2
|
||||
PUSH 0
|
||||
CALL
|
||||
STORE final
|
||||
LOAD format
|
||||
LOAD final
|
||||
PUSH 1
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
`)
|
||||
|
||||
const result = await run(bytecode, {
|
||||
add: (a: number, b: number) => a + b,
|
||||
multiply: (a: number, b: number) => a * b,
|
||||
format: (n: number) => `Result: ${n}`
|
||||
})
|
||||
|
||||
expect(result).toEqual({ type: 'string', value: 'Result: 16' })
|
||||
})
|
||||
|
||||
test("function overriding - later registration wins", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
LOAD getValue
|
||||
PUSH 5
|
||||
PUSH 1
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
`)
|
||||
|
||||
const vm = new VM(bytecode, {
|
||||
getValue: () => 100
|
||||
})
|
||||
|
||||
// Override with manual registration
|
||||
vm.set('getValue', () => 200)
|
||||
|
||||
const result = await vm.run()
|
||||
expect(result).toEqual({ type: 'number', value: 200 })
|
||||
})
|
||||
})
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { test, expect } from "bun:test"
|
||||
import { toBytecode } from "#bytecode"
|
||||
import { toValue, run } from "#reef"
|
||||
import { VM } from "#vm"
|
||||
|
||||
test("MAKE_FUNCTION - creates function with captured scope", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
MAKE_FUNCTION () #999
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result.type).toBe('function')
|
||||
if (result.type === 'function') {
|
||||
expect(result.body).toBe(999)
|
||||
|
|
@ -26,7 +26,7 @@ test("CALL and RETURN - basic function call", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ test("CALL and RETURN - function with one parameter", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result).toEqual({ type: 'number', value: 100 })
|
||||
})
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ test("CALL and RETURN - function with two parameters", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result).toEqual({ type: 'number', value: 30 })
|
||||
})
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ test("CALL - variadic function with no fixed params", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result).toEqual({
|
||||
type: 'array',
|
||||
value: [
|
||||
|
|
@ -104,7 +104,7 @@ test("CALL - variadic function with one fixed param", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
// x should be 10, rest should be [20, 30]
|
||||
expect(result).toEqual({
|
||||
type: 'array',
|
||||
|
|
@ -130,7 +130,7 @@ test("CALL - variadic function with two fixed params", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
// a=1, b=2, rest=[3, 4]
|
||||
expect(result).toEqual({
|
||||
type: 'array',
|
||||
|
|
@ -153,7 +153,7 @@ test("CALL - variadic function with no extra args", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
// rest should be empty array
|
||||
expect(result).toEqual({ type: 'array', value: [] })
|
||||
})
|
||||
|
|
@ -169,7 +169,7 @@ test("CALL - variadic function with defaults on fixed params", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
// x should use default value 5
|
||||
expect(result).toEqual({ type: 'number', value: 5 })
|
||||
})
|
||||
|
|
@ -188,7 +188,7 @@ test("TAIL_CALL - variadic function", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
// Should return the rest array [2, 3]
|
||||
expect(result).toEqual({
|
||||
type: 'array',
|
||||
|
|
@ -214,7 +214,7 @@ test("CALL - named args function with no fixed params", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result.type).toBe('dict')
|
||||
if (result.type === 'dict') {
|
||||
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Bob' })
|
||||
|
|
@ -236,7 +236,7 @@ test("CALL - named args function with one fixed param", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result.type).toBe('dict')
|
||||
if (result.type === 'dict') {
|
||||
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Alice' })
|
||||
|
|
@ -258,7 +258,7 @@ test("CALL - named args with matching param name should bind to param not named"
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
// name should be bound as regular param, not collected in named
|
||||
expect(result).toEqual({ type: 'string', value: 'Bob' })
|
||||
})
|
||||
|
|
@ -278,7 +278,7 @@ test("CALL - named args that match param names should not be in named", async ()
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result.type).toBe('dict')
|
||||
if (result.type === 'dict') {
|
||||
// Only city should be in named, name should be bound to param
|
||||
|
|
@ -304,7 +304,7 @@ test("CALL - mixed variadic and named args", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
// rest should have [2, 3]
|
||||
expect(result).toEqual({
|
||||
type: 'array',
|
||||
|
|
@ -331,7 +331,7 @@ test("CALL - mixed variadic and named args, check named", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result.type).toBe('dict')
|
||||
if (result.type === 'dict') {
|
||||
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Bob' })
|
||||
|
|
@ -350,7 +350,7 @@ test("CALL - named args with no extra named args", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
// named should be empty dict
|
||||
expect(result.type).toBe('dict')
|
||||
if (result.type === 'dict') {
|
||||
|
|
@ -371,34 +371,11 @@ test("CALL - named args with defaults on fixed params", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
// x should use default value 5
|
||||
expect(result).toEqual({ type: 'number', value: 5 })
|
||||
})
|
||||
|
||||
test("CALL - fixed params can be named", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
MAKE_FUNCTION (a b) .func_0
|
||||
STORE minus
|
||||
TRY_LOAD minus
|
||||
PUSH 200
|
||||
PUSH 'a'
|
||||
PUSH 900
|
||||
PUSH 1
|
||||
PUSH 1
|
||||
CALL
|
||||
HALT
|
||||
.func_0:
|
||||
TRY_LOAD a
|
||||
TRY_LOAD b
|
||||
SUB
|
||||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual(toValue(700))
|
||||
})
|
||||
|
||||
test("TRY_CALL - calls function if found", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["MAKE_FUNCTION", [], ".body"],
|
||||
|
|
@ -410,7 +387,7 @@ test("TRY_CALL - calls function if found", async () => {
|
|||
["RETURN"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
|
|
@ -422,7 +399,7 @@ test("TRY_CALL - pushes value if variable exists but is not a function", async (
|
|||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result).toEqual({ type: 'number', value: 99 })
|
||||
})
|
||||
|
||||
|
|
@ -432,7 +409,7 @@ test("TRY_CALL - pushes string if variable not found", async () => {
|
|||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result).toEqual({ type: 'string', value: 'unknownVar' })
|
||||
})
|
||||
|
||||
|
|
@ -446,7 +423,7 @@ test("TRY_CALL - handles arrays", async () => {
|
|||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result.type).toBe('array')
|
||||
if (result.type === 'array') {
|
||||
expect(result.value).toEqual([
|
||||
|
|
@ -466,7 +443,7 @@ test("TRY_CALL - handles dicts", async () => {
|
|||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result.type).toBe('dict')
|
||||
if (result.type === 'dict') {
|
||||
expect(result.value.get('key')).toEqual({ type: 'string', value: 'value' })
|
||||
|
|
@ -481,13 +458,13 @@ test("TRY_CALL - handles null values", async () => {
|
|||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
expect(result).toEqual({ type: 'null', value: null })
|
||||
})
|
||||
|
||||
test("TRY_CALL - function can access its parameters", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["MAKE_FUNCTION", ["x=0"], ".body"],
|
||||
["MAKE_FUNCTION", ["x"], ".body"],
|
||||
["STORE", "addFive"],
|
||||
["PUSH", 10],
|
||||
["STORE", "x"],
|
||||
|
|
@ -500,9 +477,9 @@ test("TRY_CALL - function can access its parameters", async () => {
|
|||
["RETURN"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
// Function is called with 0 args, so x defaults to 0
|
||||
// Then we add 5 to 0
|
||||
const result = await new VM(bytecode).run()
|
||||
// Function is called with 0 args, so x inside function should be null
|
||||
// Then we add 5 to null (which coerces to 0)
|
||||
expect(result).toEqual({ type: 'number', value: 5 })
|
||||
})
|
||||
|
||||
|
|
@ -516,209 +493,6 @@ test("TRY_CALL - with string format", async () => {
|
|||
RETURN
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
const result = await new VM(bytecode).run()
|
||||
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 })
|
||||
})
|
||||
|
|
|
|||
2276
tests/native.test.ts
2276
tests/native.test.ts
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,459 +0,0 @@
|
|||
import { test, expect, describe } from "bun:test"
|
||||
import { run } from "#index"
|
||||
import { toBytecode } from "#bytecode"
|
||||
|
||||
describe("RegExp", () => {
|
||||
test("basic pattern parsing", async () => {
|
||||
const str = `
|
||||
PUSH /hello/
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('regex')
|
||||
if (result.type === 'regex') {
|
||||
expect(result.value.source).toBe('hello')
|
||||
expect(result.value.flags).toBe('')
|
||||
}
|
||||
})
|
||||
|
||||
test("pattern with flags", async () => {
|
||||
const str = `
|
||||
PUSH /test/gi
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('regex')
|
||||
if (result.type === 'regex') {
|
||||
expect(result.value.source).toBe('test')
|
||||
expect(result.value.global).toBe(true)
|
||||
expect(result.value.ignoreCase).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test("multiple flag combinations", async () => {
|
||||
// Test i flag
|
||||
const str1 = `
|
||||
PUSH /pattern/i
|
||||
`
|
||||
const result1 = await run(toBytecode(str1))
|
||||
expect(result1.type).toBe('regex')
|
||||
if (result1.type === 'regex') {
|
||||
expect(result1.value.ignoreCase).toBe(true)
|
||||
}
|
||||
|
||||
// Test g flag
|
||||
const str2 = `
|
||||
PUSH /pattern/g
|
||||
`
|
||||
const result2 = await run(toBytecode(str2))
|
||||
expect(result2.type).toBe('regex')
|
||||
if (result2.type === 'regex') {
|
||||
expect(result2.value.global).toBe(true)
|
||||
}
|
||||
|
||||
// Test m flag
|
||||
const str3 = `
|
||||
PUSH /pattern/m
|
||||
`
|
||||
const result3 = await run(toBytecode(str3))
|
||||
expect(result3.type).toBe('regex')
|
||||
if (result3.type === 'regex') {
|
||||
expect(result3.value.multiline).toBe(true)
|
||||
}
|
||||
|
||||
// Test combined flags
|
||||
const str4 = `
|
||||
PUSH /pattern/gim
|
||||
`
|
||||
const result4 = await run(toBytecode(str4))
|
||||
expect(result4.type).toBe('regex')
|
||||
if (result4.type === 'regex') {
|
||||
expect(result4.value.global).toBe(true)
|
||||
expect(result4.value.ignoreCase).toBe(true)
|
||||
expect(result4.value.multiline).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test("complex patterns", async () => {
|
||||
// Character class
|
||||
const str1 = `
|
||||
PUSH /[a-z0-9]+/
|
||||
`
|
||||
const result1 = await run(toBytecode(str1))
|
||||
expect(result1.type).toBe('regex')
|
||||
if (result1.type === 'regex') {
|
||||
expect(result1.value.source).toBe('[a-z0-9]+')
|
||||
}
|
||||
|
||||
// Quantifiers
|
||||
const str2 = `
|
||||
PUSH /a{2,4}/
|
||||
`
|
||||
const result2 = await run(toBytecode(str2))
|
||||
expect(result2.type).toBe('regex')
|
||||
if (result2.type === 'regex') {
|
||||
expect(result2.value.source).toBe('a{2,4}')
|
||||
}
|
||||
|
||||
// Groups and alternation
|
||||
const str3 = `
|
||||
PUSH /(foo|bar)/
|
||||
`
|
||||
const result3 = await run(toBytecode(str3))
|
||||
expect(result3.type).toBe('regex')
|
||||
if (result3.type === 'regex') {
|
||||
expect(result3.value.source).toBe('(foo|bar)')
|
||||
}
|
||||
|
||||
// Anchors and special chars
|
||||
const str4 = `
|
||||
PUSH /^[a-z]+$/
|
||||
`
|
||||
const result4 = await run(toBytecode(str4))
|
||||
expect(result4.type).toBe('regex')
|
||||
if (result4.type === 'regex') {
|
||||
expect(result4.value.source).toBe('^[a-z]+$')
|
||||
}
|
||||
})
|
||||
|
||||
test("escaping special characters", async () => {
|
||||
const str = `
|
||||
PUSH /\\d+\\.\\d+/
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('regex')
|
||||
if (result.type === 'regex') {
|
||||
expect(result.value.source).toBe('\\d+\\.\\d+')
|
||||
}
|
||||
})
|
||||
|
||||
test("store and load", async () => {
|
||||
const str = `
|
||||
PUSH /test/i
|
||||
STORE pattern
|
||||
LOAD pattern
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('regex')
|
||||
if (result.type === 'regex') {
|
||||
expect(result.value.source).toBe('test')
|
||||
expect(result.value.ignoreCase).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test("TRY_LOAD with regex", async () => {
|
||||
const str = `
|
||||
PUSH /hello/g
|
||||
STORE regex
|
||||
TRY_LOAD regex
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('regex')
|
||||
if (result.type === 'regex') {
|
||||
expect(result.value.source).toBe('hello')
|
||||
expect(result.value.global).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test("NEQ comparison", async () => {
|
||||
const str = `
|
||||
PUSH /foo/
|
||||
PUSH /bar/
|
||||
NEQ
|
||||
`
|
||||
await expect(str).toBeBoolean(true)
|
||||
|
||||
const str2 = `
|
||||
PUSH /test/i
|
||||
PUSH /test/i
|
||||
NEQ
|
||||
`
|
||||
await expect(str2).toBeBoolean(false)
|
||||
})
|
||||
|
||||
test("is truthy", async () => {
|
||||
// Regex values should be truthy (not null or false)
|
||||
const str = `
|
||||
PUSH /test/
|
||||
JUMP_IF_FALSE .end
|
||||
PUSH 42
|
||||
.end:
|
||||
`
|
||||
await expect(str).toBeNumber(42)
|
||||
})
|
||||
|
||||
test("NOT returns false (regex is truthy)", async () => {
|
||||
const str = `
|
||||
PUSH /pattern/
|
||||
NOT
|
||||
`
|
||||
await expect(str).toBeBoolean(false)
|
||||
})
|
||||
|
||||
test("in arrays", async () => {
|
||||
const str = `
|
||||
PUSH /first/
|
||||
PUSH /second/i
|
||||
PUSH /third/g
|
||||
MAKE_ARRAY #3
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('array')
|
||||
if (result.type === 'array') {
|
||||
expect(result.value).toHaveLength(3)
|
||||
|
||||
expect(result.value[0]!.type).toBe('regex')
|
||||
if (result.value[0]!.type === 'regex') {
|
||||
expect(result.value[0]!.value.source).toBe('first')
|
||||
}
|
||||
|
||||
expect(result.value[1]!.type).toBe('regex')
|
||||
if (result.value[1]!.type === 'regex') {
|
||||
expect(result.value[1]!.value.source).toBe('second')
|
||||
expect(result.value[1]!.value.ignoreCase).toBe(true)
|
||||
}
|
||||
|
||||
expect(result.value[2]!.type).toBe('regex')
|
||||
if (result.value[2]!.type === 'regex') {
|
||||
expect(result.value[2]!.value.source).toBe('third')
|
||||
expect(result.value[2]!.value.global).toBe(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test("retrieve from array", async () => {
|
||||
const str = `
|
||||
PUSH /pattern/i
|
||||
PUSH /test/g
|
||||
MAKE_ARRAY #2
|
||||
PUSH 1
|
||||
ARRAY_GET
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('regex')
|
||||
if (result.type === 'regex') {
|
||||
expect(result.value.source).toBe('test')
|
||||
expect(result.value.global).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test("in dicts", async () => {
|
||||
const str = `
|
||||
PUSH 'email'
|
||||
PUSH /^[a-z@.]+$/i
|
||||
PUSH 'phone'
|
||||
PUSH /\\d{3}-\\d{4}/
|
||||
MAKE_DICT #2
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('dict')
|
||||
if (result.type === 'dict') {
|
||||
expect(result.value.size).toBe(2)
|
||||
|
||||
const email = result.value.get('email')
|
||||
expect(email?.type).toBe('regex')
|
||||
if (email?.type === 'regex') {
|
||||
expect(email.value.source).toBe('^[a-z@.]+$')
|
||||
expect(email.value.ignoreCase).toBe(true)
|
||||
}
|
||||
|
||||
const phone = result.value.get('phone')
|
||||
expect(phone?.type).toBe('regex')
|
||||
if (phone?.type === 'regex') {
|
||||
expect(phone.value.source).toBe('\\d{3}-\\d{4}')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test("retrieve from dict", async () => {
|
||||
const str = `
|
||||
PUSH 'pattern'
|
||||
PUSH /test/gim
|
||||
MAKE_DICT #1
|
||||
PUSH 'pattern'
|
||||
DICT_GET
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('regex')
|
||||
if (result.type === 'regex') {
|
||||
expect(result.value.source).toBe('test')
|
||||
expect(result.value.global).toBe(true)
|
||||
expect(result.value.ignoreCase).toBe(true)
|
||||
expect(result.value.multiline).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
test("with STR_CONCAT converts to string", async () => {
|
||||
const str = `
|
||||
PUSH "Pattern: "
|
||||
PUSH /test/gi
|
||||
STR_CONCAT #2
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('string')
|
||||
if (result.type === 'string') {
|
||||
expect(result.value).toBe('Pattern: /test/gi')
|
||||
}
|
||||
})
|
||||
|
||||
test("multiple regex in STR_CONCAT", async () => {
|
||||
const str = `
|
||||
PUSH /foo/
|
||||
PUSH " and "
|
||||
PUSH /bar/i
|
||||
STR_CONCAT #3
|
||||
`
|
||||
await expect(str).toBeString('/foo/ and /bar/i')
|
||||
})
|
||||
|
||||
test("DUP with regex", async () => {
|
||||
const str = `
|
||||
PUSH /pattern/i
|
||||
DUP
|
||||
EQ
|
||||
`
|
||||
// Same regex duplicated should be equal
|
||||
await expect(str).toBeBoolean(true)
|
||||
})
|
||||
|
||||
test("empty pattern", async () => {
|
||||
const str = `
|
||||
PUSH //
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('regex')
|
||||
if (result.type === 'regex') {
|
||||
expect(result.value.source).toBe('(?:)')
|
||||
}
|
||||
})
|
||||
|
||||
test("pattern with forward slashes escaped", async () => {
|
||||
const str = `
|
||||
PUSH /https:\\/\\//
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('regex')
|
||||
if (result.type === 'regex') {
|
||||
expect(result.value.source).toBe('https:\\/\\/')
|
||||
}
|
||||
})
|
||||
|
||||
test("unicode patterns", async () => {
|
||||
const str = `
|
||||
PUSH /こんにちは/
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('regex')
|
||||
if (result.type === 'regex') {
|
||||
expect(result.value.source).toBe('こんにちは')
|
||||
}
|
||||
})
|
||||
|
||||
test("emoji in pattern", async () => {
|
||||
const str = `
|
||||
PUSH /🎉+/
|
||||
`
|
||||
const result = await run(toBytecode(str))
|
||||
expect(result.type).toBe('regex')
|
||||
if (result.type === 'regex') {
|
||||
expect(result.value.source).toBe('🎉+')
|
||||
}
|
||||
})
|
||||
|
||||
test("comparing different regex types", async () => {
|
||||
// Different patterns
|
||||
const str1 = `
|
||||
PUSH /abc/
|
||||
PUSH /xyz/
|
||||
EQ
|
||||
`
|
||||
await expect(str1).toBeBoolean(false)
|
||||
|
||||
// Same pattern, different flags
|
||||
const str2 = `
|
||||
PUSH /test/
|
||||
PUSH /test/i
|
||||
EQ
|
||||
`
|
||||
await expect(str2).toBeBoolean(false)
|
||||
|
||||
// Different order of flags (should be equal)
|
||||
const str3 = `
|
||||
PUSH /test/ig
|
||||
PUSH /test/gi
|
||||
EQ
|
||||
`
|
||||
await expect(str3).toBeBoolean(true)
|
||||
})
|
||||
|
||||
test("with native functions", async () => {
|
||||
const { VM } = await import("#vm")
|
||||
const bytecode = toBytecode(`
|
||||
LOAD match
|
||||
PUSH "hello world"
|
||||
PUSH /world/
|
||||
PUSH 2
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
`)
|
||||
|
||||
const vm = new VM(bytecode)
|
||||
|
||||
// Register a native function that takes a string and regex
|
||||
vm.set('match', (str: string, pattern: RegExp) => {
|
||||
return pattern.test(str)
|
||||
})
|
||||
|
||||
const result = await vm.run()
|
||||
expect(result).toEqual({ type: 'boolean', value: true })
|
||||
})
|
||||
|
||||
test("native function with regex replacement", async () => {
|
||||
const { VM } = await import("#vm")
|
||||
const bytecode = toBytecode(`
|
||||
LOAD replace
|
||||
PUSH "hello world"
|
||||
PUSH /o/g
|
||||
PUSH "0"
|
||||
PUSH 3
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
`)
|
||||
|
||||
const vm = new VM(bytecode)
|
||||
|
||||
vm.set('replace', (str: string, pattern: RegExp, replacement: string) => {
|
||||
return str.replace(pattern, replacement)
|
||||
})
|
||||
|
||||
const result = await vm.run()
|
||||
expect(result).toEqual({ type: 'string', value: 'hell0 w0rld' })
|
||||
})
|
||||
|
||||
test("native function extracting matches", async () => {
|
||||
const { VM } = await import("#vm")
|
||||
const bytecode = toBytecode(`
|
||||
LOAD extractNumbers
|
||||
PUSH "test123abc456"
|
||||
PUSH /\\d+/g
|
||||
PUSH 2
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
`)
|
||||
|
||||
const vm = new VM(bytecode)
|
||||
|
||||
vm.set('extractNumbers', (str: string, pattern: RegExp) => {
|
||||
return str.match(pattern) || []
|
||||
})
|
||||
|
||||
const result = await vm.run()
|
||||
expect(result.type).toBe('array')
|
||||
if (result.type === 'array') {
|
||||
expect(result.value).toHaveLength(2)
|
||||
expect(result.value[0]).toEqual({ type: 'string', value: '123' })
|
||||
expect(result.value[1]).toEqual({ type: 'string', value: '456' })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
import { test, expect, describe } from "bun:test"
|
||||
import { run } from "#index"
|
||||
import { toBytecode } from "#bytecode"
|
||||
|
||||
describe("Unicode and Emoji", () => {
|
||||
test("emoji variable names - string format", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 5
|
||||
STORE 💎
|
||||
LOAD 💎
|
||||
HALT
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 5 })
|
||||
})
|
||||
|
||||
test("emoji variable names - array format", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["PUSH", 100],
|
||||
["STORE", "💰"],
|
||||
["LOAD", "💰"],
|
||||
["PUSH", 50],
|
||||
["ADD"],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 150 })
|
||||
})
|
||||
|
||||
test("unicode variable names - Japanese", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
PUSH 42
|
||||
STORE 変数
|
||||
LOAD 変数
|
||||
HALT
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("unicode variable names - Chinese", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["PUSH", 888],
|
||||
["STORE", "数字"],
|
||||
["LOAD", "数字"],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 888 })
|
||||
})
|
||||
|
||||
test("emoji in function parameters", async () => {
|
||||
const bytecode = toBytecode(`
|
||||
MAKE_FUNCTION (💎 🌟) .add
|
||||
STORE add
|
||||
JUMP .after
|
||||
.add:
|
||||
LOAD 💎
|
||||
LOAD 🌟
|
||||
ADD
|
||||
RETURN
|
||||
.after:
|
||||
LOAD add
|
||||
PUSH 10
|
||||
PUSH 20
|
||||
PUSH 2
|
||||
PUSH 0
|
||||
CALL
|
||||
HALT
|
||||
`)
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 30 })
|
||||
})
|
||||
|
||||
test("emoji with defaults and variadic", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["MAKE_FUNCTION", ["🎯=100", "...🎨"], ".fn"],
|
||||
["STORE", "fn"],
|
||||
["JUMP", ".after"],
|
||||
[".fn:"],
|
||||
["LOAD", "🎯"],
|
||||
["RETURN"],
|
||||
[".after:"],
|
||||
["LOAD", "fn"],
|
||||
["PUSH", 0],
|
||||
["PUSH", 0],
|
||||
["CALL"],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 100 })
|
||||
})
|
||||
|
||||
test("mixed emoji and regular names", async () => {
|
||||
const bytecode = toBytecode([
|
||||
["PUSH", 10],
|
||||
["STORE", "💎"],
|
||||
["PUSH", 20],
|
||||
["STORE", "value"],
|
||||
["PUSH", 30],
|
||||
["STORE", "🌟"],
|
||||
["LOAD", "💎"],
|
||||
["LOAD", "value"],
|
||||
["ADD"],
|
||||
["LOAD", "🌟"],
|
||||
["ADD"],
|
||||
["HALT"]
|
||||
])
|
||||
|
||||
const result = await run(bytecode)
|
||||
expect(result).toEqual({ type: 'number', value: 60 })
|
||||
})
|
||||
})
|
||||
|
|
@ -201,17 +201,17 @@ test("formatValidationErrors produces readable output", () => {
|
|||
expect(formatted).toContain("UNKNOWN")
|
||||
})
|
||||
|
||||
test("detects JUMP without .label", () => {
|
||||
test("detects JUMP without # or .label", () => {
|
||||
const source = `
|
||||
JUMP 5
|
||||
HALT
|
||||
`
|
||||
const result = validateBytecode(source)
|
||||
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 = `
|
||||
PUSH true
|
||||
JUMP_IF_TRUE 2
|
||||
|
|
@ -219,10 +219,10 @@ test("detects JUMP_IF_TRUE without .label", () => {
|
|||
`
|
||||
const result = validateBytecode(source)
|
||||
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 = `
|
||||
PUSH false
|
||||
JUMP_IF_FALSE 2
|
||||
|
|
@ -230,18 +230,17 @@ test("detects JUMP_IF_FALSE without .label", () => {
|
|||
`
|
||||
const result = validateBytecode(source)
|
||||
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 = `
|
||||
JUMP #2
|
||||
PUSH 999
|
||||
HALT
|
||||
`
|
||||
const result = validateBytecode(source)
|
||||
expect(result.valid).toBe(false)
|
||||
expect(result.errors[0]!.message).toContain("JUMP requires label (.label)")
|
||||
expect(result.valid).toBe(true)
|
||||
})
|
||||
|
||||
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