vm.call(name, ...args)

This commit is contained in:
Chris Wanstrath 2025-10-24 10:53:00 -07:00
parent 91d3eb43e4
commit 797eb281cb
7 changed files with 512 additions and 14 deletions

View File

@ -169,6 +169,43 @@ Auto-wrapping handles:
- Sync and async functions
- Arrays, objects, primitives, null, RegExp
### Calling Reef Functions from TypeScript
Use `vm.call()` to invoke Reef functions from TypeScript:
```typescript
const bytecode = toBytecode(`
MAKE_FUNCTION (x y=10) .add
STORE add
HALT
.add:
LOAD x
LOAD y
ADD
RETURN
`)
const vm = new VM(bytecode)
await vm.run()
// Positional arguments
const result1 = await vm.call('add', 5, 3) // → 8
// Named arguments (pass final object)
const result2 = await vm.call('add', 5, { y: 20 }) // → 25
// All named arguments
const result3 = await vm.call('add', { x: 10, y: 15 }) // → 25
```
**How it works**:
- Looks up function in VM scope
- Converts it to a callable JavaScript function using `fnFromValue`
- Automatically converts arguments to ReefVM Values
- Executes the function in a fresh VM context
- Converts result back to JavaScript types
### Label Usage (Preferred)
Use labels instead of numeric offsets for readability:
```

View File

@ -676,6 +676,55 @@ 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
Once you have Reef functions defined in the VM, you can call them from TypeScript using `vm.call()`:
```typescript
const bytecode = toBytecode(`
MAKE_FUNCTION (name greeting="Hello") .greet
STORE greet
HALT
.greet:
LOAD greeting
PUSH " "
LOAD name
PUSH "!"
STR_CONCAT #4
RETURN
`)
const vm = new VM(bytecode)
await vm.run()
// Call with positional arguments
const result1 = await vm.call('greet', 'Alice')
// Returns: "Hello Alice!"
// Call with named arguments (pass as final object)
const result2 = await vm.call('greet', 'Bob', { greeting: 'Hi' })
// Returns: "Hi Bob!"
// Call with only named arguments
const result3 = await vm.call('greet', { name: 'Carol', greeting: 'Hey' })
// Returns: "Hey Carol!"
```
**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)
- Returns the result (automatically converted back to JavaScript types)
**Named arguments**: Pass a plain object as the final argument to provide named arguments. If the last argument is a non-array object, it's treated as named arguments. All preceding arguments are treated as positional.
**Type conversion**: Arguments and return values are automatically converted between JavaScript types and ReefVM Values:
- Primitives: `number`, `string`, `boolean`, `null`
- Arrays: converted recursively
- Objects: converted to ReefVM dicts
- Functions: Reef functions are converted to callable JavaScript functions
### Empty Stack
- RETURN with empty stack returns null
- HALT with empty stack returns null

View File

@ -48,7 +48,9 @@ Commands: `clear`, `reset`, `exit`.
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
- Native function interop with auto-wrapping for native TypeScript types
- Native functions stored in scope, called via LOAD + CALL
- Native functions support `atXxx` parameters (e.g., `atOptions`) to collect unmatched named args
- Pass functions directly to `run(bytecode, { fnName: fn })` or `new VM(bytecode, { fnName: fn })`
- Call Reef functions from TypeScript with `vm.call(name, ...args)` with automatic type conversion
## Design Decisions

View File

@ -10,10 +10,10 @@ export type ParamInfo = {
const WRAPPED_MARKER = Symbol('reef-wrapped')
export function wrapNative(vm: VM, fn: Function): (...args: Value[]) => Promise<Value> {
const wrapped = async (...values: Value[]) => {
export function wrapNative(vm: VM, fn: Function): (this: VM, ...args: Value[]) => Promise<Value> {
const wrapped = async function (this: VM, ...values: Value[]) {
const nativeArgs = values.map(arg => fromValue(arg, vm))
const result = await fn(...nativeArgs)
const result = await fn.call(this, ...nativeArgs)
return toValue(result)
}

View File

@ -171,7 +171,7 @@ export function fromValue(v: Value, vm?: VM): any {
return v.value
case 'function':
if (!vm || !(vm instanceof VM)) throw new Error('VM is required for function conversion')
return functionFromValue(v, vm)
return fnFromValue(v, vm)
case 'native':
return '<function>'
}
@ -181,22 +181,34 @@ export function toNull(): Value {
return toValue(null)
}
function functionFromValue(fn: Value, vm: VM): Function {
export function fnFromValue(fn: Value, vm: VM): Function {
if (fn.type !== 'function')
throw new Error('Value is not a function')
return async function (...args: any[]) {
let positional: any[] = args
let named: Record<string, any> = {}
if (args.length > 0 && !Array.isArray(args[args.length - 1]) && args[args.length - 1].constructor === Object) {
named = args[args.length - 1]
positional = args.slice(0, -1)
}
const newVM = new VM({
instructions: vm.instructions,
constants: vm.constants,
labels: vm.labels,
labels: vm.labels
})
newVM.scope = fn.parentScope
newVM.stack.push(fn)
newVM.stack.push(...args.map(toValue))
newVM.stack.push(toValue(args.length))
newVM.stack.push(toValue(0))
newVM.stack.push(...positional.map(toValue))
for (const [key, val] of Object.entries(named)) {
newVM.stack.push(toValue(key))
newVM.stack.push(toValue(val))
}
newVM.stack.push(toValue(positional.length))
newVM.stack.push(toValue(Object.keys(named).length))
const targetDepth = newVM.callStack.length
await newVM.execute({ op: OpCode.CALL })

View File

@ -3,9 +3,11 @@ import type { ExceptionHandler } from "./exception"
import { type Frame } from "./frame"
import { OpCode } from "./opcode"
import { Scope } from "./scope"
import { type Value, type NativeFunction, toValue, toNumber, isTrue, isEqual, toString, fromValue } from "./value"
import { type Value, type NativeFunction, toValue, toNumber, isTrue, isEqual, toString, fromValue, fnFromValue } from "./value"
import { extractParamInfo, wrapNative, isWrapped, getOriginalFunction } from "./function"
type Fn = (this: VM, ...args: any[]) => any
export class VM {
pc = 0
stopped = false
@ -18,7 +20,7 @@ export class VM {
labels: Map<number, string> = new Map()
nativeFunctions: Map<string, NativeFunction> = new Map()
constructor(bytecode: Bytecode, functions?: Record<string, Function>) {
constructor(bytecode: Bytecode, functions?: Record<string, Fn>) {
this.instructions = bytecode.instructions
this.constants = bytecode.constants
this.labels = bytecode.labels || new Map()
@ -29,7 +31,17 @@ export class VM {
this.registerFunction(name, functions[name]!)
}
registerFunction(name: string, fn: Function) {
async call(name: string, ...args: any) {
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}`)
const fn = fnFromValue(value, this)
return await fn(...args)
}
registerFunction(name: string, fn: Fn) {
const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(this, fn)
this.scope.set(name, { type: 'native', fn: wrapped, value: '<function>' })
}
@ -489,7 +501,7 @@ export class VM {
}
// Call the native function with bound args
const result = await fn.fn(...nativeArgs)
const result = await fn.fn.call(this, ...nativeArgs)
this.stack.push(result)
break
}

