import { test, expect, describe } from "bun:test" import { run } from "#index" import { toBytecode } from "#bytecode" describe("ADD", () => { test("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("concatenate two strings", async () => { const str = ` PUSH "hello" PUSH " world" ADD ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'hello world' }) const str2 = ` PUSH "foo" PUSH "bar" ADD ` expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'foobar' }) }) test("concatenate string with number", async () => { const str = ` PUSH "count: " PUSH 42 ADD ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'count: 42' }) const str2 = ` PUSH 100 PUSH " items" ADD ` expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: '100 items' }) }) test("concatenate string with boolean", async () => { const str = ` PUSH "result: " PUSH true ADD ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'result: true' }) const str2 = ` PUSH false PUSH " value" ADD ` expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'false value' }) }) test("concatenate string with null", async () => { const str = ` PUSH "value: " PUSH null ADD ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'value: null' }) }) test("concatenate multiple strings in sequence", async () => { const str = ` PUSH "hello" PUSH " " ADD PUSH "world" ADD PUSH "!" ADD ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'hello world!' }) }) test("mixed arithmetic and string concatenation", async () => { const str = ` PUSH "Result: " PUSH 10 PUSH 5 ADD ADD ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'Result: 15' }) }) test("concatenate two arrays", async () => { const bytecode = toBytecode([ ["PUSH", 1], ["PUSH", 2], ["PUSH", 3], ["MAKE_ARRAY", 3], ["PUSH", 4], ["MAKE_ARRAY", 1], ["ADD"], ["HALT"] ]) const result = await run(bytecode) expect(result.type).toBe('array') if (result.type === 'array') { expect(result.value.length).toBe(4) expect(result.value[0]).toEqual({ type: 'number', value: 1 }) expect(result.value[1]).toEqual({ type: 'number', value: 2 }) expect(result.value[2]).toEqual({ type: 'number', value: 3 }) expect(result.value[3]).toEqual({ type: 'number', value: 4 }) } }) test("concatenate empty arrays", async () => { const bytecode = toBytecode([ ["MAKE_ARRAY", 0], ["PUSH", 1], ["PUSH", 2], ["MAKE_ARRAY", 2], ["ADD"], ["HALT"] ]) const result = await run(bytecode) expect(result.type).toBe('array') if (result.type === 'array') { expect(result.value.length).toBe(2) expect(result.value[0]).toEqual({ type: 'number', value: 1 }) expect(result.value[1]).toEqual({ type: 'number', value: 2 }) } }) test("concatenate multiple arrays in sequence", async () => { const bytecode = toBytecode([ ["PUSH", 1], ["MAKE_ARRAY", 1], ["PUSH", 2], ["MAKE_ARRAY", 1], ["ADD"], ["PUSH", 3], ["MAKE_ARRAY", 1], ["ADD"], ["HALT"] ]) const result = await run(bytecode) expect(result.type).toBe('array') if (result.type === 'array') { expect(result.value.length).toBe(3) expect(result.value[0]).toEqual({ type: 'number', value: 1 }) expect(result.value[1]).toEqual({ type: 'number', value: 2 }) expect(result.value[2]).toEqual({ type: 'number', value: 3 }) } }) test("concatenate arrays with different types", async () => { const bytecode = toBytecode([ ["PUSH", 1], ["PUSH", "hello"], ["MAKE_ARRAY", 2], ["PUSH", true], ["PUSH", null], ["MAKE_ARRAY", 2], ["ADD"], ["HALT"] ]) const result = await run(bytecode) expect(result.type).toBe('array') if (result.type === 'array') { expect(result.value.length).toBe(4) expect(result.value[0]).toEqual({ type: 'number', value: 1 }) expect(result.value[1]).toEqual({ type: 'string', value: 'hello' }) expect(result.value[2]).toEqual({ type: 'boolean', value: true }) expect(result.value[3]).toEqual({ type: 'null', value: null }) } }) test("concatenate arrays containing nested arrays", async () => { const bytecode = toBytecode([ ["PUSH", 1], ["PUSH", 2], ["MAKE_ARRAY", 2], ["MAKE_ARRAY", 1], ["PUSH", 3], ["MAKE_ARRAY", 1], ["ADD"], ["HALT"] ]) const result = await run(bytecode) expect(result.type).toBe('array') if (result.type === 'array') { expect(result.value.length).toBe(2) // First element is nested array [1, 2] expect(result.value[0]?.type).toBe('array') if (result.value[0]?.type === 'array') { expect(result.value[0].value.length).toBe(2) expect(result.value[0].value[0]).toEqual({ type: 'number', value: 1 }) expect(result.value[0].value[1]).toEqual({ type: 'number', value: 2 }) } // Second element is 3 expect(result.value[1]).toEqual({ type: 'number', value: 3 }) } }) test("merge two dicts", async () => { const bytecode = toBytecode([ ["PUSH", "a"], ["PUSH", 1], ["MAKE_DICT", 1], ["PUSH", "b"], ["PUSH", 2], ["MAKE_DICT", 1], ["ADD"], ["HALT"] ]) const result = await run(bytecode) expect(result.type).toBe('dict') if (result.type === 'dict') { expect(result.value.size).toBe(2) expect(result.value.get('a')).toEqual({ type: 'number', value: 1 }) expect(result.value.get('b')).toEqual({ type: 'number', value: 2 }) } }) test("merge dicts with overlapping keys (second overwrites)", async () => { const bytecode = toBytecode([ ["PUSH", "a"], ["PUSH", 1], ["PUSH", "b"], ["PUSH", 2], ["MAKE_DICT", 2], ["PUSH", "b"], ["PUSH", 99], ["PUSH", "c"], ["PUSH", 3], ["MAKE_DICT", 2], ["ADD"], ["HALT"] ]) const result = await run(bytecode) expect(result.type).toBe('dict') if (result.type === 'dict') { expect(result.value.size).toBe(3) expect(result.value.get('a')).toEqual({ type: 'number', value: 1 }) expect(result.value.get('b')).toEqual({ type: 'number', value: 99 }) // overwritten expect(result.value.get('c')).toEqual({ type: 'number', value: 3 }) } }) test("merge empty dicts", async () => { const bytecode = toBytecode([ ["MAKE_DICT", 0], ["PUSH", "x"], ["PUSH", 42], ["MAKE_DICT", 1], ["ADD"], ["HALT"] ]) const result = await run(bytecode) expect(result.type).toBe('dict') if (result.type === 'dict') { expect(result.value.size).toBe(1) expect(result.value.get('x')).toEqual({ type: 'number', value: 42 }) } }) test("merge multiple dicts in sequence", async () => { const bytecode = toBytecode([ ["PUSH", "a"], ["PUSH", 1], ["MAKE_DICT", 1], ["PUSH", "b"], ["PUSH", 2], ["MAKE_DICT", 1], ["ADD"], ["PUSH", "c"], ["PUSH", 3], ["MAKE_DICT", 1], ["ADD"], ["HALT"] ]) const result = await run(bytecode) expect(result.type).toBe('dict') if (result.type === 'dict') { expect(result.value.size).toBe(3) expect(result.value.get('a')).toEqual({ type: 'number', value: 1 }) expect(result.value.get('b')).toEqual({ type: 'number', value: 2 }) expect(result.value.get('c')).toEqual({ type: 'number', value: 3 }) } }) test("merge dicts with different value types", async () => { const bytecode = toBytecode([ ["PUSH", "num"], ["PUSH", 42], ["PUSH", "str"], ["PUSH", "hello"], ["MAKE_DICT", 2], ["PUSH", "bool"], ["PUSH", true], ["PUSH", "null"], ["PUSH", null], ["MAKE_DICT", 2], ["ADD"], ["HALT"] ]) const result = await run(bytecode) expect(result.type).toBe('dict') if (result.type === 'dict') { expect(result.value.size).toBe(4) expect(result.value.get('num')).toEqual({ type: 'number', value: 42 }) expect(result.value.get('str')).toEqual({ type: 'string', value: 'hello' }) expect(result.value.get('bool')).toEqual({ type: 'boolean', value: true }) expect(result.value.get('null')).toEqual({ type: 'null', value: null }) } }) test("merge dicts with nested structures", async () => { const bytecode = toBytecode([ ["PUSH", "a"], ["PUSH", 1], ["PUSH", 2], ["MAKE_ARRAY", 2], ["MAKE_DICT", 1], ["PUSH", "b"], ["PUSH", "x"], ["PUSH", 99], ["MAKE_DICT", 1], ["MAKE_DICT", 1], ["ADD"], ["HALT"] ]) const result = await run(bytecode) expect(result.type).toBe('dict') if (result.type === 'dict') { expect(result.value.size).toBe(2) // a contains an array [1, 2] const aValue = result.value.get('a') expect(aValue?.type).toBe('array') if (aValue?.type === 'array') { expect(aValue.value.length).toBe(2) expect(aValue.value[0]).toEqual({ type: 'number', value: 1 }) expect(aValue.value[1]).toEqual({ type: 'number', value: 2 }) } // b contains a nested dict {x: 99} const bValue = result.value.get('b') expect(bValue?.type).toBe('dict') if (bValue?.type === 'dict') { expect(bValue.value.size).toBe(1) expect(bValue.value.get('x')).toEqual({ type: 'number', value: 99 }) } } }) test("cannot add boolean + boolean", async () => { const bytecode = toBytecode([ ["PUSH", true], ["PUSH", false], ["ADD"], ["HALT"] ]) await expect(run(bytecode)).rejects.toThrow('ADD: Cannot add boolean and boolean') }) test("cannot add null + number", async () => { const bytecode = toBytecode([ ["PUSH", null], ["PUSH", 5], ["ADD"], ["HALT"] ]) await expect(run(bytecode)).rejects.toThrow('ADD: Cannot add null and number') }) test("cannot add array + dict", async () => { const bytecode = toBytecode([ ["PUSH", 1], ["MAKE_ARRAY", 1], ["MAKE_DICT", 0], ["ADD"], ["HALT"] ]) await expect(run(bytecode)).rejects.toThrow('ADD: Cannot add array and dict') }) test("cannot add array + number", async () => { const bytecode = toBytecode([ ["PUSH", 1], ["MAKE_ARRAY", 1], ["PUSH", 5], ["ADD"], ["HALT"] ]) await expect(run(bytecode)).rejects.toThrow('ADD: Cannot add array and number') }) test("cannot add dict + number", async () => { const bytecode = toBytecode([ ["MAKE_DICT", 0], ["PUSH", 5], ["ADD"], ["HALT"] ]) await expect(run(bytecode)).rejects.toThrow('ADD: Cannot add dict and number') }) test("cannot add function + number", async () => { const bytecode = toBytecode([ ["MAKE_FUNCTION", [], ".body"], ["PUSH", 5], ["ADD"], ["HALT"], [".body:"], ["RETURN"] ]) await expect(run(bytecode)).rejects.toThrow('ADD: Cannot add function and number') }) test("cannot add boolean + null", async () => { const bytecode = toBytecode([ ["PUSH", true], ["PUSH", null], ["ADD"], ["HALT"] ]) await expect(run(bytecode)).rejects.toThrow('ADD: Cannot add boolean and null') }) }) describe("SUB", () => { test("subtract two numbers", async () => { const str = ` PUSH 5 PUSH 2 SUB ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 }) }) }) describe("MUL", () => { test("multiply two numbers", async () => { const str = ` PUSH 5 PUSH 2 MUL ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 }) }) }) describe("DIV", () => { test("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 }) }) }) describe("MOD", () => { test("modulo two numbers", async () => { const str = ` PUSH 17 PUSH 5 MOD ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 }) }) }) describe("PUSH", () => { test("pushes value onto stack", async () => { const str = ` PUSH 42 ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) }) }) describe("POP", () => { test("removes top value", async () => { const str = ` PUSH 10 PUSH 20 POP ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 }) }) }) describe("DUP", () => { test("duplicates top value", async () => { const str = ` PUSH 5 DUP ADD ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 }) }) }) describe("SWAP", () => { test("swaps two numbers", async () => { const str = ` PUSH 10 PUSH 20 SWAP ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 }) }) test("swap and use in subtraction", async () => { const str = ` PUSH 5 PUSH 10 SWAP SUB ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 5 }) }) test("swap different types", async () => { const str = ` PUSH "hello" PUSH 42 SWAP ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'hello' }) }) }) describe("EQ", () => { test("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('equality with regexes', async () => { const str = ` PUSH /cool/i PUSH /cool/i EQ ` expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) const str2 = ` PUSH /cool/ PUSH /cool/i EQ ` expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false }) const str3 = ` PUSH /not-cool/ PUSH /cool/ EQ ` expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false }) }) }) describe("NEQ", () => { test("not equal comparison", async () => { const str = ` PUSH 5 PUSH 10 NEQ ` expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) }) }) describe("LT", () => { test("less than", async () => { const str = ` PUSH 5 PUSH 10 LT ` expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) }) }) describe("GT", () => { test("greater than", async () => { const str = ` PUSH 10 PUSH 5 GT ` expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) }) }) describe("LTE", () => { test("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 }) }) }) describe("GTE", () => { test("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 }) }) }) describe("Logical patterns", () => { 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 }) }) }) describe("NOT", () => { test("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 }) }) }) describe("Truthiness", () => { test("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 }) }) }) describe("HALT", () => { test("stops execution", async () => { const str = ` PUSH 42 HALT PUSH 100 ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) }) }) describe("LOAD / STORE", () => { test("variables", async () => { const str = ` PUSH 42 STORE x PUSH 21 LOAD x ` expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) }) test("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 }) }) }) describe("TRY_LOAD", () => { test("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("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("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("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("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("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("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') }) }) describe("JUMP", () => { test("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("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("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 }) }) }) describe("JUMP_IF_FALSE", () => { test("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("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 }) }) }) describe("JUMP_IF_TRUE", () => { test("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 }) }) }) describe("MAKE_ARRAY", () => { test("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 }) } }) }) describe("ARRAY_GET", () => { test("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 }) }) }) describe("ARRAY_SET", () => { test("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 }) }) }) describe("ARRAY_PUSH", () => { test("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("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 }) }) }) describe("ARRAY_LEN", () => { test("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 }) }) }) describe("MAKE_DICT", () => { test("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 }) } }) }) describe("DICT_GET", () => { test("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' }) }) }) describe("DICT_SET", () => { test("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' }) }) }) describe("DICT_HAS", () => { test("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("checks key missing", async () => { const str = ` MAKE_DICT #0 PUSH 'missing' DICT_HAS ` expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false }) }) }) describe("DOT_GET", () => { test("gets an ARRAY index", async () => { const bytecode = toBytecode(` PUSH 100 PUSH 200 PUSH 300 PUSH 400 MAKE_ARRAY #4 PUSH 2 DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'number', value: 300 }) }) test("gets a DICT value", async () => { const bytecode = toBytecode(` PUSH 'name' PUSH 'Bob' PUSH 'age' PUSH 106 MAKE_DICT #2 PUSH 'name' DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'string', value: 'Bob' }) }) test("fails to work on NUMBER", async () => { const bytecode = toBytecode(` PUSH 500 PUSH 0 DOT_GET `) try { await run(bytecode) expect(true).toBe(false) // Should not reach here } catch (e: any) { expect(e.message).toContain('DOT_GET: number not supported') } }) test("fails to work on STRING", async () => { const bytecode = toBytecode(` PUSH 'ShrimpVM?' PUSH 1 DOT_GET `) try { await run(bytecode) expect(true).toBe(false) // Should not reach here } catch (e: any) { expect(e.message).toContain('DOT_GET: string not supported') } }) test("array - returns null for out of bounds", async () => { const bytecode = toBytecode(` PUSH 10 PUSH 20 MAKE_ARRAY #2 PUSH 5 DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'null', value: null }) }) test("array - negative index", async () => { const bytecode = toBytecode(` PUSH 10 PUSH 20 PUSH 30 MAKE_ARRAY #3 PUSH -1 DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'null', value: null }) }) test("array - first and last element", async () => { const bytecode = toBytecode(` PUSH 100 PUSH 200 PUSH 300 MAKE_ARRAY #3 DUP PUSH 0 DOT_GET STORE first PUSH 2 DOT_GET STORE last LOAD first LOAD last ADD `) expect(await run(bytecode)).toEqual({ type: 'number', value: 400 }) }) test("dict - returns null for missing key", async () => { const bytecode = toBytecode(` PUSH 'name' PUSH 'Alice' MAKE_DICT #1 PUSH 'age' DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'null', value: null }) }) test("dict - with numeric key", async () => { const bytecode = toBytecode(` PUSH '123' PUSH 'value' MAKE_DICT #1 PUSH 123 DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'string', value: 'value' }) }) test("array - with string value", async () => { const bytecode = toBytecode(` PUSH 'foo' PUSH 'bar' PUSH 'baz' MAKE_ARRAY #3 PUSH 1 DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'string', value: 'bar' }) }) test("array - with boolean values", async () => { const bytecode = toBytecode(` PUSH true PUSH false PUSH true MAKE_ARRAY #3 PUSH 1 DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'boolean', value: false }) }) test("dict - with boolean value", async () => { const bytecode = toBytecode(` PUSH 'active' PUSH true PUSH 'visible' PUSH false MAKE_DICT #2 PUSH 'active' DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'boolean', value: true }) }) test("array - nested arrays", async () => { const bytecode = toBytecode(` PUSH 1 PUSH 2 MAKE_ARRAY #2 PUSH 3 PUSH 4 MAKE_ARRAY #2 MAKE_ARRAY #2 PUSH 1 DOT_GET `) const result = await run(bytecode) expect(result.type).toBe('array') if (result.type === 'array') { expect(result.value).toHaveLength(2) expect(result.value[0]).toEqual({ type: 'number', value: 3 }) expect(result.value[1]).toEqual({ type: 'number', value: 4 }) } }) test("dict - nested dicts", async () => { const bytecode = toBytecode(` PUSH 'user' PUSH 'name' PUSH 'Alice' MAKE_DICT #1 MAKE_DICT #1 PUSH 'user' DOT_GET `) const result = await run(bytecode) expect(result.type).toBe('dict') if (result.type === 'dict') { expect(result.value.get('name')).toEqual({ type: 'string', value: 'Alice' }) } }) test("array format", async () => { const bytecode = toBytecode([ ["PUSH", "apple"], ["PUSH", "banana"], ["PUSH", "cherry"], ["MAKE_ARRAY", 3], ["PUSH", 1], ["DOT_GET"], ["HALT"] ]) expect(await run(bytecode)).toEqual({ type: 'string', value: 'banana' }) }) test("with variables - array", async () => { const bytecode = toBytecode(` PUSH 10 PUSH 20 PUSH 30 MAKE_ARRAY #3 STORE arr PUSH 1 STORE idx LOAD arr LOAD idx DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'number', value: 20 }) }) test("with variables - dict", async () => { const bytecode = toBytecode(` PUSH 'x' PUSH 100 PUSH 'y' PUSH 200 MAKE_DICT #2 STORE point PUSH 'y' STORE key LOAD point LOAD key DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'number', value: 200 }) }) test("with expression as index", async () => { const bytecode = toBytecode(` PUSH 10 PUSH 20 PUSH 30 PUSH 40 MAKE_ARRAY #4 PUSH 1 PUSH 2 ADD DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'number', value: 40 }) }) test("chained access - array of dicts", async () => { const bytecode = toBytecode(` PUSH 'name' PUSH 'Alice' MAKE_DICT #1 PUSH 'name' PUSH 'Bob' MAKE_DICT #1 MAKE_ARRAY #2 PUSH 1 DOT_GET PUSH 'name' DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'string', value: 'Bob' }) }) test("chained access - dict of arrays", async () => { const bytecode = toBytecode(` PUSH 'nums' PUSH 1 PUSH 2 PUSH 3 MAKE_ARRAY #3 MAKE_DICT #1 PUSH 'nums' DOT_GET PUSH 1 DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'number', value: 2 }) }) test("with null value in array", async () => { const bytecode = toBytecode(` PUSH 10 PUSH null PUSH 30 MAKE_ARRAY #3 PUSH 1 DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'null', value: null }) }) test("with null value in dict", async () => { const bytecode = toBytecode(` PUSH 'name' PUSH 'Alice' PUSH 'middle' PUSH null MAKE_DICT #2 PUSH 'middle' DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'null', value: null }) }) test("array with regex values", async () => { const bytecode = toBytecode(` PUSH /foo/ PUSH /bar/i PUSH /baz/g MAKE_ARRAY #3 PUSH 1 DOT_GET `) const result = await run(bytecode) expect(result.type).toBe('regex') if (result.type === 'regex') { expect(result.value.source).toBe('bar') expect(result.value.ignoreCase).toBe(true) } }) test("dict with regex values", async () => { const bytecode = toBytecode(` PUSH 'email' PUSH /^[a-z@.]+$/i PUSH 'url' PUSH /^https?:\\/\\// MAKE_DICT #2 PUSH 'email' DOT_GET `) const result = await run(bytecode) expect(result.type).toBe('regex') if (result.type === 'regex') { expect(result.value.source).toBe('^[a-z@.]+$') expect(result.value.ignoreCase).toBe(true) } }) test("dict - key coercion from number", async () => { const bytecode = toBytecode(` PUSH '0' PUSH 'zero' PUSH '1' PUSH 'one' MAKE_DICT #2 PUSH 0 DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'string', value: 'zero' }) }) test("fails on boolean", async () => { const bytecode = toBytecode(` PUSH true PUSH 0 DOT_GET `) try { await run(bytecode) expect(true).toBe(false) } catch (e: any) { expect(e.message).toContain('DOT_GET: boolean not supported') } }) test("fails on null", async () => { const bytecode = toBytecode(` PUSH null PUSH 0 DOT_GET `) try { await run(bytecode) expect(true).toBe(false) } catch (e: any) { expect(e.message).toContain('DOT_GET: null not supported') } }) test("fails on regex", async () => { const bytecode = toBytecode(` PUSH /test/ PUSH 0 DOT_GET `) try { await run(bytecode) expect(true).toBe(false) } catch (e: any) { expect(e.message).toContain('DOT_GET: regex not supported') } }) test("empty array access", async () => { const bytecode = toBytecode(` MAKE_ARRAY #0 PUSH 0 DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'null', value: null }) }) test("empty dict access", async () => { const bytecode = toBytecode(` MAKE_DICT #0 PUSH 'key' DOT_GET `) expect(await run(bytecode)).toEqual({ type: 'null', value: null }) }) }) describe("STR_CONCAT", () => { test("concats together strings", async () => { const str = ` PUSH "Hi " PUSH "friend" PUSH "!" STR_CONCAT #3 ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hi friend!" }) const str2 = ` PUSH "Holy smokes!" PUSH "It's " PUSH "alive!" STR_CONCAT #2 ` expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "It's alive!" }) const str3 = ` PUSH 1 PUSH " + " PUSH 1 PUSH " = " PUSH 1 PUSH 1 ADD STR_CONCAT #5 ` expect(await run(toBytecode(str3))).toEqual({ type: 'string', value: "1 + 1 = 2" }) }) test("empty concat (count=0)", async () => { const str = ` PUSH "leftover" STR_CONCAT #0 ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "" }) }) test("single string", async () => { const str = ` PUSH "hello" STR_CONCAT #1 ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "hello" }) }) test("converts numbers to strings", async () => { const str = ` PUSH 42 PUSH 100 PUSH 7 STR_CONCAT #3 ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "421007" }) }) test("converts booleans to strings", async () => { const str = ` PUSH "Result: " PUSH true STR_CONCAT #2 ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Result: true" }) const str2 = ` PUSH false PUSH " is false" STR_CONCAT #2 ` expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "false is false" }) }) test("converts null to strings", async () => { const str = ` PUSH "Value: " PUSH null STR_CONCAT #2 ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Value: null" }) }) test("mixed types", async () => { const str = ` PUSH "Count: " PUSH 42 PUSH ", Active: " PUSH true PUSH ", Total: " PUSH null STR_CONCAT #6 ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Count: 42, Active: true, Total: null" }) }) test("array format", async () => { const bytecode = toBytecode([ ["PUSH", "Hello"], ["PUSH", " "], ["PUSH", "World"], ["STR_CONCAT", 3], ["HALT"] ]) const result = await run(bytecode) expect(result).toEqual({ type: 'string', value: "Hello World" }) }) test("with variables", async () => { const str = ` PUSH "Alice" STORE name PUSH "Hello, " LOAD name PUSH "!" STR_CONCAT #3 ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello, Alice!" }) }) test("composable (multiple concatenations)", async () => { const str = ` PUSH "Hello" PUSH " " PUSH "World" STR_CONCAT #3 PUSH "!" STR_CONCAT #2 ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello World!" }) }) test("with emoji and unicode", async () => { const str = ` PUSH "Hello " PUSH "🌍" PUSH "!" STR_CONCAT #3 ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello 🌍!" }) const str2 = ` PUSH "こんにけは" PUSH "δΈ–η•Œ" STR_CONCAT #2 ` expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "γ“γ‚“γ«γ‘γ―δΈ–η•Œ" }) }) test("with expressions", async () => { const str = ` PUSH "Result: " PUSH 10 PUSH 5 ADD STR_CONCAT #2 ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Result: 15" }) }) test("large concat", async () => { const str = ` PUSH "a" PUSH "b" PUSH "c" PUSH "d" PUSH "e" PUSH "f" PUSH "g" PUSH "h" PUSH "i" PUSH "j" STR_CONCAT #10 ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "abcdefghij" }) }) }) describe("BREAK", () => { test("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("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 }) }) })