forked from defunkt/ReefVM
Compare commits
85 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2a6021fb8 | |||
| 5350bb8c2b | |||
| 47c6d3b32f | |||
| 5ade608278 | |||
| 614f5c4f91 | |||
| 3e2e68b31f | |||
| d7a971db24 | |||
| f439c25742 | |||
| 47e227f50c | |||
| 15884ac239 | |||
| bffb83a528 | |||
| bd1736b474 | |||
| f4e24f427f | |||
| e7201e691c | |||
| 11b119a322 | |||
| 33ea94a247 | |||
| f1cc717711 | |||
| 54cd9ce8e8 | |||
| 0f39e9401e | |||
| 676f53c66b | |||
| fa021e3f18 | |||
| 4b2fd61554 | |||
| c69b172c78 | |||
| 0b5d3e634c | |||
| ba8376e2c3 | |||
| 956fd576f8 | |||
| 9618dd6414 | |||
| b58f848a65 | |||
| 3647159286 | |||
| 030eb74871 | |||
| 052f989e82 | |||
|
|
e542070677 | ||
| 97b6722a11 | |||
| d50b143c9d | |||
| e300946c48 | |||
| bf6607d368 | |||
| da61c1de50 | |||
| d359d6e27d | |||
| eb128ec831 | |||
| 286d5ff943 | |||
| aa8ecb7cf6 | |||
| bbdfcdb54a | |||
| 8d9510e9ae | |||
| 17d846b999 | |||
| 1fb5effb0a | |||
| 937861e27b | |||
| 42c0e62597 | |||
| 46829df28b | |||
| e1e7cdf1ef | |||
| f79fea33c5 | |||
| 797eb281cb | |||
| 91d3eb43e4 | |||
| eb4f103ba3 | |||
| 995487f2d5 | |||
| a885a59140 | |||
| 47f829fcad | |||
| 7f4f73dd41 | |||
| fa55eb7170 | |||
| 1cf14636ff | |||
| fe7586a5fa | |||
| 93eff53a76 | |||
| 4d2ae1c9fe | |||
| 62f890e59d | |||
| 1a18a713d7 | |||
| b16351ac95 | |||
| 23fcf05439 | |||
| e5be82e75a | |||
| dd9ec2a7d1 | |||
| fa7589293a | |||
| 1bc46045ba | |||
| 2be87c381d | |||
| f630d7934a | |||
| cabdc15253 | |||
| b7cc0fc064 | |||
| 82d727cc74 | |||
| 77f86ce829 | |||
| 79f449bc6c | |||
| 4a5618e900 | |||
| f3759c2259 | |||
| 0844e99d2d | |||
|
|
4898a6bb5a | ||
|
|
173fd28d6f | ||
|
|
df9af925d3 | ||
| 82e7b181ec | |||
| 81973541af |
178
CLAUDE.md
178
CLAUDE.md
|
|
@ -42,8 +42,7 @@ No build step required - Bun runs TypeScript directly.
|
||||||
- Stack-based execution with program counter (PC)
|
- Stack-based execution with program counter (PC)
|
||||||
- Call stack for function frames
|
- Call stack for function frames
|
||||||
- Exception handler stack for try/catch/finally
|
- Exception handler stack for try/catch/finally
|
||||||
- Lexical scope chain with parent references
|
- Lexical scope chain with parent references (includes native functions)
|
||||||
- Native function registry for TypeScript interop
|
|
||||||
|
|
||||||
**Key subsystems**:
|
**Key subsystems**:
|
||||||
- **bytecode.ts**: Compiler that converts both string and array formats to executable bytecode. Handles label resolution, constant pool management, and function definition parsing. The `toBytecode()` function accepts either a string (human-readable) or typed array format (programmatic).
|
- **bytecode.ts**: Compiler that converts both string and array formats to executable bytecode. Handles label resolution, constant pool management, and function definition parsing. The `toBytecode()` function accepts either a string (human-readable) or typed array format (programmatic).
|
||||||
|
|
@ -56,7 +55,7 @@ No build step required - Bun runs TypeScript directly.
|
||||||
|
|
||||||
### Critical Design Decisions
|
### Critical Design Decisions
|
||||||
|
|
||||||
**Relative jumps**: All JUMP instructions use PC-relative offsets (not absolute addresses), making bytecode position-independent. PUSH_TRY/PUSH_FINALLY use absolute addresses.
|
**Label-based jumps**: All JUMP instructions (`JUMP`, `JUMP_IF_FALSE`, `JUMP_IF_TRUE`) require label operands (`.label`), not numeric offsets. Labels are resolved to PC-relative offsets during compilation, making bytecode position-independent. PUSH_TRY/PUSH_FINALLY use absolute addresses and can accept either labels or numeric offsets.
|
||||||
|
|
||||||
**Truthiness semantics**: Only `null` and `false` are falsy. Unlike JavaScript, `0`, `""`, empty arrays, and empty dicts are truthy.
|
**Truthiness semantics**: Only `null` and `false` are falsy. Unlike JavaScript, `0`, `""`, empty arrays, and empty dicts are truthy.
|
||||||
|
|
||||||
|
|
@ -70,19 +69,22 @@ No build step required - Bun runs TypeScript directly.
|
||||||
|
|
||||||
**Parameter binding priority**: Named args bind to fixed params first. Unmatched named args go to `@named` dict parameter. Fixed params bind in order: named arg > positional arg > default > null.
|
**Parameter binding priority**: Named args bind to fixed params first. Unmatched named args go to `@named` dict parameter. Fixed params bind in order: named arg > positional arg > default > null.
|
||||||
|
|
||||||
**Native function calling**: CALL_NATIVE consumes the entire stack as arguments (different from CALL which pops specific argument counts).
|
**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.
|
||||||
|
|
||||||
## Testing Strategy
|
## Testing Strategy
|
||||||
|
|
||||||
Tests are organized by feature area:
|
Tests are organized by feature area:
|
||||||
- **basic.test.ts**: Stack ops, arithmetic, comparisons, variables, control flow
|
- **opcodes.test.ts**: Stack ops, arithmetic, comparisons, variables, control flow
|
||||||
- **functions.test.ts**: Function creation, calls, closures, defaults, variadic, named args
|
- **functions.test.ts**: Function creation, calls, closures, defaults, variadic, named args
|
||||||
- **tail-call.test.ts**: Tail call optimization and unbounded recursion
|
- **tail-call.test.ts**: Tail call optimization and unbounded recursion
|
||||||
- **exceptions.test.ts**: Try/catch/finally, exception unwinding, nested handlers
|
- **exceptions.test.ts**: Try/catch/finally, exception unwinding, nested handlers
|
||||||
- **native.test.ts**: Native function interop (sync and async)
|
- **native.test.ts**: Native function interop (sync and async)
|
||||||
|
- **functions-parameter.test.ts**: Convenience parameter for passing functions to run() and VM
|
||||||
- **bytecode.test.ts**: Bytecode string parser, label resolution, constants
|
- **bytecode.test.ts**: Bytecode string parser, label resolution, constants
|
||||||
- **programmatic.test.ts**: Array format API, typed tuples, labels, functions
|
- **programmatic.test.ts**: Array format API, typed tuples, labels, functions
|
||||||
- **validator.test.ts**: Bytecode validation rules
|
- **validator.test.ts**: Bytecode validation rules
|
||||||
|
- **unicode.test.ts**: Unicode and emoji identifiers
|
||||||
|
- **regex.test.ts**: RegExp support
|
||||||
- **examples.test.ts**: Integration tests for example programs
|
- **examples.test.ts**: Integration tests for example programs
|
||||||
|
|
||||||
When adding features:
|
When adding features:
|
||||||
|
|
@ -135,53 +137,100 @@ Array format features:
|
||||||
- Function params as string arrays: `["MAKE_FUNCTION", ["x", "y=10"], ".body"]`
|
- Function params as string arrays: `["MAKE_FUNCTION", ["x", "y=10"], ".body"]`
|
||||||
- See `tests/programmatic.test.ts` and `examples/programmatic.ts` for examples
|
- See `tests/programmatic.test.ts` and `examples/programmatic.ts` for examples
|
||||||
|
|
||||||
### Native Function Registration
|
### Native Function Registration and Global Values
|
||||||
|
|
||||||
ReefVM supports two ways to register native functions:
|
**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 }
|
||||||
|
})
|
||||||
|
|
||||||
**1. Native TypeScript functions (recommended)** - Auto-converts between native TS and ReefVM types:
|
// Or with VM constructor
|
||||||
|
const vm = new VM(bytecode, { add, greet, pi, config })
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2**: Set values with `vm.set()` (manual)
|
||||||
```typescript
|
```typescript
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
// Works with native TypeScript types!
|
// Set functions (auto-wrapped to native functions)
|
||||||
vm.registerFunction('add', (a: number, b: number) => {
|
vm.set('add', (a: number, b: number) => a + b)
|
||||||
return a + b
|
|
||||||
})
|
|
||||||
|
|
||||||
// Supports defaults (like NOSE commands)
|
// Set any other values (auto-converted to ReefVM Values)
|
||||||
vm.registerFunction('ls', (path: string, link = false) => {
|
vm.set('pi', 3.14159)
|
||||||
return link ? `listing ${path} with links` : `listing ${path}`
|
vm.set('config', { debug: true, port: 8080 })
|
||||||
})
|
|
||||||
|
|
||||||
// Async functions work too
|
|
||||||
vm.registerFunction('fetch', async (url: string) => {
|
|
||||||
const response = await fetch(url)
|
|
||||||
return await response.text()
|
|
||||||
})
|
|
||||||
|
|
||||||
await vm.run()
|
await vm.run()
|
||||||
```
|
```
|
||||||
|
|
||||||
**2. Value-based functions (manual)** - For functions that need direct Value access:
|
**Option 3**: Set Value-based functions with `vm.setValueFunction()` (advanced)
|
||||||
|
|
||||||
|
For functions that work directly with ReefVM Value types:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
// Set Value-based function (no wrapping, works directly with Values)
|
||||||
// Direct access to Value types
|
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
return toValue(toNumber(a) + toNumber(b))
|
return toValue(toNumber(a) + toNumber(b))
|
||||||
})
|
})
|
||||||
|
|
||||||
await vm.run()
|
await vm.run()
|
||||||
```
|
```
|
||||||
|
|
||||||
The auto-wrapping handles:
|
Auto-wrapping handles:
|
||||||
- Converting Value → native types on input (using `fromValue`)
|
- Functions: wrapped as native functions with Value ↔ native type conversion
|
||||||
- Converting native types → Value on output (using `toValue`)
|
- Sync and async functions
|
||||||
- Both sync and async functions
|
- Arrays, objects, primitives, null, RegExp
|
||||||
- Arrays, objects, primitives, and null
|
- All values converted via `toValue()`
|
||||||
|
|
||||||
### Label Usage (Preferred)
|
### Calling Functions from TypeScript
|
||||||
Use labels instead of numeric offsets for readability:
|
|
||||||
|
Use `vm.call()` to invoke Reef or native functions from TypeScript:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
MAKE_FUNCTION (x y=10) .add
|
||||||
|
STORE add
|
||||||
|
HALT
|
||||||
|
|
||||||
|
.add:
|
||||||
|
LOAD x
|
||||||
|
LOAD y
|
||||||
|
ADD
|
||||||
|
RETURN
|
||||||
|
`)
|
||||||
|
|
||||||
|
const vm = new VM(bytecode, {
|
||||||
|
log: (msg: string) => console.log(msg) // Native function
|
||||||
|
})
|
||||||
|
await vm.run()
|
||||||
|
|
||||||
|
// Call Reef function with positional arguments
|
||||||
|
const result1 = await vm.call('add', 5, 3) // → 8
|
||||||
|
|
||||||
|
// Call Reef function with named arguments (pass final object)
|
||||||
|
const result2 = await vm.call('add', 5, { y: 20 }) // → 25
|
||||||
|
|
||||||
|
// Call Reef function with all named arguments
|
||||||
|
const result3 = await vm.call('add', { x: 10, y: 15 }) // → 25
|
||||||
|
|
||||||
|
// Call native function
|
||||||
|
await vm.call('log', 'Hello!')
|
||||||
|
```
|
||||||
|
|
||||||
|
**How it works**:
|
||||||
|
- Looks up function (Reef or native) in VM scope
|
||||||
|
- For Reef functions: converts to callable JavaScript function using `fnFromValue`
|
||||||
|
- For native functions: calls directly
|
||||||
|
- Automatically converts arguments to ReefVM Values
|
||||||
|
- Converts result back to JavaScript types
|
||||||
|
|
||||||
|
### Label Usage (Required for JUMP instructions)
|
||||||
|
All JUMP instructions must use labels:
|
||||||
```
|
```
|
||||||
JUMP .skip
|
JUMP .skip
|
||||||
PUSH 42
|
PUSH 42
|
||||||
|
|
@ -191,6 +240,67 @@ HALT
|
||||||
HALT
|
HALT
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Function Definition Patterns
|
||||||
|
|
||||||
|
When defining functions, you MUST prevent the PC from falling through into function bodies. Two patterns:
|
||||||
|
|
||||||
|
**Pattern 1: JUMP over function bodies (Recommended)**
|
||||||
|
```
|
||||||
|
MAKE_FUNCTION (params) .body
|
||||||
|
STORE function_name
|
||||||
|
JUMP .end ; Skip over function body
|
||||||
|
.body:
|
||||||
|
<function code>
|
||||||
|
RETURN
|
||||||
|
.end:
|
||||||
|
<continue with program>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern 2: Function bodies after HALT**
|
||||||
|
```
|
||||||
|
MAKE_FUNCTION (params) .body
|
||||||
|
STORE function_name
|
||||||
|
<use the function>
|
||||||
|
HALT ; Stop before function bodies
|
||||||
|
.body:
|
||||||
|
<function code>
|
||||||
|
RETURN
|
||||||
|
```
|
||||||
|
|
||||||
|
Pattern 1 is required for:
|
||||||
|
- Defining multiple functions before using them
|
||||||
|
- REPL mode
|
||||||
|
- Any case where execution continues after defining a function
|
||||||
|
|
||||||
|
Pattern 2 only works if you HALT before reaching function bodies.
|
||||||
|
|
||||||
|
### REPL Mode (Incremental Execution)
|
||||||
|
|
||||||
|
For building REPLs (like the Shrimp REPL), use `vm.continue()` and `vm.appendBytecode()`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const vm = new VM(toBytecode([]), natives)
|
||||||
|
await vm.run() // Initialize (empty bytecode)
|
||||||
|
|
||||||
|
// User enters: x = 42
|
||||||
|
const line1 = compileLine("x = 42") // No HALT!
|
||||||
|
vm.appendBytecode(line1)
|
||||||
|
await vm.continue() // Execute only line 1
|
||||||
|
|
||||||
|
// User enters: x + 10
|
||||||
|
const line2 = compileLine("x + 10") // No HALT!
|
||||||
|
vm.appendBytecode(line2)
|
||||||
|
await vm.continue() // Execute only line 2, result is 52
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key points**:
|
||||||
|
- `vm.run()` resets PC to 0 (re-executes everything) - use for initial setup only
|
||||||
|
- `vm.continue()` resumes from current PC (executes only new bytecode)
|
||||||
|
- `vm.appendBytecode(bytecode)` properly handles constant index remapping
|
||||||
|
- Don't use HALT in REPL lines - let VM stop naturally
|
||||||
|
- Scope and variables persist across all lines
|
||||||
|
- Side effects only run once
|
||||||
|
|
||||||
## TypeScript Configuration
|
## TypeScript Configuration
|
||||||
|
|
||||||
- Import alias: `#reef` maps to `./src/index.ts`
|
- Import alias: `#reef` maps to `./src/index.ts`
|
||||||
|
|
@ -376,14 +486,12 @@ Run `bun test` to verify all tests pass before committing.
|
||||||
|
|
||||||
## Common Gotchas
|
## Common Gotchas
|
||||||
|
|
||||||
**Jump offsets**: JUMP/JUMP_IF_FALSE/JUMP_IF_TRUE use relative offsets from the next instruction (PC + 1). PUSH_TRY/PUSH_FINALLY use absolute instruction indices.
|
**Label requirements**: JUMP/JUMP_IF_FALSE/JUMP_IF_TRUE require label operands (`.label`), not numeric offsets. The bytecode compiler resolves labels to PC-relative offsets internally. PUSH_TRY/PUSH_FINALLY can use either labels or absolute instruction indices (`#N`).
|
||||||
|
|
||||||
**Stack operations**: Most binary operations pop in reverse order (second operand is popped first, then first operand).
|
**Stack operations**: Most binary operations pop in reverse order (second operand is popped first, then first operand).
|
||||||
|
|
||||||
**MAKE_ARRAY operand**: Specifies count, not a stack index. `MAKE_ARRAY #3` pops 3 items.
|
**MAKE_ARRAY operand**: Specifies count, not a stack index. `MAKE_ARRAY #3` pops 3 items.
|
||||||
|
|
||||||
**CALL_NATIVE stack behavior**: Unlike CALL, it consumes all stack values as arguments and clears the stack.
|
|
||||||
|
|
||||||
**Finally blocks**: The compiler must generate explicit JUMPs to finally blocks for successful try/catch completion. The VM only auto-jumps to finally on THROW.
|
**Finally blocks**: The compiler must generate explicit JUMPs to finally blocks for successful try/catch completion. The VM only auto-jumps to finally on THROW.
|
||||||
|
|
||||||
**Variable scoping**: STORE updates existing variables in parent scopes or creates in current scope. It does NOT shadow by default.
|
**Variable scoping**: STORE updates existing variables in parent scopes or creates in current scope. It does NOT shadow by default.
|
||||||
|
|
|
||||||
530
GUIDE.md
530
GUIDE.md
|
|
@ -42,9 +42,6 @@ OPCODE operand ; comment
|
||||||
- Booleans: `PUSH true`, `PUSH false`
|
- Booleans: `PUSH true`, `PUSH false`
|
||||||
- Null: `PUSH null`
|
- Null: `PUSH null`
|
||||||
|
|
||||||
**Native function names**: Registered TypeScript functions
|
|
||||||
- `CALL_NATIVE print`
|
|
||||||
|
|
||||||
## Array Format
|
## Array Format
|
||||||
|
|
||||||
The programmatic array format uses TypeScript tuples for type safety:
|
The programmatic array format uses TypeScript tuples for type safety:
|
||||||
|
|
@ -99,11 +96,6 @@ const result = await run(bytecode)
|
||||||
["MAKE_DICT", 2] // Pop 2 key-value pairs
|
["MAKE_DICT", 2] // Pop 2 key-value pairs
|
||||||
```
|
```
|
||||||
|
|
||||||
**Native function names**: Strings for registered functions
|
|
||||||
```typescript
|
|
||||||
["CALL_NATIVE", "print"]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Functions in Array Format
|
### Functions in Array Format
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
|
|
@ -187,12 +179,25 @@ PUSH 1 ; Named count
|
||||||
CALL
|
CALL
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Null triggers defaults**: Pass `null` to use default values:
|
||||||
|
```
|
||||||
|
; Function: greet(name='Guest', msg='Hello')
|
||||||
|
LOAD greet
|
||||||
|
PUSH null ; Use default for 'name'
|
||||||
|
PUSH "Hi" ; Provide 'msg'
|
||||||
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL ; → "Hi, Guest"
|
||||||
|
```
|
||||||
|
|
||||||
## Opcodes
|
## Opcodes
|
||||||
|
|
||||||
### Stack
|
### Stack
|
||||||
- `PUSH <const>` - Push constant
|
- `PUSH <const>` - Push constant
|
||||||
- `POP` - Remove top
|
- `POP` - Remove top
|
||||||
- `DUP` - Duplicate top
|
- `DUP` - Duplicate top
|
||||||
|
- `SWAP` - Swap top two values
|
||||||
|
- `TYPE` - Pop value, push its type as string
|
||||||
|
|
||||||
### Variables
|
### Variables
|
||||||
- `LOAD <name>` - Push variable value (throws if not found)
|
- `LOAD <name>` - Push variable value (throws if not found)
|
||||||
|
|
@ -202,6 +207,10 @@ CALL
|
||||||
### Arithmetic
|
### Arithmetic
|
||||||
- `ADD`, `SUB`, `MUL`, `DIV`, `MOD` - Binary ops (pop 2, push result)
|
- `ADD`, `SUB`, `MUL`, `DIV`, `MOD` - Binary ops (pop 2, push result)
|
||||||
|
|
||||||
|
### Bitwise
|
||||||
|
- `BIT_AND`, `BIT_OR`, `BIT_XOR` - Bitwise logical ops (pop 2, push result)
|
||||||
|
- `BIT_SHL`, `BIT_SHR`, `BIT_USHR` - Bitwise shift ops (pop 2, push result)
|
||||||
|
|
||||||
### Comparison
|
### Comparison
|
||||||
- `EQ`, `NEQ`, `LT`, `GT`, `LTE`, `GTE` - Pop 2, push boolean
|
- `EQ`, `NEQ`, `LT`, `GT`, `LTE`, `GTE` - Pop 2, push boolean
|
||||||
|
|
||||||
|
|
@ -235,17 +244,54 @@ CALL
|
||||||
- `DICT_SET` - Pop value, key, dict; mutate dict
|
- `DICT_SET` - Pop value, key, dict; mutate dict
|
||||||
- `DICT_HAS` - Pop key and dict, push boolean
|
- `DICT_HAS` - Pop key and dict, push boolean
|
||||||
|
|
||||||
|
### Unified Access
|
||||||
|
- `DOT_GET` - Pop index/key and array/dict, push value (null if missing)
|
||||||
|
|
||||||
|
### Strings
|
||||||
|
- `STR_CONCAT #N` - Pop N values, convert to strings, concatenate, push result
|
||||||
|
|
||||||
### Exceptions
|
### Exceptions
|
||||||
- `PUSH_TRY .catch` - Register exception handler
|
- `PUSH_TRY .catch` - Register exception handler
|
||||||
- `PUSH_FINALLY .finally` - Add finally to current handler
|
- `PUSH_FINALLY .finally` - Add finally to current handler
|
||||||
- `POP_TRY` - Remove handler (try succeeded)
|
- `POP_TRY` - Remove handler (try succeeded)
|
||||||
- `THROW` - Throw exception (pops error value)
|
- `THROW` - Throw exception (pops error value)
|
||||||
|
|
||||||
### Native
|
|
||||||
- `CALL_NATIVE <name>` - Call registered TypeScript function (consumes entire stack as args)
|
|
||||||
|
|
||||||
## Compiler Patterns
|
## Compiler Patterns
|
||||||
|
|
||||||
|
### Function Definitions
|
||||||
|
|
||||||
|
When defining functions, you must prevent the PC from "falling through" into the function body during sequential execution. There are two standard patterns:
|
||||||
|
|
||||||
|
**Pattern 1: JUMP over function bodies (Recommended)**
|
||||||
|
```
|
||||||
|
MAKE_FUNCTION (params) .body
|
||||||
|
STORE function_name
|
||||||
|
JUMP .end ; Skip over function body
|
||||||
|
.body:
|
||||||
|
<function code>
|
||||||
|
RETURN
|
||||||
|
.end:
|
||||||
|
<continue with program>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern 2: Function bodies after HALT**
|
||||||
|
```
|
||||||
|
MAKE_FUNCTION (params) .body
|
||||||
|
STORE function_name
|
||||||
|
<use the function>
|
||||||
|
HALT ; Stop execution before function bodies
|
||||||
|
.body:
|
||||||
|
<function code>
|
||||||
|
RETURN
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**: Pattern 2 only works if you HALT before reaching function bodies. Pattern 1 is more flexible and required for:
|
||||||
|
- Defining multiple functions before using them
|
||||||
|
- REPL mode (incremental execution)
|
||||||
|
- Any case where execution continues after defining a function
|
||||||
|
|
||||||
|
**Why?** `MAKE_FUNCTION` creates a function value but doesn't jump to the body—it just stores the body's address. Without JUMP or HALT, the PC increments into the function body and executes it as top-level code.
|
||||||
|
|
||||||
### If-Else
|
### If-Else
|
||||||
```
|
```
|
||||||
<condition>
|
<condition>
|
||||||
|
|
@ -331,6 +377,125 @@ POP
|
||||||
.end: ; Result on stack
|
.end: ; Result on stack
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Reversing Operand Order
|
||||||
|
Use SWAP to reverse operand order for non-commutative operations:
|
||||||
|
|
||||||
|
```
|
||||||
|
; Compute 10 / 2 when values are in reverse order
|
||||||
|
PUSH 2
|
||||||
|
PUSH 10
|
||||||
|
SWAP ; Now: [10, 2]
|
||||||
|
DIV ; 10 / 2 = 5
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
; Compute "hello" - "world" (subtraction with strings coerced to numbers)
|
||||||
|
PUSH "world"
|
||||||
|
PUSH "hello"
|
||||||
|
SWAP ; Now: ["hello", "world"]
|
||||||
|
SUB ; Result based on operand order
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Division and subtraction when operands are in wrong order
|
||||||
|
- String concatenation with specific order
|
||||||
|
- Preparing arguments for functions that care about position
|
||||||
|
|
||||||
|
### Bitwise Operations
|
||||||
|
All bitwise operations work with 32-bit signed integers:
|
||||||
|
|
||||||
|
```
|
||||||
|
; Bitwise AND (masking)
|
||||||
|
PUSH 5
|
||||||
|
PUSH 3
|
||||||
|
BIT_AND ; → 1 (0101 & 0011 = 0001)
|
||||||
|
|
||||||
|
; Bitwise OR (combining flags)
|
||||||
|
PUSH 5
|
||||||
|
PUSH 3
|
||||||
|
BIT_OR ; → 7 (0101 | 0011 = 0111)
|
||||||
|
|
||||||
|
; Bitwise XOR (toggling bits)
|
||||||
|
PUSH 5
|
||||||
|
PUSH 3
|
||||||
|
BIT_XOR ; → 6 (0101 ^ 0011 = 0110)
|
||||||
|
|
||||||
|
; Left shift (multiply by power of 2)
|
||||||
|
PUSH 5
|
||||||
|
PUSH 2
|
||||||
|
BIT_SHL ; → 20 (5 << 2 = 5 * 4)
|
||||||
|
|
||||||
|
; Arithmetic right shift (divide by power of 2, preserves sign)
|
||||||
|
PUSH 20
|
||||||
|
PUSH 2
|
||||||
|
BIT_SHR ; → 5 (20 >> 2 = 20 / 4)
|
||||||
|
|
||||||
|
PUSH -20
|
||||||
|
PUSH 2
|
||||||
|
BIT_SHR ; → -5 (sign preserved)
|
||||||
|
|
||||||
|
; Logical right shift (zero-fill)
|
||||||
|
PUSH -1
|
||||||
|
PUSH 1
|
||||||
|
BIT_USHR ; → 2147483647 (unsigned shift)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Flags and bit masks: `flags band MASK` to test, `flags bor FLAG` to set
|
||||||
|
- Fast multiplication/division by powers of 2
|
||||||
|
- Color manipulation: extract RGB components
|
||||||
|
- Low-level bit manipulation for protocols or file formats
|
||||||
|
|
||||||
|
### Runtime Type Checking (TYPE)
|
||||||
|
Get the type of a value as a string for runtime introspection:
|
||||||
|
|
||||||
|
```
|
||||||
|
; Basic type check
|
||||||
|
PUSH 42
|
||||||
|
TYPE ; → "number"
|
||||||
|
|
||||||
|
PUSH "hello"
|
||||||
|
TYPE ; → "string"
|
||||||
|
|
||||||
|
MAKE_ARRAY #3
|
||||||
|
TYPE ; → "array"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Type Guard Pattern** (check type before operation):
|
||||||
|
```
|
||||||
|
; Safe addition - only add if both are numbers
|
||||||
|
LOAD x
|
||||||
|
DUP
|
||||||
|
TYPE
|
||||||
|
PUSH "number"
|
||||||
|
EQ
|
||||||
|
JUMP_IF_FALSE .not_number
|
||||||
|
|
||||||
|
LOAD y
|
||||||
|
DUP
|
||||||
|
TYPE
|
||||||
|
PUSH "number"
|
||||||
|
EQ
|
||||||
|
JUMP_IF_FALSE .cleanup_not_number
|
||||||
|
|
||||||
|
ADD ; Safe to add
|
||||||
|
JUMP .end
|
||||||
|
|
||||||
|
.cleanup_not_number:
|
||||||
|
POP ; Remove y
|
||||||
|
.not_number:
|
||||||
|
POP ; Remove x
|
||||||
|
PUSH null
|
||||||
|
.end:
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common Use Cases**:
|
||||||
|
- Type validation before operations
|
||||||
|
- Polymorphic functions that handle multiple types
|
||||||
|
- Debugging and introspection
|
||||||
|
- Dynamic dispatch in DSLs
|
||||||
|
- Safe coercion with fallbacks
|
||||||
|
|
||||||
### Try-Catch
|
### Try-Catch
|
||||||
```
|
```
|
||||||
PUSH_TRY .catch
|
PUSH_TRY .catch
|
||||||
|
|
@ -367,7 +532,8 @@ Functions automatically capture current scope:
|
||||||
PUSH 0
|
PUSH 0
|
||||||
STORE counter
|
STORE counter
|
||||||
MAKE_FUNCTION () .increment
|
MAKE_FUNCTION () .increment
|
||||||
RETURN
|
STORE increment_fn
|
||||||
|
JUMP .main
|
||||||
|
|
||||||
.increment:
|
.increment:
|
||||||
LOAD counter ; Captured variable
|
LOAD counter ; Captured variable
|
||||||
|
|
@ -376,6 +542,18 @@ RETURN
|
||||||
STORE counter
|
STORE counter
|
||||||
LOAD counter
|
LOAD counter
|
||||||
RETURN
|
RETURN
|
||||||
|
|
||||||
|
.main:
|
||||||
|
LOAD increment_fn
|
||||||
|
PUSH 0
|
||||||
|
PUSH 0
|
||||||
|
CALL ; Returns 1
|
||||||
|
POP
|
||||||
|
LOAD increment_fn
|
||||||
|
PUSH 0
|
||||||
|
PUSH 0
|
||||||
|
CALL ; Returns 2 (counter persists!)
|
||||||
|
HALT
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tail Recursion
|
### Tail Recursion
|
||||||
|
|
@ -383,7 +561,7 @@ Use TAIL_CALL instead of CALL for last call:
|
||||||
```
|
```
|
||||||
MAKE_FUNCTION (n acc) .factorial
|
MAKE_FUNCTION (n acc) .factorial
|
||||||
STORE factorial
|
STORE factorial
|
||||||
<...>
|
JUMP .main
|
||||||
|
|
||||||
.factorial:
|
.factorial:
|
||||||
LOAD n
|
LOAD n
|
||||||
|
|
@ -403,6 +581,15 @@ STORE factorial
|
||||||
PUSH 2
|
PUSH 2
|
||||||
PUSH 0
|
PUSH 0
|
||||||
TAIL_CALL ; Reuses stack frame
|
TAIL_CALL ; Reuses stack frame
|
||||||
|
|
||||||
|
.main:
|
||||||
|
LOAD factorial
|
||||||
|
PUSH 5
|
||||||
|
PUSH 1
|
||||||
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL ; factorial(5, 1) = 120
|
||||||
|
HALT
|
||||||
```
|
```
|
||||||
|
|
||||||
### Optional Function Calls (TRY_CALL)
|
### Optional Function Calls (TRY_CALL)
|
||||||
|
|
@ -431,6 +618,96 @@ TRY_CALL unknown ; Pushes "unknown" as string
|
||||||
- Shell-like languages where unknown identifiers become strings
|
- Shell-like languages where unknown identifiers become strings
|
||||||
- Templating systems with optional transformers
|
- 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
|
## Key Concepts
|
||||||
|
|
||||||
### Truthiness
|
### Truthiness
|
||||||
|
|
@ -456,6 +733,8 @@ Only `null` and `false` are falsy. Everything else (including `0`, `""`, empty a
|
||||||
|
|
||||||
**Arithmetic ops** (ADD, SUB, MUL, DIV, MOD) coerce both operands to numbers.
|
**Arithmetic ops** (ADD, SUB, MUL, DIV, MOD) coerce both operands to numbers.
|
||||||
|
|
||||||
|
**Bitwise ops** (BIT_AND, BIT_OR, BIT_XOR, BIT_SHL, BIT_SHR, BIT_USHR) coerce both operands to 32-bit signed integers.
|
||||||
|
|
||||||
**Comparison ops** (LT, GT, LTE, GTE) coerce both operands to numbers.
|
**Comparison ops** (LT, GT, LTE, GTE) coerce both operands to numbers.
|
||||||
|
|
||||||
**Equality ops** (EQ, NEQ) use type-aware comparison with deep equality for arrays/dicts.
|
**Equality ops** (EQ, NEQ) use type-aware comparison with deep equality for arrays/dicts.
|
||||||
|
|
@ -480,11 +759,24 @@ Variable and function parameter names support Unicode and emoji:
|
||||||
|
|
||||||
### Parameter Binding Priority
|
### Parameter Binding Priority
|
||||||
For function calls, parameters bound in order:
|
For function calls, parameters bound in order:
|
||||||
1. Positional argument (if provided)
|
1. Named argument (if provided and matches param name)
|
||||||
2. Named argument (if provided and matches param name)
|
2. Positional argument (if provided)
|
||||||
3. Default value (if defined)
|
3. Default value (if defined)
|
||||||
4. Null
|
4. Null
|
||||||
|
|
||||||
|
**Null Triggering Defaults**: Passing `null` as an argument (positional or named) triggers the default value if one exists. This allows callers to explicitly "opt-in" to defaults:
|
||||||
|
```
|
||||||
|
# Function with defaults: greet(name='Guest', greeting='Hello')
|
||||||
|
LOAD greet
|
||||||
|
PUSH null # Triggers default: name='Guest'
|
||||||
|
PUSH 'Hi' # Provided: greeting='Hi'
|
||||||
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL # Returns "Hi, Guest"
|
||||||
|
```
|
||||||
|
|
||||||
|
This works for both ReefVM functions and native TypeScript functions. If no default exists, `null` is bound as-is.
|
||||||
|
|
||||||
### Exception Handlers
|
### Exception Handlers
|
||||||
- PUSH_TRY uses absolute addresses for catch blocks
|
- PUSH_TRY uses absolute addresses for catch blocks
|
||||||
- Nested try blocks form a stack
|
- Nested try blocks form a stack
|
||||||
|
|
@ -493,7 +785,7 @@ For function calls, parameters bound in order:
|
||||||
- Finally execution in all cases is compiler's responsibility, not VM's
|
- Finally execution in all cases is compiler's responsibility, not VM's
|
||||||
|
|
||||||
### Calling Convention
|
### Calling Convention
|
||||||
All calls push arguments in order:
|
All calls (including native functions) push arguments in order:
|
||||||
1. Function
|
1. Function
|
||||||
2. Positional args (in order)
|
2. Positional args (in order)
|
||||||
3. Named args (key1, val1, key2, val2, ...)
|
3. Named args (key1, val1, key2, val2, ...)
|
||||||
|
|
@ -501,8 +793,210 @@ All calls push arguments in order:
|
||||||
5. Named count (as number)
|
5. Named count (as number)
|
||||||
6. CALL or TAIL_CALL
|
6. CALL or TAIL_CALL
|
||||||
|
|
||||||
### CALL_NATIVE Behavior
|
Native functions use the same calling convention as Reef functions. They are registered into scope and called via LOAD + CALL.
|
||||||
Unlike CALL, CALL_NATIVE consumes the **entire stack** as arguments and clears the stack. The native function receives all values that were on the stack at the time of the call.
|
|
||||||
|
### Registering Native Functions
|
||||||
|
|
||||||
|
Native TypeScript functions are registered into the VM's scope and accessed like regular variables.
|
||||||
|
|
||||||
|
**Method 1**: Pass to `run()` or `VM` constructor
|
||||||
|
```typescript
|
||||||
|
const result = await run(bytecode, {
|
||||||
|
add: (a: number, b: number) => a + b,
|
||||||
|
greet: (name: string) => `Hello, ${name}!`
|
||||||
|
})
|
||||||
|
|
||||||
|
// Or with VM
|
||||||
|
const vm = new VM(bytecode, { add, greet })
|
||||||
|
```
|
||||||
|
|
||||||
|
**Method 2**: Register after construction
|
||||||
|
```typescript
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.set('add', (a: number, b: number) => a + b)
|
||||||
|
await vm.run()
|
||||||
|
```
|
||||||
|
|
||||||
|
**Method 3**: Value-based functions (for full control)
|
||||||
|
```typescript
|
||||||
|
vm.setValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
|
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Auto-wrapping**: `vm.set()` automatically converts between native TypeScript types and ReefVM Value types. Both sync and async functions work.
|
||||||
|
|
||||||
|
**Usage in bytecode**:
|
||||||
|
```
|
||||||
|
; Positional arguments
|
||||||
|
LOAD add ; Load native function from scope
|
||||||
|
PUSH 5
|
||||||
|
PUSH 10
|
||||||
|
PUSH 2 ; positionalCount
|
||||||
|
PUSH 0 ; namedCount
|
||||||
|
CALL ; Call like any other function
|
||||||
|
|
||||||
|
; Named arguments
|
||||||
|
LOAD greet
|
||||||
|
PUSH "name"
|
||||||
|
PUSH "Alice"
|
||||||
|
PUSH "greeting"
|
||||||
|
PUSH "Hi"
|
||||||
|
PUSH 0 ; positionalCount
|
||||||
|
PUSH 2 ; namedCount
|
||||||
|
CALL ; → "Hi, Alice!"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Named Arguments**: Native functions support named arguments. Parameter names are extracted from the function signature at call time, and arguments are bound using the same priority as Reef functions (named arg > positional arg > default > null).
|
||||||
|
|
||||||
|
**@named Pattern**: Parameters starting with `at` followed by an uppercase letter (e.g., `atOptions`, `atNamed`) collect unmatched named arguments:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Basic @named - collects all named args
|
||||||
|
vm.set('greet', (atNamed: any = {}) => {
|
||||||
|
return `Hello, ${atNamed.name || 'World'}!`
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mixed positional and @named
|
||||||
|
vm.set('configure', (name: string, atOptions: any = {}) => {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
debug: atOptions.debug || false,
|
||||||
|
port: atOptions.port || 3000
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Bytecode example:
|
||||||
|
```
|
||||||
|
; Call with mixed positional and named args
|
||||||
|
LOAD configure
|
||||||
|
PUSH "myApp" ; positional arg → name
|
||||||
|
PUSH "debug"
|
||||||
|
PUSH true
|
||||||
|
PUSH "port"
|
||||||
|
PUSH 8080
|
||||||
|
PUSH 1 ; 1 positional arg
|
||||||
|
PUSH 2 ; 2 named args (debug, port)
|
||||||
|
CALL ; atOptions receives {debug: true, port: 8080}
|
||||||
|
```
|
||||||
|
|
||||||
|
Named arguments that match fixed parameter names are bound to those parameters. Remaining unmatched named arguments are collected into the `atXxx` parameter as a plain JavaScript object.
|
||||||
|
|
||||||
|
### Calling Functions from TypeScript
|
||||||
|
|
||||||
|
You can call both Reef and native functions from TypeScript using `vm.call()`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
MAKE_FUNCTION (name greeting="Hello") .greet
|
||||||
|
STORE greet
|
||||||
|
HALT
|
||||||
|
|
||||||
|
.greet:
|
||||||
|
LOAD greeting
|
||||||
|
PUSH " "
|
||||||
|
LOAD name
|
||||||
|
PUSH "!"
|
||||||
|
STR_CONCAT #4
|
||||||
|
RETURN
|
||||||
|
`)
|
||||||
|
|
||||||
|
const vm = new VM(bytecode, {
|
||||||
|
log: (msg: string) => console.log(msg) // Native function
|
||||||
|
})
|
||||||
|
await vm.run()
|
||||||
|
|
||||||
|
// Call Reef function with positional arguments
|
||||||
|
const result1 = await vm.call('greet', 'Alice')
|
||||||
|
// Returns: "Hello Alice!"
|
||||||
|
|
||||||
|
// Call Reef function with named arguments (pass as final object)
|
||||||
|
const result2 = await vm.call('greet', 'Bob', { greeting: 'Hi' })
|
||||||
|
// Returns: "Hi Bob!"
|
||||||
|
|
||||||
|
// Call Reef function with only named arguments
|
||||||
|
const result3 = await vm.call('greet', { name: 'Carol', greeting: 'Hey' })
|
||||||
|
// Returns: "Hey Carol!"
|
||||||
|
|
||||||
|
// Call native function
|
||||||
|
await vm.call('log', 'Hello from TypeScript!')
|
||||||
|
```
|
||||||
|
|
||||||
|
**How it works**:
|
||||||
|
- `vm.call(functionName, ...args)` looks up the function (Reef or native) in the VM's scope
|
||||||
|
- For Reef functions: converts to callable JavaScript function
|
||||||
|
- For native functions: calls directly
|
||||||
|
- Arguments are automatically converted to ReefVM Values
|
||||||
|
- Returns the result (automatically converted back to JavaScript types)
|
||||||
|
|
||||||
|
**Named arguments**: Pass a plain object as the final argument to provide named arguments. If the last argument is a non-array object, it's treated as named arguments. All preceding arguments are treated as positional.
|
||||||
|
|
||||||
|
**Type conversion**: Arguments and return values are automatically converted between JavaScript types and ReefVM Values:
|
||||||
|
- Primitives: `number`, `string`, `boolean`, `null`
|
||||||
|
- Arrays: converted recursively
|
||||||
|
- Objects: converted to ReefVM dicts
|
||||||
|
- Functions: Reef functions are converted to callable JavaScript functions
|
||||||
|
|
||||||
|
### REPL Mode (Incremental Compilation)
|
||||||
|
|
||||||
|
ReefVM supports incremental bytecode execution for building REPLs. This allows you to execute code line-by-line while preserving scope and avoiding re-execution of side effects.
|
||||||
|
|
||||||
|
**The Problem**: By default, `vm.run()` resets the program counter (PC) to 0, re-executing all previous bytecode. This makes it impossible to implement a REPL where each line executes only once.
|
||||||
|
|
||||||
|
**The Solution**: Use `vm.continue()` to resume execution from where you left off:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Line 1: Define variable
|
||||||
|
const line1 = toBytecode([
|
||||||
|
["PUSH", 42],
|
||||||
|
["STORE", "x"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const vm = new VM(line1)
|
||||||
|
await vm.run() // Execute first line
|
||||||
|
|
||||||
|
// Line 2: Use the variable
|
||||||
|
const line2 = toBytecode([
|
||||||
|
["LOAD", "x"],
|
||||||
|
["PUSH", 10],
|
||||||
|
["ADD"]
|
||||||
|
])
|
||||||
|
|
||||||
|
vm.appendBytecode(line2) // Append new bytecode with proper constant remapping
|
||||||
|
await vm.continue() // Execute ONLY the new bytecode
|
||||||
|
|
||||||
|
// Result: 52 (42 + 10)
|
||||||
|
// The first line never re-executed!
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key methods**:
|
||||||
|
- `vm.run()`: Resets PC to 0 and runs from the beginning (normal execution)
|
||||||
|
- `vm.continue()`: Continues from current PC (REPL mode)
|
||||||
|
- `vm.appendBytecode(bytecode)`: Helper that properly appends bytecode with constant index remapping
|
||||||
|
|
||||||
|
**Important**: Don't use `HALT` in REPL mode! The VM naturally stops when it runs out of instructions. Using `HALT` sets `vm.stopped = true`, which prevents `continue()` from resuming.
|
||||||
|
|
||||||
|
**Example REPL pattern**:
|
||||||
|
```typescript
|
||||||
|
const vm = new VM(toBytecode([]), { /* native functions */ })
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const input = await getUserInput() // Get next line from user
|
||||||
|
const bytecode = compileLine(input) // Compile to bytecode (no HALT!)
|
||||||
|
|
||||||
|
vm.appendBytecode(bytecode) // Append to VM
|
||||||
|
const result = await vm.continue() // Execute only the new code
|
||||||
|
|
||||||
|
console.log(fromValue(result)) // Show result to user
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This pattern ensures:
|
||||||
|
- Variables persist between lines
|
||||||
|
- Side effects (like `echo` or function calls) only run once
|
||||||
|
- Previous bytecode never re-executes
|
||||||
|
- Scope accumulates across all lines
|
||||||
|
|
||||||
### Empty Stack
|
### Empty Stack
|
||||||
- RETURN with empty stack returns null
|
- RETURN with empty stack returns null
|
||||||
|
|
|
||||||
500
IDEAS.md
Normal file
500
IDEAS.md
Normal file
|
|
@ -0,0 +1,500 @@
|
||||||
|
# ReefVM Architectural Improvement Ideas
|
||||||
|
|
||||||
|
This document contains architectural ideas for improving ReefVM. These focus on enhancing the VM's capabilities through structural improvements rather than just adding new opcodes.
|
||||||
|
|
||||||
|
## 1. Scope Resolution Optimization
|
||||||
|
|
||||||
|
**Current Issue**: Variable lookups are O(n) through the scope chain on every `LOAD`. This becomes expensive in deeply nested closures.
|
||||||
|
|
||||||
|
**Architectural Solution**: Implement **static scope analysis** with **lexical addressing**:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Instead of: LOAD x (runtime scope chain walk)
|
||||||
|
// Compile to: LOAD_FAST 2 1 (scope depth 2, slot 1 - O(1) lookup)
|
||||||
|
|
||||||
|
class Scope {
|
||||||
|
locals: Map<string, Value>
|
||||||
|
parent?: Scope
|
||||||
|
|
||||||
|
// NEW: Add indexed slots for fast access
|
||||||
|
slots: Value[] // Direct array access
|
||||||
|
nameToSlot: Map<string, number> // Compile-time mapping
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- O(1) variable access instead of O(n)
|
||||||
|
- Critical for hot loops and deeply nested functions
|
||||||
|
- Compiler can still fall back to named lookup for dynamic cases
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Module System Architecture
|
||||||
|
|
||||||
|
**Current Gap**: No way to organize code across multiple files or create reusable libraries.
|
||||||
|
|
||||||
|
**Architectural Solution**: Add first-class module support:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// New opcodes: IMPORT, EXPORT, MAKE_MODULE
|
||||||
|
// New bytecode structure:
|
||||||
|
type Bytecode = {
|
||||||
|
instructions: Instruction[]
|
||||||
|
constants: Constant[]
|
||||||
|
exports?: Map<string, number> // Exported symbols
|
||||||
|
imports?: Import[] // Import declarations
|
||||||
|
}
|
||||||
|
|
||||||
|
type Import = {
|
||||||
|
modulePath: string
|
||||||
|
symbols: string[] // [] means import all
|
||||||
|
alias?: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pattern**:
|
||||||
|
```
|
||||||
|
MAKE_MODULE .module_body
|
||||||
|
EXPORT add
|
||||||
|
EXPORT subtract
|
||||||
|
HALT
|
||||||
|
|
||||||
|
.module_body:
|
||||||
|
MAKE_FUNCTION (x y) .add_impl
|
||||||
|
RETURN
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Code organization and reusability
|
||||||
|
- Circular dependency detection at load time
|
||||||
|
- Natural namespace isolation
|
||||||
|
- Enables standard library architecture
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Source Map Integration
|
||||||
|
|
||||||
|
**Current Issue**: Runtime errors show bytecode addresses, not source locations.
|
||||||
|
|
||||||
|
**Architectural Solution**: Add source mapping layer:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type Bytecode = {
|
||||||
|
instructions: Instruction[]
|
||||||
|
constants: Constant[]
|
||||||
|
sourceMap?: SourceMap // NEW
|
||||||
|
}
|
||||||
|
|
||||||
|
type SourceMap = {
|
||||||
|
file?: string
|
||||||
|
mappings: SourceMapping[] // Instruction index → source location
|
||||||
|
}
|
||||||
|
|
||||||
|
type SourceMapping = {
|
||||||
|
instruction: number
|
||||||
|
line: number
|
||||||
|
column: number
|
||||||
|
source?: string // Original source text
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Meaningful error messages with line/column
|
||||||
|
- Debugger can show original source
|
||||||
|
- Stack traces map to source code
|
||||||
|
- Critical for production debugging
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Debugger Hook Architecture
|
||||||
|
|
||||||
|
**Current Gap**: No way to pause execution, inspect state, or step through code.
|
||||||
|
|
||||||
|
**Architectural Solution**: Add debug event system:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class VM {
|
||||||
|
debugger?: Debugger
|
||||||
|
|
||||||
|
async execute(instruction: Instruction) {
|
||||||
|
// Before execution
|
||||||
|
await this.debugger?.onInstruction(this.pc, instruction, this)
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
switch (instruction.op) { ... }
|
||||||
|
|
||||||
|
// After execution
|
||||||
|
await this.debugger?.afterInstruction(this.pc, this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Debugger {
|
||||||
|
breakpoints: Set<number>
|
||||||
|
onInstruction(pc: number, instruction: Instruction, vm: VM): Promise<void>
|
||||||
|
afterInstruction(pc: number, vm: VM): Promise<void>
|
||||||
|
onCall(fn: Value, args: Value[]): Promise<void>
|
||||||
|
onReturn(value: Value): Promise<void>
|
||||||
|
onException(error: Value): Promise<void>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Step-through debugging
|
||||||
|
- Breakpoints at any instruction
|
||||||
|
- State inspection at any point
|
||||||
|
- Non-invasive (no bytecode modification)
|
||||||
|
- Can build IDE integrations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Bytecode Optimization Pass Framework
|
||||||
|
|
||||||
|
**Current Gap**: Bytecode is emitted directly, no optimization.
|
||||||
|
|
||||||
|
**Architectural Solution**: Add optimization pipeline:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type Optimizer = (bytecode: Bytecode) => Bytecode
|
||||||
|
|
||||||
|
// Framework for composable optimization passes
|
||||||
|
class BytecodeOptimizer {
|
||||||
|
passes: Optimizer[] = []
|
||||||
|
|
||||||
|
add(pass: Optimizer): this {
|
||||||
|
this.passes.push(pass)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
optimize(bytecode: Bytecode): Bytecode {
|
||||||
|
return this.passes.reduce((bc, pass) => pass(bc), bytecode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example passes:
|
||||||
|
const optimizer = new BytecodeOptimizer()
|
||||||
|
.add(constantFolding) // PUSH 2; PUSH 3; ADD → PUSH 5
|
||||||
|
.add(deadCodeElimination) // Remove unreachable code after HALT/RETURN
|
||||||
|
.add(jumpChaining) // JUMP .a → .a: JUMP .b → JUMP .b directly
|
||||||
|
.add(peepholeOptimization) // DUP; POP → (nothing)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Faster execution without changing compiler
|
||||||
|
- Can add passes without modifying VM
|
||||||
|
- Composable and testable
|
||||||
|
- Enables aggressive optimizations (inlining, constant folding, etc.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Value Memory Management Architecture
|
||||||
|
|
||||||
|
**Current Issue**: No tracking of memory usage, no GC hooks, unbounded growth.
|
||||||
|
|
||||||
|
**Architectural Solution**: Add memory management layer:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class MemoryManager {
|
||||||
|
allocatedBytes: number = 0
|
||||||
|
maxBytes?: number
|
||||||
|
|
||||||
|
allocateValue(value: Value): Value {
|
||||||
|
const size = this.sizeOf(value)
|
||||||
|
if (this.maxBytes && this.allocatedBytes + size > this.maxBytes) {
|
||||||
|
throw new Error('Out of memory')
|
||||||
|
}
|
||||||
|
this.allocatedBytes += size
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
sizeOf(value: Value): number {
|
||||||
|
// Estimate memory footprint
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook for custom GC
|
||||||
|
gc?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
class VM {
|
||||||
|
memory: MemoryManager
|
||||||
|
|
||||||
|
// All value-creating operations check memory
|
||||||
|
push(value: Value) {
|
||||||
|
this.memory.allocateValue(value)
|
||||||
|
this.stack.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Memory limits for sandboxing
|
||||||
|
- Memory profiling
|
||||||
|
- Custom GC strategies
|
||||||
|
- Prevents runaway memory usage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Instruction Profiler Architecture
|
||||||
|
|
||||||
|
**Current Gap**: No way to identify performance bottlenecks in bytecode.
|
||||||
|
|
||||||
|
**Architectural Solution**: Add instrumentation layer:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class Profiler {
|
||||||
|
instructionCounts: Map<number, number> = new Map()
|
||||||
|
instructionTime: Map<number, number> = new Map()
|
||||||
|
hotFunctions: Map<number, FunctionProfile> = new Map()
|
||||||
|
|
||||||
|
recordInstruction(pc: number, duration: number) {
|
||||||
|
this.instructionCounts.set(pc, (this.instructionCounts.get(pc) || 0) + 1)
|
||||||
|
this.instructionTime.set(pc, (this.instructionTime.get(pc) || 0) + duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
getHotSpots(): HotSpot[] {
|
||||||
|
// Identify most-executed instructions
|
||||||
|
}
|
||||||
|
|
||||||
|
generateReport(): ProfileReport {
|
||||||
|
// Human-readable performance report
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VM {
|
||||||
|
profiler?: Profiler
|
||||||
|
|
||||||
|
async execute(instruction: Instruction) {
|
||||||
|
const start = performance.now()
|
||||||
|
// ... execute ...
|
||||||
|
const duration = performance.now() - start
|
||||||
|
this.profiler?.recordInstruction(this.pc, duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Identify hot loops and functions
|
||||||
|
- Guide optimization efforts
|
||||||
|
- Measure impact of changes
|
||||||
|
- Can feed into JIT compiler (future)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Standard Library Plugin Architecture
|
||||||
|
|
||||||
|
**Current Issue**: Native functions registered manually, no standard library structure.
|
||||||
|
|
||||||
|
**Architectural Solution**: Module-based native libraries:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface NativeModule {
|
||||||
|
name: string
|
||||||
|
exports: Record<string, any>
|
||||||
|
init?(vm: VM): void
|
||||||
|
}
|
||||||
|
|
||||||
|
class VM {
|
||||||
|
modules: Map<string, NativeModule> = new Map()
|
||||||
|
|
||||||
|
registerModule(module: NativeModule) {
|
||||||
|
this.modules.set(module.name, module)
|
||||||
|
module.init?.(this)
|
||||||
|
|
||||||
|
// Auto-register exports to global scope
|
||||||
|
for (const [name, value] of Object.entries(module.exports)) {
|
||||||
|
this.set(name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadModule(name: string): NativeModule {
|
||||||
|
return this.modules.get(name) || throw new Error(`Module ${name} not found`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage:
|
||||||
|
const mathModule: NativeModule = {
|
||||||
|
name: 'math',
|
||||||
|
exports: {
|
||||||
|
sin: Math.sin,
|
||||||
|
cos: Math.cos,
|
||||||
|
sqrt: Math.sqrt,
|
||||||
|
PI: Math.PI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.registerModule(mathModule)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Organized standard library
|
||||||
|
- Lazy loading of modules
|
||||||
|
- Third-party plugin system
|
||||||
|
- Clear namespace boundaries
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Streaming Bytecode Execution
|
||||||
|
|
||||||
|
**Current Limitation**: Must load entire bytecode before execution.
|
||||||
|
|
||||||
|
**Architectural Solution**: Incremental bytecode loading:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class StreamingBytecode {
|
||||||
|
chunks: BytecodeChunk[] = []
|
||||||
|
|
||||||
|
append(chunk: BytecodeChunk) {
|
||||||
|
// Remap addresses, merge constants
|
||||||
|
this.chunks.push(chunk)
|
||||||
|
}
|
||||||
|
|
||||||
|
getInstruction(pc: number): Instruction | undefined {
|
||||||
|
// Resolve across chunks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VM {
|
||||||
|
async runStreaming(stream: ReadableStream<BytecodeChunk>) {
|
||||||
|
for await (const chunk of stream) {
|
||||||
|
this.bytecode.append(chunk)
|
||||||
|
await this.continue() // Execute new chunk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Execute before full load (faster startup)
|
||||||
|
- Network streaming of bytecode
|
||||||
|
- Incremental compilation
|
||||||
|
- Better REPL experience
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Type Annotation System (Optional Runtime Types)
|
||||||
|
|
||||||
|
**Current Gap**: All values dynamically typed, no way to enforce types.
|
||||||
|
|
||||||
|
**Architectural Solution**: Optional type metadata:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
type TypedValue = Value & {
|
||||||
|
typeAnnotation?: TypeAnnotation
|
||||||
|
}
|
||||||
|
|
||||||
|
type TypeAnnotation =
|
||||||
|
| { kind: 'number' }
|
||||||
|
| { kind: 'string' }
|
||||||
|
| { kind: 'array', elementType?: TypeAnnotation }
|
||||||
|
| { kind: 'dict', valueType?: TypeAnnotation }
|
||||||
|
| { kind: 'function', params: TypeAnnotation[], return: TypeAnnotation }
|
||||||
|
|
||||||
|
// New opcodes: TYPE_CHECK, TYPE_ASSERT
|
||||||
|
// Functions can declare parameter types:
|
||||||
|
MAKE_FUNCTION (x:number y:string) .body
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Catch type errors earlier
|
||||||
|
- Self-documenting code
|
||||||
|
- Enables static analysis tools
|
||||||
|
- Optional (doesn't break existing code)
|
||||||
|
- Can enable optimizations (known number type → skip toNumber())
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. VM State Serialization
|
||||||
|
|
||||||
|
**Current Gap**: Can't save/restore VM execution state.
|
||||||
|
|
||||||
|
**Architectural Solution**: Serializable VM state:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class VM {
|
||||||
|
serialize(): SerializedState {
|
||||||
|
return {
|
||||||
|
instructions: this.instructions,
|
||||||
|
constants: this.constants,
|
||||||
|
pc: this.pc,
|
||||||
|
stack: this.stack.map(serializeValue),
|
||||||
|
callStack: this.callStack.map(serializeFrame),
|
||||||
|
scope: serializeScope(this.scope),
|
||||||
|
handlers: this.handlers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static deserialize(state: SerializedState): VM {
|
||||||
|
const vm = new VM(/* ... */)
|
||||||
|
vm.restore(state)
|
||||||
|
return vm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Save/restore execution state
|
||||||
|
- Distributed computing (send state to workers)
|
||||||
|
- Crash recovery
|
||||||
|
- Time-travel debugging
|
||||||
|
- Checkpoint/restart
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Async Iterator Support
|
||||||
|
|
||||||
|
**Current Gap**: Iterators work via break, but no async iteration.
|
||||||
|
|
||||||
|
**Architectural Solution**: First-class async iteration:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// New value type:
|
||||||
|
type Value = ... | { type: 'async_iterator', value: AsyncIterableIterator<Value> }
|
||||||
|
|
||||||
|
// New opcodes: MAKE_ASYNC_ITERATOR, AWAIT_NEXT, YIELD_ASYNC
|
||||||
|
|
||||||
|
// Pattern:
|
||||||
|
for_await (item in asyncIterable) {
|
||||||
|
// Compiles to AWAIT_NEXT loop
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Benefits**:
|
||||||
|
- Stream processing
|
||||||
|
- Async I/O without blocking
|
||||||
|
- Natural async patterns
|
||||||
|
- Matches JavaScript async iterators
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Priority Recommendations
|
||||||
|
|
||||||
|
### Tier 1 (Highest Impact):
|
||||||
|
1. **Source Map Integration** - Critical for usability
|
||||||
|
2. **Module System** - Essential for scaling beyond toy programs
|
||||||
|
3. **Scope Resolution Optimization** - Performance multiplier
|
||||||
|
|
||||||
|
### Tier 2 (High Value):
|
||||||
|
4. **Debugger Hook Architecture** - Developer experience game-changer
|
||||||
|
5. **Standard Library Plugin Architecture** - Enables ecosystem
|
||||||
|
6. **Bytecode Optimization Framework** - Performance without complexity
|
||||||
|
|
||||||
|
### Tier 3 (Nice to Have):
|
||||||
|
7. **Instruction Profiler** - Guides future optimization
|
||||||
|
8. **Memory Management** - Important for production use
|
||||||
|
9. **VM State Serialization** - Enables advanced use cases
|
||||||
|
|
||||||
|
### Tier 4 (Future/Experimental):
|
||||||
|
10. **Type Annotations** - Optional, doesn't break existing code
|
||||||
|
11. **Streaming Bytecode** - Mostly useful for large programs
|
||||||
|
12. **Async Iterators** - Specialized use case
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design Principles
|
||||||
|
|
||||||
|
These improvements focus on:
|
||||||
|
- **Performance** (scope optimization, bytecode optimization)
|
||||||
|
- **Developer Experience** (source maps, debugger, profiler)
|
||||||
|
- **Scalability** (modules, standard library architecture)
|
||||||
|
- **Production Readiness** (memory management, serialization)
|
||||||
|
|
||||||
|
All ideas maintain ReefVM's core design philosophy of simplicity, orthogonality, and explicit behavior.
|
||||||
11
README.md
11
README.md
|
|
@ -44,10 +44,14 @@ Commands: `clear`, `reset`, `exit`.
|
||||||
- Variadic functions with positional rest parameters (`...rest`)
|
- Variadic functions with positional rest parameters (`...rest`)
|
||||||
- Named arguments (named) that collect unmatched named args into a dict (`@named`)
|
- Named arguments (named) that collect unmatched named args into a dict (`@named`)
|
||||||
- Mixed positional and named arguments with proper priority binding
|
- Mixed positional and named arguments with proper priority binding
|
||||||
|
- Default parameter values with null-triggering: passing `null` explicitly uses the default value
|
||||||
- Tail call optimization with unbounded recursion (10,000+ iterations without stack overflow)
|
- Tail call optimization with unbounded recursion (10,000+ iterations without stack overflow)
|
||||||
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
||||||
- Native function interop (CALL_NATIVE) with sync and async functions
|
- Native function interop with auto-wrapping for native TypeScript types
|
||||||
- Write native functions with regular TypeScript types instead of Shrimp's internal Value 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
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|
||||||
|
|
@ -56,4 +60,5 @@ Commands: `clear`, `reset`, `exit`.
|
||||||
- Short-circuiting via compiler: No AND/OR opcodes—compilers use JUMP patterns for proper short-circuit evaluation
|
- Short-circuiting via compiler: No AND/OR opcodes—compilers use JUMP patterns for proper short-circuit evaluation
|
||||||
- Variadic parameters: Functions can collect remaining positional arguments into an array using `...rest` syntax
|
- Variadic parameters: Functions can collect remaining positional arguments into an array using `...rest` syntax
|
||||||
- Named parameters: Functions can collect unmatched named arguments into a dict using `@named` syntax
|
- Named parameters: Functions can collect unmatched named arguments into a dict using `@named` syntax
|
||||||
- Argument binding priority: Named args bind to regular params first, with unmatched ones going to `@named`
|
- Argument binding priority: Named args bind to regular params first, with unmatched ones going to `@named`
|
||||||
|
- Null triggers defaults: Passing `null` to a parameter with a default value explicitly uses that default (applies to both ReefVM and native functions)
|
||||||
413
SPEC.md
413
SPEC.md
|
|
@ -13,10 +13,9 @@ The ReefVM is a stack-based bytecode virtual machine designed for the Shrimp pro
|
||||||
- **Value Stack**: Operand stack for computation
|
- **Value Stack**: Operand stack for computation
|
||||||
- **Call Stack**: Call frames for function invocations
|
- **Call Stack**: Call frames for function invocations
|
||||||
- **Exception Handlers**: Stack of try/catch handlers
|
- **Exception Handlers**: Stack of try/catch handlers
|
||||||
- **Scope Chain**: Linked scopes for lexical variable resolution
|
- **Scope Chain**: Linked scopes for lexical variable resolution (includes native functions)
|
||||||
- **Program Counter (PC)**: Current instruction index
|
- **Program Counter (PC)**: Current instruction index
|
||||||
- **Constants Pool**: Immutable values and function metadata
|
- **Constants Pool**: Immutable values and function metadata
|
||||||
- **Native Function Registry**: External functions callable from Shrimp
|
|
||||||
|
|
||||||
### Execution Model
|
### Execution Model
|
||||||
|
|
||||||
|
|
@ -40,6 +39,7 @@ type Value =
|
||||||
| { type: 'dict', value: Map<string, Value> }
|
| { type: 'dict', value: Map<string, Value> }
|
||||||
| { type: 'function', params: string[], defaults: Record<string, number>,
|
| { type: 'function', params: string[], defaults: Record<string, number>,
|
||||||
body: number, parentScope: Scope, variadic: boolean, named: boolean }
|
body: number, parentScope: Scope, variadic: boolean, named: boolean }
|
||||||
|
| { type: 'native', fn: NativeFunction, value: '<function>' }
|
||||||
```
|
```
|
||||||
|
|
||||||
### Type Coercion
|
### Type Coercion
|
||||||
|
|
@ -138,6 +138,24 @@ type ExceptionHandler = {
|
||||||
**Effect**: Duplicate top of stack
|
**Effect**: Duplicate top of stack
|
||||||
**Stack**: [value] → [value, value]
|
**Stack**: [value] → [value, value]
|
||||||
|
|
||||||
|
#### SWAP
|
||||||
|
**Operand**: None
|
||||||
|
**Effect**: Swap the top two values on the stack
|
||||||
|
**Stack**: [value1, value2] → [value2, value1]
|
||||||
|
|
||||||
|
#### TYPE
|
||||||
|
**Operand**: None
|
||||||
|
**Effect**: Pop value from stack, push its type as a string
|
||||||
|
**Stack**: [value] → [typeString]
|
||||||
|
|
||||||
|
Returns the type of a value as a string.
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
```
|
||||||
|
PUSH 42
|
||||||
|
TYPE ; Pushes "number"
|
||||||
|
```
|
||||||
|
|
||||||
### Variable Operations
|
### Variable Operations
|
||||||
|
|
||||||
#### LOAD
|
#### LOAD
|
||||||
|
|
@ -179,7 +197,29 @@ All arithmetic operations pop two values, perform operation, push result as numb
|
||||||
|
|
||||||
#### ADD
|
#### ADD
|
||||||
**Stack**: [a, b] → [a + b]
|
**Stack**: [a, b] → [a + b]
|
||||||
**Note**: Only for numbers (use separate string concat if needed)
|
|
||||||
|
Performs different operations depending on operand types:
|
||||||
|
- If either operand is a string, converts both to strings and concatenates
|
||||||
|
- Else if both operands are arrays, concatenates the arrays
|
||||||
|
- Else if both operands are dicts, merges them (b's keys overwrite a's keys on conflict)
|
||||||
|
- Else if both operands are numbers, performs numeric addition
|
||||||
|
- Otherwise, throws an error
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
- `5 + 3` → `8` (numeric addition)
|
||||||
|
- `"hello" + " world"` → `"hello world"` (string concatenation)
|
||||||
|
- `"count: " + 42` → `"count: 42"` (string concatenation)
|
||||||
|
- `100 + " items"` → `"100 items"` (string concatenation)
|
||||||
|
- `[1, 2, 3] + [4]` → `[1, 2, 3, 4]` (array concatenation)
|
||||||
|
- `[1, 2] + [3, 4]` → `[1, 2, 3, 4]` (array concatenation)
|
||||||
|
- `{a: 1} + {b: 2}` → `{a: 1, b: 2}` (dict merge)
|
||||||
|
- `{a: 1, b: 2} + {b: 99}` → `{a: 1, b: 99}` (dict merge, b overwrites)
|
||||||
|
|
||||||
|
**Invalid operations** (throw errors):
|
||||||
|
- `true + false` → Error
|
||||||
|
- `null + 5` → Error
|
||||||
|
- `[1] + 5` → Error
|
||||||
|
- `{a: 1} + 5` → Error
|
||||||
|
|
||||||
#### SUB
|
#### SUB
|
||||||
**Stack**: [a, b] → [a - b]
|
**Stack**: [a, b] → [a - b]
|
||||||
|
|
@ -193,6 +233,62 @@ All arithmetic operations pop two values, perform operation, push result as numb
|
||||||
#### MOD
|
#### MOD
|
||||||
**Stack**: [a, b] → [a % b]
|
**Stack**: [a, b] → [a % b]
|
||||||
|
|
||||||
|
### Bitwise Operations
|
||||||
|
|
||||||
|
All bitwise operations coerce operands to 32-bit signed integers, perform the operation, and push the result as a number.
|
||||||
|
|
||||||
|
#### BIT_AND
|
||||||
|
**Operand**: None
|
||||||
|
**Stack**: [a, b] → [a & b]
|
||||||
|
|
||||||
|
Performs bitwise AND operation. Both operands are coerced to 32-bit signed integers.
|
||||||
|
|
||||||
|
**Example**: `5 & 3` → `1` (binary: `0101 & 0011` → `0001`)
|
||||||
|
|
||||||
|
#### BIT_OR
|
||||||
|
**Operand**: None
|
||||||
|
**Stack**: [a, b] → [a | b]
|
||||||
|
|
||||||
|
Performs bitwise OR operation. Both operands are coerced to 32-bit signed integers.
|
||||||
|
|
||||||
|
**Example**: `5 | 3` → `7` (binary: `0101 | 0011` → `0111`)
|
||||||
|
|
||||||
|
#### BIT_XOR
|
||||||
|
**Operand**: None
|
||||||
|
**Stack**: [a, b] → [a ^ b]
|
||||||
|
|
||||||
|
Performs bitwise XOR (exclusive OR) operation. Both operands are coerced to 32-bit signed integers.
|
||||||
|
|
||||||
|
**Example**: `5 ^ 3` → `6` (binary: `0101 ^ 0011` → `0110`)
|
||||||
|
|
||||||
|
#### BIT_SHL
|
||||||
|
**Operand**: None
|
||||||
|
**Stack**: [a, b] → [a << b]
|
||||||
|
|
||||||
|
Performs left shift operation. Left operand is coerced to 32-bit signed integer, right operand determines shift amount (masked to 0-31).
|
||||||
|
|
||||||
|
**Example**: `5 << 2` → `20` (binary: `0101` shifted left 2 positions → `10100`)
|
||||||
|
|
||||||
|
#### BIT_SHR
|
||||||
|
**Operand**: None
|
||||||
|
**Stack**: [a, b] → [a >> b]
|
||||||
|
|
||||||
|
Performs sign-preserving right shift operation. Left operand is coerced to 32-bit signed integer, right operand determines shift amount (masked to 0-31). The sign bit is preserved (arithmetic shift).
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
- `20 >> 2` → `5` (binary: `10100` shifted right 2 positions → `0101`)
|
||||||
|
- `-20 >> 2` → `-5` (sign bit preserved)
|
||||||
|
|
||||||
|
#### BIT_USHR
|
||||||
|
**Operand**: None
|
||||||
|
**Stack**: [a, b] → [a >>> b]
|
||||||
|
|
||||||
|
Performs zero-fill right shift operation. Left operand is coerced to 32-bit signed integer, right operand determines shift amount (masked to 0-31). Zeros are shifted in from the left (logical shift).
|
||||||
|
|
||||||
|
**Example**:
|
||||||
|
- `-1 >>> 1` → `2147483647` (all bits shift right, zero fills from left)
|
||||||
|
- `-8 >>> 1` → `2147483644`
|
||||||
|
|
||||||
### Comparison Operations
|
### Comparison Operations
|
||||||
|
|
||||||
All comparison operations pop two values, compare, push boolean result.
|
All comparison operations pop two values, compare, push boolean result.
|
||||||
|
|
@ -231,39 +327,45 @@ All comparison operations pop two values, compare, push boolean result.
|
||||||
```
|
```
|
||||||
<evaluate left>
|
<evaluate left>
|
||||||
DUP
|
DUP
|
||||||
JUMP_IF_FALSE 2 # skip POP and <evaluate right>
|
JUMP_IF_FALSE .end
|
||||||
POP
|
POP
|
||||||
<evaluate right>
|
<evaluate right>
|
||||||
end:
|
.end:
|
||||||
```
|
```
|
||||||
|
|
||||||
**OR pattern** (short-circuits if left side is true):
|
**OR pattern** (short-circuits if left side is true):
|
||||||
```
|
```
|
||||||
<evaluate left>
|
<evaluate left>
|
||||||
DUP
|
DUP
|
||||||
JUMP_IF_TRUE 2 # skip POP and <evaluate right>
|
JUMP_IF_TRUE .end
|
||||||
POP
|
POP
|
||||||
<evaluate right>
|
<evaluate right>
|
||||||
end:
|
.end:
|
||||||
```
|
```
|
||||||
|
|
||||||
### Control Flow
|
### Control Flow
|
||||||
|
|
||||||
#### JUMP
|
#### JUMP
|
||||||
**Operand**: Offset (number)
|
**Operand**: Label (string)
|
||||||
**Effect**: Add offset to PC (relative jump)
|
**Effect**: Jump to the specified label
|
||||||
**Stack**: No change
|
**Stack**: No change
|
||||||
|
|
||||||
|
**Note**: JUMP only accepts label operands (`.label`), not numeric offsets. The VM resolves labels to relative offsets internally.
|
||||||
|
|
||||||
#### JUMP_IF_FALSE
|
#### JUMP_IF_FALSE
|
||||||
**Operand**: Offset (number)
|
**Operand**: Label (string)
|
||||||
**Effect**: If top of stack is falsy, add offset to PC (relative jump)
|
**Effect**: If top of stack is falsy, jump to the specified label
|
||||||
**Stack**: [condition] → []
|
**Stack**: [condition] → []
|
||||||
|
|
||||||
|
**Note**: JUMP_IF_FALSE only accepts label operands (`.label`), not numeric offsets.
|
||||||
|
|
||||||
#### JUMP_IF_TRUE
|
#### JUMP_IF_TRUE
|
||||||
**Operand**: Offset (number)
|
**Operand**: Label (string)
|
||||||
**Effect**: If top of stack is truthy, add offset to PC (relative jump)
|
**Effect**: If top of stack is truthy, jump to the specified label
|
||||||
**Stack**: [condition] → []
|
**Stack**: [condition] → []
|
||||||
|
|
||||||
|
**Note**: JUMP_IF_TRUE only accepts label operands (`.label`), not numeric offsets.
|
||||||
|
|
||||||
#### BREAK
|
#### BREAK
|
||||||
**Operand**: None
|
**Operand**: None
|
||||||
**Effect**: Unwind call stack until frame with `isBreakTarget = true`, resume there
|
**Effect**: Unwind call stack until frame with `isBreakTarget = true`, resume there
|
||||||
|
|
@ -357,15 +459,20 @@ The created function captures `currentScope` as its `parentScope`.
|
||||||
3. Pop named arguments (name/value pairs) from stack
|
3. Pop named arguments (name/value pairs) from stack
|
||||||
4. Pop positional arguments from stack
|
4. Pop positional arguments from stack
|
||||||
5. Pop function from stack
|
5. Pop function from stack
|
||||||
6. Mark current frame (if exists) as break target (`isBreakTarget = true`)
|
6. **If function is native**:
|
||||||
7. Push new call frame with current PC and scope
|
- Mark current frame (if exists) as break target
|
||||||
8. Create new scope with function's parentScope as parent
|
- Call native function with positional args
|
||||||
9. Bind parameters:
|
- 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:
|
||||||
- For regular functions: bind params by position, then by name, then defaults, then null
|
- For regular functions: bind params by position, then by name, then defaults, then null
|
||||||
- For variadic functions: bind fixed params, collect rest into array
|
- For variadic functions: bind fixed params, collect rest into array
|
||||||
- For functions with `named: true`: bind fixed params by position/name, collect unmatched named args into dict
|
- For functions with `named: true`: bind fixed params by position/name, collect unmatched named args into dict
|
||||||
10. Set currentScope to new scope
|
11. Set currentScope to new scope
|
||||||
11. Jump to function body
|
12. Jump to function body
|
||||||
|
|
||||||
**Parameter Binding Priority** (for fixed params):
|
**Parameter Binding Priority** (for fixed params):
|
||||||
1. Named argument (if provided and matches param name)
|
1. Named argument (if provided and matches param name)
|
||||||
|
|
@ -373,12 +480,21 @@ The created function captures `currentScope` as its `parentScope`.
|
||||||
3. Default value (if defined)
|
3. Default value (if defined)
|
||||||
4. Null
|
4. Null
|
||||||
|
|
||||||
|
**Null Value Semantics**:
|
||||||
|
- Passing `null` as an argument explicitly triggers the default value (if one exists)
|
||||||
|
- This allows callers to "opt-in" to defaults even when providing arguments positionally
|
||||||
|
- If no default exists, `null` is bound as-is
|
||||||
|
- This applies to both ReefVM functions and native TypeScript functions
|
||||||
|
- Example: `fn(null, 20)` where `fn(x=10, y)` binds `x=10` (default triggered), `y=20`
|
||||||
|
|
||||||
**Named Args Handling**:
|
**Named Args Handling**:
|
||||||
- Named args that match fixed parameter names are bound to those params
|
- Named args that match fixed parameter names are bound to those params
|
||||||
- If the function has `named: true`, remaining named args (that don't match any fixed param) are collected into the last parameter as a dict
|
- If the function has `named: true`, remaining named args (that don't match any fixed param) are collected into the last parameter as a dict
|
||||||
- This allows flexible calling: `fn(x=10, y=20, extra=30)` where `extra` goes to the named args dict
|
- This allows flexible calling: `fn(x=10, y=20, extra=30)` where `extra` goes to the named args dict
|
||||||
|
- **Native functions support named arguments** - parameter names are extracted from the function signature at call time
|
||||||
|
- Passing `null` via named args also triggers defaults: `fn(x=null)` triggers `x`'s default
|
||||||
|
|
||||||
**Errors**: Throws if top of stack is not a function
|
**Errors**: Throws if top of stack is not a function (or native function)
|
||||||
|
|
||||||
#### TAIL_CALL
|
#### TAIL_CALL
|
||||||
**Operand**: None
|
**Operand**: None
|
||||||
|
|
@ -509,30 +625,190 @@ Key is coerced to string.
|
||||||
Key is coerced to string.
|
Key is coerced to string.
|
||||||
**Errors**: Throws if not dict
|
**Errors**: Throws if not dict
|
||||||
|
|
||||||
### TypeScript Interop
|
### Unified Access
|
||||||
|
|
||||||
#### CALL_NATIVE
|
#### DOT_GET
|
||||||
**Operand**: Function name (string)
|
**Operand**: None
|
||||||
**Effect**: Call registered TypeScript function
|
**Effect**: Get value from array or dict
|
||||||
**Stack**: [...args] → [returnValue]
|
**Stack**: [array|dict, index|key] → [value]
|
||||||
|
|
||||||
**Behavior**:
|
**Behavior**:
|
||||||
1. Look up function by name in registry
|
- If target is array: coerce index to number and access `array[index]`
|
||||||
2. Mark current frame (if exists) as break target
|
- If target is dict: coerce key to string and access `dict.get(key)`
|
||||||
3. Await function call (native function receives arguments and returns a Value)
|
- Returns null if index out of bounds or key not found
|
||||||
4. Push return value onto stack
|
|
||||||
|
|
||||||
**Notes**:
|
**Errors**: Throws if target is not array or dict
|
||||||
- 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
|
**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
|
||||||
|
|
||||||
**TypeScript Function Signature**:
|
**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.
|
||||||
|
|
||||||
|
**Registration**:
|
||||||
```typescript
|
```typescript
|
||||||
type TypeScriptFunction = (...args: Value[]) => Promise<Value> | Value;
|
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!"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Special
|
### Special
|
||||||
|
|
@ -544,14 +820,16 @@ type TypeScriptFunction = (...args: Value[]) => Promise<Value> | Value;
|
||||||
|
|
||||||
## Label Syntax
|
## Label Syntax
|
||||||
|
|
||||||
The bytecode format supports labels for improved readability:
|
The bytecode format requires labels for control flow jumps:
|
||||||
|
|
||||||
**Label Definition**: `.label_name:` marks an instruction position
|
**Label Definition**: `.label_name:` marks an instruction position
|
||||||
**Label Reference**: `.label_name` in operands (e.g., `JUMP .loop_start`)
|
**Label Reference**: `.label_name` in operands (e.g., `JUMP .loop_start`)
|
||||||
|
|
||||||
Labels are resolved to numeric offsets during parsing. The original numeric offset syntax (`#N`) is still supported for backwards compatibility.
|
Labels are resolved to relative PC offsets during bytecode compilation. All JUMP instructions (`JUMP`, `JUMP_IF_FALSE`, `JUMP_IF_TRUE`) require label operands.
|
||||||
|
|
||||||
Example with labels:
|
**Note**: Exception handling instructions (`PUSH_TRY`, `PUSH_FINALLY`) and function definitions (`MAKE_FUNCTION`) can use either labels or absolute instruction indices (`#N`).
|
||||||
|
|
||||||
|
Example:
|
||||||
```
|
```
|
||||||
JUMP .skip
|
JUMP .skip
|
||||||
.middle:
|
.middle:
|
||||||
|
|
@ -562,15 +840,6 @@ JUMP .skip
|
||||||
HALT
|
HALT
|
||||||
```
|
```
|
||||||
|
|
||||||
Equivalent with numeric offsets:
|
|
||||||
```
|
|
||||||
JUMP #2
|
|
||||||
PUSH 999
|
|
||||||
HALT
|
|
||||||
PUSH 42
|
|
||||||
HALT
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Bytecode Patterns
|
## Common Bytecode Patterns
|
||||||
|
|
||||||
### If-Else Statement
|
### If-Else Statement
|
||||||
|
|
@ -646,6 +915,29 @@ PUSH 1 # namedCount
|
||||||
CALL
|
CALL
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Null Triggering Default Values
|
||||||
|
```
|
||||||
|
# Function: greet(name='Guest', greeting='Hello')
|
||||||
|
MAKE_FUNCTION (name='Guest' greeting='Hello') .greet_body
|
||||||
|
STORE 'greet'
|
||||||
|
JUMP .main
|
||||||
|
.greet_body:
|
||||||
|
LOAD 'greeting'
|
||||||
|
PUSH ', '
|
||||||
|
ADD
|
||||||
|
LOAD 'name'
|
||||||
|
ADD
|
||||||
|
RETURN
|
||||||
|
.main:
|
||||||
|
# Call with null for first param - triggers default
|
||||||
|
LOAD 'greet'
|
||||||
|
PUSH null # name will use default 'Guest'
|
||||||
|
PUSH 'Hi' # greeting='Hi' (provided)
|
||||||
|
PUSH 2 # positionalCount
|
||||||
|
PUSH 0 # namedCount
|
||||||
|
CALL # Returns "Hi, Guest"
|
||||||
|
```
|
||||||
|
|
||||||
### Tail Recursive Function
|
### Tail Recursive Function
|
||||||
```
|
```
|
||||||
MAKE_FUNCTION (n acc) .factorial_body
|
MAKE_FUNCTION (n acc) .factorial_body
|
||||||
|
|
@ -692,10 +984,9 @@ All of these should throw errors:
|
||||||
6. **Break Outside Loop**: BREAK with no break target
|
6. **Break Outside Loop**: BREAK with no break target
|
||||||
7. **Continue Outside Loop**: CONTINUE with no continue target
|
7. **Continue Outside Loop**: CONTINUE with no continue target
|
||||||
8. **Return Outside Function**: RETURN with no call frame
|
8. **Return Outside Function**: RETURN with no call frame
|
||||||
9. **Unknown Function**: CALL_NATIVE with unregistered function
|
9. **Mismatched Handler**: POP_TRY with no handler
|
||||||
10. **Mismatched Handler**: POP_TRY with no handler
|
10. **Invalid Constant**: PUSH with invalid constant index
|
||||||
11. **Invalid Constant**: PUSH with invalid constant index
|
11. **Invalid Function Definition**: MAKE_FUNCTION with non-function_def constant
|
||||||
12. **Invalid Function Definition**: MAKE_FUNCTION with non-function_def constant
|
|
||||||
|
|
||||||
## Edge Cases
|
## Edge Cases
|
||||||
|
|
||||||
|
|
@ -740,11 +1031,21 @@ All of these should throw errors:
|
||||||
## VM Initialization
|
## VM Initialization
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const vm = new VM(bytecode);
|
// Register native functions during construction
|
||||||
vm.registerFunction('add', (a, b) => {
|
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 => {
|
||||||
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
||||||
})
|
})
|
||||||
const result = await vm.execute()
|
|
||||||
|
const result = await vm.run()
|
||||||
```
|
```
|
||||||
|
|
||||||
## Testing Considerations
|
## Testing Considerations
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,8 @@ function formatValue(value: Value): string {
|
||||||
} else if (value.type === 'function') {
|
} else if (value.type === 'function') {
|
||||||
const params = value.params.join(', ')
|
const params = value.params.join(', ')
|
||||||
return `${colors.dim}<fn(${params})>${colors.reset}`
|
return `${colors.dim}<fn(${params})>${colors.reset}`
|
||||||
|
} else if (value.type === 'native') {
|
||||||
|
return `${colors.dim}<native>${colors.reset}`
|
||||||
}
|
}
|
||||||
return String(value)
|
return String(value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
bin/repl
2
bin/repl
|
|
@ -38,6 +38,8 @@ function formatValue(value: Value): string {
|
||||||
} else if (value.type === 'function') {
|
} else if (value.type === 'function') {
|
||||||
const params = value.params.join(', ')
|
const params = value.params.join(', ')
|
||||||
return `${colors.dim}<fn(${params})>${colors.reset}`
|
return `${colors.dim}<fn(${params})>${colors.reset}`
|
||||||
|
} else if (value.type === 'native') {
|
||||||
|
return `${colors.dim}<native>${colors.reset}`
|
||||||
}
|
}
|
||||||
return String(value)
|
return String(value)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
bunfig.toml
Normal file
2
bunfig.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[test]
|
||||||
|
preload = ["./tests/setup.ts"]
|
||||||
116
examples/add-with-arrays.ts
Normal file
116
examples/add-with-arrays.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
/**
|
||||||
|
* Demonstrates the ADD opcode working with arrays
|
||||||
|
*
|
||||||
|
* ADD now handles array concatenation:
|
||||||
|
* - [1, 2, 3] + [4] === [1, 2, 3, 4]
|
||||||
|
* - If both operands are arrays, they are concatenated
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { toBytecode, run } from "#reef"
|
||||||
|
|
||||||
|
// Basic array concatenation
|
||||||
|
const basicConcat = toBytecode([
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 2],
|
||||||
|
["PUSH", 3],
|
||||||
|
["MAKE_ARRAY", 3],
|
||||||
|
["PUSH", 4],
|
||||||
|
["MAKE_ARRAY", 1],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('Basic array concatenation ([1, 2, 3] + [4]):')
|
||||||
|
const result1 = await run(basicConcat)
|
||||||
|
console.log(result1)
|
||||||
|
// Output: { type: 'array', value: [1, 2, 3, 4] }
|
||||||
|
|
||||||
|
// Concatenate two multi-element arrays
|
||||||
|
const multiConcat = toBytecode([
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 2],
|
||||||
|
["MAKE_ARRAY", 2],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 4],
|
||||||
|
["MAKE_ARRAY", 2],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('\nConcatenate two arrays ([1, 2] + [3, 4]):')
|
||||||
|
const result2 = await run(multiConcat)
|
||||||
|
console.log(result2)
|
||||||
|
// Output: { type: 'array', value: [1, 2, 3, 4] }
|
||||||
|
|
||||||
|
// Concatenate multiple arrays in sequence
|
||||||
|
const multipleConcat = toBytecode([
|
||||||
|
["PUSH", 1],
|
||||||
|
["MAKE_ARRAY", 1],
|
||||||
|
["PUSH", 2],
|
||||||
|
["MAKE_ARRAY", 1],
|
||||||
|
["ADD"],
|
||||||
|
["PUSH", 3],
|
||||||
|
["MAKE_ARRAY", 1],
|
||||||
|
["ADD"],
|
||||||
|
["PUSH", 4],
|
||||||
|
["MAKE_ARRAY", 1],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('\nMultiple concatenations ([1] + [2] + [3] + [4]):')
|
||||||
|
const result3 = await run(multipleConcat)
|
||||||
|
console.log(result3)
|
||||||
|
// Output: { type: 'array', value: [1, 2, 3, 4] }
|
||||||
|
|
||||||
|
// Concatenate arrays with mixed types
|
||||||
|
const mixedTypes = toBytecode([
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", "hello"],
|
||||||
|
["MAKE_ARRAY", 2],
|
||||||
|
["PUSH", true],
|
||||||
|
["PUSH", null],
|
||||||
|
["MAKE_ARRAY", 2],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('\nConcatenate arrays with mixed types ([1, "hello"] + [true, null]):')
|
||||||
|
const result4 = await run(mixedTypes)
|
||||||
|
console.log(result4)
|
||||||
|
// Output: { type: 'array', value: [1, "hello", true, null] }
|
||||||
|
|
||||||
|
// Concatenate empty array with non-empty
|
||||||
|
const emptyConcat = toBytecode([
|
||||||
|
["MAKE_ARRAY", 0],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 2],
|
||||||
|
["PUSH", 3],
|
||||||
|
["MAKE_ARRAY", 3],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('\nConcatenate empty array with [1, 2, 3] ([] + [1, 2, 3]):')
|
||||||
|
const result5 = await run(emptyConcat)
|
||||||
|
console.log(result5)
|
||||||
|
// Output: { type: 'array', value: [1, 2, 3] }
|
||||||
|
|
||||||
|
// Nested arrays
|
||||||
|
const nestedConcat = toBytecode([
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 2],
|
||||||
|
["MAKE_ARRAY", 2],
|
||||||
|
["MAKE_ARRAY", 1],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 4],
|
||||||
|
["MAKE_ARRAY", 2],
|
||||||
|
["MAKE_ARRAY", 1],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('\nConcatenate nested arrays ([[1, 2]] + [[3, 4]]):')
|
||||||
|
const result6 = await run(nestedConcat)
|
||||||
|
console.log(result6)
|
||||||
|
// Output: { type: 'array', value: [[1, 2], [3, 4]] }
|
||||||
158
examples/add-with-dicts.ts
Normal file
158
examples/add-with-dicts.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
/**
|
||||||
|
* Demonstrates the ADD opcode working with dicts
|
||||||
|
*
|
||||||
|
* ADD now handles dict merging:
|
||||||
|
* - {a: 1} + {b: 2} === {a: 1, b: 2}
|
||||||
|
* - If both operands are dicts, they are merged
|
||||||
|
* - Keys from the second dict overwrite keys from the first on conflict
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { toBytecode, run } from "#reef"
|
||||||
|
|
||||||
|
// Basic dict merge
|
||||||
|
const basicMerge = toBytecode([
|
||||||
|
["PUSH", "a"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["MAKE_DICT", 1],
|
||||||
|
["PUSH", "b"],
|
||||||
|
["PUSH", 2],
|
||||||
|
["MAKE_DICT", 1],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('Basic dict merge ({a: 1} + {b: 2}):')
|
||||||
|
const result1 = await run(basicMerge)
|
||||||
|
console.log(result1)
|
||||||
|
// Output: { type: 'dict', value: Map { a: 1, b: 2 } }
|
||||||
|
|
||||||
|
// Merge with overlapping keys
|
||||||
|
const overlapMerge = toBytecode([
|
||||||
|
["PUSH", "a"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", "b"],
|
||||||
|
["PUSH", 2],
|
||||||
|
["MAKE_DICT", 2],
|
||||||
|
["PUSH", "b"],
|
||||||
|
["PUSH", 99],
|
||||||
|
["PUSH", "c"],
|
||||||
|
["PUSH", 3],
|
||||||
|
["MAKE_DICT", 2],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('\nMerge with overlapping keys ({a: 1, b: 2} + {b: 99, c: 3}):')
|
||||||
|
const result2 = await run(overlapMerge)
|
||||||
|
console.log(result2)
|
||||||
|
console.log('Note: b is overwritten from 2 to 99')
|
||||||
|
// Output: { type: 'dict', value: Map { a: 1, b: 99, c: 3 } }
|
||||||
|
|
||||||
|
// Merge multiple dicts in sequence
|
||||||
|
const multipleMerge = toBytecode([
|
||||||
|
["PUSH", "a"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["MAKE_DICT", 1],
|
||||||
|
["PUSH", "b"],
|
||||||
|
["PUSH", 2],
|
||||||
|
["MAKE_DICT", 1],
|
||||||
|
["ADD"],
|
||||||
|
["PUSH", "c"],
|
||||||
|
["PUSH", 3],
|
||||||
|
["MAKE_DICT", 1],
|
||||||
|
["ADD"],
|
||||||
|
["PUSH", "d"],
|
||||||
|
["PUSH", 4],
|
||||||
|
["MAKE_DICT", 1],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('\nMultiple merges ({a: 1} + {b: 2} + {c: 3} + {d: 4}):')
|
||||||
|
const result3 = await run(multipleMerge)
|
||||||
|
console.log(result3)
|
||||||
|
// Output: { type: 'dict', value: Map { a: 1, b: 2, c: 3, d: 4 } }
|
||||||
|
|
||||||
|
// Merge dicts with different value types
|
||||||
|
const mixedTypes = toBytecode([
|
||||||
|
["PUSH", "num"],
|
||||||
|
["PUSH", 42],
|
||||||
|
["PUSH", "str"],
|
||||||
|
["PUSH", "hello"],
|
||||||
|
["MAKE_DICT", 2],
|
||||||
|
["PUSH", "bool"],
|
||||||
|
["PUSH", true],
|
||||||
|
["PUSH", "null"],
|
||||||
|
["PUSH", null],
|
||||||
|
["MAKE_DICT", 2],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('\nMerge dicts with different types ({num: 42, str: "hello"} + {bool: true, null: null}):')
|
||||||
|
const result4 = await run(mixedTypes)
|
||||||
|
console.log(result4)
|
||||||
|
// Output: { type: 'dict', value: Map { num: 42, str: 'hello', bool: true, null: null } }
|
||||||
|
|
||||||
|
// Merge empty dict with non-empty
|
||||||
|
const emptyMerge = toBytecode([
|
||||||
|
["MAKE_DICT", 0],
|
||||||
|
["PUSH", "x"],
|
||||||
|
["PUSH", 100],
|
||||||
|
["PUSH", "y"],
|
||||||
|
["PUSH", 200],
|
||||||
|
["MAKE_DICT", 2],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('\nMerge empty dict with {x: 100, y: 200} ({} + {x: 100, y: 200}):')
|
||||||
|
const result5 = await run(emptyMerge)
|
||||||
|
console.log(result5)
|
||||||
|
// Output: { type: 'dict', value: Map { x: 100, y: 200 } }
|
||||||
|
|
||||||
|
// Merge dicts with nested structures
|
||||||
|
const nestedMerge = toBytecode([
|
||||||
|
["PUSH", "data"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 2],
|
||||||
|
["MAKE_ARRAY", 2],
|
||||||
|
["MAKE_DICT", 1],
|
||||||
|
["PUSH", "config"],
|
||||||
|
["PUSH", "debug"],
|
||||||
|
["PUSH", true],
|
||||||
|
["MAKE_DICT", 1],
|
||||||
|
["MAKE_DICT", 1],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('\nMerge dicts with nested structures:')
|
||||||
|
const result6 = await run(nestedMerge)
|
||||||
|
console.log(result6)
|
||||||
|
// Output: { type: 'dict', value: Map { data: [1, 2], config: { debug: true } } }
|
||||||
|
|
||||||
|
// Building configuration objects
|
||||||
|
const configBuild = toBytecode([
|
||||||
|
// Default config
|
||||||
|
["PUSH", "debug"],
|
||||||
|
["PUSH", false],
|
||||||
|
["PUSH", "port"],
|
||||||
|
["PUSH", 3000],
|
||||||
|
["PUSH", "host"],
|
||||||
|
["PUSH", "localhost"],
|
||||||
|
["MAKE_DICT", 3],
|
||||||
|
// Override with user config
|
||||||
|
["PUSH", "debug"],
|
||||||
|
["PUSH", true],
|
||||||
|
["PUSH", "port"],
|
||||||
|
["PUSH", 8080],
|
||||||
|
["MAKE_DICT", 2],
|
||||||
|
["ADD"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
console.log('\nBuilding config (defaults + overrides):')
|
||||||
|
const result7 = await run(configBuild)
|
||||||
|
console.log(result7)
|
||||||
|
// Output: { type: 'dict', value: Map { debug: true, port: 8080, host: 'localhost' } }
|
||||||
73
examples/add-with-strings.ts
Normal file
73
examples/add-with-strings.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
/**
|
||||||
|
* Demonstrates the ADD opcode working with both numbers and strings
|
||||||
|
*
|
||||||
|
* ADD now behaves like JavaScript's + operator:
|
||||||
|
* - If either operand is a string, it does string concatenation
|
||||||
|
* - Otherwise, it does numeric addition
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { toBytecode, run } from "#reef"
|
||||||
|
|
||||||
|
// Numeric addition
|
||||||
|
const numericAdd = toBytecode(`
|
||||||
|
PUSH 10
|
||||||
|
PUSH 5
|
||||||
|
ADD
|
||||||
|
HALT
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log('Numeric addition (10 + 5):')
|
||||||
|
console.log(await run(numericAdd))
|
||||||
|
// Output: { type: 'number', value: 15 }
|
||||||
|
|
||||||
|
// String concatenation
|
||||||
|
const stringConcat = toBytecode(`
|
||||||
|
PUSH "hello"
|
||||||
|
PUSH " world"
|
||||||
|
ADD
|
||||||
|
HALT
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log('\nString concatenation ("hello" + " world"):')
|
||||||
|
console.log(await run(stringConcat))
|
||||||
|
// Output: { type: 'string', value: 'hello world' }
|
||||||
|
|
||||||
|
// Mixed: string + number
|
||||||
|
const mixedConcat = toBytecode(`
|
||||||
|
PUSH "count: "
|
||||||
|
PUSH 42
|
||||||
|
ADD
|
||||||
|
HALT
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log('\nMixed concatenation ("count: " + 42):')
|
||||||
|
console.log(await run(mixedConcat))
|
||||||
|
// Output: { type: 'string', value: 'count: 42' }
|
||||||
|
|
||||||
|
// Building a message
|
||||||
|
const buildMessage = toBytecode(`
|
||||||
|
PUSH "You have "
|
||||||
|
PUSH 3
|
||||||
|
ADD
|
||||||
|
PUSH " new messages"
|
||||||
|
ADD
|
||||||
|
HALT
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log('\nBuilding a message:')
|
||||||
|
console.log(await run(buildMessage))
|
||||||
|
// Output: { type: 'string', value: 'You have 3 new messages' }
|
||||||
|
|
||||||
|
// Computing then concatenating
|
||||||
|
const computeAndConcat = toBytecode(`
|
||||||
|
PUSH "Result: "
|
||||||
|
PUSH 10
|
||||||
|
PUSH 5
|
||||||
|
ADD
|
||||||
|
ADD
|
||||||
|
HALT
|
||||||
|
`)
|
||||||
|
|
||||||
|
console.log('\nComputing then concatenating ("Result: " + (10 + 5)):')
|
||||||
|
console.log(await run(computeAndConcat))
|
||||||
|
// Output: { type: 'string', value: 'Result: 15' }
|
||||||
|
|
@ -9,7 +9,7 @@ const bytecode = toBytecode(`
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
vm.registerFunction('print', (...args: Value[]): Value => {
|
vm.set('print', (...args: Value[]): Value => {
|
||||||
console.log(...args.map(toString))
|
console.log(...args.map(toString))
|
||||||
return toNull()
|
return toNull()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "reefvm",
|
"name": "reefvm",
|
||||||
"module": "src/index.ts",
|
"exports": "./src/index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check": "bunx tsc --noEmit"
|
"check": "bunx tsc --noEmit"
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,15 @@ export type Constant =
|
||||||
| Value
|
| Value
|
||||||
| FunctionDef
|
| FunctionDef
|
||||||
|
|
||||||
type Atom = number | string | boolean | null
|
type Atom = RegExp | number | string | boolean | null
|
||||||
|
|
||||||
type InstructionTuple =
|
type InstructionTuple =
|
||||||
// Stack
|
// Stack
|
||||||
| ["PUSH", Atom]
|
| ["PUSH", Atom]
|
||||||
| ["POP"]
|
| ["POP"]
|
||||||
| ["DUP"]
|
| ["DUP"]
|
||||||
|
| ["SWAP"]
|
||||||
|
| ["TYPE"]
|
||||||
|
|
||||||
// Variables
|
// Variables
|
||||||
| ["LOAD", string]
|
| ["LOAD", string]
|
||||||
|
|
@ -32,6 +34,9 @@ type InstructionTuple =
|
||||||
// Arithmetic
|
// Arithmetic
|
||||||
| ["ADD"] | ["SUB"] | ["MUL"] | ["DIV"] | ["MOD"]
|
| ["ADD"] | ["SUB"] | ["MUL"] | ["DIV"] | ["MOD"]
|
||||||
|
|
||||||
|
// Bitwise
|
||||||
|
| ["BIT_AND"] | ["BIT_OR"] | ["BIT_XOR"] | ["BIT_SHL"] | ["BIT_SHR"] | ["BIT_USHR"]
|
||||||
|
|
||||||
// Comparison
|
// Comparison
|
||||||
| ["EQ"] | ["NEQ"] | ["LT"] | ["GT"] | ["LTE"] | ["GTE"]
|
| ["EQ"] | ["NEQ"] | ["LT"] | ["GT"] | ["LTE"] | ["GTE"]
|
||||||
|
|
||||||
|
|
@ -39,9 +44,9 @@ type InstructionTuple =
|
||||||
| ["NOT"]
|
| ["NOT"]
|
||||||
|
|
||||||
// Control flow
|
// Control flow
|
||||||
| ["JUMP", string | number]
|
| ["JUMP", string]
|
||||||
| ["JUMP_IF_FALSE", string | number]
|
| ["JUMP_IF_FALSE", string]
|
||||||
| ["JUMP_IF_TRUE", string | number]
|
| ["JUMP_IF_TRUE", string]
|
||||||
| ["BREAK"]
|
| ["BREAK"]
|
||||||
|
|
||||||
// Exception handling
|
// Exception handling
|
||||||
|
|
@ -51,7 +56,7 @@ type InstructionTuple =
|
||||||
| ["THROW"]
|
| ["THROW"]
|
||||||
|
|
||||||
// Functions
|
// Functions
|
||||||
| ["MAKE_FUNCTION", string[], string | number]
|
| ["MAKE_FUNCTION", string[], string]
|
||||||
| ["CALL"]
|
| ["CALL"]
|
||||||
| ["TAIL_CALL"]
|
| ["TAIL_CALL"]
|
||||||
| ["RETURN"]
|
| ["RETURN"]
|
||||||
|
|
@ -70,8 +75,11 @@ type InstructionTuple =
|
||||||
| ["DICT_SET"]
|
| ["DICT_SET"]
|
||||||
| ["DICT_HAS"]
|
| ["DICT_HAS"]
|
||||||
|
|
||||||
// Native
|
// Strings
|
||||||
| ["CALL_NATIVE", string]
|
| ["STR_CONCAT", number]
|
||||||
|
|
||||||
|
// Arrays and dicts
|
||||||
|
| ["DOT_GET"]
|
||||||
|
|
||||||
// Special
|
// Special
|
||||||
| ["HALT"]
|
| ["HALT"]
|
||||||
|
|
@ -80,30 +88,6 @@ type LabelDefinition = [string] // Just ".label_name:"
|
||||||
|
|
||||||
export type ProgramItem = InstructionTuple | LabelDefinition
|
export type ProgramItem = InstructionTuple | LabelDefinition
|
||||||
|
|
||||||
//
|
|
||||||
// Parse bytecode from human-readable string format.
|
|
||||||
// Operand types are determined by prefix/literal:
|
|
||||||
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
|
|
||||||
// .label -> label reference (e.g., JUMP .loop_start, MAKE_FUNCTION (x y) .body)
|
|
||||||
// name -> variable/function name (e.g., LOAD x, CALL_NATIVE add)
|
|
||||||
// 42 -> number constant (e.g., PUSH 42)
|
|
||||||
// "str" -> string constant (e.g., PUSH "hello")
|
|
||||||
// 'str' -> string constant (e.g., PUSH 'hello')
|
|
||||||
// true -> boolean constant (e.g., PUSH true)
|
|
||||||
// false -> boolean constant (e.g., PUSH false)
|
|
||||||
// null -> null constant (e.g., PUSH null)
|
|
||||||
//
|
|
||||||
// Labels:
|
|
||||||
// .label_name: -> label definition (marks current instruction position)
|
|
||||||
//
|
|
||||||
// Function definitions:
|
|
||||||
// MAKE_FUNCTION (x y) #7 -> basic function (numeric offset)
|
|
||||||
// MAKE_FUNCTION (x y) .body -> basic function (label reference)
|
|
||||||
// MAKE_FUNCTION (x y=42) #7 -> with defaults
|
|
||||||
// MAKE_FUNCTION (x ...rest) #7 -> variadic
|
|
||||||
// MAKE_FUNCTION (x @named) #7 -> named
|
|
||||||
//
|
|
||||||
|
|
||||||
function parseFunctionParams(paramStr: string, constants: Constant[]): {
|
function parseFunctionParams(paramStr: string, constants: Constant[]): {
|
||||||
params: string[]
|
params: string[]
|
||||||
defaults: Record<string, number>
|
defaults: Record<string, number>
|
||||||
|
|
@ -333,12 +317,12 @@ function toBytecodeFromArray(program: ProgramItem[]): Bytecode /* throws */ {
|
||||||
case "STORE":
|
case "STORE":
|
||||||
case "TRY_LOAD":
|
case "TRY_LOAD":
|
||||||
case "TRY_CALL":
|
case "TRY_CALL":
|
||||||
case "CALL_NATIVE":
|
|
||||||
operandValue = operand as string
|
operandValue = operand as string
|
||||||
break
|
break
|
||||||
|
|
||||||
case "MAKE_ARRAY":
|
case "MAKE_ARRAY":
|
||||||
case "MAKE_DICT":
|
case "MAKE_DICT":
|
||||||
|
case "STR_CONCAT":
|
||||||
operandValue = operand as number
|
operandValue = operand as number
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
@ -365,6 +349,29 @@ function toBytecodeFromArray(program: ProgramItem[]): Bytecode /* throws */ {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
////
|
||||||
|
// Parse bytecode from human-readable string format.
|
||||||
|
// Operand types are determined by prefix/literal:
|
||||||
|
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
|
||||||
|
// .label -> label reference (e.g., JUMP .loop_start, MAKE_FUNCTION (x y) .body)
|
||||||
|
// name -> variable/function name (e.g., LOAD x, LOAD_NATIVE add)
|
||||||
|
// 42 -> number constant (e.g., PUSH 42)
|
||||||
|
// "str" -> string constant (e.g., PUSH "hello")
|
||||||
|
// 'str' -> string constant (e.g., PUSH 'hello')
|
||||||
|
// true -> boolean constant (e.g., PUSH true)
|
||||||
|
// false -> boolean constant (e.g., PUSH false)
|
||||||
|
// null -> null constant (e.g., PUSH null)
|
||||||
|
//
|
||||||
|
// Labels:
|
||||||
|
// .label_name: -> label definition (marks current instruction position)
|
||||||
|
//
|
||||||
|
// Function definitions:
|
||||||
|
// MAKE_FUNCTION (x y) #7 -> basic function (numeric offset)
|
||||||
|
// MAKE_FUNCTION (x y) .body -> basic function (label reference)
|
||||||
|
// MAKE_FUNCTION (x y=42) #7 -> with defaults
|
||||||
|
// MAKE_FUNCTION (x ...rest) #7 -> variadic
|
||||||
|
// MAKE_FUNCTION (x @named) #7 -> named
|
||||||
function toBytecodeFromString(str: string): Bytecode /* throws */ {
|
function toBytecodeFromString(str: string): Bytecode /* throws */ {
|
||||||
const lines = str.trim().split("\n")
|
const lines = str.trim().split("\n")
|
||||||
|
|
||||||
|
|
@ -383,7 +390,7 @@ function toBytecodeFromString(str: string): Bytecode /* throws */ {
|
||||||
if (!trimmed) continue
|
if (!trimmed) continue
|
||||||
|
|
||||||
// Check for label definition (.label_name:)
|
// Check for label definition (.label_name:)
|
||||||
if (/^\.[a-zA-Z_][a-zA-Z0-9_]*:$/.test(trimmed)) {
|
if (/^\.[a-zA-Z_][a-zA-Z0-9_.]*:$/.test(trimmed)) {
|
||||||
const labelName = trimmed.slice(1, -1)
|
const labelName = trimmed.slice(1, -1)
|
||||||
labels.set(labelName, cleanLines.length)
|
labels.set(labelName, cleanLines.length)
|
||||||
continue
|
continue
|
||||||
|
|
@ -491,6 +498,18 @@ function toBytecodeFromString(str: string): Bytecode /* throws */ {
|
||||||
bytecode.constants.push(toValue(null))
|
bytecode.constants.push(toValue(null))
|
||||||
operandValue = bytecode.constants.length - 1
|
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 {
|
} else {
|
||||||
// Assume it's a variable name if it doesn't match any other pattern
|
// Assume it's a variable name if it doesn't match any other pattern
|
||||||
// This allows emoji, Unicode, and other creative identifiers
|
// This allows emoji, Unicode, and other creative identifiers
|
||||||
|
|
|
||||||
202
src/format.ts
Normal file
202
src/format.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
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
Normal file
112
src/function.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
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,12 +1,17 @@
|
||||||
import type { Bytecode } from "./bytecode"
|
import type { Bytecode } from "./bytecode"
|
||||||
import { type Value } from "./value"
|
import type { Value } from "./value"
|
||||||
import { VM } from "./vm"
|
import { VM } from "./vm"
|
||||||
|
|
||||||
export async function run(bytecode: Bytecode): Promise<Value> {
|
export async function run(bytecode: Bytecode, globals?: Record<string, any>): Promise<Value> {
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode, globals)
|
||||||
return await vm.run()
|
return await vm.run()
|
||||||
}
|
}
|
||||||
|
|
||||||
export { type Bytecode, toBytecode } from "./bytecode"
|
export { type Bytecode, toBytecode, type ProgramItem } from "./bytecode"
|
||||||
export { type Value, toValue, toString, toNumber, fromValue, toNull, wrapNative } from "./value"
|
export { bytecodeToString } from "./format"
|
||||||
|
export { wrapNative, isWrapped, type ParamInfo, extractParamInfo, getOriginalFunction } from "./function"
|
||||||
|
export { OpCode } from "./opcode"
|
||||||
|
export { Scope } from "./scope"
|
||||||
|
export type { Value, TypeScriptFunction, NativeFunction } from "./value"
|
||||||
|
export { isValue, toValue, toString, toNumber, fromValue, toNull, fnFromValue } from "./value"
|
||||||
export { VM } from "./vm"
|
export { VM } from "./vm"
|
||||||
|
|
@ -3,12 +3,16 @@ export enum OpCode {
|
||||||
PUSH, // operand: constant index (number) | stack: [] → [value]
|
PUSH, // operand: constant index (number) | stack: [] → [value]
|
||||||
POP, // operand: none | stack: [value] → []
|
POP, // operand: none | stack: [value] → []
|
||||||
DUP, // operand: none | stack: [value] → [value, value]
|
DUP, // operand: none | stack: [value] → [value, value]
|
||||||
|
SWAP, // operand: none | stack: [value1, value2] → [value2, value1]
|
||||||
|
|
||||||
// variables
|
// variables
|
||||||
LOAD, // operand: variable name (identifier) | stack: [] → [value]
|
LOAD, // operand: variable name (identifier) | stack: [] → [value]
|
||||||
STORE, // operand: variable name (identifier) | stack: [value] → []
|
STORE, // operand: variable name (identifier) | stack: [value] → []
|
||||||
TRY_LOAD, // operand: variable name (identifier) | stack: [] → [value] | load a variable (if found) or a string
|
TRY_LOAD, // operand: variable name (identifier) | stack: [] → [value] | load a variable (if found) or a string
|
||||||
|
|
||||||
|
// information
|
||||||
|
TYPE, // operand: none | stack: [a] → []
|
||||||
|
|
||||||
// math (coerce to number, pop 2, push result)
|
// math (coerce to number, pop 2, push result)
|
||||||
ADD, // operand: none | stack: [a, b] → [a + b]
|
ADD, // operand: none | stack: [a, b] → [a + b]
|
||||||
SUB, // operand: none | stack: [a, b] → [a - b]
|
SUB, // operand: none | stack: [a, b] → [a - b]
|
||||||
|
|
@ -16,6 +20,14 @@ export enum OpCode {
|
||||||
DIV, // operand: none | stack: [a, b] → [a / b]
|
DIV, // operand: none | stack: [a, b] → [a / b]
|
||||||
MOD, // operand: none | stack: [a, b] → [a % b]
|
MOD, // operand: none | stack: [a, b] → [a % b]
|
||||||
|
|
||||||
|
// bitwise operations (coerce to 32-bit integers, pop 2, push result)
|
||||||
|
BIT_AND, // operand: none | stack: [a, b] → [a & b]
|
||||||
|
BIT_OR, // operand: none | stack: [a, b] → [a | b]
|
||||||
|
BIT_XOR, // operand: none | stack: [a, b] → [a ^ b]
|
||||||
|
BIT_SHL, // operand: none | stack: [a, b] → [a << b]
|
||||||
|
BIT_SHR, // operand: none | stack: [a, b] → [a >> b] (sign-preserving)
|
||||||
|
BIT_USHR, // operand: none | stack: [a, b] → [a >>> b] (zero-fill)
|
||||||
|
|
||||||
// comparison (pop 2, push boolean)
|
// comparison (pop 2, push boolean)
|
||||||
EQ, // operand: none | stack: [a, b] → [a == b] (deep equality)
|
EQ, // operand: none | stack: [a, b] → [a == b] (deep equality)
|
||||||
NEQ, // operand: none | stack: [a, b] → [a != b]
|
NEQ, // operand: none | stack: [a, b] → [a != b]
|
||||||
|
|
@ -59,8 +71,11 @@ export enum OpCode {
|
||||||
DICT_SET, // operand: none | stack: [dict, key, value] → [] | mutates dict
|
DICT_SET, // operand: none | stack: [dict, key, value] → [] | mutates dict
|
||||||
DICT_HAS, // operand: none | stack: [dict, key] → [boolean]
|
DICT_HAS, // operand: none | stack: [dict, key] → [boolean]
|
||||||
|
|
||||||
// typescript interop
|
// arrays and dicts
|
||||||
CALL_NATIVE, // operand: function name (identifier) | stack: [...args] → [result] | consumes entire stack
|
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
|
||||||
|
|
||||||
// special
|
// special
|
||||||
HALT // operand: none | stop execution
|
HALT // operand: none | stop execution
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,15 @@ export class Scope {
|
||||||
has(name: string): boolean {
|
has(name: string): boolean {
|
||||||
return this.locals.has(name) || this.parent?.has(name) || false
|
return this.locals.has(name) || this.parent?.has(name) || false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vars(): string[] {
|
||||||
|
const vars = new Set(this.parent?.vars())
|
||||||
|
|
||||||
|
for (const name of this.locals.keys())
|
||||||
|
vars.add(name)
|
||||||
|
|
||||||
|
return [...vars].sort()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,18 +43,26 @@ const OPCODES_WITH_OPERANDS = new Set([
|
||||||
OpCode.PUSH_FINALLY,
|
OpCode.PUSH_FINALLY,
|
||||||
OpCode.MAKE_ARRAY,
|
OpCode.MAKE_ARRAY,
|
||||||
OpCode.MAKE_DICT,
|
OpCode.MAKE_DICT,
|
||||||
|
OpCode.STR_CONCAT,
|
||||||
OpCode.MAKE_FUNCTION,
|
OpCode.MAKE_FUNCTION,
|
||||||
OpCode.CALL_NATIVE,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const OPCODES_WITHOUT_OPERANDS = new Set([
|
const OPCODES_WITHOUT_OPERANDS = new Set([
|
||||||
OpCode.POP,
|
OpCode.POP,
|
||||||
OpCode.DUP,
|
OpCode.DUP,
|
||||||
|
OpCode.SWAP,
|
||||||
|
OpCode.TYPE,
|
||||||
OpCode.ADD,
|
OpCode.ADD,
|
||||||
OpCode.SUB,
|
OpCode.SUB,
|
||||||
OpCode.MUL,
|
OpCode.MUL,
|
||||||
OpCode.DIV,
|
OpCode.DIV,
|
||||||
OpCode.MOD,
|
OpCode.MOD,
|
||||||
|
OpCode.BIT_AND,
|
||||||
|
OpCode.BIT_OR,
|
||||||
|
OpCode.BIT_XOR,
|
||||||
|
OpCode.BIT_SHL,
|
||||||
|
OpCode.BIT_SHR,
|
||||||
|
OpCode.BIT_USHR,
|
||||||
OpCode.EQ,
|
OpCode.EQ,
|
||||||
OpCode.NEQ,
|
OpCode.NEQ,
|
||||||
OpCode.LT,
|
OpCode.LT,
|
||||||
|
|
@ -76,13 +84,18 @@ const OPCODES_WITHOUT_OPERANDS = new Set([
|
||||||
OpCode.DICT_GET,
|
OpCode.DICT_GET,
|
||||||
OpCode.DICT_SET,
|
OpCode.DICT_SET,
|
||||||
OpCode.DICT_HAS,
|
OpCode.DICT_HAS,
|
||||||
|
OpCode.DOT_GET,
|
||||||
])
|
])
|
||||||
|
|
||||||
// immediate = immediate number, eg #5
|
// JUMP* instructions require labels only (no numeric immediates)
|
||||||
const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
|
const OPCODES_REQUIRING_LABEL = new Set([
|
||||||
OpCode.JUMP,
|
OpCode.JUMP,
|
||||||
OpCode.JUMP_IF_FALSE,
|
OpCode.JUMP_IF_FALSE,
|
||||||
OpCode.JUMP_IF_TRUE,
|
OpCode.JUMP_IF_TRUE,
|
||||||
|
])
|
||||||
|
|
||||||
|
// PUSH_TRY/PUSH_FINALLY still allow immediate or label
|
||||||
|
const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
|
||||||
OpCode.PUSH_TRY,
|
OpCode.PUSH_TRY,
|
||||||
OpCode.PUSH_FINALLY,
|
OpCode.PUSH_FINALLY,
|
||||||
])
|
])
|
||||||
|
|
@ -91,6 +104,7 @@ const OPCODES_REQUIRING_IMMEDIATE_OR_LABEL = new Set([
|
||||||
const OPCODES_REQUIRING_IMMEDIATE = new Set([
|
const OPCODES_REQUIRING_IMMEDIATE = new Set([
|
||||||
OpCode.MAKE_ARRAY,
|
OpCode.MAKE_ARRAY,
|
||||||
OpCode.MAKE_DICT,
|
OpCode.MAKE_DICT,
|
||||||
|
OpCode.STR_CONCAT,
|
||||||
])
|
])
|
||||||
|
|
||||||
export function validateBytecode(source: string): ValidationResult {
|
export function validateBytecode(source: string): ValidationResult {
|
||||||
|
|
@ -187,6 +201,16 @@ export function validateBytecode(source: string): ValidationResult {
|
||||||
|
|
||||||
// Validate specific operand formats
|
// Validate specific operand formats
|
||||||
if (operand) {
|
if (operand) {
|
||||||
|
if (OPCODES_REQUIRING_LABEL.has(opCode)) {
|
||||||
|
if (!operand.startsWith('.')) {
|
||||||
|
errors.push({
|
||||||
|
line: lineNum,
|
||||||
|
message: `${opName} requires label (.label), got: ${operand}`,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (OPCODES_REQUIRING_IMMEDIATE_OR_LABEL.has(opCode)) {
|
if (OPCODES_REQUIRING_IMMEDIATE_OR_LABEL.has(opCode)) {
|
||||||
if (!operand.startsWith('#') && !operand.startsWith('.')) {
|
if (!operand.startsWith('#') && !operand.startsWith('.')) {
|
||||||
errors.push({
|
errors.push({
|
||||||
|
|
@ -300,11 +324,11 @@ export function validateBytecode(source: string): ValidationResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate body address
|
// Validate body address (must be a label)
|
||||||
if (!bodyAddr!.startsWith('.') && !bodyAddr!.startsWith('#')) {
|
if (!bodyAddr!.startsWith('.')) {
|
||||||
errors.push({
|
errors.push({
|
||||||
line: lineNum,
|
line: lineNum,
|
||||||
message: `Invalid body address: expected .label or #offset`,
|
message: `Invalid body address: expected .label, got: ${bodyAddr}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
200
src/value.ts
200
src/value.ts
|
|
@ -1,4 +1,12 @@
|
||||||
|
import { wrapNative, getOriginalFunction } from "./function"
|
||||||
|
import { OpCode } from "./opcode"
|
||||||
import { Scope } from "./scope"
|
import { Scope } from "./scope"
|
||||||
|
import { VM } from "./vm"
|
||||||
|
|
||||||
|
export type NativeFunction = (...args: Value[]) => Promise<Value> | Value
|
||||||
|
export type TypeScriptFunction = (this: VM, ...args: any[]) => any
|
||||||
|
const REEF_FUNCTION = Symbol('__reefFunction')
|
||||||
|
const VALUE_TYPES = new Set(['null', 'boolean', 'number', 'string', 'array', 'dict', 'regex', 'native', 'function'])
|
||||||
|
|
||||||
export type Value =
|
export type Value =
|
||||||
| { type: 'null', value: null }
|
| { type: 'null', value: null }
|
||||||
|
|
@ -7,6 +15,8 @@ export type Value =
|
||||||
| { type: 'string', value: string }
|
| { type: 'string', value: string }
|
||||||
| { type: 'array', value: Value[] }
|
| { type: 'array', value: Value[] }
|
||||||
| { type: 'dict', value: Dict }
|
| { type: 'dict', value: Dict }
|
||||||
|
| { type: 'regex', value: RegExp }
|
||||||
|
| { type: 'native', fn: NativeFunction, value: '<function>' }
|
||||||
| {
|
| {
|
||||||
type: 'function',
|
type: 'function',
|
||||||
params: string[],
|
params: string[],
|
||||||
|
|
@ -29,15 +39,22 @@ export type FunctionDef = {
|
||||||
named: boolean
|
named: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toValue(v: any): Value /* throws */ {
|
export function isValue(v: any): boolean {
|
||||||
|
return !!(v && typeof v === 'object' && VALUE_TYPES.has(v.type) && 'value' in v)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toValue(v: any, vm?: VM): Value /* throws */ {
|
||||||
if (v === null || v === undefined)
|
if (v === null || v === undefined)
|
||||||
return { type: 'null', value: null }
|
return { type: 'null', value: null }
|
||||||
|
|
||||||
if (v && typeof v === 'object' && 'type' in v && 'value' in v)
|
if (isValue(v))
|
||||||
return v as Value
|
return v as Value
|
||||||
|
|
||||||
if (Array.isArray(v))
|
if (Array.isArray(v))
|
||||||
return { type: 'array', value: v.map(toValue) }
|
return { type: 'array', value: v.map(x => toValue(x, vm)) }
|
||||||
|
|
||||||
|
if (v instanceof RegExp)
|
||||||
|
return { type: 'regex', value: v }
|
||||||
|
|
||||||
switch (typeof v) {
|
switch (typeof v) {
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
|
|
@ -47,28 +64,53 @@ export function toValue(v: any): Value /* throws */ {
|
||||||
case 'string':
|
case 'string':
|
||||||
return { type: 'string', value: v }
|
return { type: 'string', value: v }
|
||||||
case 'function':
|
case 'function':
|
||||||
throw "can't toValue() a js function yet"
|
if ((v as any)[REEF_FUNCTION])
|
||||||
|
return (v as any)[REEF_FUNCTION]
|
||||||
|
|
||||||
|
let fn = vm ? wrapNative(vm, v) : cantCallFunctionWithoutVM(v)
|
||||||
|
return { type: 'native', fn, value: '<function>' }
|
||||||
case 'object':
|
case 'object':
|
||||||
const dict: Dict = new Map()
|
const dict: Dict = new Map()
|
||||||
|
|
||||||
for (const key of Object.keys(v))
|
for (const key of Object.keys(v)) dict.set(key, toValue(v[key], vm))
|
||||||
dict.set(key, toValue(v[key]))
|
|
||||||
|
|
||||||
return { type: 'dict', value: dict }
|
return { type: 'dict', value: dict }
|
||||||
default:
|
default:
|
||||||
throw `can't toValue this: ${v}`
|
throw new Error(`can't toValue this: ${v}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cantCallFunctionWithoutVM(fn: Function) {
|
||||||
|
const name = fn.name || '<anonymous>'
|
||||||
|
const str = fn.toString().slice(0, 100)
|
||||||
|
return (...args: Value[]) => {
|
||||||
|
const stack = new Error().stack || ''
|
||||||
|
const stackLines = stack.split('\n')
|
||||||
|
.slice(1)
|
||||||
|
.filter(line => !line.includes('toValue'))
|
||||||
|
.map(line => ' ' + line.trim())
|
||||||
|
.join('\n')
|
||||||
|
throw new Error(
|
||||||
|
`can't call function that was converted without a vm\n` +
|
||||||
|
` Function: ${name}\n` +
|
||||||
|
` Source: ${str}${str.length > 100 ? '...' : ''}\n` +
|
||||||
|
` Called from:\n${stackLines}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toNumber(v: Value): number {
|
export function toNumber(v: Value): number {
|
||||||
switch (v.type) {
|
switch (v.type) {
|
||||||
case 'number': return v.value
|
case 'number':
|
||||||
case 'boolean': return v.value ? 1 : 0
|
return v.value
|
||||||
|
case 'boolean':
|
||||||
|
return v.value ? 1 : 0
|
||||||
case 'string': {
|
case 'string': {
|
||||||
const parsed = parseFloat(v.value)
|
const parsed = parseFloat(v.value)
|
||||||
return isNaN(parsed) ? 0 : parsed
|
return isNaN(parsed) ? 0 : parsed
|
||||||
}
|
}
|
||||||
default: return 0
|
default:
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,17 +127,28 @@ export function isTrue(v: Value): boolean {
|
||||||
|
|
||||||
export function toString(v: Value): string {
|
export function toString(v: Value): string {
|
||||||
switch (v.type) {
|
switch (v.type) {
|
||||||
case 'string': return v.value
|
case 'string':
|
||||||
case 'number': return String(v.value)
|
return v.value
|
||||||
case 'boolean': return String(v.value)
|
case 'number':
|
||||||
case 'null': return 'null'
|
return String(v.value)
|
||||||
case 'function': return '<function>'
|
case 'boolean':
|
||||||
case 'array': return `[${v.value.map(toString).join(', ')}]`
|
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 'dict': {
|
case 'dict': {
|
||||||
const pairs = Array.from(v.value.entries())
|
const pairs = Array.from(v.value.entries()).map(([k, v]) => `${k}: ${toString(v)}`)
|
||||||
.map(([k, v]) => `${k}: ${toString(v)}`)
|
|
||||||
return `{${pairs.join(', ')}}`
|
return `{${pairs.join(', ')}}`
|
||||||
}
|
}
|
||||||
|
case 'regex':
|
||||||
|
return String(v.value)
|
||||||
|
default:
|
||||||
|
return String(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,10 +156,12 @@ export function isEqual(a: Value, b: Value): boolean {
|
||||||
if (a.type !== b.type) return false
|
if (a.type !== b.type) return false
|
||||||
|
|
||||||
switch (a.type) {
|
switch (a.type) {
|
||||||
case 'null': return true
|
case 'null':
|
||||||
case 'boolean': return a.value === (b as typeof a).value
|
return true
|
||||||
case 'number': return a.value === (b as typeof a).value
|
case 'boolean':
|
||||||
case 'string': return a.value === (b as typeof a).value
|
case 'number':
|
||||||
|
case 'string':
|
||||||
|
return a.value === b.value
|
||||||
case 'array': {
|
case 'array': {
|
||||||
const bArr = b as typeof a
|
const bArr = b as typeof a
|
||||||
if (a.value.length !== bArr.value.length) return false
|
if (a.value.length !== bArr.value.length) return false
|
||||||
|
|
@ -121,19 +176,48 @@ export function isEqual(a: Value, b: Value): boolean {
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
case 'function': return false // functions never equal
|
case 'regex': {
|
||||||
|
return String(a.value) === String(b.value)
|
||||||
|
}
|
||||||
|
case 'function':
|
||||||
|
case 'native':
|
||||||
|
return false // functions never equal
|
||||||
|
default:
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fromValue(v: Value): any {
|
export function fromValue(v: Value, vm?: VM): any {
|
||||||
switch (v.type) {
|
switch (v.type) {
|
||||||
case 'null': return null
|
case 'null':
|
||||||
case 'boolean': return v.value
|
case 'boolean':
|
||||||
case 'number': return v.value
|
case 'number':
|
||||||
case 'string': return v.value
|
case 'string':
|
||||||
case 'array': return v.value.map(fromValue)
|
return v.value
|
||||||
case 'dict': return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v)]))
|
case 'array':
|
||||||
case 'function': return '<function>'
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -141,21 +225,49 @@ export function toNull(): Value {
|
||||||
return toValue(null)
|
return toValue(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const WRAPPED_MARKER = Symbol('reef-wrapped')
|
export function fnFromValue(fn: Value, vm: VM): Function {
|
||||||
|
if (fn.type !== 'function')
|
||||||
|
throw new Error('Value is not a function')
|
||||||
|
|
||||||
export function wrapNative(fn: Function): (...args: Value[]) => Promise<Value> {
|
const wrapper = async function (...args: any[]) {
|
||||||
const wrapped = async (...values: Value[]) => {
|
let positional: any[] = args
|
||||||
const nativeArgs = values.map(fromValue)
|
let named: Record<string, any> = {}
|
||||||
const result = await fn(...nativeArgs)
|
|
||||||
return toValue(result)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrappedObj = wrapped as any
|
// support roundtrips, eg fromValue(toValue(fn))
|
||||||
wrappedObj[WRAPPED_MARKER] = true
|
; (wrapper as any)[REEF_FUNCTION] = fn
|
||||||
|
|
||||||
return wrapped
|
return wrapper
|
||||||
}
|
|
||||||
|
|
||||||
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 { type Frame } from "./frame"
|
||||||
import { OpCode } from "./opcode"
|
import { OpCode } from "./opcode"
|
||||||
import { Scope } from "./scope"
|
import { Scope } from "./scope"
|
||||||
import { type Value, toValue, toNumber, isTrue, isEqual, toString, wrapNative, isWrapped } from "./value"
|
import type { Value, NativeFunction, TypeScriptFunction } from "./value"
|
||||||
|
import { toValue, toNumber, isTrue, isEqual, toString, fromValue, fnFromValue } from "./value"
|
||||||
type NativeFunction = (...args: Value[]) => Promise<Value> | Value
|
import { extractParamInfo, getOriginalFunction } from "./function"
|
||||||
|
|
||||||
export class VM {
|
export class VM {
|
||||||
pc = 0
|
pc = 0
|
||||||
|
|
@ -19,20 +19,69 @@ export class VM {
|
||||||
labels: Map<number, string> = new Map()
|
labels: Map<number, string> = new Map()
|
||||||
nativeFunctions: Map<string, NativeFunction> = new Map()
|
nativeFunctions: Map<string, NativeFunction> = new Map()
|
||||||
|
|
||||||
constructor(bytecode: Bytecode) {
|
constructor(bytecode: Bytecode, globals?: Record<string, any>) {
|
||||||
this.instructions = bytecode.instructions
|
this.instructions = bytecode.instructions
|
||||||
this.constants = bytecode.constants
|
this.constants = bytecode.constants
|
||||||
this.labels = bytecode.labels || new Map()
|
this.labels = bytecode.labels || new Map()
|
||||||
this.scope = new Scope()
|
this.scope = new Scope()
|
||||||
|
|
||||||
|
if (globals) {
|
||||||
|
for (const name of Object.keys(globals ?? {}))
|
||||||
|
this.set(name, globals[name])
|
||||||
|
|
||||||
|
this.scope = new Scope(this.scope)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerFunction(name: string, fn: Function) {
|
async call(name: string, ...args: any) {
|
||||||
const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(fn)
|
const value = this.scope.get(name)
|
||||||
this.nativeFunctions.set(name, wrapped)
|
|
||||||
|
if (!value) throw new Error(`Can't find ${name}`)
|
||||||
|
if (value.type !== 'function' && value.type !== 'native')
|
||||||
|
throw new Error(`Can't call ${name}`)
|
||||||
|
|
||||||
|
if (value.type === 'native') {
|
||||||
|
return await this.callNative(value.fn, args)
|
||||||
|
} else {
|
||||||
|
const fn = fnFromValue(value, this)
|
||||||
|
return await fn(...args)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
registerValueFunction(name: string, fn: NativeFunction) {
|
has(name: string): boolean {
|
||||||
this.nativeFunctions.set(name, fn)
|
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!
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<Value> {
|
async run(): Promise<Value> {
|
||||||
|
|
@ -48,6 +97,57 @@ export class VM {
|
||||||
return this.stack[this.stack.length - 1] || toValue(null)
|
return this.stack[this.stack.length - 1] || toValue(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resume execution from current PC without resetting
|
||||||
|
// Useful for REPL mode where you append bytecode incrementally
|
||||||
|
async continue(): Promise<Value> {
|
||||||
|
this.stopped = false
|
||||||
|
|
||||||
|
while (!this.stopped && this.pc < this.instructions.length) {
|
||||||
|
const instruction = this.instructions[this.pc]!
|
||||||
|
await this.execute(instruction)
|
||||||
|
this.pc++
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.stack[this.stack.length - 1] || toValue(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper for REPL mode: append new bytecode with proper constant index remapping
|
||||||
|
appendBytecode(bytecode: Bytecode): void {
|
||||||
|
const constantOffset = this.constants.length
|
||||||
|
const instructionOffset = this.instructions.length
|
||||||
|
|
||||||
|
// Remap function body addresses in constants before adding them
|
||||||
|
for (const constant of bytecode.constants) {
|
||||||
|
if (constant.type === 'function_def') {
|
||||||
|
this.constants.push({ ...constant, body: constant.body + instructionOffset })
|
||||||
|
} else {
|
||||||
|
this.constants.push(constant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const instruction of bytecode.instructions) {
|
||||||
|
if (instruction.operand !== undefined && typeof instruction.operand === 'number') {
|
||||||
|
// Opcodes that reference constants need their operand adjusted
|
||||||
|
if (instruction.op === OpCode.PUSH || instruction.op === OpCode.MAKE_FUNCTION) {
|
||||||
|
this.instructions.push({
|
||||||
|
op: instruction.op,
|
||||||
|
operand: instruction.operand + constantOffset
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.instructions.push(instruction)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.instructions.push(instruction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bytecode.labels) {
|
||||||
|
for (const [addr, label] of bytecode.labels.entries()) {
|
||||||
|
this.labels.set(addr + instructionOffset, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async execute(instruction: Instruction) /* throws */ {
|
async execute(instruction: Instruction) /* throws */ {
|
||||||
switch (instruction.op) {
|
switch (instruction.op) {
|
||||||
case OpCode.PUSH:
|
case OpCode.PUSH:
|
||||||
|
|
@ -68,8 +168,32 @@ export class VM {
|
||||||
this.stack.push(this.stack[this.stack.length - 1]!)
|
this.stack.push(this.stack[this.stack.length - 1]!)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case OpCode.SWAP:
|
||||||
|
const first = this.stack.pop()!
|
||||||
|
const second = this.stack.pop()!
|
||||||
|
this.stack.push(first)
|
||||||
|
this.stack.push(second)
|
||||||
|
break
|
||||||
|
|
||||||
case OpCode.ADD:
|
case OpCode.ADD:
|
||||||
this.binaryOp((a, b) => toNumber(a) + toNumber(b))
|
const b = this.stack.pop()!
|
||||||
|
const a = this.stack.pop()!
|
||||||
|
|
||||||
|
if (a.type === 'string' || b.type === 'string') {
|
||||||
|
this.stack.push(toValue(toString(a) + toString(b)))
|
||||||
|
} else if (a.type === 'array' && b.type === 'array') {
|
||||||
|
this.stack.push({ type: 'array', value: [...a.value, ...b.value] })
|
||||||
|
} else if (a.type === 'dict' && b.type === 'dict') {
|
||||||
|
const merged = new Map(a.value)
|
||||||
|
for (const [key, value] of b.value) {
|
||||||
|
merged.set(key, value)
|
||||||
|
}
|
||||||
|
this.stack.push({ type: 'dict', value: merged })
|
||||||
|
} else if (a.type === 'number' && b.type === 'number') {
|
||||||
|
this.stack.push(toValue(a.value + b.value))
|
||||||
|
} else {
|
||||||
|
throw new Error(`ADD: Cannot add ${a.type} and ${b.type}`)
|
||||||
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case OpCode.SUB:
|
case OpCode.SUB:
|
||||||
|
|
@ -117,6 +241,31 @@ export class VM {
|
||||||
this.stack.push({ type: 'boolean', value: !isTrue(val) })
|
this.stack.push({ type: 'boolean', value: !isTrue(val) })
|
||||||
break
|
break
|
||||||
|
|
||||||
|
// Bitwise operations
|
||||||
|
case OpCode.BIT_AND:
|
||||||
|
this.binaryOp((a, b) => (toNumber(a) | 0) & (toNumber(b) | 0))
|
||||||
|
break
|
||||||
|
|
||||||
|
case OpCode.BIT_OR:
|
||||||
|
this.binaryOp((a, b) => (toNumber(a) | 0) | (toNumber(b) | 0))
|
||||||
|
break
|
||||||
|
|
||||||
|
case OpCode.BIT_XOR:
|
||||||
|
this.binaryOp((a, b) => (toNumber(a) | 0) ^ (toNumber(b) | 0))
|
||||||
|
break
|
||||||
|
|
||||||
|
case OpCode.BIT_SHL:
|
||||||
|
this.binaryOp((a, b) => (toNumber(a) | 0) << (toNumber(b) | 0))
|
||||||
|
break
|
||||||
|
|
||||||
|
case OpCode.BIT_SHR:
|
||||||
|
this.binaryOp((a, b) => (toNumber(a) | 0) >> (toNumber(b) | 0))
|
||||||
|
break
|
||||||
|
|
||||||
|
case OpCode.BIT_USHR:
|
||||||
|
this.binaryOp((a, b) => (toNumber(a) | 0) >>> (toNumber(b) | 0))
|
||||||
|
break
|
||||||
|
|
||||||
case OpCode.HALT:
|
case OpCode.HALT:
|
||||||
this.stopped = true
|
this.stopped = true
|
||||||
break
|
break
|
||||||
|
|
@ -137,13 +286,18 @@ export class VM {
|
||||||
const value = this.scope.get(varName)
|
const value = this.scope.get(varName)
|
||||||
|
|
||||||
if (value === undefined)
|
if (value === undefined)
|
||||||
this.stack.push(toValue(varName))
|
this.stack.push(toValue(varName, this))
|
||||||
else
|
else
|
||||||
this.stack.push(value)
|
this.stack.push(value)
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case OpCode.TYPE:
|
||||||
|
const value = this.stack.pop()!
|
||||||
|
this.stack.push(toValue(value.type))
|
||||||
|
break
|
||||||
|
|
||||||
case OpCode.STORE:
|
case OpCode.STORE:
|
||||||
const name = instruction.operand as string
|
const name = instruction.operand as string
|
||||||
const toStore = this.stack.pop()!
|
const toStore = this.stack.pop()!
|
||||||
|
|
@ -334,6 +488,32 @@ export class VM {
|
||||||
this.stack.push({ type: 'boolean', value: hasDict.value.has(toString(hasKey)) })
|
this.stack.push({ type: 'boolean', value: hasDict.value.has(toString(hasKey)) })
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case OpCode.DOT_GET: {
|
||||||
|
const index = this.stack.pop()!
|
||||||
|
const target = this.stack.pop()!
|
||||||
|
|
||||||
|
if (target.type === 'array')
|
||||||
|
this.stack.push(toValue(target.value?.[Number(index.value)], this))
|
||||||
|
|
||||||
|
else if (target.type === 'dict')
|
||||||
|
this.stack.push(toValue(target.value?.get(String(index.value)), this))
|
||||||
|
|
||||||
|
else
|
||||||
|
throw new Error(`DOT_GET: ${target.type} not supported`)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case OpCode.STR_CONCAT:
|
||||||
|
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:
|
case OpCode.MAKE_FUNCTION:
|
||||||
const fnDefIdx = instruction.operand as number
|
const fnDefIdx = instruction.operand as number
|
||||||
const fnDef = this.constants[fnDefIdx]
|
const fnDef = this.constants[fnDefIdx]
|
||||||
|
|
@ -353,16 +533,17 @@ export class VM {
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
case OpCode.TRY_CALL: {
|
case OpCode.TRY_CALL: {
|
||||||
const varName = instruction.operand as string
|
const varName = instruction.operand as string
|
||||||
const value = this.scope.get(varName)
|
const value = this.scope.get(varName)
|
||||||
|
|
||||||
if (value?.type === 'function') {
|
if (value?.type === 'function' || value?.type === 'native') {
|
||||||
this.stack.push(value)
|
this.stack.push(value)
|
||||||
this.stack.push(toValue(0))
|
this.stack.push(toValue(0))
|
||||||
this.stack.push(toValue(0))
|
this.stack.push(toValue(0))
|
||||||
// No `break` here -- we want to fall through to OpCode.CALL!
|
this.instructions[this.pc] = { op: OpCode.CALL }
|
||||||
|
this.pc--
|
||||||
|
break
|
||||||
} else if (value) {
|
} else if (value) {
|
||||||
this.stack.push(value)
|
this.stack.push(value)
|
||||||
break
|
break
|
||||||
|
|
@ -372,8 +553,6 @@ export class VM {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't put any `case` statement here - `TRY_CALL` MUST go before `CALL!`
|
|
||||||
|
|
||||||
case OpCode.CALL: {
|
case OpCode.CALL: {
|
||||||
// Pop named count from stack (top)
|
// Pop named count from stack (top)
|
||||||
const namedCount = toNumber(this.stack.pop()!)
|
const namedCount = toNumber(this.stack.pop()!)
|
||||||
|
|
@ -391,9 +570,8 @@ export class VM {
|
||||||
const key = this.stack.pop()!
|
const key = this.stack.pop()!
|
||||||
namedPairs.unshift({ key: toString(key), value })
|
namedPairs.unshift({ key: toString(key), value })
|
||||||
}
|
}
|
||||||
for (const pair of namedPairs) {
|
for (const pair of namedPairs)
|
||||||
namedArgs.set(pair.key, pair.value)
|
namedArgs.set(pair.key, pair.value)
|
||||||
}
|
|
||||||
|
|
||||||
// Pop positional arguments from stack
|
// Pop positional arguments from stack
|
||||||
const positionalArgs: Value[] = []
|
const positionalArgs: Value[] = []
|
||||||
|
|
@ -402,8 +580,110 @@ export class VM {
|
||||||
|
|
||||||
const fn = this.stack.pop()!
|
const fn = this.stack.pop()!
|
||||||
|
|
||||||
|
// Handle native functions
|
||||||
|
if (fn.type === 'native') {
|
||||||
|
// Mark current frame as break target (like regular CALL does)
|
||||||
|
if (this.callStack.length > 0)
|
||||||
|
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
|
||||||
|
|
||||||
|
// Extract parameter info on-demand
|
||||||
|
const originalFn = getOriginalFunction(fn.fn)
|
||||||
|
const paramInfo = extractParamInfo(originalFn)
|
||||||
|
|
||||||
|
// Bind parameters using the same priority as Reef functions
|
||||||
|
const nativeArgs: Value[] = []
|
||||||
|
|
||||||
|
// Determine how many params are fixed (excluding variadic and named)
|
||||||
|
let nativeFixedParamCount = paramInfo.params.length
|
||||||
|
if (paramInfo.variadic) nativeFixedParamCount--
|
||||||
|
if (paramInfo.named) nativeFixedParamCount--
|
||||||
|
|
||||||
|
// Track which positional args have been consumed
|
||||||
|
let nativePositionalArgIndex = 0
|
||||||
|
|
||||||
|
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
||||||
|
// Note: null values trigger defaults (null acts as "use default")
|
||||||
|
for (let i = 0; i < nativeFixedParamCount; i++) {
|
||||||
|
const paramName = paramInfo.params[i]!
|
||||||
|
let paramValue: Value | undefined
|
||||||
|
|
||||||
|
// Check if named argument was provided for this param
|
||||||
|
if (namedArgs.has(paramName)) {
|
||||||
|
paramValue = namedArgs.get(paramName)!
|
||||||
|
namedArgs.delete(paramName) // Remove from named args so it won't go to @named
|
||||||
|
} else if (nativePositionalArgIndex < positionalArgs.length) {
|
||||||
|
paramValue = positionalArgs[nativePositionalArgIndex]!
|
||||||
|
nativePositionalArgIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the parameter value is null and a default exists, use the default
|
||||||
|
if (paramValue?.type === 'null' && paramInfo.defaults[paramName] !== undefined) {
|
||||||
|
nativeArgs.push(paramInfo.defaults[paramName]!)
|
||||||
|
} else if (paramValue) {
|
||||||
|
nativeArgs.push(paramValue)
|
||||||
|
} else if (paramInfo.defaults[paramName] !== undefined) {
|
||||||
|
nativeArgs.push(paramInfo.defaults[paramName]!)
|
||||||
|
} else {
|
||||||
|
nativeArgs.push(toValue(null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle named parameter (collect remaining unmatched named args)
|
||||||
|
// Parameter names matching atXxx pattern (e.g., atOptions, atNamed) collect extra named args
|
||||||
|
if (paramInfo.named) {
|
||||||
|
const namedDict = new Map<string, Value>()
|
||||||
|
for (const [key, value] of namedArgs) {
|
||||||
|
namedDict.set(key, value)
|
||||||
|
}
|
||||||
|
// Convert dict to plain JavaScript object for the native function
|
||||||
|
const namedObj = fromValue({ type: 'dict', value: namedDict }, this)
|
||||||
|
nativeArgs.push(toValue(namedObj, this))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle variadic parameter (TypeScript rest parameters)
|
||||||
|
// For TypeScript functions with ...rest, we spread the remaining args
|
||||||
|
// rather than wrapping them in an array
|
||||||
|
if (paramInfo.variadic) {
|
||||||
|
const remainingArgs = positionalArgs.slice(nativePositionalArgIndex)
|
||||||
|
nativeArgs.push(...remainingArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the native function with bound args
|
||||||
|
try {
|
||||||
|
const result = await fn.fn.call(this, ...nativeArgs)
|
||||||
|
this.stack.push(result)
|
||||||
|
break
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
const errorValue = toValue(errorMessage)
|
||||||
|
|
||||||
|
// no exception handlers, let it crash
|
||||||
|
if (this.exceptionHandlers.length === 0) {
|
||||||
|
throw new Error(errorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use existing THROW logic
|
||||||
|
const throwHandler = this.exceptionHandlers.pop()!
|
||||||
|
|
||||||
|
while (this.callStack.length > throwHandler.callStackDepth)
|
||||||
|
this.callStack.pop()
|
||||||
|
|
||||||
|
this.scope = throwHandler.scope
|
||||||
|
this.stack.push(errorValue)
|
||||||
|
|
||||||
|
// Jump to `finally` if present, otherwise jump to `catch`
|
||||||
|
const targetAddress = throwHandler.finallyAddress !== undefined
|
||||||
|
? throwHandler.finallyAddress
|
||||||
|
: throwHandler.catchAddress
|
||||||
|
|
||||||
|
// subtract 1 because pc will be incremented
|
||||||
|
this.pc = targetAddress - 1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (fn.type !== 'function')
|
if (fn.type !== 'function')
|
||||||
throw new Error('CALL: not a function')
|
throw new Error(`CALL: ${fn.value} is not a function`)
|
||||||
|
|
||||||
if (this.callStack.length > 0)
|
if (this.callStack.length > 0)
|
||||||
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
|
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
|
||||||
|
|
@ -421,16 +701,33 @@ export class VM {
|
||||||
if (fn.variadic) fixedParamCount--
|
if (fn.variadic) fixedParamCount--
|
||||||
if (fn.named) 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
|
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
||||||
|
// Note: null values trigger defaults (null acts as "use default")
|
||||||
for (let i = 0; i < fixedParamCount; i++) {
|
for (let i = 0; i < fixedParamCount; i++) {
|
||||||
const paramName = fn.params[i]!
|
const paramName = fn.params[i]!
|
||||||
|
let paramValue: Value | undefined
|
||||||
|
|
||||||
// Check if named argument was provided for this param
|
// Check if named argument was provided for this param
|
||||||
if (namedArgs.has(paramName)) {
|
if (namedArgs.has(paramName)) {
|
||||||
this.scope.set(paramName, namedArgs.get(paramName)!)
|
paramValue = namedArgs.get(paramName)!
|
||||||
namedArgs.delete(paramName) // Remove from named args so it won't go to named
|
namedArgs.delete(paramName) // Remove from named args so it won't go to named
|
||||||
} else if (positionalArgs[i] !== undefined) {
|
} else if (positionalArgIndex < positionalArgs.length) {
|
||||||
this.scope.set(paramName, positionalArgs[i]!)
|
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 (fn.defaults[paramName] !== undefined) {
|
} else if (fn.defaults[paramName] !== undefined) {
|
||||||
const defaultIdx = fn.defaults[paramName]!
|
const defaultIdx = fn.defaults[paramName]!
|
||||||
const defaultValue = this.constants[defaultIdx]!
|
const defaultValue = this.constants[defaultIdx]!
|
||||||
|
|
@ -445,7 +742,7 @@ export class VM {
|
||||||
// Handle variadic parameter (collect remaining positional args)
|
// Handle variadic parameter (collect remaining positional args)
|
||||||
if (fn.variadic) {
|
if (fn.variadic) {
|
||||||
const variadicParamName = fn.params[fn.params.length - (fn.named ? 2 : 1)]!
|
const variadicParamName = fn.params[fn.params.length - (fn.named ? 2 : 1)]!
|
||||||
const remainingArgs = positionalArgs.slice(fixedParamCount)
|
const remainingArgs = positionalArgs.slice(positionalArgIndex)
|
||||||
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs })
|
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -491,7 +788,7 @@ export class VM {
|
||||||
const tailFn = this.stack.pop()!
|
const tailFn = this.stack.pop()!
|
||||||
|
|
||||||
if (tailFn.type !== 'function')
|
if (tailFn.type !== 'function')
|
||||||
throw new Error('TAIL_CALL: not a function')
|
throw new Error(`TAIL_CALL: ${tailFn.value} is not a function`)
|
||||||
|
|
||||||
this.scope = new Scope(tailFn.parentScope)
|
this.scope = new Scope(tailFn.parentScope)
|
||||||
|
|
||||||
|
|
@ -500,6 +797,9 @@ export class VM {
|
||||||
if (tailFn.variadic) tailFixedParamCount--
|
if (tailFn.variadic) tailFixedParamCount--
|
||||||
if (tailFn.named) tailFixedParamCount--
|
if (tailFn.named) tailFixedParamCount--
|
||||||
|
|
||||||
|
// Track which positional args have been consumed
|
||||||
|
let tailPositionalArgIndex = 0
|
||||||
|
|
||||||
// Bind fixed parameters
|
// Bind fixed parameters
|
||||||
for (let i = 0; i < tailFixedParamCount; i++) {
|
for (let i = 0; i < tailFixedParamCount; i++) {
|
||||||
const paramName = tailFn.params[i]!
|
const paramName = tailFn.params[i]!
|
||||||
|
|
@ -507,8 +807,9 @@ export class VM {
|
||||||
if (tailNamedArgs.has(paramName)) {
|
if (tailNamedArgs.has(paramName)) {
|
||||||
this.scope.set(paramName, tailNamedArgs.get(paramName)!)
|
this.scope.set(paramName, tailNamedArgs.get(paramName)!)
|
||||||
tailNamedArgs.delete(paramName)
|
tailNamedArgs.delete(paramName)
|
||||||
} else if (tailPositionalArgs[i] !== undefined) {
|
} else if (tailPositionalArgIndex < tailPositionalArgs.length) {
|
||||||
this.scope.set(paramName, tailPositionalArgs[i]!)
|
this.scope.set(paramName, tailPositionalArgs[tailPositionalArgIndex]!)
|
||||||
|
tailPositionalArgIndex++
|
||||||
} else if (tailFn.defaults[paramName] !== undefined) {
|
} else if (tailFn.defaults[paramName] !== undefined) {
|
||||||
const defaultIdx = tailFn.defaults[paramName]!
|
const defaultIdx = tailFn.defaults[paramName]!
|
||||||
const defaultValue = this.constants[defaultIdx]!
|
const defaultValue = this.constants[defaultIdx]!
|
||||||
|
|
@ -523,7 +824,7 @@ export class VM {
|
||||||
// Handle variadic parameter
|
// Handle variadic parameter
|
||||||
if (tailFn.variadic) {
|
if (tailFn.variadic) {
|
||||||
const variadicParamName = tailFn.params[tailFn.params.length - (tailFn.named ? 2 : 1)]!
|
const variadicParamName = tailFn.params[tailFn.params.length - (tailFn.named ? 2 : 1)]!
|
||||||
const remainingArgs = tailPositionalArgs.slice(tailFixedParamCount)
|
const remainingArgs = tailPositionalArgs.slice(tailPositionalArgIndex)
|
||||||
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs })
|
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -555,30 +856,8 @@ export class VM {
|
||||||
this.stack.push(returnValue)
|
this.stack.push(returnValue)
|
||||||
break
|
break
|
||||||
|
|
||||||
case OpCode.CALL_NATIVE:
|
|
||||||
const functionName = instruction.operand as string
|
|
||||||
const tsFunction = this.nativeFunctions.get(functionName)
|
|
||||||
|
|
||||||
if (!tsFunction)
|
|
||||||
throw new Error(`CALL_NATIVE: function not found: ${functionName}`)
|
|
||||||
|
|
||||||
// Mark current frame as break target (like CALL does)
|
|
||||||
if (this.callStack.length > 0)
|
|
||||||
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
|
|
||||||
|
|
||||||
// Pop all arguments from stack (TypeScript function consumes entire stack)
|
|
||||||
const tsArgs = [...this.stack]
|
|
||||||
this.stack = []
|
|
||||||
|
|
||||||
// Call the TypeScript function and await if necessary
|
|
||||||
const tsResult = await tsFunction(...tsArgs)
|
|
||||||
|
|
||||||
// Push result back onto stack
|
|
||||||
this.stack.push(tsResult)
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw `Unknown op: ${instruction.op}`
|
throw new Error(`Unknown op: ${instruction.op}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -595,4 +874,26 @@ export class VM {
|
||||||
const result = fn(a, b)
|
const result = fn(a, b)
|
||||||
this.stack.push({ type: 'boolean', value: result })
|
this.stack.push({ type: 'boolean', value: result })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async callNative(nativeFn: NativeFunction, args: any[]): Promise<Value> {
|
||||||
|
const originalFn = getOriginalFunction(nativeFn)
|
||||||
|
|
||||||
|
const lastArg = args[args.length - 1]
|
||||||
|
if (lastArg && !Array.isArray(lastArg) && typeof lastArg === 'object') {
|
||||||
|
const paramInfo = extractParamInfo(originalFn)
|
||||||
|
const positional = args.slice(0, -1)
|
||||||
|
const named = lastArg
|
||||||
|
|
||||||
|
args = [...positional]
|
||||||
|
for (let i = positional.length; i < paramInfo.params.length; i++) {
|
||||||
|
const paramName = paramInfo.params[i]!
|
||||||
|
if (named[paramName] !== undefined) {
|
||||||
|
args[i] = named[paramName]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await originalFn.call(this, ...args)
|
||||||
|
return toValue(result, this)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,861 +0,0 @@
|
||||||
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 })
|
|
||||||
})
|
|
||||||
|
|
||||||
107
tests/bitwise.test.ts
Normal file
107
tests/bitwise.test.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
import { expect, describe, test } from 'bun:test'
|
||||||
|
import { toBytecode, run } from '#reef'
|
||||||
|
|
||||||
|
describe('bitwise operations', () => {
|
||||||
|
test('BIT_AND', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 5], ["PUSH", 3], ["BIT_AND"], ["HALT"]
|
||||||
|
])
|
||||||
|
await expect(bytecode).toBeNumber(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BIT_AND with zero', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 5], ["PUSH", 0], ["BIT_AND"], ["HALT"]
|
||||||
|
])
|
||||||
|
await expect(bytecode).toBeNumber(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BIT_OR', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 5], ["PUSH", 3], ["BIT_OR"], ["HALT"]
|
||||||
|
])
|
||||||
|
await expect(bytecode).toBeNumber(7)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BIT_OR with zero', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 5], ["PUSH", 0], ["BIT_OR"], ["HALT"]
|
||||||
|
])
|
||||||
|
await expect(bytecode).toBeNumber(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BIT_XOR', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 5], ["PUSH", 3], ["BIT_XOR"], ["HALT"]
|
||||||
|
])
|
||||||
|
await expect(bytecode).toBeNumber(6)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BIT_XOR with itself returns zero', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 5], ["PUSH", 5], ["BIT_XOR"], ["HALT"]
|
||||||
|
])
|
||||||
|
await expect(bytecode).toBeNumber(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BIT_SHL', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 5], ["PUSH", 2], ["BIT_SHL"], ["HALT"]
|
||||||
|
])
|
||||||
|
await expect(bytecode).toBeNumber(20)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BIT_SHL by zero', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 5], ["PUSH", 0], ["BIT_SHL"], ["HALT"]
|
||||||
|
])
|
||||||
|
await expect(bytecode).toBeNumber(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BIT_SHR', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 20], ["PUSH", 2], ["BIT_SHR"], ["HALT"]
|
||||||
|
])
|
||||||
|
await expect(bytecode).toBeNumber(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BIT_SHR preserves sign for negative numbers', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", -20], ["PUSH", 2], ["BIT_SHR"], ["HALT"]
|
||||||
|
])
|
||||||
|
await expect(bytecode).toBeNumber(-5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BIT_USHR', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", -1], ["PUSH", 1], ["BIT_USHR"], ["HALT"]
|
||||||
|
])
|
||||||
|
await expect(bytecode).toBeNumber(2147483647)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('BIT_USHR does not preserve sign', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", -8], ["PUSH", 1], ["BIT_USHR"], ["HALT"]
|
||||||
|
])
|
||||||
|
await expect(bytecode).toBeNumber(2147483644)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('compound bitwise operations', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
// (5 & 3) | (8 ^ 12)
|
||||||
|
["PUSH", 5], ["PUSH", 3], ["BIT_AND"], // stack: [1]
|
||||||
|
["PUSH", 8], ["PUSH", 12], ["BIT_XOR"], // stack: [1, 4]
|
||||||
|
["BIT_OR"], // stack: [5]
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
await expect(bytecode).toBeNumber(5)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('shift with large shift amounts', async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["PUSH", 1], ["PUSH", 31], ["BIT_SHL"], ["HALT"]
|
||||||
|
])
|
||||||
|
// 1 << 31 = -2147483648 (most significant bit set)
|
||||||
|
await expect(bytecode).toBeNumber(-2147483648)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -15,7 +15,7 @@ test("PUSH_TRY and POP_TRY - no exception thrown", async () => {
|
||||||
PUSH 999
|
PUSH 999
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 })
|
await expect(str).toBeNumber(52)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("THROW - catch exception with error value", async () => {
|
test("THROW - catch exception with error value", async () => {
|
||||||
|
|
@ -29,7 +29,7 @@ test("THROW - catch exception with error value", async () => {
|
||||||
.catch:
|
.catch:
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error occurred' })
|
await expect(str).toBeString('error occurred')
|
||||||
})
|
})
|
||||||
|
|
||||||
test("THROW - uncaught exception throws JS error", async () => {
|
test("THROW - uncaught exception throws JS error", async () => {
|
||||||
|
|
@ -58,7 +58,7 @@ test("THROW - exception with nested try blocks", async () => {
|
||||||
PUSH "outer error"
|
PUSH "outer error"
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'inner error' })
|
await expect(str).toBeString('inner error')
|
||||||
})
|
})
|
||||||
|
|
||||||
test("THROW - exception skips outer handler", async () => {
|
test("THROW - exception skips outer handler", async () => {
|
||||||
|
|
@ -75,7 +75,7 @@ test("THROW - exception skips outer handler", async () => {
|
||||||
.outer_catch:
|
.outer_catch:
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error message' })
|
await expect(str).toBeString('error message')
|
||||||
})
|
})
|
||||||
|
|
||||||
test("THROW - exception unwinds call stack", async () => {
|
test("THROW - exception unwinds call stack", async () => {
|
||||||
|
|
@ -150,7 +150,7 @@ test("PUSH_FINALLY - finally executes after successful try", async () => {
|
||||||
ADD
|
ADD
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 110 })
|
await expect(str).toBeNumber(110)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("PUSH_FINALLY - finally executes after exception", async () => {
|
test("PUSH_FINALLY - finally executes after exception", async () => {
|
||||||
|
|
@ -169,7 +169,7 @@ test("PUSH_FINALLY - finally executes after exception", async () => {
|
||||||
PUSH "finally ran"
|
PUSH "finally ran"
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'finally ran' })
|
await expect(str).toBeString('finally ran')
|
||||||
})
|
})
|
||||||
|
|
||||||
test("PUSH_FINALLY - finally without catch", async () => {
|
test("PUSH_FINALLY - finally without catch", async () => {
|
||||||
|
|
@ -189,7 +189,7 @@ test("PUSH_FINALLY - finally without catch", async () => {
|
||||||
ADD
|
ADD
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 })
|
await expect(str).toBeNumber(52)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("PUSH_FINALLY - nested try-finally blocks", async () => {
|
test("PUSH_FINALLY - nested try-finally blocks", async () => {
|
||||||
|
|
@ -214,7 +214,7 @@ test("PUSH_FINALLY - nested try-finally blocks", async () => {
|
||||||
ADD
|
ADD
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 11 })
|
await expect(str).toBeNumber(11)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("PUSH_FINALLY - error when no handler", async () => {
|
test("PUSH_FINALLY - error when no handler", async () => {
|
||||||
|
|
|
||||||
232
tests/functions-parameter.test.ts
Normal file
232
tests/functions-parameter.test.ts
Normal file
|
|
@ -0,0 +1,232 @@
|
||||||
|
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 { test, expect } from "bun:test"
|
||||||
import { toBytecode } from "#bytecode"
|
import { toBytecode } from "#bytecode"
|
||||||
import { VM } from "#vm"
|
import { toValue, run } from "#reef"
|
||||||
|
|
||||||
test("MAKE_FUNCTION - creates function with captured scope", async () => {
|
test("MAKE_FUNCTION - creates function with captured scope", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
MAKE_FUNCTION () #999
|
MAKE_FUNCTION () #999
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result.type).toBe('function')
|
expect(result.type).toBe('function')
|
||||||
if (result.type === 'function') {
|
if (result.type === 'function') {
|
||||||
expect(result.body).toBe(999)
|
expect(result.body).toBe(999)
|
||||||
|
|
@ -26,7 +26,7 @@ test("CALL and RETURN - basic function call", async () => {
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result).toEqual({ type: 'number', value: 42 })
|
expect(result).toEqual({ type: 'number', value: 42 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ test("CALL and RETURN - function with one parameter", async () => {
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result).toEqual({ type: 'number', value: 100 })
|
expect(result).toEqual({ type: 'number', value: 100 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ test("CALL and RETURN - function with two parameters", async () => {
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result).toEqual({ type: 'number', value: 30 })
|
expect(result).toEqual({ type: 'number', value: 30 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -79,7 +79,7 @@ test("CALL - variadic function with no fixed params", async () => {
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
type: 'array',
|
type: 'array',
|
||||||
value: [
|
value: [
|
||||||
|
|
@ -104,7 +104,7 @@ test("CALL - variadic function with one fixed param", async () => {
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
// x should be 10, rest should be [20, 30]
|
// x should be 10, rest should be [20, 30]
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|
@ -130,7 +130,7 @@ test("CALL - variadic function with two fixed params", async () => {
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
// a=1, b=2, rest=[3, 4]
|
// a=1, b=2, rest=[3, 4]
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|
@ -153,7 +153,7 @@ test("CALL - variadic function with no extra args", async () => {
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
// rest should be empty array
|
// rest should be empty array
|
||||||
expect(result).toEqual({ type: 'array', value: [] })
|
expect(result).toEqual({ type: 'array', value: [] })
|
||||||
})
|
})
|
||||||
|
|
@ -169,7 +169,7 @@ test("CALL - variadic function with defaults on fixed params", async () => {
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
// x should use default value 5
|
// x should use default value 5
|
||||||
expect(result).toEqual({ type: 'number', value: 5 })
|
expect(result).toEqual({ type: 'number', value: 5 })
|
||||||
})
|
})
|
||||||
|
|
@ -188,7 +188,7 @@ test("TAIL_CALL - variadic function", async () => {
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
// Should return the rest array [2, 3]
|
// Should return the rest array [2, 3]
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|
@ -214,7 +214,7 @@ test("CALL - named args function with no fixed params", async () => {
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result.type).toBe('dict')
|
expect(result.type).toBe('dict')
|
||||||
if (result.type === 'dict') {
|
if (result.type === 'dict') {
|
||||||
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Bob' })
|
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
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result.type).toBe('dict')
|
expect(result.type).toBe('dict')
|
||||||
if (result.type === 'dict') {
|
if (result.type === 'dict') {
|
||||||
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Alice' })
|
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
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
// name should be bound as regular param, not collected in named
|
// name should be bound as regular param, not collected in named
|
||||||
expect(result).toEqual({ type: 'string', value: 'Bob' })
|
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
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result.type).toBe('dict')
|
expect(result.type).toBe('dict')
|
||||||
if (result.type === 'dict') {
|
if (result.type === 'dict') {
|
||||||
// Only city should be in named, name should be bound to param
|
// 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
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
// rest should have [2, 3]
|
// rest should have [2, 3]
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
type: 'array',
|
type: 'array',
|
||||||
|
|
@ -331,7 +331,7 @@ test("CALL - mixed variadic and named args, check named", async () => {
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result.type).toBe('dict')
|
expect(result.type).toBe('dict')
|
||||||
if (result.type === 'dict') {
|
if (result.type === 'dict') {
|
||||||
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Bob' })
|
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
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
// named should be empty dict
|
// named should be empty dict
|
||||||
expect(result.type).toBe('dict')
|
expect(result.type).toBe('dict')
|
||||||
if (result.type === 'dict') {
|
if (result.type === 'dict') {
|
||||||
|
|
@ -371,11 +371,34 @@ test("CALL - named args with defaults on fixed params", async () => {
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
// x should use default value 5
|
// x should use default value 5
|
||||||
expect(result).toEqual({ type: 'number', 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 () => {
|
test("TRY_CALL - calls function if found", async () => {
|
||||||
const bytecode = toBytecode([
|
const bytecode = toBytecode([
|
||||||
["MAKE_FUNCTION", [], ".body"],
|
["MAKE_FUNCTION", [], ".body"],
|
||||||
|
|
@ -387,7 +410,7 @@ test("TRY_CALL - calls function if found", async () => {
|
||||||
["RETURN"]
|
["RETURN"]
|
||||||
])
|
])
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result).toEqual({ type: 'number', value: 42 })
|
expect(result).toEqual({ type: 'number', value: 42 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -399,7 +422,7 @@ test("TRY_CALL - pushes value if variable exists but is not a function", async (
|
||||||
["HALT"]
|
["HALT"]
|
||||||
])
|
])
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result).toEqual({ type: 'number', value: 99 })
|
expect(result).toEqual({ type: 'number', value: 99 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -409,7 +432,7 @@ test("TRY_CALL - pushes string if variable not found", async () => {
|
||||||
["HALT"]
|
["HALT"]
|
||||||
])
|
])
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result).toEqual({ type: 'string', value: 'unknownVar' })
|
expect(result).toEqual({ type: 'string', value: 'unknownVar' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -423,7 +446,7 @@ test("TRY_CALL - handles arrays", async () => {
|
||||||
["HALT"]
|
["HALT"]
|
||||||
])
|
])
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result.type).toBe('array')
|
expect(result.type).toBe('array')
|
||||||
if (result.type === 'array') {
|
if (result.type === 'array') {
|
||||||
expect(result.value).toEqual([
|
expect(result.value).toEqual([
|
||||||
|
|
@ -443,7 +466,7 @@ test("TRY_CALL - handles dicts", async () => {
|
||||||
["HALT"]
|
["HALT"]
|
||||||
])
|
])
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result.type).toBe('dict')
|
expect(result.type).toBe('dict')
|
||||||
if (result.type === 'dict') {
|
if (result.type === 'dict') {
|
||||||
expect(result.value.get('key')).toEqual({ type: 'string', value: 'value' })
|
expect(result.value.get('key')).toEqual({ type: 'string', value: 'value' })
|
||||||
|
|
@ -458,13 +481,13 @@ test("TRY_CALL - handles null values", async () => {
|
||||||
["HALT"]
|
["HALT"]
|
||||||
])
|
])
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result).toEqual({ type: 'null', value: null })
|
expect(result).toEqual({ type: 'null', value: null })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("TRY_CALL - function can access its parameters", async () => {
|
test("TRY_CALL - function can access its parameters", async () => {
|
||||||
const bytecode = toBytecode([
|
const bytecode = toBytecode([
|
||||||
["MAKE_FUNCTION", ["x"], ".body"],
|
["MAKE_FUNCTION", ["x=0"], ".body"],
|
||||||
["STORE", "addFive"],
|
["STORE", "addFive"],
|
||||||
["PUSH", 10],
|
["PUSH", 10],
|
||||||
["STORE", "x"],
|
["STORE", "x"],
|
||||||
|
|
@ -477,9 +500,9 @@ test("TRY_CALL - function can access its parameters", async () => {
|
||||||
["RETURN"]
|
["RETURN"]
|
||||||
])
|
])
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
// Function is called with 0 args, so x inside function should be null
|
// Function is called with 0 args, so x defaults to 0
|
||||||
// Then we add 5 to null (which coerces to 0)
|
// Then we add 5 to 0
|
||||||
expect(result).toEqual({ type: 'number', value: 5 })
|
expect(result).toEqual({ type: 'number', value: 5 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -493,6 +516,209 @@ test("TRY_CALL - with string format", async () => {
|
||||||
RETURN
|
RETURN
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const result = await new VM(bytecode).run()
|
const result = await run(bytecode)
|
||||||
expect(result).toEqual({ type: 'number', value: 100 })
|
expect(result).toEqual({ type: 'number', value: 100 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("CALL - passing null triggers default value for single parameter", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x=42"], ".body"],
|
||||||
|
["STORE", "func"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "func"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// Passing null should trigger the default value of 42
|
||||||
|
expect(result).toEqual({ type: 'number', value: 42 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - passing null triggers default value for multiple parameters", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["a=10", "b=20", "c=30"], ".body"],
|
||||||
|
["STORE", "func"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "a"],
|
||||||
|
["LOAD", "b"],
|
||||||
|
["ADD"],
|
||||||
|
["LOAD", "c"],
|
||||||
|
["ADD"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "func"],
|
||||||
|
["PUSH", 5],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// a=5 (provided), b=20 (null triggers default), c=30 (null triggers default)
|
||||||
|
// Result: 5 + 20 + 30 = 55
|
||||||
|
expect(result).toEqual({ type: 'number', value: 55 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - null in middle parameter triggers default", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x=100", "y=200", "z=300"], ".body"],
|
||||||
|
["STORE", "func"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["LOAD", "y"],
|
||||||
|
["ADD"],
|
||||||
|
["LOAD", "z"],
|
||||||
|
["ADD"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "func"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// x=1, y=200 (null triggers default), z=3
|
||||||
|
// Result: 1 + 200 + 3 = 204
|
||||||
|
expect(result).toEqual({ type: 'number', value: 204 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - null with named arguments triggers default", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x=50", "y=75"], ".body"],
|
||||||
|
["STORE", "func"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["LOAD", "y"],
|
||||||
|
["ADD"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "func"],
|
||||||
|
["PUSH", "x"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", "y"],
|
||||||
|
["PUSH", 25],
|
||||||
|
["PUSH", 0],
|
||||||
|
["PUSH", 2],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// x=50 (null triggers default), y=25 (provided via named arg)
|
||||||
|
// Result: 50 + 25 = 75
|
||||||
|
expect(result).toEqual({ type: 'number', value: 75 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - null with string default value", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["name='Guest'"], ".body"],
|
||||||
|
["STORE", "greet"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["PUSH", "Hello, "],
|
||||||
|
["LOAD", "name"],
|
||||||
|
["ADD"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "greet"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// Passing null should trigger the default value 'Guest'
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'Hello, Guest' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - null with no default still results in null", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x"], ".body"],
|
||||||
|
["STORE", "func"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "func"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// No default value, so null should be returned
|
||||||
|
expect(result).toEqual({ type: 'null', value: null })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - null triggers default with variadic parameters", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x=99", "...rest"], ".body"],
|
||||||
|
["STORE", "func"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "func"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 2],
|
||||||
|
["PUSH", 3],
|
||||||
|
["PUSH", 4],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// x should be 99 (null triggers default), rest gets [1, 2, 3]
|
||||||
|
expect(result).toEqual({ type: 'number', value: 99 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("CALL - null triggers default with @named parameter", async () => {
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x=777", "@named"], ".body"],
|
||||||
|
["STORE", "func"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["LOAD", "func"],
|
||||||
|
["PUSH", null],
|
||||||
|
["PUSH", "foo"],
|
||||||
|
["PUSH", "bar"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 1],
|
||||||
|
["CALL"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// x should be 777 (null triggers default)
|
||||||
|
expect(result).toEqual({ type: 'number', value: 777 })
|
||||||
|
})
|
||||||
|
|
|
||||||
2276
tests/native.test.ts
2276
tests/native.test.ts
File diff suppressed because it is too large
Load Diff
2092
tests/opcodes.test.ts
Normal file
2092
tests/opcodes.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
459
tests/regex.test.ts
Normal file
459
tests/regex.test.ts
Normal file
|
|
@ -0,0 +1,459 @@
|
||||||
|
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' })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
149
tests/repl.test.ts
Normal file
149
tests/repl.test.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
import { test, expect } from "bun:test"
|
||||||
|
import { VM, toBytecode } from "#reef"
|
||||||
|
|
||||||
|
test("REPL mode - demonstrates PC reset problem", async () => {
|
||||||
|
// Track how many times each line executes
|
||||||
|
let line1Count = 0
|
||||||
|
let line2Count = 0
|
||||||
|
|
||||||
|
// Line 1: Set x = 5, track execution
|
||||||
|
const line1 = toBytecode([
|
||||||
|
["LOAD", "trackLine1"],
|
||||||
|
["PUSH", 0],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["POP"],
|
||||||
|
["PUSH", 5],
|
||||||
|
["STORE", "x"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const vm = new VM(line1, {
|
||||||
|
trackLine1: () => { line1Count++; return null },
|
||||||
|
trackLine2: () => { line2Count++; return null }
|
||||||
|
})
|
||||||
|
|
||||||
|
await vm.run()
|
||||||
|
expect(vm.scope.get("x")).toEqual({ type: "number", value: 5 })
|
||||||
|
expect(line1Count).toBe(1)
|
||||||
|
expect(line2Count).toBe(0)
|
||||||
|
|
||||||
|
// Line 2: Track execution, load x
|
||||||
|
const line2 = toBytecode([
|
||||||
|
["LOAD", "trackLine2"],
|
||||||
|
["PUSH", 0],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["POP"],
|
||||||
|
["LOAD", "x"]
|
||||||
|
])
|
||||||
|
|
||||||
|
// Append line2 bytecode to VM (what a REPL would do)
|
||||||
|
vm.instructions.push(...line2.instructions)
|
||||||
|
for (const constant of line2.constants) {
|
||||||
|
if (!vm.constants.includes(constant)) {
|
||||||
|
vm.constants.push(constant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current behavior: run() resets PC to 0, re-executing everything
|
||||||
|
await vm.run()
|
||||||
|
|
||||||
|
// PROBLEM: Line 1 ran AGAIN (count is now 2, not 1)
|
||||||
|
// This is the issue for REPL - side effects run multiple times
|
||||||
|
expect(line1Count).toBe(2) // Ran twice! Should still be 1
|
||||||
|
expect(line2Count).toBe(1) // This is correct
|
||||||
|
|
||||||
|
// What we WANT for REPL:
|
||||||
|
// - Only execute the NEW bytecode (line 2)
|
||||||
|
// - line1Count should stay at 1
|
||||||
|
// - line2Count should be 1
|
||||||
|
})
|
||||||
|
|
||||||
|
test("REPL mode - continue() executes only new bytecode", async () => {
|
||||||
|
// Track how many times each line executes
|
||||||
|
let line1Count = 0
|
||||||
|
let line2Count = 0
|
||||||
|
|
||||||
|
// Line 1: Set x = 5, track execution
|
||||||
|
const line1 = toBytecode([
|
||||||
|
["LOAD", "trackLine1"],
|
||||||
|
["PUSH", 0],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["POP"],
|
||||||
|
["PUSH", 5],
|
||||||
|
["STORE", "x"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const vm = new VM(line1, {
|
||||||
|
trackLine1: () => { line1Count++; return null },
|
||||||
|
trackLine2: () => { line2Count++; return null }
|
||||||
|
})
|
||||||
|
|
||||||
|
await vm.run()
|
||||||
|
expect(vm.scope.get("x")).toEqual({ type: "number", value: 5 })
|
||||||
|
expect(line1Count).toBe(1)
|
||||||
|
expect(line2Count).toBe(0)
|
||||||
|
|
||||||
|
// Line 2: Track execution, load x and add 10
|
||||||
|
const line2 = toBytecode([
|
||||||
|
["LOAD", "trackLine2"],
|
||||||
|
["PUSH", 0],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"],
|
||||||
|
["POP"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["PUSH", 10],
|
||||||
|
["ADD"]
|
||||||
|
])
|
||||||
|
|
||||||
|
// Append line2 bytecode to VM using the helper method
|
||||||
|
vm.appendBytecode(line2)
|
||||||
|
|
||||||
|
// SOLUTION: Use continue() instead of run() to resume from current PC
|
||||||
|
await vm.continue()
|
||||||
|
|
||||||
|
const result = vm.stack[vm.stack.length - 1]
|
||||||
|
|
||||||
|
// SUCCESS: Line 1 only ran once, line 2 ran once
|
||||||
|
expect(line1Count).toBe(1) // Still 1! Side effect didn't re-run
|
||||||
|
expect(line2Count).toBe(1) // Ran once as expected
|
||||||
|
expect(result).toEqual({ type: "number", value: 15 }) // 5 + 10
|
||||||
|
})
|
||||||
|
|
||||||
|
test("REPL mode - function calls work across chunks", async () => {
|
||||||
|
const vm = new VM(toBytecode([]))
|
||||||
|
await vm.run()
|
||||||
|
|
||||||
|
// Line 1: Define a function
|
||||||
|
const line1 = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x"], ".body"],
|
||||||
|
["STORE", "add1"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["ADD"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"]
|
||||||
|
])
|
||||||
|
|
||||||
|
vm.appendBytecode(line1)
|
||||||
|
await vm.continue()
|
||||||
|
|
||||||
|
expect(vm.scope.get("add1")?.type).toBe("function")
|
||||||
|
|
||||||
|
// Line 2: Call the function
|
||||||
|
const line2 = toBytecode([
|
||||||
|
["LOAD", "add1"],
|
||||||
|
["PUSH", 10],
|
||||||
|
["PUSH", 1],
|
||||||
|
["PUSH", 0],
|
||||||
|
["CALL"]
|
||||||
|
])
|
||||||
|
|
||||||
|
vm.appendBytecode(line2)
|
||||||
|
const result = await vm.continue()
|
||||||
|
|
||||||
|
expect(result).toEqual({ type: "number", value: 11 })
|
||||||
|
})
|
||||||
243
tests/scope.test.ts
Normal file
243
tests/scope.test.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
||||||
|
import { test, expect } from "bun:test"
|
||||||
|
import { Scope, toValue } from "#reef"
|
||||||
|
|
||||||
|
test("Scope - create empty scope", () => {
|
||||||
|
const scope = new Scope()
|
||||||
|
expect(scope.parent).toBeUndefined()
|
||||||
|
expect(scope.locals.size).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope - create child scope with parent", () => {
|
||||||
|
const parent = new Scope()
|
||||||
|
const child = new Scope(parent)
|
||||||
|
|
||||||
|
expect(child.parent).toBe(parent)
|
||||||
|
expect(child.locals.size).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope - set and get variable in same scope", () => {
|
||||||
|
const scope = new Scope()
|
||||||
|
const value = toValue(42)
|
||||||
|
|
||||||
|
scope.set("x", value)
|
||||||
|
expect(scope.get("x")).toBe(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope - get returns undefined for non-existent variable", () => {
|
||||||
|
const scope = new Scope()
|
||||||
|
expect(scope.get("x")).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope - set updates existing variable in same scope", () => {
|
||||||
|
const scope = new Scope()
|
||||||
|
|
||||||
|
scope.set("x", toValue(10))
|
||||||
|
expect(scope.get("x")).toEqual({ type: "number", value: 10 })
|
||||||
|
|
||||||
|
scope.set("x", toValue(20))
|
||||||
|
expect(scope.get("x")).toEqual({ type: "number", value: 20 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope - get searches parent scope", () => {
|
||||||
|
const parent = new Scope()
|
||||||
|
const child = new Scope(parent)
|
||||||
|
|
||||||
|
parent.set("x", toValue(42))
|
||||||
|
expect(child.get("x")).toEqual({ type: "number", value: 42 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope - get searches multiple levels up", () => {
|
||||||
|
const grandparent = new Scope()
|
||||||
|
const parent = new Scope(grandparent)
|
||||||
|
const child = new Scope(parent)
|
||||||
|
|
||||||
|
grandparent.set("x", toValue(100))
|
||||||
|
expect(child.get("x")).toEqual({ type: "number", value: 100 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope - child updates parent variable when it exists (no shadowing)", () => {
|
||||||
|
const parent = new Scope()
|
||||||
|
const child = new Scope(parent)
|
||||||
|
|
||||||
|
parent.set("x", toValue(10))
|
||||||
|
child.set("x", toValue(20))
|
||||||
|
|
||||||
|
// set() updates parent's variable, not shadow it
|
||||||
|
expect(parent.get("x")).toEqual({ type: "number", value: 20 })
|
||||||
|
expect(child.get("x")).toEqual({ type: "number", value: 20 })
|
||||||
|
expect(child.locals.has("x")).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope - set updates parent scope variable when it exists", () => {
|
||||||
|
const parent = new Scope()
|
||||||
|
const child = new Scope(parent)
|
||||||
|
|
||||||
|
parent.set("x", toValue(10))
|
||||||
|
child.set("x", toValue(20))
|
||||||
|
|
||||||
|
// Should update parent's variable, not create new local one
|
||||||
|
expect(parent.get("x")).toEqual({ type: "number", value: 20 })
|
||||||
|
expect(child.get("x")).toEqual({ type: "number", value: 20 })
|
||||||
|
expect(child.locals.has("x")).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope - set creates new local variable when not found in parent", () => {
|
||||||
|
const parent = new Scope()
|
||||||
|
const child = new Scope(parent)
|
||||||
|
|
||||||
|
parent.set("x", toValue(10))
|
||||||
|
child.set("y", toValue(20))
|
||||||
|
|
||||||
|
expect(parent.get("x")).toEqual({ type: "number", value: 10 })
|
||||||
|
expect(parent.get("y")).toBeUndefined()
|
||||||
|
expect(child.get("x")).toEqual({ type: "number", value: 10 })
|
||||||
|
expect(child.get("y")).toEqual({ type: "number", value: 20 })
|
||||||
|
expect(child.locals.has("y")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope - has returns true for variable in current scope", () => {
|
||||||
|
const scope = new Scope()
|
||||||
|
scope.set("x", toValue(42))
|
||||||
|
|
||||||
|
expect(scope.has("x")).toBe(true)
|
||||||
|
expect(scope.has("y")).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope - has returns true for variable in parent scope", () => {
|
||||||
|
const parent = new Scope()
|
||||||
|
const child = new Scope(parent)
|
||||||
|
|
||||||
|
parent.set("x", toValue(42))
|
||||||
|
|
||||||
|
expect(child.has("x")).toBe(true)
|
||||||
|
expect(child.has("y")).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope - has searches multiple levels up", () => {
|
||||||
|
const grandparent = new Scope()
|
||||||
|
const parent = new Scope(grandparent)
|
||||||
|
const child = new Scope(parent)
|
||||||
|
|
||||||
|
grandparent.set("x", toValue(100))
|
||||||
|
|
||||||
|
expect(child.has("x")).toBe(true)
|
||||||
|
expect(parent.has("x")).toBe(true)
|
||||||
|
expect(grandparent.has("x")).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope.vars() - returns empty array for empty scope", () => {
|
||||||
|
const scope = new Scope()
|
||||||
|
expect(scope.vars()).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope.vars() - returns single variable from current scope", () => {
|
||||||
|
const scope = new Scope()
|
||||||
|
scope.set("x", toValue(42))
|
||||||
|
|
||||||
|
expect(scope.vars()).toEqual(["x"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope.vars() - returns multiple variables from current scope", () => {
|
||||||
|
const scope = new Scope()
|
||||||
|
scope.set("x", toValue(1))
|
||||||
|
scope.set("y", toValue(2))
|
||||||
|
scope.set("z", toValue(3))
|
||||||
|
|
||||||
|
const vars = scope.vars()
|
||||||
|
expect(vars.length).toBe(3)
|
||||||
|
expect(vars).toContain("x")
|
||||||
|
expect(vars).toContain("y")
|
||||||
|
expect(vars).toContain("z")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope.vars() - includes variables from parent scope", () => {
|
||||||
|
const parent = new Scope()
|
||||||
|
const child = new Scope(parent)
|
||||||
|
|
||||||
|
parent.set("x", toValue(1))
|
||||||
|
child.set("y", toValue(2))
|
||||||
|
|
||||||
|
const vars = child.vars()
|
||||||
|
expect(vars.length).toBe(2)
|
||||||
|
expect(vars).toContain("x")
|
||||||
|
expect(vars).toContain("y")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope.vars() - includes variables from multiple parent scopes", () => {
|
||||||
|
const grandparent = new Scope()
|
||||||
|
const parent = new Scope(grandparent)
|
||||||
|
const child = new Scope(parent)
|
||||||
|
|
||||||
|
grandparent.set("x", toValue(1))
|
||||||
|
parent.set("y", toValue(2))
|
||||||
|
child.set("z", toValue(3))
|
||||||
|
|
||||||
|
const vars = child.vars()
|
||||||
|
expect(vars.length).toBe(3)
|
||||||
|
expect(vars).toContain("x")
|
||||||
|
expect(vars).toContain("y")
|
||||||
|
expect(vars).toContain("z")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope.vars() - no duplicates when child updates parent variable", () => {
|
||||||
|
const parent = new Scope()
|
||||||
|
const child = new Scope(parent)
|
||||||
|
|
||||||
|
parent.set("x", toValue(10))
|
||||||
|
child.set("x", toValue(20)) // Updates parent, doesn't create local
|
||||||
|
|
||||||
|
// Only one "x" since child doesn't have its own local x
|
||||||
|
const vars = child.vars()
|
||||||
|
expect(vars).toEqual(["x"])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope.vars() - can't have duplicates if manually set in locals", () => {
|
||||||
|
const parent = new Scope()
|
||||||
|
const child = new Scope(parent)
|
||||||
|
|
||||||
|
parent.set("x", toValue(10))
|
||||||
|
child.locals.set("x", toValue(20))
|
||||||
|
const vars = child.vars()
|
||||||
|
expect(vars).toEqual(["x"])
|
||||||
|
|
||||||
|
expect(child.get("x")).toEqual({ type: "number", value: 20 })
|
||||||
|
expect(parent.get("x")).toEqual({ type: "number", value: 10 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope.vars() - handles deep scope chains", () => {
|
||||||
|
let scope = new Scope()
|
||||||
|
|
||||||
|
// Create a deep chain: level0 -> level1 -> ... -> level5
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
scope.set(`var${i}`, toValue(i))
|
||||||
|
scope = new Scope(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final scope should have all variables from the chain
|
||||||
|
const vars = scope.vars()
|
||||||
|
expect(vars.length).toBe(5)
|
||||||
|
expect(vars).toContain("var0")
|
||||||
|
expect(vars).toContain("var1")
|
||||||
|
expect(vars).toContain("var2")
|
||||||
|
expect(vars).toContain("var3")
|
||||||
|
expect(vars).toContain("var4")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Scope - can store and retrieve functions", () => {
|
||||||
|
const scope = new Scope()
|
||||||
|
|
||||||
|
const fnValue = {
|
||||||
|
type: 'function' as const,
|
||||||
|
value: '<function>' as const,
|
||||||
|
params: ['x'],
|
||||||
|
defaults: {},
|
||||||
|
body: 0,
|
||||||
|
variadic: false,
|
||||||
|
named: false,
|
||||||
|
parentScope: scope
|
||||||
|
}
|
||||||
|
|
||||||
|
scope.set("myFunc", fnValue)
|
||||||
|
expect(scope.get("myFunc")).toBe(fnValue)
|
||||||
|
expect(scope.get("myFunc")?.type).toBe("function")
|
||||||
|
})
|
||||||
249
tests/setup.ts
Normal file
249
tests/setup.ts
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
import { expect } from "bun:test"
|
||||||
|
import { toValue, fromValue, type Value, toBytecode, run, type Bytecode } from "#reef"
|
||||||
|
import { isEqual } from "../src/value"
|
||||||
|
|
||||||
|
declare module "bun:test" {
|
||||||
|
interface Matchers<T> {
|
||||||
|
/**
|
||||||
|
* Run bytecode and assert that the result equals a JavaScript value after conversion via toValue()
|
||||||
|
* @example expect(bytecode).toEqualValue(42)
|
||||||
|
* @example expect("PUSH 5\nPUSH 3\nADD").toEqualValue(8)
|
||||||
|
* @example expect([["PUSH", 42]]).toEqualValue(42)
|
||||||
|
*/
|
||||||
|
toEqualValue(expected: any): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run bytecode and assert that the result is null
|
||||||
|
* @example expect(bytecode).toBeNull()
|
||||||
|
* @example expect("PUSH null").toBeNull()
|
||||||
|
*/
|
||||||
|
toBeNull(): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run bytecode and assert that the result is a boolean with the expected value
|
||||||
|
* @example expect(bytecode).toBeBoolean(true)
|
||||||
|
* @example expect("PUSH true").toBeBoolean(true)
|
||||||
|
*/
|
||||||
|
toBeBoolean(expected: boolean): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run bytecode and assert that the result is a number with the expected value
|
||||||
|
* @example expect(bytecode).toBeNumber(42)
|
||||||
|
* @example expect("PUSH 42").toBeNumber(42)
|
||||||
|
*/
|
||||||
|
toBeNumber(expected: number): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run bytecode and assert that the result is a string with the expected value
|
||||||
|
* @example expect(bytecode).toBeString("hello")
|
||||||
|
* @example expect("PUSH \"hello\"").toBeString("hello")
|
||||||
|
*/
|
||||||
|
toBeString(expected: string): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run bytecode and assert that the result is an array with the expected values
|
||||||
|
* @example expect(bytecode).toBeArray([1, 2, 3])
|
||||||
|
*/
|
||||||
|
toBeArray(expected: any[]): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run bytecode and assert that the result is a dict with the expected key-value pairs
|
||||||
|
* @example expect(bytecode).toBeDict({ x: 10, y: 20 })
|
||||||
|
*/
|
||||||
|
toBeDict(expected: Record<string, any>): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run bytecode and assert that the result is a function (Reef or native)
|
||||||
|
* @example expect(bytecode).toBeFunction()
|
||||||
|
*/
|
||||||
|
toBeFunction(): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run bytecode and assert that the result is truthy according to ReefVM semantics
|
||||||
|
* (only null and false are falsy)
|
||||||
|
* @example expect(bytecode).toBeTruthy()
|
||||||
|
*/
|
||||||
|
toBeTruthy(): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run bytecode and assert that the result is falsy according to ReefVM semantics
|
||||||
|
* (only null and false are falsy)
|
||||||
|
* @example expect(bytecode).toBeFalsy()
|
||||||
|
*/
|
||||||
|
toBeFalsy(): Promise<void>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect.extend({
|
||||||
|
async toEqualValue(this: void, received: unknown, expected: any) {
|
||||||
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
||||||
|
const result = await run(bytecode)
|
||||||
|
const expectedValue = toValue(expected)
|
||||||
|
const pass = isEqual(result, expectedValue)
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected value NOT to equal ${formatValue(expectedValue)}, but it did`
|
||||||
|
: `Expected value to equal ${formatValue(expectedValue)}, but received ${formatValue(result)}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toBeNull(this: void, received: unknown) {
|
||||||
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
||||||
|
const result = await run(bytecode)
|
||||||
|
const pass = result.type === "null"
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected value NOT to be null, but it was`
|
||||||
|
: `Expected value to be null, but received ${formatValue(result)}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toBeBoolean(this: void, received: unknown, expected: boolean) {
|
||||||
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
||||||
|
const result = await run(bytecode)
|
||||||
|
const pass = result.type === "boolean" && result.value === expected
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected value NOT to be boolean ${expected}, but it was`
|
||||||
|
: `Expected value to be boolean ${expected}, but received ${formatValue(result)}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toBeNumber(this: void, received: unknown, expected: number) {
|
||||||
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
||||||
|
const result = await run(bytecode)
|
||||||
|
const pass = result.type === "number" && result.value === expected
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected value NOT to be number ${expected}, but it was`
|
||||||
|
: `Expected value to be number ${expected}, but received ${formatValue(result)}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toBeString(this: void, received: unknown, expected: string) {
|
||||||
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
||||||
|
const result = await run(bytecode)
|
||||||
|
const pass = result.type === "string" && result.value === expected
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected value NOT to be string "${expected}", but it was`
|
||||||
|
: `Expected value to be string "${expected}", but received ${formatValue(result)}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toBeArray(this: void, received: unknown, expected: any[]) {
|
||||||
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
||||||
|
const result = await run(bytecode)
|
||||||
|
const expectedValue = toValue(expected)
|
||||||
|
const pass = result.type === "array" && isEqual(result, expectedValue)
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected value NOT to be array ${formatValue(expectedValue)}, but it was`
|
||||||
|
: `Expected value to be array ${formatValue(expectedValue)}, but received ${formatValue(result)}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toBeDict(this: void, received: unknown, expected: Record<string, any>) {
|
||||||
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
||||||
|
const result = await run(bytecode)
|
||||||
|
const expectedValue = toValue(expected)
|
||||||
|
const pass = result.type === "dict" && isEqual(result, expectedValue)
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected value NOT to be dict ${formatValue(expectedValue)}, but it was`
|
||||||
|
: `Expected value to be dict ${formatValue(expectedValue)}, but received ${formatValue(result)}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toBeFunction(this: void, received: unknown) {
|
||||||
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
||||||
|
const result = await run(bytecode)
|
||||||
|
const pass = result.type === "function" || result.type === "native"
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected value NOT to be a function, but it was`
|
||||||
|
: `Expected value to be a function, but received ${formatValue(result)}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toBeTruthy(this: void, received: unknown) {
|
||||||
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// ReefVM semantics: only null and false are falsy
|
||||||
|
const pass = !(result.type === "null" || (result.type === "boolean" && !result.value))
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected value NOT to be truthy, but it was: ${formatValue(result)}`
|
||||||
|
: `Expected value to be truthy, but received ${formatValue(result)}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async toBeFalsy(this: void, received: unknown) {
|
||||||
|
const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received as string | any[]) : received as Bytecode
|
||||||
|
const result = await run(bytecode)
|
||||||
|
// ReefVM semantics: only null and false are falsy
|
||||||
|
const pass = result.type === "null" || (result.type === "boolean" && !result.value)
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected value NOT to be falsy, but it was: ${formatValue(result)}`
|
||||||
|
: `Expected value to be falsy, but received ${formatValue(result)}`,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function formatValue(value: Value): string {
|
||||||
|
switch (value.type) {
|
||||||
|
case "null":
|
||||||
|
return "null"
|
||||||
|
case "boolean":
|
||||||
|
case "number":
|
||||||
|
return String(value.value)
|
||||||
|
case "string":
|
||||||
|
return `"${value.value}"`
|
||||||
|
case "array":
|
||||||
|
return `[${value.value.map(formatValue).join(", ")}]`
|
||||||
|
case "dict": {
|
||||||
|
const entries = Array.from(value.value.entries())
|
||||||
|
.map(([k, v]) => `${k}: ${formatValue(v)}`)
|
||||||
|
.join(", ")
|
||||||
|
return `{${entries}}`
|
||||||
|
}
|
||||||
|
case "regex":
|
||||||
|
return String(value.value)
|
||||||
|
case "function":
|
||||||
|
case "native":
|
||||||
|
return "<function>"
|
||||||
|
default:
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
119
tests/unicode.test.ts
Normal file
119
tests/unicode.test.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
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")
|
expect(formatted).toContain("UNKNOWN")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("detects JUMP without # or .label", () => {
|
test("detects JUMP without .label", () => {
|
||||||
const source = `
|
const source = `
|
||||||
JUMP 5
|
JUMP 5
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
const result = validateBytecode(source)
|
const result = validateBytecode(source)
|
||||||
expect(result.valid).toBe(false)
|
expect(result.valid).toBe(false)
|
||||||
expect(result.errors[0]!.message).toContain("JUMP requires immediate (#number) or label (.label)")
|
expect(result.errors[0]!.message).toContain("JUMP requires label (.label)")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("detects JUMP_IF_TRUE without # or .label", () => {
|
test("detects JUMP_IF_TRUE without .label", () => {
|
||||||
const source = `
|
const source = `
|
||||||
PUSH true
|
PUSH true
|
||||||
JUMP_IF_TRUE 2
|
JUMP_IF_TRUE 2
|
||||||
|
|
@ -219,10 +219,10 @@ test("detects JUMP_IF_TRUE without # or .label", () => {
|
||||||
`
|
`
|
||||||
const result = validateBytecode(source)
|
const result = validateBytecode(source)
|
||||||
expect(result.valid).toBe(false)
|
expect(result.valid).toBe(false)
|
||||||
expect(result.errors[0]!.message).toContain("JUMP_IF_TRUE requires immediate (#number) or label (.label)")
|
expect(result.errors[0]!.message).toContain("JUMP_IF_TRUE requires label (.label)")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("detects JUMP_IF_FALSE without # or .label", () => {
|
test("detects JUMP_IF_FALSE without .label", () => {
|
||||||
const source = `
|
const source = `
|
||||||
PUSH false
|
PUSH false
|
||||||
JUMP_IF_FALSE 2
|
JUMP_IF_FALSE 2
|
||||||
|
|
@ -230,17 +230,18 @@ test("detects JUMP_IF_FALSE without # or .label", () => {
|
||||||
`
|
`
|
||||||
const result = validateBytecode(source)
|
const result = validateBytecode(source)
|
||||||
expect(result.valid).toBe(false)
|
expect(result.valid).toBe(false)
|
||||||
expect(result.errors[0]!.message).toContain("JUMP_IF_FALSE requires immediate (#number) or label (.label)")
|
expect(result.errors[0]!.message).toContain("JUMP_IF_FALSE requires label (.label)")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("allows JUMP with immediate number", () => {
|
test("rejects JUMP with immediate number", () => {
|
||||||
const source = `
|
const source = `
|
||||||
JUMP #2
|
JUMP #2
|
||||||
PUSH 999
|
PUSH 999
|
||||||
HALT
|
HALT
|
||||||
`
|
`
|
||||||
const result = validateBytecode(source)
|
const result = validateBytecode(source)
|
||||||
expect(result.valid).toBe(true)
|
expect(result.valid).toBe(false)
|
||||||
|
expect(result.errors[0]!.message).toContain("JUMP requires label (.label)")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("detects MAKE_ARRAY without #", () => {
|
test("detects MAKE_ARRAY without #", () => {
|
||||||
|
|
|
||||||
302
tests/value.test.ts
Normal file
302
tests/value.test.ts
Normal file
|
|
@ -0,0 +1,302 @@
|
||||||
|
import { test, expect } from "bun:test"
|
||||||
|
import { isValue, toValue } from "#reef"
|
||||||
|
|
||||||
|
test("isValue - recognizes valid Value objects", () => {
|
||||||
|
expect(isValue({ type: 'number', value: 42 })).toBe(true)
|
||||||
|
expect(isValue({ type: 'number', value: 0 })).toBe(true)
|
||||||
|
expect(isValue({ type: 'string', value: 'hello' })).toBe(true)
|
||||||
|
expect(isValue({ type: 'string', value: '' })).toBe(true)
|
||||||
|
expect(isValue({ type: 'boolean', value: true })).toBe(true)
|
||||||
|
expect(isValue({ type: 'boolean', value: false })).toBe(true)
|
||||||
|
expect(isValue({ type: 'null', value: null })).toBe(true)
|
||||||
|
expect(isValue({ type: 'array', value: [] })).toBe(true)
|
||||||
|
expect(isValue({ type: 'array', value: [toValue(1), toValue(2)] })).toBe(true)
|
||||||
|
expect(isValue({ type: 'dict', value: new Map() })).toBe(true)
|
||||||
|
expect(isValue({ type: 'regex', value: /test/ })).toBe(true)
|
||||||
|
expect(isValue({
|
||||||
|
type: 'function',
|
||||||
|
value: '<function>',
|
||||||
|
params: [],
|
||||||
|
defaults: {},
|
||||||
|
body: 0,
|
||||||
|
variadic: false,
|
||||||
|
named: false,
|
||||||
|
parentScope: null as any
|
||||||
|
})).toBe(true)
|
||||||
|
expect(isValue({
|
||||||
|
type: 'native',
|
||||||
|
value: '<function>',
|
||||||
|
fn: (() => { }) as any
|
||||||
|
})).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("isValue - rejects primitives", () => {
|
||||||
|
expect(isValue(42)).toBe(false)
|
||||||
|
expect(isValue(0)).toBe(false)
|
||||||
|
expect(isValue('hello')).toBe(false)
|
||||||
|
expect(isValue('')).toBe(false)
|
||||||
|
expect(isValue(true)).toBe(false)
|
||||||
|
expect(isValue(false)).toBe(false)
|
||||||
|
expect(isValue(null)).toBe(false)
|
||||||
|
expect(isValue(undefined)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("isValue - rejects plain objects", () => {
|
||||||
|
expect(isValue({})).toBe(false)
|
||||||
|
expect(isValue({ foo: 'bar' })).toBe(false)
|
||||||
|
expect(isValue({ type: 'number' })).toBe(false)
|
||||||
|
expect(isValue({ value: 42 })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("isValue - rejects arrays and functions", () => {
|
||||||
|
expect(isValue([])).toBe(false)
|
||||||
|
expect(isValue([1, 2, 3])).toBe(false)
|
||||||
|
expect(isValue(() => { })).toBe(false)
|
||||||
|
expect(isValue(function () { })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("isValue - rejects other object types", () => {
|
||||||
|
expect(isValue(new Date())).toBe(false)
|
||||||
|
expect(isValue(/regex/)).toBe(false)
|
||||||
|
expect(isValue(new Map())).toBe(false)
|
||||||
|
expect(isValue(new Set())).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("isValue - used by toValue to detect already-converted values", () => {
|
||||||
|
const value = toValue(42)
|
||||||
|
expect(isValue(value)).toBe(true)
|
||||||
|
|
||||||
|
const result = toValue(value)
|
||||||
|
expect(result).toBe(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("isValue - edge cases with type and value properties", () => {
|
||||||
|
expect(isValue({ type: 'number', value: 42, extra: 'data' })).toBe(true)
|
||||||
|
|
||||||
|
expect(isValue({ type: null, value: 42 })).toBe(false)
|
||||||
|
expect(isValue({ type: 'number', value: undefined })).toBe(true)
|
||||||
|
|
||||||
|
expect(isValue({ type: 'number', val: 42 })).toBe(false)
|
||||||
|
expect(isValue({ typ: 'number', value: 42 })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("isValue - rejects objects with invalid type values", () => {
|
||||||
|
expect(isValue({ type: 'text', value: 'Bob' })).toBe(false)
|
||||||
|
expect(isValue({ type: 'email', value: 'test@example.com' })).toBe(false)
|
||||||
|
expect(isValue({ type: 'password', value: 'secret' })).toBe(false)
|
||||||
|
expect(isValue({ type: 'checkbox', value: true })).toBe(false)
|
||||||
|
expect(isValue({ type: 'custom', value: 123 })).toBe(false)
|
||||||
|
expect(isValue({ type: 'unknown', value: null })).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("toValue - correctly handles HTML input props", async () => {
|
||||||
|
const { VM, toBytecode, toValue } = await import("#reef")
|
||||||
|
|
||||||
|
const bytecode = toBytecode([["HALT"]])
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
|
const inputProps = { type: 'text', value: 'Bob' }
|
||||||
|
const converted = toValue(inputProps, vm)
|
||||||
|
|
||||||
|
expect(converted.type).toBe('dict')
|
||||||
|
expect(converted.value.get('type')).toEqual({ type: 'string', value: 'text' })
|
||||||
|
expect(converted.value.get('value')).toEqual({ type: 'string', value: 'Bob' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("toValue - converts wrapped Reef functions back to original Value", async () => {
|
||||||
|
const { VM, toBytecode, fnFromValue } = await import("#reef")
|
||||||
|
|
||||||
|
// Create a Reef function
|
||||||
|
const bytecode = toBytecode([
|
||||||
|
["MAKE_FUNCTION", ["x"], ".body"],
|
||||||
|
["STORE", "add1"],
|
||||||
|
["JUMP", ".end"],
|
||||||
|
[".body:"],
|
||||||
|
["LOAD", "x"],
|
||||||
|
["PUSH", 1],
|
||||||
|
["ADD"],
|
||||||
|
["RETURN"],
|
||||||
|
[".end:"],
|
||||||
|
["HALT"]
|
||||||
|
])
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
await vm.run()
|
||||||
|
|
||||||
|
const reefFunction = vm.scope.get("add1")!
|
||||||
|
expect(reefFunction.type).toBe("function")
|
||||||
|
|
||||||
|
// Convert to JS function
|
||||||
|
const jsFunction = fnFromValue(reefFunction, vm)
|
||||||
|
expect(typeof jsFunction).toBe("function")
|
||||||
|
|
||||||
|
// Convert back to Value - should return the original Reef function
|
||||||
|
const backToValue = toValue(jsFunction)
|
||||||
|
expect(backToValue).toBe(reefFunction) // Same reference
|
||||||
|
expect(backToValue.type).toBe("function")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("fromValue - converts native function back to original JS function", async () => {
|
||||||
|
const { VM, toBytecode, toValue, fromValue } = await import("#reef")
|
||||||
|
|
||||||
|
const bytecode = toBytecode([["HALT"]])
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
|
// Create a native JS function
|
||||||
|
const originalFn = (x: number, y: number) => x * y
|
||||||
|
|
||||||
|
// Convert to Value (wraps it as a native function)
|
||||||
|
const nativeValue = toValue(originalFn, vm)
|
||||||
|
expect(nativeValue.type).toBe("native")
|
||||||
|
|
||||||
|
// Convert back to JS - should get the original function
|
||||||
|
const convertedFn = fromValue(nativeValue, vm)
|
||||||
|
expect(typeof convertedFn).toBe("function")
|
||||||
|
|
||||||
|
// Verify it's the same function
|
||||||
|
expect(convertedFn).toBe(originalFn)
|
||||||
|
|
||||||
|
// Verify it works correctly
|
||||||
|
expect(convertedFn(3, 4)).toBe(12)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("fromValue - native function roundtrip preserves functionality", async () => {
|
||||||
|
const { VM, toBytecode, toValue, fromValue } = await import("#reef")
|
||||||
|
|
||||||
|
const bytecode = toBytecode([["HALT"]])
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
|
// Create a native function with state
|
||||||
|
let callCount = 0
|
||||||
|
const countingFn = (n: number) => {
|
||||||
|
callCount++
|
||||||
|
return n * callCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// Roundtrip through Value
|
||||||
|
const nativeValue = toValue(countingFn, vm)
|
||||||
|
const roundtrippedFn = fromValue(nativeValue, vm)
|
||||||
|
|
||||||
|
// Verify it maintains state across calls
|
||||||
|
expect(roundtrippedFn(10)).toBe(10) // 10 * 1
|
||||||
|
expect(roundtrippedFn(10)).toBe(20) // 10 * 2
|
||||||
|
expect(roundtrippedFn(10)).toBe(30) // 10 * 3
|
||||||
|
expect(callCount).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("fromValue - async native function roundtrip", async () => {
|
||||||
|
const { VM, toBytecode, toValue, fromValue } = await import("#reef")
|
||||||
|
|
||||||
|
const bytecode = toBytecode([["HALT"]])
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
|
const asyncFn = async (x: number, y: number) => {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1))
|
||||||
|
return x + y
|
||||||
|
}
|
||||||
|
|
||||||
|
const nativeValue = toValue(asyncFn, vm)
|
||||||
|
expect(nativeValue.type).toBe("native")
|
||||||
|
|
||||||
|
const roundtrippedFn = fromValue(nativeValue, vm)
|
||||||
|
|
||||||
|
const result = await roundtrippedFn(5, 7)
|
||||||
|
expect(result).toBe(12)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("toValue - throws helpful error when calling function converted without VM", async () => {
|
||||||
|
function myFunction(x: number) {
|
||||||
|
return x * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = toValue(myFunction)
|
||||||
|
expect(value.type).toBe('native')
|
||||||
|
|
||||||
|
// Error is thrown when calling the function, not when converting
|
||||||
|
await expect(async () => {
|
||||||
|
await value.fn({ type: 'number', value: 5 })
|
||||||
|
}).toThrow(/can't call function that was converted without a vm/)
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
await value.fn({ type: 'number', value: 5 })
|
||||||
|
}).toThrow(/Function: myFunction/)
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
await value.fn({ type: 'number', value: 5 })
|
||||||
|
}).toThrow(/Source:/)
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
await value.fn({ type: 'number', value: 5 })
|
||||||
|
}).toThrow(/Called from:/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("toValue - error message shows function info for arrow functions", async () => {
|
||||||
|
const anonymousFn = (x: number) => x * 2
|
||||||
|
|
||||||
|
const value = toValue(anonymousFn)
|
||||||
|
expect(value.type).toBe('native')
|
||||||
|
|
||||||
|
// Arrow functions show as <anonymous>
|
||||||
|
await expect(async () => {
|
||||||
|
await value.fn({ type: 'number', value: 5 })
|
||||||
|
}).toThrow(/Function: <anonymous>/)
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
await value.fn({ type: 'number', value: 5 })
|
||||||
|
}).toThrow(/Source:/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("toValue - error when function is nested in object without VM", async () => {
|
||||||
|
const obj = {
|
||||||
|
name: "test",
|
||||||
|
handler: (x: number) => x * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = toValue(obj)
|
||||||
|
expect(value.type).toBe('dict')
|
||||||
|
|
||||||
|
const handlerValue = value.value.get('handler')!
|
||||||
|
expect(handlerValue.type).toBe('native')
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
await (handlerValue as any).fn({ type: 'number', value: 5 })
|
||||||
|
}).toThrow(/can't call function that was converted without a vm/)
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
await (handlerValue as any).fn({ type: 'number', value: 5 })
|
||||||
|
}).toThrow(/Function: handler/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("toValue - error when function is nested in array without VM", async () => {
|
||||||
|
const arr = [1, 2, (x: number) => x * 2]
|
||||||
|
|
||||||
|
const value = toValue(arr)
|
||||||
|
expect(value.type).toBe('array')
|
||||||
|
|
||||||
|
const fnValue = value.value[2]!
|
||||||
|
expect(fnValue.type).toBe('native')
|
||||||
|
|
||||||
|
await expect(async () => {
|
||||||
|
await (fnValue as any).fn({ type: 'number', value: 5 })
|
||||||
|
}).toThrow(/can't call function that was converted without a vm/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("fromValue - throws helpful error when converting function without VM", async () => {
|
||||||
|
const { Scope, fromValue } = await import("#reef")
|
||||||
|
|
||||||
|
const reefFunction = {
|
||||||
|
type: 'function' as const,
|
||||||
|
params: ['x', 'y'],
|
||||||
|
defaults: {},
|
||||||
|
body: 10,
|
||||||
|
parentScope: new Scope(),
|
||||||
|
variadic: false,
|
||||||
|
named: false,
|
||||||
|
value: '<function>' as const
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(() => fromValue(reefFunction)).toThrow(/VM is required for function conversion/)
|
||||||
|
expect(() => fromValue(reefFunction)).toThrow(/Function params: \[x, y\]/)
|
||||||
|
expect(() => fromValue(reefFunction)).toThrow(/Function body at instruction: 10/)
|
||||||
|
expect(() => fromValue(reefFunction)).toThrow(/Called from:/)
|
||||||
|
})
|
||||||
54
tests/vm.test.ts
Normal file
54
tests/vm.test.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { test, expect, describe } from "bun:test"
|
||||||
|
import { VM } from "#vm"
|
||||||
|
import { toBytecode } from "#bytecode"
|
||||||
|
import { toValue } from "#value"
|
||||||
|
|
||||||
|
describe("VM scope methods", () => {
|
||||||
|
test("pushScope creates isolated child scope", async () => {
|
||||||
|
const bytecode = toBytecode([["HALT"]])
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
|
vm.set("x", 42)
|
||||||
|
vm.pushScope()
|
||||||
|
|
||||||
|
const xValue = vm.scope.get("x")
|
||||||
|
expect(xValue).toEqual({ type: "number", value: 42 })
|
||||||
|
|
||||||
|
vm.set("y", 100)
|
||||||
|
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
|
||||||
|
expect(vm.scope.get("y")).toEqual({ type: "number", value: 100 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("popScope returns to parent scope and child variables are not accessible", async () => {
|
||||||
|
const bytecode = toBytecode([["HALT"]])
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
|
vm.set("x", 42)
|
||||||
|
vm.pushScope()
|
||||||
|
vm.set("y", 100)
|
||||||
|
|
||||||
|
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
|
||||||
|
expect(vm.scope.get("y")).toEqual({ type: "number", value: 100 })
|
||||||
|
|
||||||
|
vm.popScope()
|
||||||
|
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
|
||||||
|
expect(vm.scope.get("y")).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("pushScope with locals initializes child scope with variables", async () => {
|
||||||
|
const bytecode = toBytecode([["HALT"]])
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
|
vm.set("x", 42)
|
||||||
|
vm.pushScope({ y: 100, z: "hello" })
|
||||||
|
|
||||||
|
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
|
||||||
|
expect(vm.scope.get("y")).toEqual({ type: "number", value: 100 })
|
||||||
|
expect(vm.scope.get("z")).toEqual({ type: "string", value: "hello" })
|
||||||
|
|
||||||
|
vm.popScope()
|
||||||
|
expect(vm.scope.get("x")).toEqual({ type: "number", value: 42 })
|
||||||
|
expect(vm.scope.get("y")).toBeUndefined()
|
||||||
|
expect(vm.scope.get("z")).toBeUndefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user