vm.call() native functions too

This commit is contained in:
Chris Wanstrath 2025-10-24 16:25:55 -07:00
parent f79fea33c5
commit e1e7cdf1ef
4 changed files with 169 additions and 21 deletions

View File

@ -169,9 +169,9 @@ Auto-wrapping handles:
- Sync and async functions
- Arrays, objects, primitives, null, RegExp
### Calling Reef Functions from TypeScript
### Calling Functions from TypeScript
Use `vm.call()` to invoke Reef functions from TypeScript:
Use `vm.call()` to invoke Reef or native functions from TypeScript:
```typescript
const bytecode = toBytecode(`
@ -186,24 +186,29 @@ const bytecode = toBytecode(`
RETURN
`)
const vm = new VM(bytecode)
const vm = new VM(bytecode, {
log: (msg: string) => console.log(msg) // Native function
})
await vm.run()
// Positional arguments
// Call Reef function with positional arguments
const result1 = await vm.call('add', 5, 3) // → 8
// Named arguments (pass final object)
// Call Reef function with named arguments (pass final object)
const result2 = await vm.call('add', 5, { y: 20 }) // → 25
// All named arguments
// Call Reef function with all named arguments
const result3 = await vm.call('add', { x: 10, y: 15 }) // → 25
// Call native function
await vm.call('log', 'Hello!')
```
**How it works**:
- Looks up function in VM scope
- Converts it to a callable JavaScript function using `fnFromValue`
- Looks up function (Reef or native) in VM scope
- For Reef functions: converts to callable JavaScript function using `fnFromValue`
- For native functions: calls directly
- Automatically converts arguments to ReefVM Values
- Executes the function in a fresh VM context
- Converts result back to JavaScript types
### Label Usage (Preferred)

View File

@ -676,9 +676,9 @@ CALL ; atOptions receives {debug: true, port: 8080}
Named arguments that match fixed parameter names are bound to those parameters. Remaining unmatched named arguments are collected into the `atXxx` parameter as a plain JavaScript object.
### Calling Reef Functions from TypeScript
### Calling Functions from TypeScript
Once you have Reef functions defined in the VM, you can call them from TypeScript using `vm.call()`:
You can call both Reef and native functions from TypeScript using `vm.call()`:
```typescript
const bytecode = toBytecode(`
@ -695,26 +695,32 @@ const bytecode = toBytecode(`
RETURN
`)
const vm = new VM(bytecode)
const vm = new VM(bytecode, {
log: (msg: string) => console.log(msg) // Native function
})
await vm.run()
// Call with positional arguments
// Call Reef function with positional arguments
const result1 = await vm.call('greet', 'Alice')
// Returns: "Hello Alice!"
// Call with named arguments (pass as final object)
// Call Reef function with named arguments (pass as final object)
const result2 = await vm.call('greet', 'Bob', { greeting: 'Hi' })
// Returns: "Hi Bob!"
// Call with only named arguments
// Call Reef function with only named arguments
const result3 = await vm.call('greet', { name: 'Carol', greeting: 'Hey' })
// Returns: "Hey Carol!"
// Call native function
await vm.call('log', 'Hello from TypeScript!')
```
**How it works**:
- `vm.call(functionName, ...args)` looks up the function in the VM's scope
- Converts it to a callable JavaScript function
- Calls it with the provided arguments (automatically converted to ReefVM Values)
- `vm.call(functionName, ...args)` looks up the function (Reef or native) in the VM's scope
- For Reef functions: converts to callable JavaScript function
- For native functions: calls directly
- Arguments are automatically converted to ReefVM Values
- Returns the result (automatically converted back to JavaScript types)
**Named arguments**: Pass a plain object as the final argument to provide named arguments. If the last argument is a non-array object, it's treated as named arguments. All preceding arguments are treated as positional.

View File

@ -35,11 +35,15 @@ export class VM {
const value = this.scope.get(name)
if (!value) throw new Error(`Can't find ${name}`)
if (value.type !== 'function') throw new Error(`Can't call ${name}`)
if (value.type !== 'function' && value.type !== 'native') throw new Error(`Can't call ${name}`)
if (value.type === 'native') {
return await this.callNative(value.fn, args)
} else {
const fn = fnFromValue(value, this)
return await fn(...args)
}
}
registerFunction(name: string, fn: Fn) {
const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(this, fn)
@ -685,4 +689,26 @@ export class VM {
const result = fn(a, b)
this.stack.push({ type: 'boolean', value: result })
}
async callNative(nativeFn: NativeFunction, args: any[]): Promise<Value> {
const originalFn = getOriginalFunction(nativeFn)
const lastArg = args[args.length - 1]
if (lastArg && !Array.isArray(lastArg) && typeof lastArg === 'object') {
const paramInfo = extractParamInfo(originalFn)
const positional = args.slice(0, -1)
const named = lastArg
args = [...positional]
for (let i = positional.length; i < paramInfo.params.length; i++) {
const paramName = paramInfo.params[i]!
if (named[paramName] !== undefined) {
args[i] = named[paramName]
}
}
}
const result = await originalFn.call(this, ...args)
return toValue(result)
}
}

View File

@ -1630,3 +1630,114 @@ test("Native function receives Reef closure with mixed positional and variadic",
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 110 }) // 60 + 50
})
test("vm.call() with native function - basic sync", async () => {
const bytecode = toBytecode(`
HALT
`)
const vm = new VM(bytecode, {
add: (a: number, b: number) => a + b
})
await vm.run()
// Call native function via vm.call()
const result = await vm.call('add', 10, 20)
expect(result).toEqual({ type: 'number', value: 30 })
})
test("vm.call() with native function - async", async () => {
const bytecode = toBytecode(`
HALT
`)
const vm = new VM(bytecode, {
asyncDouble: async (n: number) => {
await new Promise(resolve => setTimeout(resolve, 1))
return n * 2
}
})
await vm.run()
const result = await vm.call('asyncDouble', 21)
expect(result).toEqual({ type: 'number', value: 42 })
})
test("vm.call() with native function - returns array", async () => {
const bytecode = toBytecode(`
HALT
`)
const vm = new VM(bytecode, {
makeRange: (n: number) => Array.from({ length: n }, (_, i) => i)
})
await vm.run()
const result = await vm.call('makeRange', 4)
expect(result.type).toBe('array')
if (result.type === 'array') {
expect(result.value).toEqual([
toValue(0),
toValue(1),
toValue(2),
toValue(3)
])
}
})
test("vm.call() with native function - returns object (becomes dict)", async () => {
const bytecode = toBytecode(`
HALT
`)
const vm = new VM(bytecode, {
makeUser: (name: string, age: number) => ({ name, age })
})
await vm.run()
const result = await vm.call('makeUser', "Alice", 30)
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("vm.call() with native function - named arguments", async () => {
const bytecode = toBytecode(`
HALT
`)
const vm = new VM(bytecode, {
greet: (name: string, greeting = 'Hello') => `${greeting}, ${name}!`
})
await vm.run()
// Call with positional
const result1 = await vm.call('greet', "Alice")
expect(result1).toEqual({ type: 'string', value: "Hello, Alice!" })
// Call with named args
const result2 = await vm.call('greet', "Bob", { greeting: "Hi" })
expect(result2).toEqual({ type: 'string', value: "Hi, Bob!" })
})
test("vm.call() with native function - variadic parameters", async () => {
const bytecode = toBytecode(`
HALT
`)
const vm = new VM(bytecode, {
sum: (...nums: number[]) => nums.reduce((acc, n) => acc + n, 0)
})
await vm.run()
const result = await vm.call('sum', 1, 2, 3, 4, 5)
expect(result).toEqual({ type: 'number', value: 15 })
})