diff --git a/src/function.ts b/src/function.ts index eea025c..f2b9e2e 100644 --- a/src/function.ts +++ b/src/function.ts @@ -1,4 +1,5 @@ import { type Value, type NativeFunction, fromValue, toValue } from "./value" +import { VM } from "./vm" export type ParamInfo = { params: string[] @@ -9,9 +10,9 @@ export type ParamInfo = { const WRAPPED_MARKER = Symbol('reef-wrapped') -export function wrapNative(fn: Function): (...args: Value[]) => Promise { +export function wrapNative(vm: VM, fn: Function): (...args: Value[]) => Promise { const wrapped = async (...values: Value[]) => { - const nativeArgs = values.map(fromValue) + const nativeArgs = values.map(arg => fromValue(arg, vm)) const result = await fn(...nativeArgs) return toValue(result) } diff --git a/src/value.ts b/src/value.ts index 823d0eb..d8f1426 100644 --- a/src/value.ts +++ b/src/value.ts @@ -1,4 +1,6 @@ +import { OpCode } from "./opcode" import { Scope } from "./scope" +import { VM } from "./vm" export type NativeFunction = (...args: Value[]) => Promise | Value @@ -154,23 +156,22 @@ export function isEqual(a: Value, b: Value): boolean { } } -export function fromValue(v: Value): any { +export function fromValue(v: Value, vm?: VM): any { switch (v.type) { case 'null': - return null case 'boolean': - return v.value case 'number': - return v.value case 'string': return v.value case 'array': - return v.value.map(fromValue) + return v.value.map(x => fromValue(x, vm)) case 'dict': - return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v)])) + return Object.fromEntries(v.value.entries().map(([k, v]) => [k, fromValue(v, vm)])) case 'regex': return v.value case 'function': + if (!vm || !(vm instanceof VM)) throw new Error('VM is required for function conversion') + return functionFromValue(v, vm) case 'native': return '' } @@ -179,3 +180,33 @@ export function fromValue(v: Value): any { export function toNull(): Value { return toValue(null) } + +function functionFromValue(fn: Value, vm: VM): Function { + if (fn.type !== 'function') + throw new Error('Value is not a function') + + return async function (...args: any[]) { + const newVM = new VM({ + instructions: vm.instructions, + constants: vm.constants, + 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)) + + const targetDepth = newVM.callStack.length + await newVM.execute({ op: OpCode.CALL }) + newVM.pc++ + + while (newVM.callStack.length > targetDepth && newVM.pc < newVM.instructions.length) { + await newVM.execute(newVM.instructions[newVM.pc]!) + newVM.pc++ + } + + return fromValue(newVM.stack.pop() || toNull(), vm) + } +} \ No newline at end of file diff --git a/src/vm.ts b/src/vm.ts index ab1debe..9de2b01 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -30,7 +30,7 @@ export class VM { } registerFunction(name: string, fn: Function) { - const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(fn) + const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(this, fn) this.scope.set(name, { type: 'native', fn: wrapped, value: '' }) } @@ -476,7 +476,7 @@ export class VM { namedDict.set(key, value) } // Convert dict to plain JavaScript object for the native function - const namedObj = fromValue({ type: 'dict', value: namedDict }) + const namedObj = fromValue({ type: 'dict', value: namedDict }, this) nativeArgs.push(toValue(namedObj)) } diff --git a/tests/native.test.ts b/tests/native.test.ts index 4800c09..a76e993 100644 --- a/tests/native.test.ts +++ b/tests/native.test.ts @@ -646,3 +646,299 @@ test("@named pattern - collects only unmatched named args", async () => { expect(result.value.get('extra2')).toEqual(toValue('value2')) } }) + +test("Native function receives Reef function as callback - basic map", 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 'map' with array and Reef function + LOAD map + PUSH 1 + PUSH 2 + PUSH 3 + MAKE_ARRAY #3 + LOAD double + PUSH 2 + PUSH 0 + CALL + `) + + const vm = new VM(bytecode) + + // Native function that takes an array and a callback + vm.registerFunction('map', async (array: any[], callback: Function) => { + const results = [] + for (const item of array) { + results.push(await callback(item)) + } + return results + }) + + const result = await vm.run() + expect(result.type).toBe('array') + if (result.type === 'array') { + console.log(result.value) + expect(result.value).toEqual([ + toValue(2), + toValue(4), + toValue(6) + ]) + } else { + expect(true).toBe(false) + } +}) + +test("Native function receives Reef function - iterator pattern with each", async () => { + const bytecode = toBytecode(` + ; Define a Reef function that adds to a sum + MAKE_FUNCTION (item) .add_to_sum_body + STORE add_to_sum + JUMP .skip_add_to_sum + .add_to_sum_body: + LOAD sum + LOAD item + ADD + STORE sum + RETURN + .skip_add_to_sum: + + ; Initialize sum + PUSH 0 + STORE sum + + ; Call native 'each' with array and Reef function + LOAD each + PUSH 10 + PUSH 20 + PUSH 30 + MAKE_ARRAY #3 + LOAD add_to_sum + PUSH 2 + PUSH 0 + CALL + + ; Return the sum + LOAD sum + `) + + const vm = new VM(bytecode) + + // Native 'each' function (like Ruby's each) + vm.registerFunction('each', async (array: any[], callback: Function) => { + for (const item of array) { + await callback(item) + } + return null + }) + + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 60 }) +}) + +test("Native function receives Reef function - filter pattern", async () => { + const bytecode = toBytecode(` + ; Define a Reef function that checks if number > 5 + MAKE_FUNCTION (x) .is_greater_body + STORE is_greater + JUMP .skip_is_greater + .is_greater_body: + LOAD x + PUSH 5 + GT + RETURN + .skip_is_greater: + + ; Call native 'filter' with array and Reef function + LOAD filter + PUSH 3 + PUSH 7 + PUSH 2 + PUSH 9 + PUSH 1 + MAKE_ARRAY #5 + LOAD is_greater + PUSH 2 + PUSH 0 + CALL + `) + + const vm = new VM(bytecode) + + // Native filter function + vm.registerFunction('filter', async (array: any[], predicate: Function) => { + const results = [] + for (const item of array) { + if (await predicate(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(7), + toValue(9) + ]) + } +}) + +test("Native function receives Reef function - closure capturing", async () => { + const bytecode = toBytecode(` + ; Store 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: + + ; Call native 'map' with the closure function + LOAD map + PUSH 1 + PUSH 2 + PUSH 3 + MAKE_ARRAY #3 + LOAD multiply_by_ten + PUSH 2 + PUSH 0 + CALL + `) + + const vm = new VM(bytecode) + + vm.registerFunction('map', async (array: any[], callback: Function) => { + const results = [] + for (const item of array) { + results.push(await callback(item)) + } + return results + }) + + const result = await vm.run() + expect(result.type).toBe('array') + if (result.type === 'array') { + expect(result.value).toEqual([ + toValue(10), + toValue(20), + toValue(30) + ]) + } +}) + +test("Native function receives Reef function - multiple arguments", async () => { + const bytecode = toBytecode(` + ; Define a Reef function that takes two args + MAKE_FUNCTION (a b) .add_body + STORE add + JUMP .skip_add + .add_body: + LOAD a + LOAD b + ADD + RETURN + .skip_add: + + ; Call native 'reduce' with array, initial value, and Reef function + LOAD reduce + PUSH 1 + PUSH 2 + PUSH 3 + PUSH 4 + MAKE_ARRAY #4 + PUSH 0 + LOAD add + PUSH 3 + PUSH 0 + CALL + `) + + const vm = new VM(bytecode) + + // Native reduce function (accumulator, array, callback) + vm.registerFunction('reduce', async (array: any[], initial: any, callback: Function) => { + let acc = initial + for (const item of array) { + acc = await callback(acc, item) + } + return acc + }) + + const result = await vm.run() + expect(result).toEqual({ type: 'number', value: 10 }) +}) + +test("Native function receives Reef function - returns non-primitive", async () => { + const bytecode = toBytecode(` + ; Define a Reef function that returns a dict + MAKE_FUNCTION (name) .make_user_body + STORE make_user + JUMP .skip_make_user + .make_user_body: + PUSH "name" + LOAD name + PUSH "active" + PUSH true + MAKE_DICT #2 + RETURN + .skip_make_user: + + ; Call native 'map' to create users + LOAD map + PUSH "Alice" + PUSH "Bob" + MAKE_ARRAY #2 + LOAD make_user + PUSH 2 + PUSH 0 + CALL + `) + + const vm = new VM(bytecode) + + vm.registerFunction('map', async (array: any[], callback: Function) => { + const results = [] + for (const item of array) { + results.push(await callback(item)) + } + return results + }) + + const result = await vm.run() + expect(result.type).toBe('array') + if (result.type === 'array') { + expect(result.value.length).toBe(2) + + const first = result.value[0] + expect(first?.type).toBe('dict') + if (first?.type === 'dict') { + expect(first.value.get('name')).toEqual(toValue('Alice')) + expect(first.value.get('active')).toEqual(toValue(true)) + } + + const second = result.value[1] + expect(second?.type).toBe('dict') + if (second?.type === 'dict') { + expect(second.value.get('name')).toEqual(toValue('Bob')) + expect(second.value.get('active')).toEqual(toValue(true)) + } + } +}) \ No newline at end of file