From 797eb281cb39954e223d679ac72624386be0c936 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Fri, 24 Oct 2025 10:53:00 -0700 Subject: [PATCH] vm.call(name, ...args) --- CLAUDE.md | 37 +++++ GUIDE.md | 49 ++++++ README.md | 2 + src/function.ts | 6 +- src/value.ts | 26 ++- src/vm.ts | 20 ++- tests/native.test.ts | 386 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 512 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e08c940..92c7a64 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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: ``` diff --git a/GUIDE.md b/GUIDE.md index fa1e4c9..b0ef2e9 100644 --- a/GUIDE.md +++ b/GUIDE.md @@ -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 diff --git a/README.md b/README.md index c77dff6..094f599 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/function.ts b/src/function.ts index f2b9e2e..61d0e87 100644 --- a/src/function.ts +++ b/src/function.ts @@ -10,10 +10,10 @@ export type ParamInfo = { const WRAPPED_MARKER = Symbol('reef-wrapped') -export function wrapNative(vm: VM, fn: Function): (...args: Value[]) => Promise { - const wrapped = async (...values: Value[]) => { +export function wrapNative(vm: VM, fn: Function): (this: VM, ...args: Value[]) => Promise { + 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) } diff --git a/src/value.ts b/src/value.ts index d8f1426..330fb63 100644 --- a/src/value.ts +++ b/src/value.ts @@ -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 '' } @@ -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 = {} + + 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 }) diff --git a/src/vm.ts b/src/vm.ts index 9de2b01..bc569d1 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -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 = new Map() nativeFunctions: Map = new Map() - constructor(bytecode: Bytecode, functions?: Record) { + constructor(bytecode: Bytecode, functions?: Record) { 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: '' }) } @@ -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 } diff --git a/tests/native.test.ts b/tests/native.test.ts index a76e993..04bd664 100644 --- a/tests/native.test.ts +++ b/tests/native.test.ts @@ -941,4 +941,390 @@ test("Native function receives Reef function - returns non-primitive", async () expect(second.value.get('active')).toEqual(toValue(true)) } } +}) + +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!" }) }) \ No newline at end of file