From e1e7cdf1ef8027bba91237187b2306d10777dfb9 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 24 Oct 2025 16:25:55 -0700 Subject: [PATCH] vm.call() native functions too --- CLAUDE.md | 23 +++++---- GUIDE.md | 24 ++++++---- src/vm.ts | 32 +++++++++++-- tests/native.test.ts | 111 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 21 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 92c7a64..44bf4d0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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) diff --git a/GUIDE.md b/GUIDE.md index b0ef2e9..7c02d2f 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -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. diff --git a/src/vm.ts b/src/vm.ts index bc569d1..be574e0 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -35,10 +35,14 @@ 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}`) - const fn = fnFromValue(value, this) - return await fn(...args) + 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) { @@ -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 { + 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) + } } \ No newline at end of file diff --git a/tests/native.test.ts b/tests/native.test.ts index 887b32f..159fc15 100644 --- a/tests/native.test.ts +++ b/tests/native.test.ts @@ -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 }) +})