View File

@ -942,3 +942,389 @@ test("Native function receives Reef function - returns non-primitive", async ()
}
}
})
test("Native function calls Reef function - basic", async () => {
const bytecode = toBytecode(`
; Define a Reef function that doubles a number
MAKE_FUNCTION (x) .double_body
STORE double
JUMP .skip_double
.double_body:
LOAD x
PUSH 2
MUL
RETURN
.skip_double:
; Call native function that will call the Reef function
LOAD process
PUSH 5
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('process', async function (n: number) {
return await this.call('double', n)
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 10 })
})
test("Native function calls multiple Reef functions", async () => {
const bytecode = toBytecode(`
; Define helper functions
MAKE_FUNCTION (x) .double_body
STORE double
JUMP .skip_double
.double_body:
LOAD x
PUSH 2
MUL
RETURN
.skip_double:
MAKE_FUNCTION (x) .triple_body
STORE triple
JUMP .skip_triple
.triple_body:
LOAD x
PUSH 3
MUL
RETURN
.skip_triple:
; Call native orchestrator
LOAD orchestrate
PUSH 5
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('orchestrate', async function (n: number) {
const doubled = await this.call('double', n)
const tripled = await this.call('triple', n)
return doubled + tripled // 10 + 15 = 25
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 25 })
})
test("Native function conditionally calls Reef functions", async () => {
const bytecode = toBytecode(`
; Define validation functions
MAKE_FUNCTION (x) .is_positive_body
STORE is_positive
JUMP .skip_is_positive
.is_positive_body:
LOAD x
PUSH 0
GT
RETURN
.skip_is_positive:
MAKE_FUNCTION (x) .negate_body
STORE negate
JUMP .skip_negate
.negate_body:
PUSH 0
LOAD x
SUB
RETURN
.skip_negate:
; Test with positive number
LOAD validate
PUSH 5
PUSH 1
PUSH 0
CALL
STORE result1
; Test with negative number
LOAD validate
PUSH -3
PUSH 1
PUSH 0
CALL
STORE result2
; Return sum
LOAD result1
LOAD result2
ADD
`)
const vm = new VM(bytecode)
vm.registerFunction('validate', async function (n: number) {
const isPositive = await this.call('is_positive', n)
if (isPositive) {
return n // Already positive
} else {
return await this.call('negate', n) // Make it positive
}
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 8 }) // 5 + 3
})
test("Native function calls Reef function with closure", async () => {
const bytecode = toBytecode(`
; Set up a multiplier in scope
PUSH 10
STORE multiplier
; Define a Reef function that uses the closure variable
MAKE_FUNCTION (x) .multiply_body
STORE multiply_by_ten
JUMP .skip_multiply
.multiply_body:
LOAD x
LOAD multiplier
MUL
RETURN
.skip_multiply:
; Native function calls the closure
LOAD transform
PUSH 7
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('transform', async function (n: number) {
return await this.call('multiply_by_ten', n)
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 70 })
})
test("Native function uses Reef function as filter predicate", async () => {
const bytecode = toBytecode(`
; Define a predicate function
MAKE_FUNCTION (x) .is_even_body
STORE is_even
JUMP .skip_is_even
.is_even_body:
LOAD x
PUSH 2
MOD
PUSH 0
EQ
RETURN
.skip_is_even:
; Call native filter
LOAD filter_evens
PUSH 1
PUSH 2
PUSH 3
PUSH 4
PUSH 5
MAKE_ARRAY #5
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('filter_evens', async function (array: any[]) {
const results = []
for (const item of array) {
if (await this.call('is_even', item)) {
results.push(item)
}
}
return results
})
const result = await vm.run()
expect(result.type).toBe('array')
if (result.type === 'array') {
expect(result.value).toEqual([
toValue(2),
toValue(4)
])
}
})
test("Reef calls native calls Reef - roundtrip", async () => {
const bytecode = toBytecode(`
; Reef function that squares a number
MAKE_FUNCTION (x) .square_body
STORE square
JUMP .skip_square
.square_body:
LOAD x
LOAD x
MUL
RETURN
.skip_square:
; Reef function that calls native which calls back to Reef
MAKE_FUNCTION (x) .process_body
STORE process
JUMP .skip_process
.process_body:
LOAD native_helper
LOAD x
PUSH 1
PUSH 0
CALL
RETURN
.skip_process:
; Call the Reef function
LOAD process
PUSH 3
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('native_helper', async function (n: number) {
const squared = await this.call('square', n)
return squared + 1 // Add 1 to the squared result
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 10 }) // 3^2 + 1 = 10
})
test("Native function calls Reef function with multiple arguments", async () => {
const bytecode = toBytecode(`
; Reef function that adds three numbers
MAKE_FUNCTION (a b c) .add_three_body
STORE add_three
JUMP .skip_add_three
.add_three_body:
LOAD a
LOAD b
ADD
LOAD c
ADD
RETURN
.skip_add_three:
; Native function that calls it
LOAD calculate
PUSH 0
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('calculate', async function () {
return await this.call('add_three', 10, 20, 30)
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 60 })
})
test("Native function calls Reef function that returns complex type", async () => {
const bytecode = toBytecode(`
; Reef function that creates a user dict
MAKE_FUNCTION (name age) .make_user_body
STORE make_user
JUMP .skip_make_user
.make_user_body:
PUSH "name"
LOAD name
PUSH "age"
LOAD age
PUSH "active"
PUSH true
MAKE_DICT #3
RETURN
.skip_make_user:
; Native function calls it
LOAD create_user
PUSH 0
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('create_user', async function () {
return await this.call('make_user', "Alice", 30)
})
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))
expect(result.value.get('active')).toEqual(toValue(true))
}
})
test("Native function calls non-existent Reef function - throws error", async () => {
const bytecode = toBytecode(`
LOAD bad_caller
PUSH 0
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('bad_caller', async function () {
return await this.call('nonexistent', 42)
})
await expect(vm.run()).rejects.toThrow()
})
test("Native function calls Reef function with named arguments", async () => {
const bytecode = toBytecode(`
; Reef function with default parameters
MAKE_FUNCTION (name greeting='Hello') .greet_body
STORE greet
JUMP .skip_greet
.greet_body:
LOAD greeting
PUSH " "
LOAD name
PUSH "!"
STR_CONCAT #4
RETURN
.skip_greet:
LOAD call_greet
PUSH 0
PUSH 0
CALL
`)
const vm = new VM(bytecode, {
call_greet: async function () {
// Call with named argument as last positional (object)
return await this.call('greet', "Alice", { greeting: "Hi" })
}
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: "Hi Alice!" })
})