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') })