1330 lines
29 KiB
TypeScript
1330 lines
29 KiB
TypeScript
import { test, expect } from "bun:test"
|
|
import { VM } from "#vm"
|
|
import { toBytecode } from "#bytecode"
|
|
import { toValue, toNumber, toString } from "#value"
|
|
|
|
test("LOAD - basic function call", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD add
|
|
PUSH 5
|
|
PUSH 10
|
|
PUSH 2
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
// Register a Value-based function
|
|
vm.registerValueFunction('add', (a, b) => {
|
|
return toValue(toNumber(a) + toNumber(b))
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 15 })
|
|
})
|
|
|
|
test("LOAD - function with string manipulation", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD concat
|
|
PUSH "hello"
|
|
PUSH "world"
|
|
PUSH 2
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
vm.registerValueFunction('concat', (a, b) => {
|
|
const aStr = a.type === 'string' ? a.value : toString(a)
|
|
const bStr = b.type === 'string' ? b.value : toString(b)
|
|
return toValue(aStr + ' ' + bStr)
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'hello world' })
|
|
})
|
|
|
|
test("LOAD - async function", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD asyncDouble
|
|
PUSH 42
|
|
PUSH 1
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
vm.registerValueFunction('asyncDouble', async (a) => {
|
|
// Simulate async operation
|
|
await new Promise(resolve => setTimeout(resolve, 1))
|
|
return toValue(toNumber(a) * 2)
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 84 })
|
|
})
|
|
|
|
test("LOAD - function with no arguments", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD getAnswer
|
|
PUSH 0
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
vm.registerValueFunction('getAnswer', () => {
|
|
return toValue(42)
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 42 })
|
|
})
|
|
|
|
test("LOAD - function with multiple arguments", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD sum
|
|
PUSH 2
|
|
PUSH 3
|
|
PUSH 4
|
|
PUSH 3
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
vm.registerValueFunction('sum', (...args) => {
|
|
const total = args.reduce((acc, val) => acc + toNumber(val), 0)
|
|
return toValue(total)
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 9 })
|
|
})
|
|
|
|
test("LOAD - function returns array", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD makeRange
|
|
PUSH 3
|
|
PUSH 1
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
vm.registerValueFunction('makeRange', (n) => {
|
|
const count = toNumber(n)
|
|
const arr = []
|
|
for (let i = 0; i < count; i++) {
|
|
arr.push(toValue(i))
|
|
}
|
|
return { type: 'array', value: arr }
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result.type).toBe('array')
|
|
if (result.type === 'array') {
|
|
expect(result.value.length).toBe(3)
|
|
expect(result.value).toEqual([
|
|
toValue(0),
|
|
toValue(1),
|
|
toValue(2)
|
|
])
|
|
}
|
|
})
|
|
|
|
test("LOAD - function not found", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD nonexistent
|
|
PUSH 0
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
expect(vm.run()).rejects.toThrow('Undefined variable: nonexistent')
|
|
})
|
|
|
|
test("LOAD - using result in subsequent operations", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD triple
|
|
PUSH 5
|
|
PUSH 1
|
|
PUSH 0
|
|
CALL
|
|
PUSH 10
|
|
ADD
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
vm.registerValueFunction('triple', (n) => {
|
|
return toValue(toNumber(n) * 3)
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 25 })
|
|
})
|
|
|
|
test("Native function wrapping - basic sync function with native types", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD add
|
|
PUSH 5
|
|
PUSH 10
|
|
PUSH 2
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
// Register with native TypeScript types - auto-wraps!
|
|
vm.registerFunction('add', (a: number, b: number) => {
|
|
return a + b
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 15 })
|
|
})
|
|
|
|
test("Native function wrapping - async function with native types", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD asyncDouble
|
|
PUSH 42
|
|
PUSH 1
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
// Async native function
|
|
vm.registerFunction('asyncDouble', async (n: number) => {
|
|
await new Promise(resolve => setTimeout(resolve, 1))
|
|
return n * 2
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 84 })
|
|
})
|
|
|
|
test("Native function wrapping - string manipulation", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD concat
|
|
PUSH "hello"
|
|
PUSH "world"
|
|
PUSH 2
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
// Native string function
|
|
vm.registerFunction('concat', (a: string, b: string) => {
|
|
return a + ' ' + b
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'hello world' })
|
|
})
|
|
|
|
test("Native function wrapping - with default parameters", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD ls
|
|
PUSH "/home/user"
|
|
PUSH 1
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
// Function with default parameter (like NOSE commands)
|
|
vm.registerFunction('ls', (path: string, link = false) => {
|
|
return link ? `listing ${path} with links` : `listing ${path}`
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'listing /home/user' })
|
|
})
|
|
|
|
test("Native function wrapping - returns array", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD makeRange
|
|
PUSH 3
|
|
PUSH 1
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
// Return native array - auto-converts to Value array
|
|
vm.registerFunction('makeRange', (n: number) => {
|
|
return Array.from({ length: n }, (_, i) => i)
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result.type).toBe('array')
|
|
if (result.type === 'array') {
|
|
expect(result.value.length).toBe(3)
|
|
expect(result.value).toEqual([
|
|
toValue(0),
|
|
toValue(1),
|
|
toValue(2)
|
|
])
|
|
}
|
|
})
|
|
|
|
test("Native function wrapping - returns object (becomes dict)", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD makeUser
|
|
PUSH "Alice"
|
|
PUSH 30
|
|
PUSH 2
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
// Return plain object - auto-converts to dict
|
|
vm.registerFunction('makeUser', (name: string, age: number) => {
|
|
return { name, age }
|
|
})
|
|
|
|
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))
|
|
}
|
|
})
|
|
|
|
test("Native function wrapping - mixed with manual Value functions", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD nativeAdd
|
|
PUSH 5
|
|
PUSH 1
|
|
PUSH 0
|
|
CALL
|
|
STORE sum
|
|
LOAD manualDouble
|
|
LOAD sum
|
|
PUSH 1
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
// Native function (auto-wrapped by registerFunction)
|
|
vm.registerFunction('nativeAdd', (n: number) => n + 10)
|
|
|
|
// Manual Value function (use registerValueFunction)
|
|
vm.registerValueFunction('manualDouble', (v) => {
|
|
return toValue(toNumber(v) * 2)
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 30 })
|
|
})
|
|
|
|
test("Named arguments - basic named arg", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD greet
|
|
PUSH "name"
|
|
PUSH "Alice"
|
|
PUSH 0
|
|
PUSH 1
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
vm.registerFunction('greet', (name: string) => `Hello, ${name}!`)
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'Hello, Alice!' })
|
|
})
|
|
|
|
test("Named arguments - mixed positional and named", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD makeUser
|
|
PUSH "Alice"
|
|
PUSH "age"
|
|
PUSH 30
|
|
PUSH 1
|
|
PUSH 1
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
vm.registerFunction('makeUser', (name: string, age: number) => {
|
|
return { name, age }
|
|
})
|
|
|
|
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))
|
|
}
|
|
})
|
|
|
|
test("Named arguments - named takes priority over positional", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD add
|
|
PUSH 100
|
|
PUSH "a"
|
|
PUSH 5
|
|
PUSH "b"
|
|
PUSH 10
|
|
PUSH 1
|
|
PUSH 2
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
vm.registerFunction('add', (a: number, b: number) => a + b)
|
|
|
|
const result = await vm.run()
|
|
// Named args should be: a=5, b=10
|
|
// Positional arg (100) is provided but named args take priority
|
|
expect(result).toEqual({ type: 'number', value: 15 })
|
|
})
|
|
|
|
test("Named arguments - with defaults", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD greet
|
|
PUSH "name"
|
|
PUSH "Bob"
|
|
PUSH 0
|
|
PUSH 1
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
vm.registerFunction('greet', (name: string, greeting = 'Hello') => {
|
|
return `${greeting}, ${name}!`
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'Hello, Bob!' })
|
|
})
|
|
|
|
test("Named arguments - override defaults with named args", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD greet
|
|
PUSH "name"
|
|
PUSH "Bob"
|
|
PUSH "greeting"
|
|
PUSH "Hi"
|
|
PUSH 0
|
|
PUSH 2
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
vm.registerFunction('greet', (name: string, greeting = 'Hello') => {
|
|
return `${greeting}, ${name}!`
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'Hi, Bob!' })
|
|
})
|
|
|
|
test("Named arguments - with variadic function", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD sum
|
|
PUSH 1
|
|
PUSH 2
|
|
PUSH 3
|
|
PUSH "multiplier"
|
|
PUSH 2
|
|
PUSH 3
|
|
PUSH 1
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
vm.registerFunction('sum', (multiplier: number, ...nums: number[]) => {
|
|
const total = nums.reduce((acc, n) => acc + n, 0)
|
|
return total * multiplier
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 12 }) // (1 + 2 + 3) * 2
|
|
})
|
|
|
|
test("Named arguments - works with both wrapped and non-wrapped functions", async () => {
|
|
const bytecode = toBytecode(`
|
|
; Test wrapped function (registerFunction)
|
|
LOAD wrappedAdd
|
|
PUSH "a"
|
|
PUSH 5
|
|
PUSH "b"
|
|
PUSH 10
|
|
PUSH 0
|
|
PUSH 2
|
|
CALL
|
|
STORE result1
|
|
|
|
; Test non-wrapped function (registerValueFunction)
|
|
LOAD valueAdd
|
|
PUSH "a"
|
|
PUSH 3
|
|
PUSH "b"
|
|
PUSH 7
|
|
PUSH 0
|
|
PUSH 2
|
|
CALL
|
|
STORE result2
|
|
|
|
; Return both results
|
|
LOAD result1
|
|
LOAD result2
|
|
ADD
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
|
|
// Wrapped function - auto-converts types
|
|
vm.registerFunction('wrappedAdd', (a: number, b: number) => a + b)
|
|
|
|
// Non-wrapped function - works directly with Values
|
|
vm.registerValueFunction('valueAdd', (a, b) => {
|
|
return toValue(toNumber(a) + toNumber(b))
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 25 }) // 15 + 10
|
|
})
|
|
|
|
test("@named pattern - basic atNamed parameter", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD greet
|
|
PUSH "name"
|
|
PUSH "Alice"
|
|
PUSH "greeting"
|
|
PUSH "Hi"
|
|
PUSH "extra"
|
|
PUSH "value"
|
|
PUSH 0
|
|
PUSH 3
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
vm.registerFunction('greet', (atNamed: any = {}) => {
|
|
const name = atNamed.name || 'Unknown'
|
|
const greeting = atNamed.greeting || 'Hello'
|
|
const extra = atNamed.extra || ''
|
|
return `${greeting}, ${name}! Extra: ${extra}`
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'Hi, Alice! Extra: value' })
|
|
})
|
|
|
|
test('@named pattern - atNamed parameters with varadic args', async () => {
|
|
const bytecode = toBytecode(`
|
|
TRY_LOAD cmd
|
|
TRY_LOAD rest1
|
|
TRY_LOAD rest2
|
|
PUSH 'named'
|
|
TRY_LOAD named-value
|
|
PUSH 2
|
|
PUSH 1
|
|
CALL
|
|
HALT
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
vm.registerFunction('cmd', (atNamed: any = {}, ...rest: string[]) => {
|
|
return { rest, namedArg: atNamed['named'] }
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result.type).toBe('dict')
|
|
if (!(result.value instanceof Map)) throw new Error('Expected dict')
|
|
expect(result.value.get('rest')).toEqual(toValue(['rest1', 'rest2']))
|
|
expect(result.value.get('namedArg')).toEqual(toValue('named-value'))
|
|
})
|
|
|
|
test("@named pattern - mixed positional and atOptions", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD configure
|
|
PUSH "app"
|
|
PUSH "debug"
|
|
PUSH true
|
|
PUSH "port"
|
|
PUSH 8080
|
|
PUSH 1
|
|
PUSH 2
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
vm.registerFunction('configure', (name: string, atOptions: any = {}) => {
|
|
return {
|
|
name,
|
|
debug: atOptions.debug || false,
|
|
port: atOptions.port || 3000
|
|
}
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result.type).toBe('dict')
|
|
if (result.type === 'dict') {
|
|
expect(result.value.get('name')).toEqual(toValue('app'))
|
|
expect(result.value.get('debug')).toEqual(toValue(true))
|
|
expect(result.value.get('port')).toEqual(toValue(8080))
|
|
}
|
|
})
|
|
|
|
test("@named pattern - with default empty object", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD build
|
|
PUSH "project"
|
|
PUSH 1
|
|
PUSH 0
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
vm.registerFunction('build', (name: string, atConfig: any = {}) => {
|
|
return `Building ${name} with ${Object.keys(atConfig).length} options`
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'Building project with 0 options' })
|
|
})
|
|
|
|
test("@named pattern - collects only unmatched named args", async () => {
|
|
const bytecode = toBytecode(`
|
|
LOAD process
|
|
PUSH "file"
|
|
PUSH "test.txt"
|
|
PUSH "mode"
|
|
PUSH "read"
|
|
PUSH "extra1"
|
|
PUSH "value1"
|
|
PUSH "extra2"
|
|
PUSH "value2"
|
|
PUSH 0
|
|
PUSH 4
|
|
CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
vm.registerFunction('process', (file: string, mode: string, atOptions: any = {}) => {
|
|
// file and mode are matched, extra1 and extra2 go to atOptions
|
|
return {
|
|
file,
|
|
mode,
|
|
optionCount: Object.keys(atOptions).length,
|
|
extra1: atOptions.extra1,
|
|
extra2: atOptions.extra2
|
|
}
|
|
})
|
|
|
|
const result = await vm.run()
|
|
expect(result.type).toBe('dict')
|
|
if (result.type === 'dict') {
|
|
expect(result.value.get('file')).toEqual(toValue('test.txt'))
|
|
expect(result.value.get('mode')).toEqual(toValue('read'))
|
|
expect(result.value.get('optionCount')).toEqual(toValue(2))
|
|
expect(result.value.get('extra1')).toEqual(toValue('value1'))
|
|
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))
|
|
}
|
|
}
|
|
})
|
|
|
|
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!" })
|
|
}) |