import { test, expect, describe } from "bun:test" import { run } from "#index" import { toBytecode } from "#bytecode" describe("ADD", () => { test("add two numbers", async () => { await expect(` PUSH 1 PUSH 5 ADD `).toEqualValue(6) await expect(` PUSH 100 PUSH 500 ADD `).toBeNumber(600) }) test("concatenate two strings", async () => { await expect(` PUSH "hello" PUSH " world" ADD `).toEqualValue('hello world') await expect(` PUSH "foo" PUSH "bar" ADD `).toBeString('foobar') }) test("concatenate string with number", async () => { await expect(` PUSH "count: " PUSH 42 ADD `).toEqualValue('count: 42') await expect(` PUSH 100 PUSH " items" ADD `).toBeString('100 items') }) test("concatenate string with boolean", async () => { await expect(` PUSH "result: " PUSH true ADD `).toBeString('result: true') await expect(` PUSH false PUSH " value" ADD `).toEqualValue('false value') }) test("concatenate string with null", async () => { await expect(` PUSH "value: " PUSH null ADD `).toBeString('value: null') }) test("concatenate multiple strings in sequence", async () => { const str = ` PUSH "hello" PUSH " " ADD PUSH "world" ADD PUSH "!" ADD ` await expect(str).toBeString('hello world!') }) test("mixed arithmetic and string concatenation", async () => { const str = ` PUSH "Result: " PUSH 10 PUSH 5 ADD ADD ` await expect(str).toBeString('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 ` await expect(str).toBeNumber(3) }) }) describe("MUL", () => { test("multiply two numbers", async () => { const str = ` PUSH 5 PUSH 2 MUL ` await expect(str).toBeNumber(10) }) }) describe("DIV", () => { test("divide two numbers", async () => { const str = ` PUSH 10 PUSH 2 DIV ` await expect(str).toBeNumber(5) const str2 = ` PUSH 10 PUSH 0 DIV ` await expect(str2).toBeNumber(Infinity) }) }) describe("MOD", () => { test("modulo two numbers", async () => { const str = ` PUSH 17 PUSH 5 MOD ` await expect(str).toBeNumber(2) }) }) describe("PUSH", () => { test("pushes value onto stack", async () => { const str = ` PUSH 42 ` await expect(str).toBeNumber(42) }) }) describe("POP", () => { test("removes top value", async () => { const str = ` PUSH 10 PUSH 20 POP ` await expect(str).toBeNumber(10) }) }) describe("DUP", () => { test("duplicates top value", async () => { const str = ` PUSH 5 DUP ADD ` await expect(str).toBeNumber(10) }) }) describe("SWAP", () => { test("swaps two numbers", async () => { const str = ` PUSH 10 PUSH 20 SWAP ` await expect(str).toBeNumber(10) }) test("swap and use in subtraction", async () => { const str = ` PUSH 5 PUSH 10 SWAP SUB ` await expect(str).toBeNumber(5) }) test("swap different types", async () => { const str = ` PUSH "hello" PUSH 42 SWAP ` await expect(str).toBeString('hello') }) }) describe("EQ", () => { test("equality comparison", async () => { const str = ` PUSH 5 PUSH 5 EQ ` await expect(str).toBeBoolean(true) const str2 = ` PUSH 5 PUSH 10 EQ ` await expect(str2).toBeBoolean(false) }) test('equality with regexes', async () => { const str = ` PUSH /cool/i PUSH /cool/i EQ ` await expect(str).toBeBoolean(true) const str2 = ` PUSH /cool/ PUSH /cool/i EQ ` await expect(str2).toBeBoolean(false) const str3 = ` PUSH /not-cool/ PUSH /cool/ EQ ` await expect(str3).toBeBoolean(false) }) }) describe("NEQ", () => { test("not equal comparison", async () => { const str = ` PUSH 5 PUSH 10 NEQ ` await expect(str).toBeBoolean(true) }) }) describe("LT", () => { test("less than", async () => { const str = ` PUSH 5 PUSH 10 LT ` await expect(str).toBeBoolean(true) }) }) describe("GT", () => { test("greater than", async () => { const str = ` PUSH 10 PUSH 5 GT ` await expect(str).toBeBoolean(true) }) }) describe("LTE", () => { test("less than or equal", async () => { // equal case const str = ` PUSH 5 PUSH 5 LTE ` await expect(str).toBeBoolean(true) // less than case const str2 = ` PUSH 3 PUSH 5 LTE ` await expect(str2).toBeBoolean(true) // greater than case (false) const str3 = ` PUSH 10 PUSH 5 LTE ` await expect(str3).toBeBoolean(false) }) }) describe("GTE", () => { test("greater than or equal", async () => { // equal case const str = ` PUSH 5 PUSH 5 GTE ` await expect(str).toBeBoolean(true) // greater than case const str2 = ` PUSH 10 PUSH 5 GTE ` await expect(str2).toBeBoolean(true) // less than case (false) const str3 = ` PUSH 3 PUSH 5 GTE ` await expect(str3).toBeBoolean(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: ` await expect(str).toBeNumber(2) }) test("OR pattern - short circuits when true", async () => { const str = ` PUSH 1 DUP JUMP_IF_TRUE .end POP PUSH 2 .end: ` await expect(str).toBeNumber(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: ` await expect(str).toBeNumber(2) }) }) describe("NOT", () => { test("logical not", async () => { // number is truthy, so NOT returns false const str = ` PUSH 1 NOT ` await expect(str).toBeBoolean(false) // 0 is truthy in this language, so NOT returns false const str2 = ` PUSH 0 NOT ` await expect(str2).toBeBoolean(false) // boolean false is falsy, so NOT returns true const str3 = ` PUSH 1 PUSH 0 EQ NOT ` await expect(str3).toBeBoolean(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: ` await expect(str1).toBeNumber(1) // empty string is truthy (unlike JS) const str2 = ` PUSH '' JUMP_IF_FALSE .end PUSH 1 .end: ` await expect(str2).toBeNumber(1) // false is falsy const str3 = ` PUSH 0 PUSH 0 EQ JUMP_IF_FALSE .end PUSH 999 .end: ` await expect(str3).toBeNumber(999) }) }) describe("HALT", () => { test("stops execution", async () => { const str = ` PUSH 42 HALT PUSH 100 ` await expect(str).toBeNumber(42) }) }) describe("TYPE", () => { test("null type", async () => { const bytecode = toBytecode([ ["PUSH", null], ["TYPE"], ["HALT"] ]) await expect(bytecode).toBeString('null') }) test("boolean type", async () => { const bytecode1 = toBytecode([ ["PUSH", true], ["TYPE"], ["HALT"] ]) await expect(bytecode1).toBeString('boolean') const bytecode2 = toBytecode([ ["PUSH", false], ["TYPE"], ["HALT"] ]) await expect(bytecode2).toBeString('boolean') }) test("number type", async () => { const bytecode1 = toBytecode([ ["PUSH", 42], ["TYPE"], ["HALT"] ]) await expect(bytecode1).toBeString('number') const bytecode2 = toBytecode([ ["PUSH", 0], ["TYPE"], ["HALT"] ]) await expect(bytecode2).toBeString('number') const bytecode3 = toBytecode([ ["PUSH", -3.14], ["TYPE"], ["HALT"] ]) await expect(bytecode3).toBeString('number') }) test("string type", async () => { const bytecode1 = toBytecode([ ["PUSH", "hello"], ["TYPE"], ["HALT"] ]) await expect(bytecode1).toBeString('string') const bytecode2 = toBytecode([ ["PUSH", ""], ["TYPE"], ["HALT"] ]) await expect(bytecode2).toBeString('string') }) test("array type", async () => { const bytecode = toBytecode([ ["PUSH", 1], ["PUSH", 2], ["PUSH", 3], ["MAKE_ARRAY", 3], ["TYPE"], ["HALT"] ]) await expect(bytecode).toBeString('array') }) test("empty array type", async () => { const bytecode = toBytecode([ ["MAKE_ARRAY", 0], ["TYPE"], ["HALT"] ]) await expect(bytecode).toBeString('array') }) test("dict type", async () => { const bytecode = toBytecode([ ["PUSH", "name"], ["PUSH", "Alice"], ["PUSH", "age"], ["PUSH", 30], ["MAKE_DICT", 2], ["TYPE"], ["HALT"] ]) await expect(bytecode).toBeString('dict') }) test("empty dict type", async () => { const bytecode = toBytecode([ ["MAKE_DICT", 0], ["TYPE"], ["HALT"] ]) await expect(bytecode).toBeString('dict') }) test("function type", async () => { const bytecode = toBytecode([ ["MAKE_FUNCTION", ["x"], ".body"], ["TYPE"], ["HALT"], [".body:"], ["LOAD", "x"], ["RETURN"] ]) await expect(bytecode).toBeString('function') }) test("native function type", async () => { const bytecode = toBytecode([ ["LOAD", "add"], ["TYPE"], ["HALT"] ]) const result = await run(bytecode, { add: (a: number, b: number) => a + b }) expect(result).toEqual({ type: 'string', value: 'native' }) }) test("regex type", async () => { const bytecode = toBytecode([ ["PUSH", /test/i], ["TYPE"], ["HALT"] ]) await expect(bytecode).toBeString('regex') }) test("TYPE with stored result", async () => { const bytecode = toBytecode([ ["PUSH", 100], ["TYPE"], ["STORE", "myType"], ["LOAD", "myType"], ["HALT"] ]) await expect(bytecode).toBeString('number') }) test("TYPE comparison - type guards", async () => { const bytecode = toBytecode([ ["PUSH", "hello world"], ["DUP"], ["TYPE"], ["PUSH", "string"], ["EQ"], ["HALT"] ]) await expect(bytecode).toBeBoolean(true) }) }) describe("LOAD / STORE", () => { test("variables", async () => { const str = ` PUSH 42 STORE x PUSH 21 LOAD x ` await expect(str).toBeNumber(42) }) test("multiple variables", async () => { const str = ` PUSH 10 STORE a PUSH 20 STORE b PUSH 44 LOAD a LOAD b ADD ` await expect(str).toBeNumber(30) }) }) describe("TRY_LOAD", () => { test("variable found", async () => { const str = ` PUSH 100 STORE count TRY_LOAD count ` await expect(str).toBeNumber(100) const str2 = ` PUSH 'Bobby' STORE name TRY_LOAD name ` await expect(str2).toBeString('Bobby') }) test("variable missing", async () => { const str = ` PUSH 100 STORE count TRY_LOAD count1 ` await expect(str).toBeString('count1') const str2 = ` PUSH 'Bobby' STORE name TRY_LOAD full-name ` await expect(str2).toBeString('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 ` await expect(str3).toBeBoolean(true) // Null const str4 = ` PUSH null STORE empty TRY_LOAD empty ` await expect(str4).toBeNull() }) 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 ` await expect(str).toBeNumber(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 ` await expect(str).toBeString('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' await expect(str).toBeBoolean(false) const str2 = ` PUSH 100 STORE count TRY_LOAD missing PUSH 'missing' EQ ` // Variable missing, so TRY_LOAD returns 'missing', which == 'missing' await expect(str2).toBeBoolean(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 ` await expect(str).toBeNumber(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 ` await expect(str).toBeNumber(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 ` await expect(str).toBeNumber(42) }) test("no jump when true", async () => { const str = ` PUSH 1 JUMP_IF_FALSE .skip PUSH 100 .skip: ` await expect(str).toBeNumber(100) }) }) describe("JUMP_IF_TRUE", () => { test("conditional jump when true", async () => { const str = ` PUSH 1 JUMP_IF_TRUE .skip PUSH 100 .skip: PUSH 42 ` await expect(str).toBeNumber(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 ` await expect(str).toBeNumber(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 ` await expect(str).toBeNumber(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 ` await expect(str).toBeNumber(3) }) test("mutates original array", async () => { const str = ` PUSH 10 PUSH 20 MAKE_ARRAY #2 DUP PUSH 30 ARRAY_PUSH PUSH 2 ARRAY_GET ` await expect(str).toBeNumber(30) }) }) describe("ARRAY_LEN", () => { test("gets length", async () => { const str = ` PUSH 10 PUSH 20 PUSH 30 MAKE_ARRAY #3 ARRAY_LEN ` await expect(str).toBeNumber(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 ` await expect(str).toBeString('Bob') }) }) describe("DICT_SET", () => { test("sets value", async () => { const str = ` MAKE_DICT #0 DUP PUSH 'key' PUSH 'value' DICT_SET PUSH 'key' DICT_GET ` await expect(str).toBeString('value') }) }) describe("DICT_HAS", () => { test("checks key exists", async () => { const str = ` PUSH 'key' PUSH 'value' MAKE_DICT #1 PUSH 'key' DICT_HAS ` await expect(str).toBeBoolean(true) }) test("checks key missing", async () => { const str = ` MAKE_DICT #0 PUSH 'missing' DICT_HAS ` await expect(str).toBeBoolean(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 `) await expect(bytecode).toBeNumber(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 `) await expect(bytecode).toBeString('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 `) await expect(bytecode).toBeNull() }) test("array - negative index", async () => { const bytecode = toBytecode(` PUSH 10 PUSH 20 PUSH 30 MAKE_ARRAY #3 PUSH -1 DOT_GET `) await expect(bytecode).toBeNull() }) 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 `) await expect(bytecode).toBeNumber(400) }) test("dict - returns null for missing key", async () => { const bytecode = toBytecode(` PUSH 'name' PUSH 'Alice' MAKE_DICT #1 PUSH 'age' DOT_GET `) await expect(bytecode).toBeNull() }) test("dict - with numeric key", async () => { const bytecode = toBytecode(` PUSH '123' PUSH 'value' MAKE_DICT #1 PUSH 123 DOT_GET `) await expect(bytecode).toBeString('value') }) test("array - with string value", async () => { const bytecode = toBytecode(` PUSH 'foo' PUSH 'bar' PUSH 'baz' MAKE_ARRAY #3 PUSH 1 DOT_GET `) await expect(bytecode).toBeString('bar') }) test("array - with boolean values", async () => { const bytecode = toBytecode(` PUSH true PUSH false PUSH true MAKE_ARRAY #3 PUSH 1 DOT_GET `) await expect(bytecode).toBeBoolean(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 `) await expect(bytecode).toBeBoolean(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"] ]) await expect(bytecode).toBeString('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 `) await expect(bytecode).toBeNumber(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 `) await expect(bytecode).toBeNumber(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 `) await expect(bytecode).toBeNumber(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 `) await expect(bytecode).toBeString('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 `) await expect(bytecode).toBeNumber(2) }) test("with null value in array", async () => { const bytecode = toBytecode(` PUSH 10 PUSH null PUSH 30 MAKE_ARRAY #3 PUSH 1 DOT_GET `) await expect(bytecode).toBeNull() }) 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 `) await expect(bytecode).toBeNull() }) 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 `) await expect(bytecode).toBeString('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 `) await expect(bytecode).toBeNull() }) test("empty dict access", async () => { const bytecode = toBytecode(` MAKE_DICT #0 PUSH 'key' DOT_GET `) await expect(bytecode).toBeNull() }) }) describe("STR_CONCAT", () => { test("concats together strings", async () => { const str = ` PUSH "Hi " PUSH "friend" PUSH "!" STR_CONCAT #3 ` await expect(str).toBeString("Hi friend!") const str2 = ` PUSH "Holy smokes!" PUSH "It's " PUSH "alive!" STR_CONCAT #2 ` await expect(str2).toBeString("It's alive!") const str3 = ` PUSH 1 PUSH " + " PUSH 1 PUSH " = " PUSH 1 PUSH 1 ADD STR_CONCAT #5 ` await expect(str3).toBeString("1 + 1 = 2") }) test("empty concat (count=0)", async () => { const str = ` PUSH "leftover" STR_CONCAT #0 ` await expect(str).toBeString("") }) test("single string", async () => { const str = ` PUSH "hello" STR_CONCAT #1 ` await expect(str).toBeString("hello") }) test("converts numbers to strings", async () => { const str = ` PUSH 42 PUSH 100 PUSH 7 STR_CONCAT #3 ` await expect(str).toBeString("421007") }) test("converts booleans to strings", async () => { const str = ` PUSH "Result: " PUSH true STR_CONCAT #2 ` await expect(str).toBeString("Result: true") const str2 = ` PUSH false PUSH " is false" STR_CONCAT #2 ` await expect(str2).toBeString("false is false") }) test("converts null to strings", async () => { const str = ` PUSH "Value: " PUSH null STR_CONCAT #2 ` await expect(str).toBeString("Value: null") }) test("mixed types", async () => { const str = ` PUSH "Count: " PUSH 42 PUSH ", Active: " PUSH true PUSH ", Total: " PUSH null STR_CONCAT #6 ` await expect(str).toBeString("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 ` await expect(str).toBeString("Hello, Alice!") }) test("composable (multiple concatenations)", async () => { const str = ` PUSH "Hello" PUSH " " PUSH "World" STR_CONCAT #3 PUSH "!" STR_CONCAT #2 ` await expect(str).toBeString("Hello World!") }) test("with emoji and unicode", async () => { const str = ` PUSH "Hello " PUSH "🌍" PUSH "!" STR_CONCAT #3 ` await expect(str).toBeString("Hello 🌍!") const str2 = ` PUSH "こんにけは" PUSH "δΈ–η•Œ" STR_CONCAT #2 ` await expect(str2).toBeString("γ“γ‚“γ«γ‘γ―δΈ–η•Œ") }) test("with expressions", async () => { const str = ` PUSH "Result: " PUSH 10 PUSH 5 ADD STR_CONCAT #2 ` await expect(str).toBeString("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 ` await expect(str).toBeString("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 }) }) })