Compare commits
6 Commits
1a18a713d7
...
fa55eb7170
| Author | SHA1 | Date | |
|---|---|---|---|
| fa55eb7170 | |||
| 1cf14636ff | |||
| fe7586a5fa | |||
| 93eff53a76 | |||
| 4d2ae1c9fe | |||
| 62f890e59d |
|
|
@ -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).
|
||||||
|
|
@ -70,7 +69,7 @@ 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
|
||||||
|
|
||||||
|
|
@ -372,8 +371,6 @@ Run `bun test` to verify all tests pass before committing.
|
||||||
|
|
||||||
**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.
|
||||||
|
|
|
||||||
88
GUIDE.md
88
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
|
||||||
|
|
@ -247,9 +239,6 @@ CALL
|
||||||
- `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
|
||||||
|
|
||||||
### If-Else
|
### If-Else
|
||||||
|
|
@ -589,7 +578,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, ...)
|
||||||
|
|
@ -597,11 +586,12 @@ 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
|
### 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
|
**Method 1**: Pass to `run()` or `VM` constructor
|
||||||
```typescript
|
```typescript
|
||||||
const result = await run(bytecode, {
|
const result = await run(bytecode, {
|
||||||
|
|
@ -613,14 +603,78 @@ const result = await run(bytecode, {
|
||||||
const vm = new VM(bytecode, { add, greet })
|
const vm = new VM(bytecode, { add, greet })
|
||||||
```
|
```
|
||||||
|
|
||||||
**Method 2**: Register manually
|
**Method 2**: Register after construction
|
||||||
```typescript
|
```typescript
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
vm.registerFunction('add', (a, b) => a + b)
|
vm.registerFunction('add', (a: number, b: number) => a + b)
|
||||||
await vm.run()
|
await vm.run()
|
||||||
```
|
```
|
||||||
|
|
||||||
Functions are auto-wrapped to convert between native TypeScript and ReefVM Value types. Both sync and async functions work.
|
**Method 3**: Value-based functions (for full control)
|
||||||
|
```typescript
|
||||||
|
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
|
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Auto-wrapping**: `registerFunction` automatically converts between native TypeScript types and ReefVM Value types. Both sync and async functions work.
|
||||||
|
|
||||||
|
**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.registerFunction('greet', (atNamed: any = {}) => {
|
||||||
|
return `Hello, ${atNamed.name || 'World'}!`
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mixed positional and @named
|
||||||
|
vm.registerFunction('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.
|
||||||
|
|
||||||
### Empty Stack
|
### Empty Stack
|
||||||
- RETURN with empty stack returns null
|
- RETURN with empty stack returns null
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ Commands: `clear`, `reset`, `exit`.
|
||||||
- Mixed positional and named arguments with proper priority binding
|
- Mixed positional and named arguments with proper priority binding
|
||||||
- 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 auto-wrapping for native TypeScript types
|
- Native function interop with auto-wrapping for native TypeScript types
|
||||||
|
- Native functions stored in scope, called via LOAD + CALL
|
||||||
- Pass functions directly to `run(bytecode, { fnName: fn })` or `new VM(bytecode, { fnName: fn })`
|
- Pass functions directly to `run(bytecode, { fnName: fn })` or `new VM(bytecode, { fnName: fn })`
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|
|
||||||
152
SPEC.md
152
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
|
||||||
|
|
@ -357,15 +357,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)
|
||||||
|
|
@ -377,8 +382,9 @@ The created function captures `currentScope` as its `parentScope`.
|
||||||
- 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
|
||||||
|
|
||||||
**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
|
||||||
|
|
@ -606,28 +612,93 @@ STR_CONCAT #4 ; → "Count: 42, Active: true"
|
||||||
|
|
||||||
### TypeScript Interop
|
### TypeScript Interop
|
||||||
|
|
||||||
#### CALL_NATIVE
|
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.
|
||||||
**Operand**: Function name (string)
|
|
||||||
**Effect**: Call registered TypeScript function
|
|
||||||
**Stack**: [...args] → [returnValue]
|
|
||||||
|
|
||||||
**Behavior**:
|
**Registration**:
|
||||||
1. Look up function by name in registry
|
|
||||||
2. Mark current frame (if exists) as break target
|
|
||||||
3. Await function call (native function receives arguments and returns a Value)
|
|
||||||
4. Push return value onto stack
|
|
||||||
|
|
||||||
**Notes**:
|
|
||||||
- TypeScript functions are passed the raw stack values as arguments
|
|
||||||
- They must return a valid Value
|
|
||||||
- They can be async (VM awaits them)
|
|
||||||
- Like CALL, but function is from TypeScript registry instead of stack
|
|
||||||
|
|
||||||
**Errors**: Throws if function not found
|
|
||||||
|
|
||||||
**TypeScript Function Signature**:
|
|
||||||
```typescript
|
```typescript
|
||||||
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.registerFunction('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 `registerFunction`): Accept and return native TypeScript types (number, string, boolean, array, object, etc.). The VM automatically converts between Value types and native types.
|
||||||
|
|
||||||
|
2. **Value-based functions** (via `registerValueFunction`): 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.registerFunction('add', (a: number, b: number) => a + b)
|
||||||
|
vm.registerFunction('greet', (name: string) => `Hello, ${name}!`)
|
||||||
|
vm.registerFunction('range', (n: number) => Array.from({ length: n }, (_, i) => i))
|
||||||
|
|
||||||
|
// With defaults
|
||||||
|
vm.registerFunction('greet', (name: string, greeting = 'Hello') => {
|
||||||
|
return `${greeting}, ${name}!`
|
||||||
|
})
|
||||||
|
|
||||||
|
// Variadic functions
|
||||||
|
vm.registerFunction('sum', (...nums: number[]) => {
|
||||||
|
return nums.reduce((acc, n) => acc + n, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Value-based for custom logic
|
||||||
|
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
|
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Async functions
|
||||||
|
vm.registerFunction('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
|
||||||
|
|
@ -787,10 +858,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
|
||||||
|
|
||||||
|
|
@ -835,11 +905,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.registerFunction('multiply', (a: number, b: number) => a * b)
|
||||||
|
|
||||||
|
// Or use Value-based functions
|
||||||
|
vm.registerValueFunction('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
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,8 @@ type InstructionTuple =
|
||||||
// Strings
|
// Strings
|
||||||
| ["STR_CONCAT", number]
|
| ["STR_CONCAT", number]
|
||||||
|
|
||||||
// Native
|
// Arrays and dicts
|
||||||
| ["CALL_NATIVE", string]
|
| ["DOT_GET"]
|
||||||
|
|
||||||
// Special
|
// Special
|
||||||
| ["HALT"]
|
| ["HALT"]
|
||||||
|
|
@ -88,7 +88,7 @@ export type ProgramItem = InstructionTuple | LabelDefinition
|
||||||
// Operand types are determined by prefix/literal:
|
// Operand types are determined by prefix/literal:
|
||||||
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
|
// #42 -> immediate number (e.g., JUMP #5, MAKE_ARRAY #3)
|
||||||
// .label -> label reference (e.g., JUMP .loop_start, MAKE_FUNCTION (x y) .body)
|
// .label -> label reference (e.g., JUMP .loop_start, MAKE_FUNCTION (x y) .body)
|
||||||
// name -> variable/function name (e.g., LOAD x, CALL_NATIVE add)
|
// name -> variable/function name (e.g., LOAD x, LOAD_NATIVE add)
|
||||||
// 42 -> number constant (e.g., PUSH 42)
|
// 42 -> number constant (e.g., PUSH 42)
|
||||||
// "str" -> string constant (e.g., PUSH "hello")
|
// "str" -> string constant (e.g., PUSH "hello")
|
||||||
// 'str' -> string constant (e.g., PUSH 'hello')
|
// 'str' -> string constant (e.g., PUSH 'hello')
|
||||||
|
|
@ -336,7 +336,6 @@ 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
|
||||||
|
|
||||||
|
|
|
||||||
109
src/function.ts
Normal file
109
src/function.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { type Value, type NativeFunction, fromValue, toValue } from "./value"
|
||||||
|
|
||||||
|
export type ParamInfo = {
|
||||||
|
params: string[]
|
||||||
|
defaults: Record<string, Value>
|
||||||
|
variadic: boolean
|
||||||
|
named: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const WRAPPED_MARKER = Symbol('reef-wrapped')
|
||||||
|
|
||||||
|
export function wrapNative(fn: Function): (...args: Value[]) => Promise<Value> {
|
||||||
|
const wrapped = async (...values: Value[]) => {
|
||||||
|
const nativeArgs = values.map(fromValue)
|
||||||
|
const result = await fn(...nativeArgs)
|
||||||
|
return toValue(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
|
@ -8,5 +8,6 @@ export async function run(bytecode: Bytecode, functions?: Record<string, Functio
|
||||||
}
|
}
|
||||||
|
|
||||||
export { type Bytecode, toBytecode, type ProgramItem } from "./bytecode"
|
export { type Bytecode, toBytecode, type ProgramItem } from "./bytecode"
|
||||||
export { type Value, toValue, toString, toNumber, fromValue, toNull, wrapNative } from "./value"
|
export { wrapNative } from "./function"
|
||||||
|
export { type Value, toValue, toString, toNumber, fromValue, toNull } from "./value"
|
||||||
export { VM } from "./vm"
|
export { VM } from "./vm"
|
||||||
|
|
@ -65,9 +65,6 @@ export enum OpCode {
|
||||||
// strings
|
// strings
|
||||||
STR_CONCAT, // operand: value count (number) | stack: [val1, ..., valN] → [string] | concatenate N values
|
STR_CONCAT, // operand: value count (number) | stack: [val1, ..., valN] → [string] | concatenate N values
|
||||||
|
|
||||||
// typescript interop
|
|
||||||
CALL_NATIVE, // operand: function name (identifier) | stack: [...args] → [result] | consumes entire stack
|
|
||||||
|
|
||||||
// special
|
// special
|
||||||
HALT // operand: none | stop execution
|
HALT // operand: none | stop execution
|
||||||
}
|
}
|
||||||
|
|
@ -45,7 +45,6 @@ const OPCODES_WITH_OPERANDS = new Set([
|
||||||
OpCode.MAKE_DICT,
|
OpCode.MAKE_DICT,
|
||||||
OpCode.STR_CONCAT,
|
OpCode.STR_CONCAT,
|
||||||
OpCode.MAKE_FUNCTION,
|
OpCode.MAKE_FUNCTION,
|
||||||
OpCode.CALL_NATIVE,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
const OPCODES_WITHOUT_OPERANDS = new Set([
|
const OPCODES_WITHOUT_OPERANDS = new Set([
|
||||||
|
|
@ -77,6 +76,7 @@ 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
|
// immediate = immediate number, eg #5
|
||||||
|
|
|
||||||
32
src/value.ts
32
src/value.ts
|
|
@ -1,5 +1,7 @@
|
||||||
import { Scope } from "./scope"
|
import { Scope } from "./scope"
|
||||||
|
|
||||||
|
export type NativeFunction = (...args: Value[]) => Promise<Value> | Value
|
||||||
|
|
||||||
export type Value =
|
export type Value =
|
||||||
| { type: 'null', value: null }
|
| { type: 'null', value: null }
|
||||||
| { type: 'boolean', value: boolean }
|
| { type: 'boolean', value: boolean }
|
||||||
|
|
@ -8,6 +10,7 @@ export type Value =
|
||||||
| { type: 'array', value: Value[] }
|
| { type: 'array', value: Value[] }
|
||||||
| { type: 'dict', value: Dict }
|
| { type: 'dict', value: Dict }
|
||||||
| { type: 'regex', value: RegExp }
|
| { type: 'regex', value: RegExp }
|
||||||
|
| { type: 'native', fn: NativeFunction, value: '<function>' }
|
||||||
| {
|
| {
|
||||||
type: 'function',
|
type: 'function',
|
||||||
params: string[],
|
params: string[],
|
||||||
|
|
@ -37,10 +40,10 @@ export function toValue(v: any): Value /* throws */ {
|
||||||
if (v && typeof v === 'object' && 'type' in v && 'value' in v)
|
if (v && typeof v === 'object' && 'type' in v && 'value' in v)
|
||||||
return v as Value
|
return v as Value
|
||||||
|
|
||||||
if (Array.isArray(v))
|
if (Array.isArray(v))
|
||||||
return { type: 'array', value: v.map(toValue) }
|
return { type: 'array', value: v.map(toValue) }
|
||||||
|
|
||||||
if (v instanceof RegExp)
|
if (v instanceof RegExp)
|
||||||
return { type: 'regex', value: v }
|
return { type: 'regex', value: v }
|
||||||
|
|
||||||
switch (typeof v) {
|
switch (typeof v) {
|
||||||
|
|
@ -101,6 +104,8 @@ export function toString(v: Value): string {
|
||||||
return 'null'
|
return 'null'
|
||||||
case 'function':
|
case 'function':
|
||||||
return '<function>'
|
return '<function>'
|
||||||
|
case 'native':
|
||||||
|
return '<function>'
|
||||||
case 'array':
|
case 'array':
|
||||||
return `[${v.value.map(toString).join(', ')}]`
|
return `[${v.value.map(toString).join(', ')}]`
|
||||||
case 'dict': {
|
case 'dict': {
|
||||||
|
|
@ -121,9 +126,7 @@ export function isEqual(a: Value, b: Value): boolean {
|
||||||
case 'null':
|
case 'null':
|
||||||
return true
|
return true
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
return a.value === b.value
|
|
||||||
case 'number':
|
case 'number':
|
||||||
return a.value === b.value
|
|
||||||
case 'string':
|
case 'string':
|
||||||
return a.value === b.value
|
return a.value === b.value
|
||||||
case 'array': {
|
case 'array': {
|
||||||
|
|
@ -144,6 +147,7 @@ export function isEqual(a: Value, b: Value): boolean {
|
||||||
return String(a.value) === String(b.value)
|
return String(a.value) === String(b.value)
|
||||||
}
|
}
|
||||||
case 'function':
|
case 'function':
|
||||||
|
case 'native':
|
||||||
return false // functions never equal
|
return false // functions never equal
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|
@ -167,6 +171,7 @@ export function fromValue(v: Value): any {
|
||||||
case 'regex':
|
case 'regex':
|
||||||
return v.value
|
return v.value
|
||||||
case 'function':
|
case 'function':
|
||||||
|
case 'native':
|
||||||
return '<function>'
|
return '<function>'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -174,22 +179,3 @@ export function fromValue(v: Value): any {
|
||||||
export function toNull(): Value {
|
export function toNull(): Value {
|
||||||
return toValue(null)
|
return toValue(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const WRAPPED_MARKER = Symbol('reef-wrapped')
|
|
||||||
|
|
||||||
export function wrapNative(fn: Function): (...args: Value[]) => Promise<Value> {
|
|
||||||
const wrapped = async (...values: Value[]) => {
|
|
||||||
const nativeArgs = values.map(fromValue)
|
|
||||||
const result = await fn(...nativeArgs)
|
|
||||||
return toValue(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrappedObj = wrapped as any
|
|
||||||
wrappedObj[WRAPPED_MARKER] = true
|
|
||||||
|
|
||||||
return wrapped
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isWrapped(fn: Function): boolean {
|
|
||||||
return !!(fn as any)[WRAPPED_MARKER]
|
|
||||||
}
|
|
||||||
|
|
|
||||||
103
src/vm.ts
103
src/vm.ts
|
|
@ -3,9 +3,8 @@ import type { ExceptionHandler } from "./exception"
|
||||||
import { type Frame } from "./frame"
|
import { type Frame } from "./frame"
|
||||||
import { OpCode } from "./opcode"
|
import { OpCode } from "./opcode"
|
||||||
import { Scope } from "./scope"
|
import { Scope } from "./scope"
|
||||||
import { type Value, toValue, toNumber, isTrue, isEqual, toString, wrapNative, isWrapped } from "./value"
|
import { type Value, type NativeFunction, toValue, toNumber, isTrue, isEqual, toString, fromValue } from "./value"
|
||||||
|
import { extractParamInfo, wrapNative, isWrapped, getOriginalFunction } from "./function"
|
||||||
type NativeFunction = (...args: Value[]) => Promise<Value> | Value
|
|
||||||
|
|
||||||
export class VM {
|
export class VM {
|
||||||
pc = 0
|
pc = 0
|
||||||
|
|
@ -32,11 +31,11 @@ export class VM {
|
||||||
|
|
||||||
registerFunction(name: string, fn: Function) {
|
registerFunction(name: string, fn: Function) {
|
||||||
const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(fn)
|
const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(fn)
|
||||||
this.nativeFunctions.set(name, wrapped)
|
this.scope.set(name, { type: 'native', fn: wrapped, value: '<function>' })
|
||||||
}
|
}
|
||||||
|
|
||||||
registerValueFunction(name: string, fn: NativeFunction) {
|
registerValueFunction(name: string, fn: NativeFunction) {
|
||||||
this.nativeFunctions.set(name, fn)
|
this.scope.set(name, { type: 'native', fn, value: '<function>' })
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(): Promise<Value> {
|
async run(): Promise<Value> {
|
||||||
|
|
@ -383,7 +382,6 @@ 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)
|
||||||
|
|
@ -392,7 +390,9 @@ export class VM {
|
||||||
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
|
||||||
|
|
@ -402,8 +402,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()!)
|
||||||
|
|
@ -431,6 +429,71 @@ 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
|
||||||
|
for (let i = 0; i < nativeFixedParamCount; i++) {
|
||||||
|
const paramName = paramInfo.params[i]!
|
||||||
|
|
||||||
|
// Check if named argument was provided for this param
|
||||||
|
if (namedArgs.has(paramName)) {
|
||||||
|
nativeArgs.push(namedArgs.get(paramName)!)
|
||||||
|
namedArgs.delete(paramName) // Remove from named args so it won't go to @named
|
||||||
|
} else if (nativePositionalArgIndex < positionalArgs.length) {
|
||||||
|
nativeArgs.push(positionalArgs[nativePositionalArgIndex]!)
|
||||||
|
nativePositionalArgIndex++
|
||||||
|
} else if (paramInfo.defaults[paramName] !== undefined) {
|
||||||
|
nativeArgs.push(paramInfo.defaults[paramName]!)
|
||||||
|
} else {
|
||||||
|
nativeArgs.push(toValue(null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 })
|
||||||
|
nativeArgs.push(toValue(namedObj))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the native function with bound args
|
||||||
|
const result = await fn.fn(...nativeArgs)
|
||||||
|
this.stack.push(result)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
if (fn.type !== 'function')
|
if (fn.type !== 'function')
|
||||||
throw new Error('CALL: not a function')
|
throw new Error('CALL: not a function')
|
||||||
|
|
||||||
|
|
@ -592,28 +655,6 @@ 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 `Unknown op: ${instruction.op}`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,12 @@ import { toBytecode } from "#bytecode"
|
||||||
describe("functions parameter", () => {
|
describe("functions parameter", () => {
|
||||||
test("pass functions to run()", async () => {
|
test("pass functions to run()", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD add
|
||||||
PUSH 5
|
PUSH 5
|
||||||
PUSH 3
|
PUSH 3
|
||||||
CALL_NATIVE add
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
@ -20,9 +23,12 @@ describe("functions parameter", () => {
|
||||||
|
|
||||||
test("pass functions to VM constructor", async () => {
|
test("pass functions to VM constructor", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD multiply
|
||||||
PUSH 10
|
PUSH 10
|
||||||
PUSH 2
|
PUSH 2
|
||||||
CALL_NATIVE multiply
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
@ -36,11 +42,19 @@ describe("functions parameter", () => {
|
||||||
|
|
||||||
test("pass multiple functions", async () => {
|
test("pass multiple functions", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD add
|
||||||
PUSH 10
|
PUSH 10
|
||||||
PUSH 5
|
PUSH 5
|
||||||
CALL_NATIVE add
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
|
STORE sum
|
||||||
|
LOAD multiply
|
||||||
|
LOAD sum
|
||||||
PUSH 3
|
PUSH 3
|
||||||
CALL_NATIVE multiply
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
@ -54,9 +68,12 @@ describe("functions parameter", () => {
|
||||||
|
|
||||||
test("auto-wraps native functions", async () => {
|
test("auto-wraps native functions", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD concat
|
||||||
PUSH "hello"
|
PUSH "hello"
|
||||||
PUSH "world"
|
PUSH "world"
|
||||||
CALL_NATIVE concat
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
@ -69,8 +86,11 @@ describe("functions parameter", () => {
|
||||||
|
|
||||||
test("works with async functions", async () => {
|
test("works with async functions", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD delay
|
||||||
PUSH 100
|
PUSH 100
|
||||||
CALL_NATIVE delay
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
@ -86,11 +106,19 @@ describe("functions parameter", () => {
|
||||||
|
|
||||||
test("can combine with manual registerFunction", async () => {
|
test("can combine with manual registerFunction", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD add
|
||||||
PUSH 5
|
PUSH 5
|
||||||
PUSH 3
|
PUSH 3
|
||||||
CALL_NATIVE add
|
|
||||||
PUSH 2
|
PUSH 2
|
||||||
CALL_NATIVE subtract
|
PUSH 0
|
||||||
|
CALL
|
||||||
|
STORE sum
|
||||||
|
LOAD subtract
|
||||||
|
LOAD sum
|
||||||
|
PUSH 2
|
||||||
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
@ -127,8 +155,11 @@ describe("functions parameter", () => {
|
||||||
|
|
||||||
test("function throws error", async () => {
|
test("function throws error", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD divide
|
||||||
PUSH 0
|
PUSH 0
|
||||||
CALL_NATIVE divide
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
@ -147,16 +178,25 @@ describe("functions parameter", () => {
|
||||||
|
|
||||||
test("complex workflow with multiple function calls", async () => {
|
test("complex workflow with multiple function calls", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD add
|
||||||
PUSH 5
|
PUSH 5
|
||||||
PUSH 3
|
PUSH 3
|
||||||
CALL_NATIVE add
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
STORE result
|
STORE result
|
||||||
|
LOAD multiply
|
||||||
LOAD result
|
LOAD result
|
||||||
PUSH 2
|
PUSH 2
|
||||||
CALL_NATIVE multiply
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
STORE final
|
STORE final
|
||||||
|
LOAD format
|
||||||
LOAD final
|
LOAD final
|
||||||
CALL_NATIVE format
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
@ -171,8 +211,11 @@ describe("functions parameter", () => {
|
||||||
|
|
||||||
test("function overriding - later registration wins", async () => {
|
test("function overriding - later registration wins", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD getValue
|
||||||
PUSH 5
|
PUSH 5
|
||||||
CALL_NATIVE getValue
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,14 @@ import { VM } from "#vm"
|
||||||
import { toBytecode } from "#bytecode"
|
import { toBytecode } from "#bytecode"
|
||||||
import { toValue, toNumber, toString } from "#value"
|
import { toValue, toNumber, toString } from "#value"
|
||||||
|
|
||||||
test("CALL_NATIVE - basic function call", async () => {
|
test("LOAD - basic function call", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD add
|
||||||
PUSH 5
|
PUSH 5
|
||||||
PUSH 10
|
PUSH 10
|
||||||
CALL_NATIVE add
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
@ -21,11 +24,14 @@ test("CALL_NATIVE - basic function call", async () => {
|
||||||
expect(result).toEqual({ type: 'number', value: 15 })
|
expect(result).toEqual({ type: 'number', value: 15 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("CALL_NATIVE - function with string manipulation", async () => {
|
test("LOAD - function with string manipulation", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD concat
|
||||||
PUSH "hello"
|
PUSH "hello"
|
||||||
PUSH "world"
|
PUSH "world"
|
||||||
CALL_NATIVE concat
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
@ -40,10 +46,13 @@ test("CALL_NATIVE - function with string manipulation", async () => {
|
||||||
expect(result).toEqual({ type: 'string', value: 'hello world' })
|
expect(result).toEqual({ type: 'string', value: 'hello world' })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("CALL_NATIVE - async function", async () => {
|
test("LOAD - async function", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD asyncDouble
|
||||||
PUSH 42
|
PUSH 42
|
||||||
CALL_NATIVE asyncDouble
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
@ -58,9 +67,12 @@ test("CALL_NATIVE - async function", async () => {
|
||||||
expect(result).toEqual({ type: 'number', value: 84 })
|
expect(result).toEqual({ type: 'number', value: 84 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("CALL_NATIVE - function with no arguments", async () => {
|
test("LOAD - function with no arguments", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
CALL_NATIVE getAnswer
|
LOAD getAnswer
|
||||||
|
PUSH 0
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
@ -73,12 +85,15 @@ test("CALL_NATIVE - function with no arguments", async () => {
|
||||||
expect(result).toEqual({ type: 'number', value: 42 })
|
expect(result).toEqual({ type: 'number', value: 42 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("CALL_NATIVE - function with multiple arguments", async () => {
|
test("LOAD - function with multiple arguments", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD sum
|
||||||
PUSH 2
|
PUSH 2
|
||||||
PUSH 3
|
PUSH 3
|
||||||
PUSH 4
|
PUSH 4
|
||||||
CALL_NATIVE sum
|
PUSH 3
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
@ -92,10 +107,13 @@ test("CALL_NATIVE - function with multiple arguments", async () => {
|
||||||
expect(result).toEqual({ type: 'number', value: 9 })
|
expect(result).toEqual({ type: 'number', value: 9 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("CALL_NATIVE - function returns array", async () => {
|
test("LOAD - function returns array", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD makeRange
|
||||||
PUSH 3
|
PUSH 3
|
||||||
CALL_NATIVE makeRange
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
@ -121,20 +139,26 @@ test("CALL_NATIVE - function returns array", async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
test("CALL_NATIVE - function not found", async () => {
|
test("LOAD - function not found", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
CALL_NATIVE nonexistent
|
LOAD nonexistent
|
||||||
|
PUSH 0
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
expect(vm.run()).rejects.toThrow('CALL_NATIVE: function not found: nonexistent')
|
expect(vm.run()).rejects.toThrow('Undefined variable: nonexistent')
|
||||||
})
|
})
|
||||||
|
|
||||||
test("CALL_NATIVE - using result in subsequent operations", async () => {
|
test("LOAD - using result in subsequent operations", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD triple
|
||||||
PUSH 5
|
PUSH 5
|
||||||
CALL_NATIVE triple
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
PUSH 10
|
PUSH 10
|
||||||
ADD
|
ADD
|
||||||
`)
|
`)
|
||||||
|
|
@ -151,9 +175,12 @@ test("CALL_NATIVE - using result in subsequent operations", async () => {
|
||||||
|
|
||||||
test("Native function wrapping - basic sync function with native types", async () => {
|
test("Native function wrapping - basic sync function with native types", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD add
|
||||||
PUSH 5
|
PUSH 5
|
||||||
PUSH 10
|
PUSH 10
|
||||||
CALL_NATIVE add
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
@ -169,8 +196,11 @@ test("Native function wrapping - basic sync function with native types", async (
|
||||||
|
|
||||||
test("Native function wrapping - async function with native types", async () => {
|
test("Native function wrapping - async function with native types", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD asyncDouble
|
||||||
PUSH 42
|
PUSH 42
|
||||||
CALL_NATIVE asyncDouble
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
@ -187,9 +217,12 @@ test("Native function wrapping - async function with native types", async () =>
|
||||||
|
|
||||||
test("Native function wrapping - string manipulation", async () => {
|
test("Native function wrapping - string manipulation", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD concat
|
||||||
PUSH "hello"
|
PUSH "hello"
|
||||||
PUSH "world"
|
PUSH "world"
|
||||||
CALL_NATIVE concat
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
@ -205,8 +238,11 @@ test("Native function wrapping - string manipulation", async () => {
|
||||||
|
|
||||||
test("Native function wrapping - with default parameters", async () => {
|
test("Native function wrapping - with default parameters", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD ls
|
||||||
PUSH "/home/user"
|
PUSH "/home/user"
|
||||||
CALL_NATIVE ls
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
@ -222,8 +258,11 @@ test("Native function wrapping - with default parameters", async () => {
|
||||||
|
|
||||||
test("Native function wrapping - returns array", async () => {
|
test("Native function wrapping - returns array", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD makeRange
|
||||||
PUSH 3
|
PUSH 3
|
||||||
CALL_NATIVE makeRange
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
@ -247,9 +286,12 @@ test("Native function wrapping - returns array", async () => {
|
||||||
|
|
||||||
test("Native function wrapping - returns object (becomes dict)", async () => {
|
test("Native function wrapping - returns object (becomes dict)", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD makeUser
|
||||||
PUSH "Alice"
|
PUSH "Alice"
|
||||||
PUSH 30
|
PUSH 30
|
||||||
CALL_NATIVE makeUser
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
@ -269,9 +311,17 @@ test("Native function wrapping - returns object (becomes dict)", async () => {
|
||||||
|
|
||||||
test("Native function wrapping - mixed with manual Value functions", async () => {
|
test("Native function wrapping - mixed with manual Value functions", async () => {
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD nativeAdd
|
||||||
PUSH 5
|
PUSH 5
|
||||||
CALL_NATIVE nativeAdd
|
PUSH 1
|
||||||
CALL_NATIVE manualDouble
|
PUSH 0
|
||||||
|
CALL
|
||||||
|
STORE sum
|
||||||
|
LOAD manualDouble
|
||||||
|
LOAD sum
|
||||||
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const vm = new VM(bytecode)
|
const vm = new VM(bytecode)
|
||||||
|
|
@ -287,3 +337,287 @@ test("Native function wrapping - mixed with manual Value functions", async () =>
|
||||||
const result = await vm.run()
|
const result = await vm.run()
|
||||||
expect(result).toEqual({ type: 'number', value: 30 })
|
expect(result).toEqual({ type: 'number', value: 30 })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("Named arguments - basic named arg", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
LOAD greet
|
||||||
|
PUSH "name"
|
||||||
|
PUSH "Alice"
|
||||||
|
PUSH 0
|
||||||
|
PUSH 1
|
||||||
|
CALL
|
||||||
|
`)
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.registerFunction('greet', (name: string) => `Hello, ${name}!`)
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'Hello, Alice!' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Named arguments - mixed positional and named", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
LOAD makeUser
|
||||||
|
PUSH "Alice"
|
||||||
|
PUSH "age"
|
||||||
|
PUSH 30
|
||||||
|
PUSH 1
|
||||||
|
PUSH 1
|
||||||
|
CALL
|
||||||
|
`)
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.registerFunction('makeUser', (name: string, age: number) => {
|
||||||
|
return { name, age }
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result.type).toBe('dict')
|
||||||
|
if (result.type === 'dict') {
|
||||||
|
expect(result.value.get('name')).toEqual(toValue('Alice'))
|
||||||
|
expect(result.value.get('age')).toEqual(toValue(30))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Named arguments - named takes priority over positional", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
LOAD add
|
||||||
|
PUSH 100
|
||||||
|
PUSH "a"
|
||||||
|
PUSH 5
|
||||||
|
PUSH "b"
|
||||||
|
PUSH 10
|
||||||
|
PUSH 1
|
||||||
|
PUSH 2
|
||||||
|
CALL
|
||||||
|
`)
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.registerFunction('add', (a: number, b: number) => a + b)
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
// Named args should be: a=5, b=10
|
||||||
|
// Positional arg (100) is provided but named args take priority
|
||||||
|
expect(result).toEqual({ type: 'number', value: 15 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Named arguments - with defaults", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
LOAD greet
|
||||||
|
PUSH "name"
|
||||||
|
PUSH "Bob"
|
||||||
|
PUSH 0
|
||||||
|
PUSH 1
|
||||||
|
CALL
|
||||||
|
`)
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.registerFunction('greet', (name: string, greeting = 'Hello') => {
|
||||||
|
return `${greeting}, ${name}!`
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'Hello, Bob!' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Named arguments - override defaults with named args", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
LOAD greet
|
||||||
|
PUSH "name"
|
||||||
|
PUSH "Bob"
|
||||||
|
PUSH "greeting"
|
||||||
|
PUSH "Hi"
|
||||||
|
PUSH 0
|
||||||
|
PUSH 2
|
||||||
|
CALL
|
||||||
|
`)
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.registerFunction('greet', (name: string, greeting = 'Hello') => {
|
||||||
|
return `${greeting}, ${name}!`
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'Hi, Bob!' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Named arguments - with variadic function", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
LOAD sum
|
||||||
|
PUSH 1
|
||||||
|
PUSH 2
|
||||||
|
PUSH 3
|
||||||
|
PUSH "multiplier"
|
||||||
|
PUSH 2
|
||||||
|
PUSH 3
|
||||||
|
PUSH 1
|
||||||
|
CALL
|
||||||
|
`)
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.registerFunction('sum', (multiplier: number, ...nums: number[]) => {
|
||||||
|
const total = nums.reduce((acc, n) => acc + n, 0)
|
||||||
|
return total * multiplier
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'number', value: 12 }) // (1 + 2 + 3) * 2
|
||||||
|
})
|
||||||
|
|
||||||
|
test("Named arguments - works with both wrapped and non-wrapped functions", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
; Test wrapped function (registerFunction)
|
||||||
|
LOAD wrappedAdd
|
||||||
|
PUSH "a"
|
||||||
|
PUSH 5
|
||||||
|
PUSH "b"
|
||||||
|
PUSH 10
|
||||||
|
PUSH 0
|
||||||
|
PUSH 2
|
||||||
|
CALL
|
||||||
|
STORE result1
|
||||||
|
|
||||||
|
; Test non-wrapped function (registerValueFunction)
|
||||||
|
LOAD valueAdd
|
||||||
|
PUSH "a"
|
||||||
|
PUSH 3
|
||||||
|
PUSH "b"
|
||||||
|
PUSH 7
|
||||||
|
PUSH 0
|
||||||
|
PUSH 2
|
||||||
|
CALL
|
||||||
|
STORE result2
|
||||||
|
|
||||||
|
; Return both results
|
||||||
|
LOAD result1
|
||||||
|
LOAD result2
|
||||||
|
ADD
|
||||||
|
`)
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
|
||||||
|
// Wrapped function - auto-converts types
|
||||||
|
vm.registerFunction('wrappedAdd', (a: number, b: number) => a + b)
|
||||||
|
|
||||||
|
// Non-wrapped function - works directly with Values
|
||||||
|
vm.registerValueFunction('valueAdd', (a, b) => {
|
||||||
|
return toValue(toNumber(a) + toNumber(b))
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'number', value: 25 }) // 15 + 10
|
||||||
|
})
|
||||||
|
|
||||||
|
test("@named pattern - basic atNamed parameter", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
LOAD greet
|
||||||
|
PUSH "name"
|
||||||
|
PUSH "Alice"
|
||||||
|
PUSH "greeting"
|
||||||
|
PUSH "Hi"
|
||||||
|
PUSH "extra"
|
||||||
|
PUSH "value"
|
||||||
|
PUSH 0
|
||||||
|
PUSH 3
|
||||||
|
CALL
|
||||||
|
`)
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.registerFunction('greet', (atNamed: any = {}) => {
|
||||||
|
const name = atNamed.name || 'Unknown'
|
||||||
|
const greeting = atNamed.greeting || 'Hello'
|
||||||
|
const extra = atNamed.extra || ''
|
||||||
|
return `${greeting}, ${name}! Extra: ${extra}`
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'Hi, Alice! Extra: value' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("@named pattern - mixed positional and atOptions", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
LOAD configure
|
||||||
|
PUSH "app"
|
||||||
|
PUSH "debug"
|
||||||
|
PUSH true
|
||||||
|
PUSH "port"
|
||||||
|
PUSH 8080
|
||||||
|
PUSH 1
|
||||||
|
PUSH 2
|
||||||
|
CALL
|
||||||
|
`)
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.registerFunction('configure', (name: string, atOptions: any = {}) => {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
debug: atOptions.debug || false,
|
||||||
|
port: atOptions.port || 3000
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result.type).toBe('dict')
|
||||||
|
if (result.type === 'dict') {
|
||||||
|
expect(result.value.get('name')).toEqual(toValue('app'))
|
||||||
|
expect(result.value.get('debug')).toEqual(toValue(true))
|
||||||
|
expect(result.value.get('port')).toEqual(toValue(8080))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test("@named pattern - with default empty object", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
LOAD build
|
||||||
|
PUSH "project"
|
||||||
|
PUSH 1
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
|
`)
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.registerFunction('build', (name: string, atConfig: any = {}) => {
|
||||||
|
return `Building ${name} with ${Object.keys(atConfig).length} options`
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result).toEqual({ type: 'string', value: 'Building project with 0 options' })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("@named pattern - collects only unmatched named args", async () => {
|
||||||
|
const bytecode = toBytecode(`
|
||||||
|
LOAD process
|
||||||
|
PUSH "file"
|
||||||
|
PUSH "test.txt"
|
||||||
|
PUSH "mode"
|
||||||
|
PUSH "read"
|
||||||
|
PUSH "extra1"
|
||||||
|
PUSH "value1"
|
||||||
|
PUSH "extra2"
|
||||||
|
PUSH "value2"
|
||||||
|
PUSH 0
|
||||||
|
PUSH 4
|
||||||
|
CALL
|
||||||
|
`)
|
||||||
|
|
||||||
|
const vm = new VM(bytecode)
|
||||||
|
vm.registerFunction('process', (file: string, mode: string, atOptions: any = {}) => {
|
||||||
|
// file and mode are matched, extra1 and extra2 go to atOptions
|
||||||
|
return {
|
||||||
|
file,
|
||||||
|
mode,
|
||||||
|
optionCount: Object.keys(atOptions).length,
|
||||||
|
extra1: atOptions.extra1,
|
||||||
|
extra2: atOptions.extra2
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await vm.run()
|
||||||
|
expect(result.type).toBe('dict')
|
||||||
|
if (result.type === 'dict') {
|
||||||
|
expect(result.value.get('file')).toEqual(toValue('test.txt'))
|
||||||
|
expect(result.value.get('mode')).toEqual(toValue('read'))
|
||||||
|
expect(result.value.get('optionCount')).toEqual(toValue(2))
|
||||||
|
expect(result.value.get('extra1')).toEqual(toValue('value1'))
|
||||||
|
expect(result.value.get('extra2')).toEqual(toValue('value2'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -387,9 +387,12 @@ describe("RegExp", () => {
|
||||||
test("with native functions", async () => {
|
test("with native functions", async () => {
|
||||||
const { VM } = await import("#vm")
|
const { VM } = await import("#vm")
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD match
|
||||||
PUSH "hello world"
|
PUSH "hello world"
|
||||||
PUSH /world/
|
PUSH /world/
|
||||||
CALL_NATIVE match
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
@ -407,10 +410,13 @@ describe("RegExp", () => {
|
||||||
test("native function with regex replacement", async () => {
|
test("native function with regex replacement", async () => {
|
||||||
const { VM } = await import("#vm")
|
const { VM } = await import("#vm")
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD replace
|
||||||
PUSH "hello world"
|
PUSH "hello world"
|
||||||
PUSH /o/g
|
PUSH /o/g
|
||||||
PUSH "0"
|
PUSH "0"
|
||||||
CALL_NATIVE replace
|
PUSH 3
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
@ -427,9 +433,12 @@ describe("RegExp", () => {
|
||||||
test("native function extracting matches", async () => {
|
test("native function extracting matches", async () => {
|
||||||
const { VM } = await import("#vm")
|
const { VM } = await import("#vm")
|
||||||
const bytecode = toBytecode(`
|
const bytecode = toBytecode(`
|
||||||
|
LOAD extractNumbers
|
||||||
PUSH "test123abc456"
|
PUSH "test123abc456"
|
||||||
PUSH /\\d+/g
|
PUSH /\\d+/g
|
||||||
CALL_NATIVE extractNumbers
|
PUSH 2
|
||||||
|
PUSH 0
|
||||||
|
CALL
|
||||||
HALT
|
HALT
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user