import { test, expect } from "bun:test" import { run } from "#index" import { toBytecode } from "#bytecode" test("ADD - add two numbers", async () => { const str = ` PUSH 1 PUSH 5 ADD ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 6 }) const str2 = ` PUSH 100 PUSH 500 ADD ` expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 600 }) }) test("SUB - subtract two numbers", async () => { const str = ` PUSH 5 PUSH 2 SUB ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 }) }) test("MUL - multiply two numbers", async () => { const str = ` PUSH 5 PUSH 2 MUL ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 }) }) test("DIV - divide two numbers", async () => { const str = ` PUSH 10 PUSH 2 DIV ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 5 }) const str2 = ` PUSH 10 PUSH 0 DIV ` expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: Infinity }) }) test("MOD - modulo two numbers", async () => { const str = ` PUSH 17 PUSH 5 MOD ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 }) }) test("PUSH - pushes value onto stack", async () => { const str = ` PUSH 42 ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) }) test("POP - removes top value", async () => { const str = ` PUSH 10 PUSH 20 POP ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 }) }) test("DUP - duplicates top value", async () => { const str = ` PUSH 5 DUP ADD ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 }) }) test("EQ - equality comparison", async () => { const str = ` PUSH 5 PUSH 5 EQ ` expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) const str2 = ` PUSH 5 PUSH 10 EQ ` expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false }) }) test("NEQ - not equal comparison", async () => { const str = ` PUSH 5 PUSH 10 NEQ ` expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) }) test("LT - less than", async () => { const str = ` PUSH 5 PUSH 10 LT ` expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) }) test("GT - greater than", async () => { const str = ` PUSH 10 PUSH 5 GT ` expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) }) test("LTE - less than or equal", async () => { // equal case const str = ` PUSH 5 PUSH 5 LTE ` expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) // less than case const str2 = ` PUSH 3 PUSH 5 LTE ` expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true }) // greater than case (false) const str3 = ` PUSH 10 PUSH 5 LTE ` expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false }) }) test("GTE - greater than or equal", async () => { // equal case const str = ` PUSH 5 PUSH 5 GTE ` expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) // greater than case const str2 = ` PUSH 10 PUSH 5 GTE ` expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true }) // less than case (false) const str3 = ` PUSH 3 PUSH 5 GTE ` expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false }) }) test("AND pattern - short circuits when false", async () => { // false && should short-circuit and return false const str = ` PUSH 1 PUSH 0 EQ DUP JUMP_IF_FALSE .end POP PUSH 999 .end: ` const result = await run(toBytecode(str)) expect(result.type).toBe('boolean') if (result.type === 'boolean') { expect(result.value).toBe(false) } }) test("AND pattern - evaluates both when true", async () => { const str = ` PUSH 1 DUP JUMP_IF_FALSE .end POP PUSH 2 .end: ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 }) }) test("OR pattern - short circuits when true", async () => { const str = ` PUSH 1 DUP JUMP_IF_TRUE .end POP PUSH 2 .end: ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 1 }) }) test("OR pattern - evaluates second when false", async () => { const str = ` PUSH 1 PUSH 0 EQ DUP JUMP_IF_TRUE .end POP PUSH 2 .end: ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 }) }) test("NOT - logical not", async () => { // number is truthy, so NOT returns false const str = ` PUSH 1 NOT ` expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false }) // 0 is truthy in this language, so NOT returns false const str2 = ` PUSH 0 NOT ` expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false }) // boolean false is falsy, so NOT returns true const str3 = ` PUSH 1 PUSH 0 EQ NOT ` expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true }) }) test("isTruthy - only null and false are falsy", async () => { // 0 is truthy (unlike JS) const str1 = ` PUSH 0 JUMP_IF_FALSE .end PUSH 1 .end: ` expect(await run(toBytecode(str1))).toEqual({ type: 'number', value: 1 }) // empty string is truthy (unlike JS) const str2 = ` PUSH '' JUMP_IF_FALSE .end PUSH 1 .end: ` expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 1 }) // false is falsy const str3 = ` PUSH 0 PUSH 0 EQ JUMP_IF_FALSE .end PUSH 999 .end: ` expect(await run(toBytecode(str3))).toEqual({ type: 'number', value: 999 }) }) test("HALT - stops execution", async () => { const str = ` PUSH 42 HALT PUSH 100 ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) }) test("STORE and LOAD - variables", async () => { const str = ` PUSH 42 STORE x PUSH 21 LOAD x ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) }) test("STORE and LOAD - multiple variables", async () => { const str = ` PUSH 10 STORE a PUSH 20 STORE b PUSH 44 LOAD a LOAD b ADD ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 30 }) }) test("TRY_LOAD - variable found", async () => { const str = ` PUSH 100 STORE count TRY_LOAD count ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 }) const str2 = ` PUSH 'Bobby' STORE name TRY_LOAD name ` expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'Bobby' }) }) test("TRY_LOAD - variable missing", async () => { const str = ` PUSH 100 STORE count TRY_LOAD count1 ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'count1' }) const str2 = ` PUSH 'Bobby' STORE name TRY_LOAD full-name ` expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'full-name' }) }) test("TRY_LOAD - with different value types", async () => { // Array const str1 = ` PUSH 1 PUSH 2 PUSH 3 MAKE_ARRAY #3 STORE arr TRY_LOAD arr ` const result1 = await run(toBytecode(str1)) expect(result1.type).toBe('array') // Dict const str2 = ` PUSH 'key' PUSH 'value' MAKE_DICT #1 STORE dict TRY_LOAD dict ` const result2 = await run(toBytecode(str2)) expect(result2.type).toBe('dict') // Boolean const str3 = ` PUSH true STORE flag TRY_LOAD flag ` expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true }) // Null const str4 = ` PUSH null STORE empty TRY_LOAD empty ` expect(await run(toBytecode(str4))).toEqual({ type: 'null', value: null }) }) test("TRY_LOAD - in nested scope", async () => { // Function should be able to TRY_LOAD variable from parent scope const str = ` PUSH 42 STORE outer MAKE_FUNCTION () .fn PUSH 0 PUSH 0 CALL HALT .fn: TRY_LOAD outer RETURN ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) }) test("TRY_LOAD - missing variable in nested scope returns name", async () => { // If variable doesn't exist in any scope, should return name as string const str = ` PUSH 42 STORE outer MAKE_FUNCTION () .fn PUSH 0 PUSH 0 CALL HALT .fn: TRY_LOAD inner RETURN ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'inner' }) }) test("TRY_LOAD - used for conditional variable existence check", async () => { // Pattern: use TRY_LOAD to check if variable exists and get its value or name const str = ` PUSH 100 STORE count TRY_LOAD count PUSH 'count' EQ ` // Variable exists, so TRY_LOAD returns 100, which != 'count' expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false }) const str2 = ` PUSH 100 STORE count TRY_LOAD missing PUSH 'missing' EQ ` // Variable missing, so TRY_LOAD returns 'missing', which == 'missing' expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true }) }) test("TRY_LOAD - with function value", async () => { const str = ` MAKE_FUNCTION () .fn STORE myFunc JUMP .skip .fn: PUSH 99 RETURN .skip: TRY_LOAD myFunc ` const result = await run(toBytecode(str)) expect(result.type).toBe('function') }) test("JUMP - relative jump forward", async () => { const str = ` PUSH 1 JUMP .skip PUSH 100 .skip: PUSH 2 ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 }) }) test("JUMP - forward jump skips instructions", async () => { // Use forward jump to skip, demonstrating relative addressing const str = ` PUSH 100 JUMP .end PUSH 200 PUSH 300 .end: PUSH 400 ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 400 }) }) test("JUMP_IF_FALSE - conditional jump when false", async () => { const str = ` PUSH 1 PUSH 0 EQ JUMP_IF_FALSE .skip PUSH 100 .skip: PUSH 42 ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) }) test("JUMP_IF_FALSE - no jump when true", async () => { const str = ` PUSH 1 JUMP_IF_FALSE .skip PUSH 100 .skip: ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 }) }) test("JUMP_IF_TRUE - conditional jump when true", async () => { const str = ` PUSH 1 JUMP_IF_TRUE .skip PUSH 100 .skip: PUSH 42 ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) }) test("MAKE_ARRAY - creates array", async () => { const str = ` PUSH 10 PUSH 20 PUSH 30 MAKE_ARRAY #3 ` const result = await run(toBytecode(str)) expect(result.type).toBe('array') if (result.type === 'array') { expect(result.value).toHaveLength(3) expect(result.value[0]).toEqual({ type: 'number', value: 10 }) expect(result.value[1]).toEqual({ type: 'number', value: 20 }) expect(result.value[2]).toEqual({ type: 'number', value: 30 }) } }) test("ARRAY_GET - gets element", async () => { const str = ` PUSH 10 PUSH 20 PUSH 30 MAKE_ARRAY #3 PUSH 1 ARRAY_GET ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 20 }) }) test("ARRAY_SET - sets element", async () => { const str = ` PUSH 10 PUSH 20 PUSH 30 MAKE_ARRAY #3 DUP PUSH 1 PUSH 99 ARRAY_SET PUSH 1 ARRAY_GET ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 99 }) }) test("ARRAY_PUSH - appends to array", async () => { const str = ` PUSH 10 PUSH 20 MAKE_ARRAY #2 DUP PUSH 30 ARRAY_PUSH ARRAY_LEN ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 }) }) test("ARRAY_PUSH - mutates original array", async () => { const str = ` PUSH 10 PUSH 20 MAKE_ARRAY #2 DUP PUSH 30 ARRAY_PUSH PUSH 2 ARRAY_GET ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 30 }) }) test("ARRAY_LEN - gets length", async () => { const str = ` PUSH 10 PUSH 20 PUSH 30 MAKE_ARRAY #3 ARRAY_LEN ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 }) }) test("MAKE_DICT - creates dict", async () => { const str = ` PUSH 'name' PUSH 'Alice' PUSH 'age' PUSH 30 MAKE_DICT #2 ` const result = await run(toBytecode(str)) expect(result.type).toBe('dict') if (result.type === 'dict') { expect(result.value.size).toBe(2) expect(result.value.get('name')).toEqual({ type: 'string', value: 'Alice' }) expect(result.value.get('age')).toEqual({ type: 'number', value: 30 }) } }) test("DICT_GET - gets value", async () => { const str = ` PUSH 'name' PUSH 'Bob' MAKE_DICT #1 PUSH 'name' DICT_GET ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'Bob' }) }) test("DICT_SET - sets value", async () => { const str = ` MAKE_DICT #0 DUP PUSH 'key' PUSH 'value' DICT_SET PUSH 'key' DICT_GET ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'value' }) }) test("DICT_HAS - checks key exists", async () => { const str = ` PUSH 'key' PUSH 'value' MAKE_DICT #1 PUSH 'key' DICT_HAS ` expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) }) test("DICT_HAS - checks key missing", async () => { const str = ` MAKE_DICT #0 PUSH 'missing' DICT_HAS ` expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false }) }) test("BREAK - throws error when no break target", async () => { // BREAK requires a break target frame on the call stack // A single function call has no previous frame to mark as break target const bytecode = toBytecode(` MAKE_FUNCTION () .fn PUSH 0 PUSH 0 CALL HALT .fn: BREAK `) try { await run(bytecode) expect(true).toBe(false) // Should not reach here } catch (e: any) { expect(e.message).toContain('no break target found') } }) test("BREAK - exits from nested function call", async () => { // BREAK unwinds to the break target (the outer function's frame) // Main calls outer, outer calls inner, inner BREAKs back to outer's caller (main) const bytecode = toBytecode(` MAKE_FUNCTION () .outer PUSH 0 PUSH 0 CALL PUSH 42 HALT .outer: MAKE_FUNCTION () .inner PUSH 0 PUSH 0 CALL PUSH 99 RETURN .inner: BREAK `) const result = await run(bytecode) expect(result).toEqual({ type: 'number', value: 42 }) }) test("JUMP backward - simple loop", async () => { // Very simple: counter starts at 0, loops 3 times incrementing // On 3rd iteration (counter==3), exits and returns counter const bytecode = toBytecode(` PUSH 0 STORE counter .loop: LOAD counter PUSH 3 EQ JUMP_IF_FALSE .body LOAD counter HALT .body: LOAD counter PUSH 1 ADD STORE counter JUMP .loop `) const result = await run(bytecode) expect(result).toEqual({ type: 'number', value: 3 }) }) test("emoji variable names - string format", async () => { const bytecode = toBytecode(` PUSH 5 STORE 💎 LOAD 💎 HALT `) const result = await run(bytecode) expect(result).toEqual({ type: 'number', value: 5 }) }) test("emoji variable names - array format", async () => { const bytecode = toBytecode([ ["PUSH", 100], ["STORE", "💰"], ["LOAD", "💰"], ["PUSH", 50], ["ADD"], ["HALT"] ]) const result = await run(bytecode) expect(result).toEqual({ type: 'number', value: 150 }) }) test("unicode variable names - Japanese", async () => { const bytecode = toBytecode(` PUSH 42 STORE 変数 LOAD 変数 HALT `) const result = await run(bytecode) expect(result).toEqual({ type: 'number', value: 42 }) }) test("unicode variable names - Chinese", async () => { const bytecode = toBytecode([ ["PUSH", 888], ["STORE", "数字"], ["LOAD", "数字"], ["HALT"] ]) const result = await run(bytecode) expect(result).toEqual({ type: 'number', value: 888 }) }) test("emoji in function parameters", async () => { const bytecode = toBytecode(` MAKE_FUNCTION (💎 🌟) .add STORE add JUMP .after .add: LOAD 💎 LOAD 🌟 ADD RETURN .after: LOAD add PUSH 10 PUSH 20 PUSH 2 PUSH 0 CALL HALT `) const result = await run(bytecode) expect(result).toEqual({ type: 'number', value: 30 }) }) test("emoji with defaults and variadic", async () => { const bytecode = toBytecode([ ["MAKE_FUNCTION", ["🎯=100", "...🎨"], ".fn"], ["STORE", "fn"], ["JUMP", ".after"], [".fn:"], ["LOAD", "🎯"], ["RETURN"], [".after:"], ["LOAD", "fn"], ["PUSH", 0], ["PUSH", 0], ["CALL"], ["HALT"] ]) const result = await run(bytecode) expect(result).toEqual({ type: 'number', value: 100 }) }) test("mixed emoji and regular names", async () => { const bytecode = toBytecode([ ["PUSH", 10], ["STORE", "💎"], ["PUSH", 20], ["STORE", "value"], ["PUSH", 30], ["STORE", "🌟"], ["LOAD", "💎"], ["LOAD", "value"], ["ADD"], ["LOAD", "🌟"], ["ADD"], ["HALT"] ]) const result = await run(bytecode) expect(result).toEqual({ type: 'number', value: 60 }) })