Compare commits

...

6 Commits

14 changed files with 814 additions and 163 deletions

View File

@ -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.

View File

@ -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

View File

@ -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
View File

@ -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

View File

@ -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
View 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 }
}

View File

@ -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"

View File

@ -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
} }

View File

@ -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

View File

@ -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[],
@ -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
View File

@ -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}`
} }

View File

@ -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
`) `)

View File

@ -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'))
}
})

View File

@ -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
`) `)