Call Reef closures inside native functions

This commit is contained in:
Chris Wanstrath 2025-10-24 10:26:18 -07:00
parent eb4f103ba3
commit 91d3eb43e4
4 changed files with 338 additions and 10 deletions

View File

@ -1,4 +1,5 @@
import { type Value, type NativeFunction, fromValue, toValue } from "./value" import { type Value, type NativeFunction, fromValue, toValue } from "./value"
import { VM } from "./vm"
export type ParamInfo = { export type ParamInfo = {
params: string[] params: string[]
@ -9,9 +10,9 @@ export type ParamInfo = {
const WRAPPED_MARKER = Symbol('reef-wrapped') const WRAPPED_MARKER = Symbol('reef-wrapped')
export function wrapNative(fn: Function): (...args: Value[]) => Promise<Value> { export function wrapNative(vm: VM, fn: Function): (...args: Value[]) => Promise<Value> {
const wrapped = async (...values: Value[]) => { const wrapped = async (...values: Value[]) => {
const nativeArgs = values.map(fromValue) const nativeArgs = values.map(arg => fromValue(arg, vm))
const result = await fn(...nativeArgs) const result = await fn(...nativeArgs)
return toValue(result) return toValue(result)
} }

View File

@ -1,4 +1,6 @@
import { OpCode } from "./opcode"
import { Scope } from "./scope" import { Scope } from "./scope"
import { VM } from "./vm"
export type NativeFunction = (...args: Value[]) => Promise<Value> | Value export type NativeFunction = (...args: Value[]) => Promise<Value> | 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) { switch (v.type) {
case 'null': case 'null':
return null
case 'boolean': case 'boolean':
return v.value
case 'number': case 'number':
return v.value
case 'string': case 'string':
return v.value return v.value
case 'array': case 'array':
return v.value.map(fromValue) return v.value.map(x => fromValue(x, vm))
case 'dict': 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': case 'regex':
return v.value return v.value
case 'function': case 'function':
if (!vm || !(vm instanceof VM)) throw new Error('VM is required for function conversion')
return functionFromValue(v, vm)
case 'native': case 'native':
return '<function>' return '<function>'
} }
@ -179,3 +180,33 @@ export function fromValue(v: Value): any {
export function toNull(): Value { export function toNull(): Value {
return toValue(null) 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)
}
}

View File

@ -30,7 +30,7 @@ export class VM {
} }
registerFunction(name: string, fn: Function) { 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: '<function>' }) this.scope.set(name, { type: 'native', fn: wrapped, value: '<function>' })
} }
@ -476,7 +476,7 @@ export class VM {
namedDict.set(key, value) namedDict.set(key, value)
} }
// Convert dict to plain JavaScript object for the native function // 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)) nativeArgs.push(toValue(namedObj))
} }

View File

@ -646,3 +646,299 @@ test("@named pattern - collects only unmatched named args", async () => {
expect(result.value.get('extra2')).toEqual(toValue('value2')) 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))
}
}
})