forked from defunkt/ReefVM
Call Reef closures inside native functions
This commit is contained in:
parent
eb4f103ba3
commit
91d3eb43e4
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
43
src/value.ts
43
src/value.ts
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue
Block a user