add named args to native functions
This commit is contained in:
parent
fe7586a5fa
commit
1cf14636ff
|
|
@ -69,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**: Native functions are stored in scope and called via LOAD + CALL, using the same calling convention as Reef functions. They do not support named arguments.
|
**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
|
||||||
|
|
||||||
|
|
|
||||||
13
GUIDE.md
13
GUIDE.md
|
|
@ -621,15 +621,26 @@ vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
|
|
||||||
**Usage in bytecode**:
|
**Usage in bytecode**:
|
||||||
```
|
```
|
||||||
|
; Positional arguments
|
||||||
LOAD add ; Load native function from scope
|
LOAD add ; Load native function from scope
|
||||||
PUSH 5
|
PUSH 5
|
||||||
PUSH 10
|
PUSH 10
|
||||||
PUSH 2 ; positionalCount
|
PUSH 2 ; positionalCount
|
||||||
PUSH 0 ; namedCount
|
PUSH 0 ; namedCount
|
||||||
CALL ; Call like any other function
|
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!"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Limitations**: Native functions do not support named arguments (namedCount must be 0).
|
**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).
|
||||||
|
|
||||||
### Empty Stack
|
### Empty Stack
|
||||||
- RETURN with empty stack returns null
|
- RETURN with empty stack returns null
|
||||||
|
|
|
||||||
39
SPEC.md
39
SPEC.md
|
|
@ -382,7 +382,7 @@ 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 do not support named arguments** - if namedCount > 0 for a native function, CALL will throw an error
|
- **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 (or native function)
|
**Errors**: Throws if top of stack is not a function (or native function)
|
||||||
|
|
||||||
|
|
@ -647,9 +647,10 @@ CALL ; Call it like any other function
|
||||||
- Supports sync and async functions
|
- Supports sync and async functions
|
||||||
- Objects convert to dicts, arrays convert to Value arrays
|
- Objects convert to dicts, arrays convert to Value arrays
|
||||||
|
|
||||||
**Limitations**:
|
**Named Arguments**:
|
||||||
- Native functions do not support named arguments
|
- Native functions support named arguments by extracting parameter names from the function signature
|
||||||
- If called with named arguments (namedCount > 0), CALL throws an error
|
- 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**:
|
**Examples**:
|
||||||
```typescript
|
```typescript
|
||||||
|
|
@ -658,6 +659,16 @@ vm.registerFunction('add', (a: number, b: number) => a + b)
|
||||||
vm.registerFunction('greet', (name: string) => `Hello, ${name}!`)
|
vm.registerFunction('greet', (name: string) => `Hello, ${name}!`)
|
||||||
vm.registerFunction('range', (n: number) => Array.from({ length: n }, (_, i) => i))
|
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
|
// Value-based for custom logic
|
||||||
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
vm.registerValueFunction('customOp', (a: Value, b: Value): Value => {
|
||||||
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
return { type: 'number', value: toNumber(a) + toNumber(b) }
|
||||||
|
|
@ -670,6 +681,26 @@ vm.registerFunction('fetchData', async (url: string) => {
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**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
|
||||||
|
|
||||||
#### HALT
|
#### HALT
|
||||||
|
|
|
||||||
101
src/function.ts
Normal file
101
src/function.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 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, we skip them and let the function's own default be used
|
||||||
|
} catch {
|
||||||
|
// If parsing fails, ignore the default
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Regular parameter
|
||||||
|
params.push(part)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We don't support @named syntax in TypeScript functions
|
||||||
|
// Users would need to manually handle named args via an options object
|
||||||
|
|
||||||
|
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"
|
||||||
23
src/value.ts
23
src/value.ts
|
|
@ -1,6 +1,6 @@
|
||||||
import { Scope } from "./scope"
|
import { Scope } from "./scope"
|
||||||
|
|
||||||
type NativeFunction = (...args: Value[]) => Promise<Value> | Value
|
export type NativeFunction = (...args: Value[]) => Promise<Value> | Value
|
||||||
|
|
||||||
export type Value =
|
export type Value =
|
||||||
| { type: 'null', value: null }
|
| { type: 'null', value: null }
|
||||||
|
|
@ -10,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[],
|
||||||
|
|
@ -20,7 +21,6 @@ export type Value =
|
||||||
named: boolean,
|
named: boolean,
|
||||||
value: '<function>'
|
value: '<function>'
|
||||||
}
|
}
|
||||||
| { type: 'native', fn: NativeFunction, value: '<function>' }
|
|
||||||
|
|
||||||
export type Dict = Map<string, Value>
|
export type Dict = Map<string, Value>
|
||||||
|
|
||||||
|
|
@ -179,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]
|
|
||||||
}
|
|
||||||
|
|
|
||||||
54
src/vm.ts
54
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 } 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
|
||||||
|
|
@ -432,15 +431,54 @@ export class VM {
|
||||||
|
|
||||||
// Handle native functions
|
// Handle native functions
|
||||||
if (fn.type === 'native') {
|
if (fn.type === 'native') {
|
||||||
if (namedCount > 0)
|
|
||||||
throw new Error('CALL: native functions do not support named arguments')
|
|
||||||
|
|
||||||
// Mark current frame as break target (like regular CALL does)
|
// Mark current frame as break target (like regular CALL does)
|
||||||
if (this.callStack.length > 0)
|
if (this.callStack.length > 0)
|
||||||
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
|
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
|
||||||
|
|
||||||
// Call the native function with positional args
|
// Extract parameter info on-demand
|
||||||
const result = await fn.fn(...positionalArgs)
|
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)
|
||||||
|
let nativeFixedParamCount = paramInfo.params.length
|
||||||
|
if (paramInfo.variadic) 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 so it doesn't cause issues
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native functions don't support @named parameter - extra named args are ignored
|
||||||
|
|
||||||
|
// Call the native function with bound args
|
||||||
|
const result = await fn.fn(...nativeArgs)
|
||||||
this.stack.push(result)
|
this.stack.push(result)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -337,3 +337,173 @@ 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
|
||||||
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user