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