ReefVM/tests/native.test.ts
2025-10-29 15:35:10 -07:00

2363 lines
50 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.setValueFunction('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.setValueFunction('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.setValueFunction('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.setValueFunction('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.setValueFunction('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.setValueFunction('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.setValueFunction('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.set('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.set('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.set('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.set('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.set('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.set('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 set)
vm.set('nativeAdd', (n: number) => n + 10)
// Manual Value function (use setValueFunction)
vm.setValueFunction('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.set('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.set('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.set('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.set('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.set('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.set('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 (vm.set)
LOAD wrappedAdd
PUSH "a"
PUSH 5
PUSH "b"
PUSH 10
PUSH 0
PUSH 2
CALL
STORE result1
; Test non-wrapped function (vm.setValueFunction)
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.set('wrappedAdd', (a: number, b: number) => a + b)
// Non-wrapped function - works directly with Values
vm.setValueFunction('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.set('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.set('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.set('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.set('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.set('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.set('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(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.set('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.set('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.set('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.set('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.set('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.setFunction('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.setFunction('orchestrate', async function (n: number) {
const doubled = await this.call('double', n)
const tripled = await this.call('triple', n)
return doubled + tripled
})
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.setFunction('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.setFunction('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.setFunction('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.setFunction('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.setFunction('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.setFunction('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.setFunction('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!" })
})
test("vm.call() with only 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 ONLY named arguments (no positional)
return await this.call('greet', { name: "Bob", greeting: "Hey" })
}
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: "Hey Bob!" })
})
test("Native function receives Reef closure with default params - calls with named args", async () => {
const bytecode = toBytecode(`
; Reef function with default parameters
MAKE_FUNCTION (x multiplier=2) .transform_body
STORE transform
JUMP .skip_transform
.transform_body:
LOAD x
LOAD multiplier
MUL
RETURN
.skip_transform:
; Pass to native map function
LOAD map
PUSH 1
PUSH 2
PUSH 3
MAKE_ARRAY #3
LOAD transform
PUSH 2
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Native function that calls the Reef closure with named arguments
vm.set('map', async (array: any[], callback: Function) => {
const results = []
for (const item of array) {
// Call with named argument to override the default
results.push(await callback(item, { multiplier: 10 }))
}
return results
})
const result = await vm.run()
expect(result.type).toBe('array')
if (result.type === 'array') {
expect(result.value).toEqual([
toValue(10), // 1 * 10
toValue(20), // 2 * 10
toValue(30) // 3 * 10
])
}
})
test("Native function receives Reef closure with variadic parameters", async () => {
const bytecode = toBytecode(`
; Reef function with variadic params
MAKE_FUNCTION (...nums) .sum_body
STORE sum_all
JUMP .skip_sum
.sum_body:
PUSH 0
STORE total
LOAD nums
STORE items
PUSH 0
STORE idx
.loop:
LOAD idx
LOAD items
ARRAY_LEN
LT
JUMP_IF_FALSE .done
LOAD total
LOAD items
LOAD idx
ARRAY_GET
ADD
STORE total
LOAD idx
PUSH 1
ADD
STORE idx
JUMP .loop
.done:
LOAD total
RETURN
.skip_sum:
; Pass to native function
LOAD process
LOAD sum_all
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Native function that calls the variadic Reef closure with multiple args
vm.set('process', async (callback: Function) => {
// Call with varying number of arguments
const result1 = await callback(1, 2, 3)
const result2 = await callback(10, 20)
return result1 + result2
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 36 }) // (1+2+3) + (10+20) = 6 + 30
})
test("Native function receives Reef closure with @named parameter", async () => {
const bytecode = toBytecode(`
; Reef function with @named parameter
MAKE_FUNCTION (message @options) .format_body
STORE format_message
JUMP .skip_format
.format_body:
; Get uppercase option (default false)
LOAD options
PUSH "uppercase"
DICT_GET
STORE use_upper
; Get prefix option (default empty)
LOAD options
PUSH "prefix"
DICT_GET
STORE prefix_val
; Build result
LOAD prefix_val
LOAD message
STR_CONCAT #2
RETURN
.skip_format:
; Pass to native function
LOAD format_messages
PUSH "hello"
PUSH "world"
MAKE_ARRAY #2
LOAD format_message
PUSH 2
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Native function that calls Reef closure with named arguments
vm.set('format_messages', async (messages: any[], formatter: Function) => {
const results = []
for (const msg of messages) {
// Call with named arguments
results.push(await formatter(msg, { prefix: "[LOG] ", uppercase: true }))
}
return results
})
const result = await vm.run()
expect(result.type).toBe('array')
if (result.type === 'array') {
expect(result.value).toEqual([
toValue('[LOG] hello'),
toValue('[LOG] world')
])
}
})
test("Native function receives Reef closure - calls with only named arguments", async () => {
const bytecode = toBytecode(`
; Reef function expecting named arguments
MAKE_FUNCTION (x=0 y=0 z=0) .add_body
STORE add_coords
JUMP .skip_add
.add_body:
LOAD x
LOAD y
ADD
LOAD z
ADD
RETURN
.skip_add:
; Pass to native function
LOAD compute
LOAD add_coords
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Native function that calls Reef closure with ONLY named arguments
vm.set('compute', async (callback: Function) => {
// First call with all named args
const result1 = await callback({ x: 10, y: 20, z: 30 })
// Second call with partial named args (rest use defaults)
const result2 = await callback({ x: 5, z: 15 })
return result1 + result2
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 80 }) // (10+20+30) + (5+0+15) = 60 + 20
})
test("Native function receives Reef closure with mixed positional and variadic", async () => {
const bytecode = toBytecode(`
; Reef function with fixed param and variadic
MAKE_FUNCTION (multiplier ...nums) .scale_body
STORE scale_numbers
JUMP .skip_scale
.scale_body:
PUSH 0
STORE total
LOAD nums
STORE items
PUSH 0
STORE idx
.loop:
LOAD idx
LOAD items
ARRAY_LEN
LT
JUMP_IF_FALSE .done
LOAD total
LOAD items
LOAD idx
ARRAY_GET
LOAD multiplier
MUL
ADD
STORE total
LOAD idx
PUSH 1
ADD
STORE idx
JUMP .loop
.done:
LOAD total
RETURN
.skip_scale:
; Pass to native function
LOAD process_numbers
LOAD scale_numbers
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Native function that calls Reef closure with mixed args
vm.set('process_numbers', async (calculator: Function) => {
// Call with fixed multiplier and variadic numbers
const result1 = await calculator(10, 1, 2, 3) // 10 * (1+2+3) = 60
const result2 = await calculator(5, 4, 6) // 5 * (4+6) = 50
return result1 + result2
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 110 }) // 60 + 50
})
test("vm.call() with native function - basic sync", async () => {
const bytecode = toBytecode(`
HALT
`)
const vm = new VM(bytecode, {
add: (a: number, b: number) => a + b
})
await vm.run()
// Call native function via vm.call()
const result = await vm.call('add', 10, 20)
expect(result).toEqual({ type: 'number', value: 30 })
})
test("vm.call() with native function - async", async () => {
const bytecode = toBytecode(`
HALT
`)
const vm = new VM(bytecode, {
asyncDouble: async (n: number) => {
await new Promise(resolve => setTimeout(resolve, 1))
return n * 2
}
})
await vm.run()
const result = await vm.call('asyncDouble', 21)
expect(result).toEqual({ type: 'number', value: 42 })
})
test("vm.call() with native function - returns array", async () => {
const bytecode = toBytecode(`
HALT
`)
const vm = new VM(bytecode, {
makeRange: (n: number) => Array.from({ length: n }, (_, i) => i)
})
await vm.run()
const result = await vm.call('makeRange', 4)
expect(result.type).toBe('array')
if (result.type === 'array') {
expect(result.value).toEqual([
toValue(0),
toValue(1),
toValue(2),
toValue(3)
])
}
})
test("vm.call() with native function - returns object (becomes dict)", async () => {
const bytecode = toBytecode(`
HALT
`)
const vm = new VM(bytecode, {
makeUser: (name: string, age: number) => ({ name, age })
})
await vm.run()
const result = await vm.call('makeUser', "Alice", 30)
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("vm.call() with native function - named arguments", async () => {
const bytecode = toBytecode(`
HALT
`)
const vm = new VM(bytecode, {
greet: (name: string, greeting = 'Hello') => `${greeting}, ${name}!`
})
await vm.run()
// Call with positional
const result1 = await vm.call('greet', "Alice")
expect(result1).toEqual({ type: 'string', value: "Hello, Alice!" })
// Call with named args
const result2 = await vm.call('greet', "Bob", { greeting: "Hi" })
expect(result2).toEqual({ type: 'string', value: "Hi, Bob!" })
})
test("vm.call() with native function - variadic parameters", async () => {
const bytecode = toBytecode(`
HALT
`)
const vm = new VM(bytecode, {
sum: (...nums: number[]) => nums.reduce((acc, n) => acc + n, 0)
})
await vm.run()
const result = await vm.call('sum', 1, 2, 3, 4, 5)
expect(result).toEqual({ type: 'number', value: 15 })
})
test("builtin global functions are placed into a higher level scope", async () => {
const bytecode = toBytecode(`
PUSH 1
STORE x
HALT
`)
const vm = new VM(bytecode, {
sum: (...nums: number[]) => nums.reduce((acc, n) => acc + n, 0),
greet: (name: string, greeting = 'Hello') => `${greeting}, ${name}!`
})
await vm.run()
const locals = Array.from(vm.scope.locals.entries()).map(([name,]) => name)
const globals = Array.from(vm.scope.parent!.locals.entries()).map(([name,]) => name)
expect(globals).toEqual(['sum', 'greet'])
expect(locals).toEqual(['x'])
})
test("builtin global scope can be values too", async () => {
const bytecode = toBytecode(`
PUSH 1
STORE x
HALT
`)
const vm = new VM(bytecode, {
pi: 3.14,
universe: true,
algorithms: {
bigOne: () => false,
}
})
await vm.run()
const locals = Array.from(vm.scope.locals.entries()).map(([name,]) => name)
const globals = Array.from(vm.scope.parent!.locals.entries()).map(([name,]) => name)
expect(globals).toEqual(['pi', 'universe', 'algorithms'])
expect(locals).toEqual(['x'])
})
// Raw flag tests - functions that work directly with Values
test("raw flag - basic raw function receives Values directly", async () => {
const bytecode = toBytecode(`
LOAD rawAdd
PUSH 5
PUSH 10
PUSH 2
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Raw function that works directly with Value types
const rawAdd = (a: any, b: any) => {
// Verify we receive Value objects, not unwrapped numbers
expect(a).toEqual({ type: 'number', value: 5 })
expect(b).toEqual({ type: 'number', value: 10 })
return toValue(toNumber(a) + toNumber(b))
}
rawAdd.raw = true
vm.set('rawAdd', rawAdd)
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 15 })
})
test("raw flag - async raw function", async () => {
const bytecode = toBytecode(`
LOAD asyncRawDouble
PUSH 42
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
const asyncRawDouble = async (a: any) => {
await new Promise(resolve => setTimeout(resolve, 1))
expect(a).toEqual({ type: 'number', value: 42 })
return toValue(toNumber(a) * 2)
}
asyncRawDouble.raw = true
vm.set('asyncRawDouble', asyncRawDouble)
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 84 })
})
test("raw flag - raw function with string Value", async () => {
const bytecode = toBytecode(`
LOAD rawConcat
PUSH "hello"
PUSH "world"
PUSH 2
PUSH 0
CALL
`)
const vm = new VM(bytecode)
const rawConcat = (a: any, b: any) => {
// Verify we receive Value objects
expect(a.type).toBe('string')
expect(b.type).toBe('string')
return toValue(toString(a) + ' ' + toString(b))
}
rawConcat.raw = true
vm.set('rawConcat', rawConcat)
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'hello world' })
})
test("raw flag - raw function with array Value", async () => {
const bytecode = toBytecode(`
LOAD rawArrayLength
PUSH 1
PUSH 2
PUSH 3
MAKE_ARRAY #3
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
const rawArrayLength = (arr: any) => {
// Verify we receive a Value object with type 'array'
expect(arr.type).toBe('array')
expect(Array.isArray(arr.value)).toBe(true)
return toValue(arr.value.length)
}
rawArrayLength.raw = true
vm.set('rawArrayLength', rawArrayLength)
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 3 })
})
test("raw flag - raw function with dict Value", async () => {
const bytecode = toBytecode(`
LOAD rawDictKeys
PUSH "name"
PUSH "Alice"
PUSH "age"
PUSH 30
MAKE_DICT #2
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
const rawDictKeys = (dict: any) => {
// Verify we receive a Value object with type 'dict'
expect(dict.type).toBe('dict')
expect(dict.value instanceof Map).toBe(true)
// Return number of keys
return toValue(dict.value.size)
}
rawDictKeys.raw = true
vm.set('rawDictKeys', rawDictKeys)
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 2 })
})
test("raw flag - raw function with variadic parameters", async () => {
const bytecode = toBytecode(`
LOAD rawSum
PUSH 1
PUSH 2
PUSH 3
PUSH 4
PUSH 4
PUSH 0
CALL
`)
const vm = new VM(bytecode)
const rawSum = (...args: any[]) => {
// All args should be Value objects
let total = 0
for (const arg of args) {
expect(arg.type).toBe('number')
total += toNumber(arg)
}
return toValue(total)
}
rawSum.raw = true
vm.set('rawSum', rawSum)
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 10 })
})
test("raw flag - compare raw vs wrapped function behavior", async () => {
const bytecode = toBytecode(`
; Call wrapped function
LOAD wrappedAdd
PUSH 5
PUSH 10
PUSH 2
PUSH 0
CALL
STORE result1
; Call raw function
LOAD rawAdd
PUSH 5
PUSH 10
PUSH 2
PUSH 0
CALL
STORE result2
; Both should give same result
LOAD result1
LOAD result2
EQ
`)
const vm = new VM(bytecode)
// Wrapped function receives native JS types
vm.set('wrappedAdd', (a: number, b: number) => {
// These are native numbers, not Values
expect(typeof a).toBe('number')
expect(typeof b).toBe('number')
return a + b
})
// Raw function receives Values
const rawAdd = (a: any, b: any) => {
// These are Value objects
expect(a.type).toBe('number')
expect(b.type).toBe('number')
return toValue(toNumber(a) + toNumber(b))
}
rawAdd.raw = true
vm.set('rawAdd', rawAdd)
const result = await vm.run()
expect(result).toEqual({ type: 'boolean', value: true })
})
test("raw flag - raw function with null Value", async () => {
const bytecode = toBytecode(`
LOAD rawCheckNull
PUSH null
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
const rawCheckNull = (val: any) => {
// Verify we receive a null Value
expect(val).toEqual({ type: 'null', value: null })
return toValue(val.type === 'null')
}
rawCheckNull.raw = true
vm.set('rawCheckNull', rawCheckNull)
const result = await vm.run()
expect(result).toEqual({ type: 'boolean', value: true })
})
test("raw flag - raw function with boolean Value", async () => {
const bytecode = toBytecode(`
LOAD rawNegate
PUSH true
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
const rawNegate = (val: any) => {
// Verify we receive a boolean Value
expect(val.type).toBe('boolean')
expect(val.value).toBe(true)
return toValue(!val.value)
}
rawNegate.raw = true
vm.set('rawNegate', rawNegate)
const result = await vm.run()
expect(result).toEqual({ type: 'boolean', value: false })
})
test("raw flag - mixed raw and wrapped functions", async () => {
const bytecode = toBytecode(`
; Call wrapped first
LOAD wrapped
PUSH 10
PUSH 1
PUSH 0
CALL
STORE temp
; Call raw with result
LOAD raw
LOAD temp
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Wrapped function
vm.set('wrapped', (n: number) => {
expect(typeof n).toBe('number')
return n * 2
})
// Raw function
const raw = (val: any) => {
expect(val.type).toBe('number')
return toValue(toNumber(val) + 5)
}
raw.raw = true
vm.set('raw', raw)
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 25 }) // (10 * 2) + 5
})
test("raw flag - raw function can manipulate Value structure directly", async () => {
const bytecode = toBytecode(`
LOAD rawTransform
PUSH 1
PUSH 2
PUSH 3
MAKE_ARRAY #3
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
const rawTransform = (arr: any) => {
// Direct manipulation of Value structure
expect(arr.type).toBe('array')
// Transform array by doubling each element
const doubled = arr.value.map((v: any) => toValue(toNumber(v) * 2))
return { type: 'array', value: doubled } as any
}
rawTransform.raw = true
vm.set('rawTransform', rawTransform)
const result = await vm.run()
expect(result.type).toBe('array')
if (result.type === 'array') {
expect(result.value).toEqual([
toValue(2),
toValue(4),
toValue(6)
])
}
})
test("raw flag - raw function receives Reef function as Value", async () => {
const bytecode = toBytecode(`
; Define a Reef function
MAKE_FUNCTION (x) .double_body
STORE double
JUMP .skip_double
.double_body:
LOAD x
PUSH 2
MUL
RETURN
.skip_double:
; Pass it to raw function
LOAD rawInspect
LOAD double
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
const rawInspect = (fn: any) => {
// Verify we receive a function Value
expect(fn.type).toBe('function')
return toValue(true)
}
rawInspect.raw = true
vm.set('rawInspect', rawInspect)
const result = await vm.run()
expect(result).toEqual({ type: 'boolean', value: true })
})
test("raw flag - raw function can return any Value type", async () => {
const bytecode = toBytecode(`
LOAD rawMakeComplex
PUSH 0
PUSH 0
CALL
`)
const vm = new VM(bytecode)
const rawMakeComplex = () => {
// Return a complex nested structure
const innerArray = [toValue(1), toValue(2), toValue(3)]
const dict = new Map()
dict.set('numbers', { type: 'array', value: innerArray })
dict.set('name', toValue('test'))
return { type: 'dict', value: dict } as any
}
rawMakeComplex.raw = true
vm.set('rawMakeComplex', rawMakeComplex)
const result = await vm.run()
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.get('name')).toEqual(toValue('test'))
const numbers = result.value.get('numbers')
expect(numbers?.type).toBe('array')
if (numbers?.type === 'array') {
expect(numbers.value).toEqual([toValue(1), toValue(2), toValue(3)])
}
}
})
test('native function error caught by try/catch - direct call', async () => {
const bytecode = toBytecode(`
PUSH_TRY .catch
LOAD failing_fn
PUSH 0
PUSH 0
CALL
POP_TRY
JUMP .end
.catch:
STORE err
PUSH 'caught: '
LOAD err
STR_CONCAT #2
JUMP .end
.end:
HALT
`)
const vm = new VM(bytecode)
vm.set('failing_fn', () => {
throw new Error('native error')
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'caught: native error' })
})
test('native function error caught by try/catch - inside reef function', async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION () .try_body
STORE call_native
JUMP .after_try_body
.try_body:
LOAD failing_fn
PUSH 0
PUSH 0
CALL
RETURN
.after_try_body:
PUSH_TRY .catch
LOAD call_native
PUSH 0
PUSH 0
CALL
POP_TRY
JUMP .end
.catch:
STORE err
PUSH 'caught: '
LOAD err
STR_CONCAT #2
JUMP .end
.end:
HALT
`)
const vm = new VM(bytecode)
vm.set('failing_fn', () => {
throw new Error('native error')
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'caught: native error' })
})
test('async native function error caught by try/catch', async () => {
const bytecode = toBytecode(`
PUSH_TRY .catch
LOAD async_fail
PUSH 0
PUSH 0
CALL
POP_TRY
JUMP .end
.catch:
STORE err
PUSH 'async caught: '
LOAD err
STR_CONCAT #2
JUMP .end
.end:
HALT
`)
const vm = new VM(bytecode)
vm.set('async_fail', async () => {
await new Promise(resolve => setTimeout(resolve, 1))
throw new Error('async error')
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'async caught: async error' })
})
test('native function error with finally block', async () => {
const bytecode = toBytecode(`
PUSH 0
STORE cleanup_count
PUSH_TRY .catch
PUSH_FINALLY .finally
LOAD failing_fn
PUSH 0
PUSH 0
CALL
POP_TRY
JUMP .finally
.catch:
STORE err
PUSH 'error handled'
JUMP .finally
.finally:
LOAD cleanup_count
PUSH 1
ADD
STORE cleanup_count
HALT
`)
const vm = new VM(bytecode)
vm.set('failing_fn', () => {
throw new Error('native error')
})
await vm.run()
const cleanupCount = vm.scope.get('cleanup_count')
expect(cleanupCount).toEqual({ type: 'number', value: 1 })
})
test('uncaught native function error crashes VM', async () => {
const bytecode = toBytecode(`
LOAD failing_fn
PUSH 0
PUSH 0
CALL
HALT
`)
const vm = new VM(bytecode)
vm.set('failing_fn', () => {
throw new Error('uncaught error')
})
await expect(vm.run()).rejects.toThrow('uncaught error')
})