import { test, expect } from "bun:test" import { toBytecode } from "#bytecode" import { VM } from "#vm" test("MAKE_FUNCTION - creates function with captured scope", async () => { const bytecode = toBytecode(` MAKE_FUNCTION () #999 `) const result = await new VM(bytecode).run() expect(result.type).toBe('function') if (result.type === 'function') { expect(result.body).toBe(999) expect(result.params).toEqual([]) } }) test("CALL and RETURN - basic function call", async () => { const bytecode = toBytecode(` MAKE_FUNCTION () #5 PUSH 0 PUSH 0 CALL HALT PUSH 42 RETURN `) const result = await new VM(bytecode).run() expect(result).toEqual({ type: 'number', value: 42 }) }) test("CALL and RETURN - function with one parameter", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (x) #6 PUSH 100 PUSH 1 PUSH 0 CALL HALT LOAD x RETURN `) const result = await new VM(bytecode).run() expect(result).toEqual({ type: 'number', value: 100 }) }) test("CALL and RETURN - function with two parameters", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (a b) #7 PUSH 10 PUSH 20 PUSH 2 PUSH 0 CALL HALT LOAD a LOAD b ADD RETURN `) const result = await new VM(bytecode).run() expect(result).toEqual({ type: 'number', value: 30 }) }) test("CALL - variadic function with no fixed params", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (...args) #8 PUSH 1 PUSH 2 PUSH 3 PUSH 3 PUSH 0 CALL HALT LOAD args RETURN `) const result = await new VM(bytecode).run() expect(result).toEqual({ type: 'array', value: [ { type: 'number', value: 1 }, { type: 'number', value: 2 }, { type: 'number', value: 3 } ] }) }) test("CALL - variadic function with one fixed param", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (x ...rest) #8 PUSH 10 PUSH 20 PUSH 30 PUSH 3 PUSH 0 CALL HALT LOAD rest RETURN `) const result = await new VM(bytecode).run() // x should be 10, rest should be [20, 30] expect(result).toEqual({ type: 'array', value: [ { type: 'number', value: 20 }, { type: 'number', value: 30 } ] }) }) test("CALL - variadic function with two fixed params", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (a b ...rest) #9 PUSH 1 PUSH 2 PUSH 3 PUSH 4 PUSH 4 PUSH 0 CALL HALT LOAD rest RETURN `) const result = await new VM(bytecode).run() // a=1, b=2, rest=[3, 4] expect(result).toEqual({ type: 'array', value: [ { type: 'number', value: 3 }, { type: 'number', value: 4 } ] }) }) test("CALL - variadic function with no extra args", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (x ...rest) #6 PUSH 10 PUSH 1 PUSH 0 CALL HALT LOAD rest RETURN `) const result = await new VM(bytecode).run() // rest should be empty array expect(result).toEqual({ type: 'array', value: [] }) }) test("CALL - variadic function with defaults on fixed params", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (x=5 ...rest) #5 PUSH 0 PUSH 0 CALL HALT LOAD x RETURN `) const result = await new VM(bytecode).run() // x should use default value 5 expect(result).toEqual({ type: 'number', value: 5 }) }) test("TAIL_CALL - variadic function", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (x ...rest) #8 PUSH 1 PUSH 2 PUSH 3 PUSH 3 PUSH 0 CALL HALT LOAD rest RETURN `) const result = await new VM(bytecode).run() // Should return the rest array [2, 3] expect(result).toEqual({ type: 'array', value: [ { type: 'number', value: 2 }, { type: 'number', value: 3 } ] }) }) test("CALL - named args function with no fixed params", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (@kwargs) #9 PUSH "name" PUSH "Bob" PUSH "age" PUSH 50 PUSH 0 PUSH 2 CALL HALT LOAD kwargs RETURN `) const result = await new VM(bytecode).run() expect(result.type).toBe('dict') if (result.type === 'dict') { expect(result.value.get('name')).toEqual({ type: 'string', value: 'Bob' }) expect(result.value.get('age')).toEqual({ type: 'number', value: 50 }) } }) test("CALL - named args function with one fixed param", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (x @kwargs) #8 PUSH 10 PUSH "name" PUSH "Alice" PUSH 1 PUSH 1 CALL HALT LOAD kwargs RETURN `) const result = await new VM(bytecode).run() expect(result.type).toBe('dict') if (result.type === 'dict') { expect(result.value.get('name')).toEqual({ type: 'string', value: 'Alice' }) expect(result.value.size).toBe(1) } }) test("CALL - named args with matching param name should bind to param not kwargs", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (name @kwargs) #8 PUSH "Bob" PUSH "age" PUSH 50 PUSH 1 PUSH 1 CALL HALT LOAD name RETURN `) const result = await new VM(bytecode).run() // name should be bound as regular param, not collected in kwargs expect(result).toEqual({ type: 'string', value: 'Bob' }) }) test("CALL - named args that match param names should not be in kwargs", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (name age @kwargs) #9 PUSH "name" PUSH "Bob" PUSH "city" PUSH "NYC" PUSH 0 PUSH 2 CALL HALT LOAD kwargs RETURN `) const result = await new VM(bytecode).run() expect(result.type).toBe('dict') if (result.type === 'dict') { // Only city should be in kwargs, name should be bound to param expect(result.value.get('city')).toEqual({ type: 'string', value: 'NYC' }) expect(result.value.has('name')).toBe(false) expect(result.value.size).toBe(1) } }) test("CALL - mixed variadic and named args", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (x ...rest @kwargs) #10 PUSH 1 PUSH 2 PUSH 3 PUSH "name" PUSH "Bob" PUSH 3 PUSH 1 CALL HALT LOAD rest RETURN `) const result = await new VM(bytecode).run() // rest should have [2, 3] expect(result).toEqual({ type: 'array', value: [ { type: 'number', value: 2 }, { type: 'number', value: 3 } ] }) }) test("CALL - mixed variadic and named args, check kwargs", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (x ...rest @kwargs) #10 PUSH 1 PUSH 2 PUSH 3 PUSH "name" PUSH "Bob" PUSH 3 PUSH 1 CALL HALT LOAD kwargs RETURN `) const result = await new VM(bytecode).run() expect(result.type).toBe('dict') if (result.type === 'dict') { expect(result.value.get('name')).toEqual({ type: 'string', value: 'Bob' }) } }) test("CALL - named args with no extra named args", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (x @kwargs) #6 PUSH 10 PUSH 1 PUSH 0 CALL HALT LOAD kwargs RETURN `) const result = await new VM(bytecode).run() // kwargs should be empty dict expect(result.type).toBe('dict') if (result.type === 'dict') { expect(result.value.size).toBe(0) } }) test("CALL - named args with defaults on fixed params", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (x=5 @kwargs) #7 PUSH "name" PUSH "Alice" PUSH 0 PUSH 1 CALL HALT LOAD x RETURN `) const result = await new VM(bytecode).run() // x should use default value 5 expect(result).toEqual({ type: 'number', value: 5 }) })