import { test, expect } from "bun:test" import { toBytecode } from "#bytecode" import { toValue, run } from "#reef" test("MAKE_FUNCTION - creates function with captured scope", async () => { const bytecode = toBytecode(` MAKE_FUNCTION () #999 `) const result = await run(bytecode) 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 run(bytecode) 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 run(bytecode) 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 run(bytecode) 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 run(bytecode) 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 run(bytecode) // 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 run(bytecode) // 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 run(bytecode) // 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 run(bytecode) // 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 run(bytecode) // 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 (@named) #9 PUSH "name" PUSH "Bob" PUSH "age" PUSH 50 PUSH 0 PUSH 2 CALL HALT LOAD named RETURN `) const result = await run(bytecode) 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 @named) #8 PUSH 10 PUSH "name" PUSH "Alice" PUSH 1 PUSH 1 CALL HALT LOAD named RETURN `) const result = await run(bytecode) 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 named", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (name @named) #8 PUSH "Bob" PUSH "age" PUSH 50 PUSH 1 PUSH 1 CALL HALT LOAD name RETURN `) const result = await run(bytecode) // name should be bound as regular param, not collected in named expect(result).toEqual({ type: 'string', value: 'Bob' }) }) test("CALL - named args that match param names should not be in named", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (name age @named) #9 PUSH "name" PUSH "Bob" PUSH "city" PUSH "NYC" PUSH 0 PUSH 2 CALL HALT LOAD named RETURN `) const result = await run(bytecode) expect(result.type).toBe('dict') if (result.type === 'dict') { // Only city should be in named, 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 @named) #10 PUSH 1 PUSH 2 PUSH 3 PUSH "name" PUSH "Bob" PUSH 3 PUSH 1 CALL HALT LOAD rest RETURN `) const result = await run(bytecode) // 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 named", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (x ...rest @named) #10 PUSH 1 PUSH 2 PUSH 3 PUSH "name" PUSH "Bob" PUSH 3 PUSH 1 CALL HALT LOAD named RETURN `) const result = await run(bytecode) 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 @named) #6 PUSH 10 PUSH 1 PUSH 0 CALL HALT LOAD named RETURN `) const result = await run(bytecode) // named 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 @named) #7 PUSH "name" PUSH "Alice" PUSH 0 PUSH 1 CALL HALT LOAD x RETURN `) const result = await run(bytecode) // x should use default value 5 expect(result).toEqual({ type: 'number', value: 5 }) }) test("CALL - fixed params can be named", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (a b) .func_0 STORE minus TRY_LOAD minus PUSH 200 PUSH 'a' PUSH 900 PUSH 1 PUSH 1 CALL HALT .func_0: TRY_LOAD a TRY_LOAD b SUB RETURN `) const result = await run(bytecode) expect(result).toEqual(toValue(700)) }) test("TRY_CALL - calls function if found", async () => { const bytecode = toBytecode([ ["MAKE_FUNCTION", [], ".body"], ["STORE", "myFunc"], ["TRY_CALL", "myFunc"], ["HALT"], [".body:"], ["PUSH", 42], ["RETURN"] ]) const result = await run(bytecode) expect(result).toEqual({ type: 'number', value: 42 }) }) test("TRY_CALL - pushes value if variable exists but is not a function", async () => { const bytecode = toBytecode([ ["PUSH", 99], ["STORE", "myVar"], ["TRY_CALL", "myVar"], ["HALT"] ]) const result = await run(bytecode) expect(result).toEqual({ type: 'number', value: 99 }) }) test("TRY_CALL - pushes string if variable not found", async () => { const bytecode = toBytecode([ ["TRY_CALL", "unknownVar"], ["HALT"] ]) const result = await run(bytecode) expect(result).toEqual({ type: 'string', value: 'unknownVar' }) }) test("TRY_CALL - handles arrays", async () => { const bytecode = toBytecode([ ["PUSH", 1], ["PUSH", 2], ["MAKE_ARRAY", 2], ["STORE", "myArray"], ["TRY_CALL", "myArray"], ["HALT"] ]) const result = await run(bytecode) expect(result.type).toBe('array') if (result.type === 'array') { expect(result.value).toEqual([ { type: 'number', value: 1 }, { type: 'number', value: 2 } ]) } }) test("TRY_CALL - handles dicts", async () => { const bytecode = toBytecode([ ["PUSH", "key"], ["PUSH", "value"], ["MAKE_DICT", 1], ["STORE", "myDict"], ["TRY_CALL", "myDict"], ["HALT"] ]) const result = await run(bytecode) expect(result.type).toBe('dict') if (result.type === 'dict') { expect(result.value.get('key')).toEqual({ type: 'string', value: 'value' }) } }) test("TRY_CALL - handles null values", async () => { const bytecode = toBytecode([ ["PUSH", null], ["STORE", "myNull"], ["TRY_CALL", "myNull"], ["HALT"] ]) const result = await run(bytecode) expect(result).toEqual({ type: 'null', value: null }) }) test("TRY_CALL - function can access its parameters", async () => { const bytecode = toBytecode([ ["MAKE_FUNCTION", ["x"], ".body"], ["STORE", "addFive"], ["PUSH", 10], ["STORE", "x"], ["TRY_CALL", "addFive"], ["HALT"], [".body:"], ["LOAD", "x"], ["PUSH", 5], ["ADD"], ["RETURN"] ]) const result = await run(bytecode) // Function is called with 0 args, so x inside function should be null // Then we add 5 to null (which coerces to 0) expect(result).toEqual({ type: 'number', value: 5 }) }) test("TRY_CALL - with string format", async () => { const bytecode = toBytecode(` MAKE_FUNCTION () #4 STORE myFunc TRY_CALL myFunc HALT PUSH 100 RETURN `) const result = await run(bytecode) expect(result).toEqual({ type: 'number', value: 100 }) })