diff --git a/tests/basic.test.ts b/tests/basic.test.ts deleted file mode 100644 index 9e50308..0000000 --- a/tests/basic.test.ts +++ /dev/null @@ -1,1513 +0,0 @@ -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('EQ - 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 }) -}) - -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("STR_CONCAT - 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("STR_CONCAT - empty concat (count=0)", async () => { - const str = ` - PUSH "leftover" - STR_CONCAT #0 - ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "" }) -}) - -test("STR_CONCAT - single string", async () => { - const str = ` - PUSH "hello" - STR_CONCAT #1 - ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "hello" }) -}) - -test("STR_CONCAT - 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("STR_CONCAT - 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("STR_CONCAT - 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("STR_CONCAT - 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("STR_CONCAT - 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("STR_CONCAT - 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("STR_CONCAT - 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("STR_CONCAT - 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("STR_CONCAT - 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("STR_CONCAT - 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" }) -}) - -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 }) -}) - -// ======================================== -// RegExp Tests -// ======================================== - -test("RegExp - basic pattern parsing", async () => { - const str = ` - PUSH /hello/ - ` - const result = await run(toBytecode(str)) - expect(result.type).toBe('regex') - if (result.type === 'regex') { - expect(result.value.source).toBe('hello') - expect(result.value.flags).toBe('') - } -}) - -test("RegExp - pattern with flags", async () => { - const str = ` - PUSH /test/gi - ` - const result = await run(toBytecode(str)) - expect(result.type).toBe('regex') - if (result.type === 'regex') { - expect(result.value.source).toBe('test') - expect(result.value.global).toBe(true) - expect(result.value.ignoreCase).toBe(true) - } -}) - -test("RegExp - multiple flag combinations", async () => { - // Test i flag - const str1 = ` - PUSH /pattern/i - ` - const result1 = await run(toBytecode(str1)) - expect(result1.type).toBe('regex') - if (result1.type === 'regex') { - expect(result1.value.ignoreCase).toBe(true) - } - - // Test g flag - const str2 = ` - PUSH /pattern/g - ` - const result2 = await run(toBytecode(str2)) - expect(result2.type).toBe('regex') - if (result2.type === 'regex') { - expect(result2.value.global).toBe(true) - } - - // Test m flag - const str3 = ` - PUSH /pattern/m - ` - const result3 = await run(toBytecode(str3)) - expect(result3.type).toBe('regex') - if (result3.type === 'regex') { - expect(result3.value.multiline).toBe(true) - } - - // Test combined flags - const str4 = ` - PUSH /pattern/gim - ` - const result4 = await run(toBytecode(str4)) - expect(result4.type).toBe('regex') - if (result4.type === 'regex') { - expect(result4.value.global).toBe(true) - expect(result4.value.ignoreCase).toBe(true) - expect(result4.value.multiline).toBe(true) - } -}) - -test("RegExp - complex patterns", async () => { - // Character class - const str1 = ` - PUSH /[a-z0-9]+/ - ` - const result1 = await run(toBytecode(str1)) - expect(result1.type).toBe('regex') - if (result1.type === 'regex') { - expect(result1.value.source).toBe('[a-z0-9]+') - } - - // Quantifiers - const str2 = ` - PUSH /a{2,4}/ - ` - const result2 = await run(toBytecode(str2)) - expect(result2.type).toBe('regex') - if (result2.type === 'regex') { - expect(result2.value.source).toBe('a{2,4}') - } - - // Groups and alternation - const str3 = ` - PUSH /(foo|bar)/ - ` - const result3 = await run(toBytecode(str3)) - expect(result3.type).toBe('regex') - if (result3.type === 'regex') { - expect(result3.value.source).toBe('(foo|bar)') - } - - // Anchors and special chars - const str4 = ` - PUSH /^[a-z]+$/ - ` - const result4 = await run(toBytecode(str4)) - expect(result4.type).toBe('regex') - if (result4.type === 'regex') { - expect(result4.value.source).toBe('^[a-z]+$') - } -}) - -test("RegExp - escaping special characters", async () => { - const str = ` - PUSH /\\d+\\.\\d+/ - ` - const result = await run(toBytecode(str)) - expect(result.type).toBe('regex') - if (result.type === 'regex') { - expect(result.value.source).toBe('\\d+\\.\\d+') - } -}) - -test("RegExp - store and load", async () => { - const str = ` - PUSH /test/i - STORE pattern - LOAD pattern - ` - const result = await run(toBytecode(str)) - expect(result.type).toBe('regex') - if (result.type === 'regex') { - expect(result.value.source).toBe('test') - expect(result.value.ignoreCase).toBe(true) - } -}) - -test("RegExp - TRY_LOAD with regex", async () => { - const str = ` - PUSH /hello/g - STORE regex - TRY_LOAD regex - ` - const result = await run(toBytecode(str)) - expect(result.type).toBe('regex') - if (result.type === 'regex') { - expect(result.value.source).toBe('hello') - expect(result.value.global).toBe(true) - } -}) - -test("RegExp - NEQ comparison", async () => { - const str = ` - PUSH /foo/ - PUSH /bar/ - NEQ - ` - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) - - const str2 = ` - PUSH /test/i - PUSH /test/i - NEQ - ` - expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false }) -}) - -test("RegExp - is truthy", async () => { - // Regex values should be truthy (not null or false) - const str = ` - PUSH /test/ - JUMP_IF_FALSE .end - PUSH 42 - .end: - ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) -}) - -test("RegExp - NOT returns false (regex is truthy)", async () => { - const str = ` - PUSH /pattern/ - NOT - ` - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false }) -}) - -test("RegExp - in arrays", async () => { - const str = ` - PUSH /first/ - PUSH /second/i - PUSH /third/g - 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]!.type).toBe('regex') - if (result.value[0]!.type === 'regex') { - expect(result.value[0]!.value.source).toBe('first') - } - - expect(result.value[1]!.type).toBe('regex') - if (result.value[1]!.type === 'regex') { - expect(result.value[1]!.value.source).toBe('second') - expect(result.value[1]!.value.ignoreCase).toBe(true) - } - - expect(result.value[2]!.type).toBe('regex') - if (result.value[2]!.type === 'regex') { - expect(result.value[2]!.value.source).toBe('third') - expect(result.value[2]!.value.global).toBe(true) - } - } -}) - -test("RegExp - retrieve from array", async () => { - const str = ` - PUSH /pattern/i - PUSH /test/g - MAKE_ARRAY #2 - PUSH 1 - ARRAY_GET - ` - const result = await run(toBytecode(str)) - expect(result.type).toBe('regex') - if (result.type === 'regex') { - expect(result.value.source).toBe('test') - expect(result.value.global).toBe(true) - } -}) - -test("RegExp - in dicts", async () => { - const str = ` - PUSH 'email' - PUSH /^[a-z@.]+$/i - PUSH 'phone' - PUSH /\\d{3}-\\d{4}/ - MAKE_DICT #2 - ` - const result = await run(toBytecode(str)) - expect(result.type).toBe('dict') - if (result.type === 'dict') { - expect(result.value.size).toBe(2) - - const email = result.value.get('email') - expect(email?.type).toBe('regex') - if (email?.type === 'regex') { - expect(email.value.source).toBe('^[a-z@.]+$') - expect(email.value.ignoreCase).toBe(true) - } - - const phone = result.value.get('phone') - expect(phone?.type).toBe('regex') - if (phone?.type === 'regex') { - expect(phone.value.source).toBe('\\d{3}-\\d{4}') - } - } -}) - -test("RegExp - retrieve from dict", async () => { - const str = ` - PUSH 'pattern' - PUSH /test/gim - MAKE_DICT #1 - PUSH 'pattern' - DICT_GET - ` - const result = await run(toBytecode(str)) - expect(result.type).toBe('regex') - if (result.type === 'regex') { - expect(result.value.source).toBe('test') - expect(result.value.global).toBe(true) - expect(result.value.ignoreCase).toBe(true) - expect(result.value.multiline).toBe(true) - } -}) - -test("RegExp - with STR_CONCAT converts to string", async () => { - const str = ` - PUSH "Pattern: " - PUSH /test/gi - STR_CONCAT #2 - ` - const result = await run(toBytecode(str)) - expect(result.type).toBe('string') - if (result.type === 'string') { - expect(result.value).toBe('Pattern: /test/gi') - } -}) - -test("RegExp - multiple regex in STR_CONCAT", async () => { - const str = ` - PUSH /foo/ - PUSH " and " - PUSH /bar/i - STR_CONCAT #3 - ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: '/foo/ and /bar/i' }) -}) - -test("RegExp - DUP with regex", async () => { - const str = ` - PUSH /pattern/i - DUP - EQ - ` - // Same regex duplicated should be equal - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) -}) - -test("RegExp - empty pattern", async () => { - const str = ` - PUSH // - ` - const result = await run(toBytecode(str)) - expect(result.type).toBe('regex') - if (result.type === 'regex') { - expect(result.value.source).toBe('(?:)') - } -}) - -test("RegExp - pattern with forward slashes escaped", async () => { - const str = ` - PUSH /https:\\/\\// - ` - const result = await run(toBytecode(str)) - expect(result.type).toBe('regex') - if (result.type === 'regex') { - expect(result.value.source).toBe('https:\\/\\/') - } -}) - -test("RegExp - unicode patterns", async () => { - const str = ` - PUSH /こんにけは/ - ` - const result = await run(toBytecode(str)) - expect(result.type).toBe('regex') - if (result.type === 'regex') { - expect(result.value.source).toBe('こんにけは') - } -}) - -test("RegExp - emoji in pattern", async () => { - const str = ` - PUSH /πŸŽ‰+/ - ` - const result = await run(toBytecode(str)) - expect(result.type).toBe('regex') - if (result.type === 'regex') { - expect(result.value.source).toBe('πŸŽ‰+') - } -}) - -test("RegExp - comparing different regex types", async () => { - // Different patterns - const str1 = ` - PUSH /abc/ - PUSH /xyz/ - EQ - ` - expect(await run(toBytecode(str1))).toEqual({ type: 'boolean', value: false }) - - // Same pattern, different flags - const str2 = ` - PUSH /test/ - PUSH /test/i - EQ - ` - expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false }) - - // Different order of flags (should be equal) - const str3 = ` - PUSH /test/ig - PUSH /test/gi - EQ - ` - expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true }) -}) - -test("RegExp - with native functions", async () => { - const { VM } = await import("#vm") - const bytecode = toBytecode(` - PUSH "hello world" - PUSH /world/ - CALL_NATIVE match - HALT - `) - - const vm = new VM(bytecode) - - // Register a native function that takes a string and regex - vm.registerFunction('match', (str: string, pattern: RegExp) => { - return pattern.test(str) - }) - - const result = await vm.run() - expect(result).toEqual({ type: 'boolean', value: true }) -}) - -test("RegExp - native function with regex replacement", async () => { - const { VM } = await import("#vm") - const bytecode = toBytecode(` - PUSH "hello world" - PUSH /o/g - PUSH "0" - CALL_NATIVE replace - HALT - `) - - const vm = new VM(bytecode) - - vm.registerFunction('replace', (str: string, pattern: RegExp, replacement: string) => { - return str.replace(pattern, replacement) - }) - - const result = await vm.run() - expect(result).toEqual({ type: 'string', value: 'hell0 w0rld' }) -}) - -test("RegExp - native function extracting matches", async () => { - const { VM } = await import("#vm") - const bytecode = toBytecode(` - PUSH "test123abc456" - PUSH /\\d+/g - CALL_NATIVE extractNumbers - HALT - `) - - const vm = new VM(bytecode) - - vm.registerFunction('extractNumbers', (str: string, pattern: RegExp) => { - return str.match(pattern) || [] - }) - - const result = await vm.run() - expect(result.type).toBe('array') - if (result.type === 'array') { - expect(result.value).toHaveLength(2) - expect(result.value[0]).toEqual({ type: 'string', value: '123' }) - expect(result.value[1]).toEqual({ type: 'string', value: '456' }) - } -}) - diff --git a/tests/opcodes.test.ts b/tests/opcodes.test.ts new file mode 100644 index 0000000..ffd4106 --- /dev/null +++ b/tests/opcodes.test.ts @@ -0,0 +1,1017 @@ +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 }) + }) +}) + +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("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("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 }) + }) +}) diff --git a/tests/regex.test.ts b/tests/regex.test.ts new file mode 100644 index 0000000..0ce1cfb --- /dev/null +++ b/tests/regex.test.ts @@ -0,0 +1,450 @@ +import { test, expect, describe } from "bun:test" +import { run } from "#index" +import { toBytecode } from "#bytecode" + +describe("RegExp", () => { + test("basic pattern parsing", async () => { + const str = ` + PUSH /hello/ + ` + const result = await run(toBytecode(str)) + expect(result.type).toBe('regex') + if (result.type === 'regex') { + expect(result.value.source).toBe('hello') + expect(result.value.flags).toBe('') + } + }) + + test("pattern with flags", async () => { + const str = ` + PUSH /test/gi + ` + const result = await run(toBytecode(str)) + expect(result.type).toBe('regex') + if (result.type === 'regex') { + expect(result.value.source).toBe('test') + expect(result.value.global).toBe(true) + expect(result.value.ignoreCase).toBe(true) + } + }) + + test("multiple flag combinations", async () => { + // Test i flag + const str1 = ` + PUSH /pattern/i + ` + const result1 = await run(toBytecode(str1)) + expect(result1.type).toBe('regex') + if (result1.type === 'regex') { + expect(result1.value.ignoreCase).toBe(true) + } + + // Test g flag + const str2 = ` + PUSH /pattern/g + ` + const result2 = await run(toBytecode(str2)) + expect(result2.type).toBe('regex') + if (result2.type === 'regex') { + expect(result2.value.global).toBe(true) + } + + // Test m flag + const str3 = ` + PUSH /pattern/m + ` + const result3 = await run(toBytecode(str3)) + expect(result3.type).toBe('regex') + if (result3.type === 'regex') { + expect(result3.value.multiline).toBe(true) + } + + // Test combined flags + const str4 = ` + PUSH /pattern/gim + ` + const result4 = await run(toBytecode(str4)) + expect(result4.type).toBe('regex') + if (result4.type === 'regex') { + expect(result4.value.global).toBe(true) + expect(result4.value.ignoreCase).toBe(true) + expect(result4.value.multiline).toBe(true) + } + }) + + test("complex patterns", async () => { + // Character class + const str1 = ` + PUSH /[a-z0-9]+/ + ` + const result1 = await run(toBytecode(str1)) + expect(result1.type).toBe('regex') + if (result1.type === 'regex') { + expect(result1.value.source).toBe('[a-z0-9]+') + } + + // Quantifiers + const str2 = ` + PUSH /a{2,4}/ + ` + const result2 = await run(toBytecode(str2)) + expect(result2.type).toBe('regex') + if (result2.type === 'regex') { + expect(result2.value.source).toBe('a{2,4}') + } + + // Groups and alternation + const str3 = ` + PUSH /(foo|bar)/ + ` + const result3 = await run(toBytecode(str3)) + expect(result3.type).toBe('regex') + if (result3.type === 'regex') { + expect(result3.value.source).toBe('(foo|bar)') + } + + // Anchors and special chars + const str4 = ` + PUSH /^[a-z]+$/ + ` + const result4 = await run(toBytecode(str4)) + expect(result4.type).toBe('regex') + if (result4.type === 'regex') { + expect(result4.value.source).toBe('^[a-z]+$') + } + }) + + test("escaping special characters", async () => { + const str = ` + PUSH /\\d+\\.\\d+/ + ` + const result = await run(toBytecode(str)) + expect(result.type).toBe('regex') + if (result.type === 'regex') { + expect(result.value.source).toBe('\\d+\\.\\d+') + } + }) + + test("store and load", async () => { + const str = ` + PUSH /test/i + STORE pattern + LOAD pattern + ` + const result = await run(toBytecode(str)) + expect(result.type).toBe('regex') + if (result.type === 'regex') { + expect(result.value.source).toBe('test') + expect(result.value.ignoreCase).toBe(true) + } + }) + + test("TRY_LOAD with regex", async () => { + const str = ` + PUSH /hello/g + STORE regex + TRY_LOAD regex + ` + const result = await run(toBytecode(str)) + expect(result.type).toBe('regex') + if (result.type === 'regex') { + expect(result.value.source).toBe('hello') + expect(result.value.global).toBe(true) + } + }) + + test("NEQ comparison", async () => { + const str = ` + PUSH /foo/ + PUSH /bar/ + NEQ + ` + expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) + + const str2 = ` + PUSH /test/i + PUSH /test/i + NEQ + ` + expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false }) + }) + + test("is truthy", async () => { + // Regex values should be truthy (not null or false) + const str = ` + PUSH /test/ + JUMP_IF_FALSE .end + PUSH 42 + .end: + ` + expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) + }) + + test("NOT returns false (regex is truthy)", async () => { + const str = ` + PUSH /pattern/ + NOT + ` + expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false }) + }) + + test("in arrays", async () => { + const str = ` + PUSH /first/ + PUSH /second/i + PUSH /third/g + 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]!.type).toBe('regex') + if (result.value[0]!.type === 'regex') { + expect(result.value[0]!.value.source).toBe('first') + } + + expect(result.value[1]!.type).toBe('regex') + if (result.value[1]!.type === 'regex') { + expect(result.value[1]!.value.source).toBe('second') + expect(result.value[1]!.value.ignoreCase).toBe(true) + } + + expect(result.value[2]!.type).toBe('regex') + if (result.value[2]!.type === 'regex') { + expect(result.value[2]!.value.source).toBe('third') + expect(result.value[2]!.value.global).toBe(true) + } + } + }) + + test("retrieve from array", async () => { + const str = ` + PUSH /pattern/i + PUSH /test/g + MAKE_ARRAY #2 + PUSH 1 + ARRAY_GET + ` + const result = await run(toBytecode(str)) + expect(result.type).toBe('regex') + if (result.type === 'regex') { + expect(result.value.source).toBe('test') + expect(result.value.global).toBe(true) + } + }) + + test("in dicts", async () => { + const str = ` + PUSH 'email' + PUSH /^[a-z@.]+$/i + PUSH 'phone' + PUSH /\\d{3}-\\d{4}/ + MAKE_DICT #2 + ` + const result = await run(toBytecode(str)) + expect(result.type).toBe('dict') + if (result.type === 'dict') { + expect(result.value.size).toBe(2) + + const email = result.value.get('email') + expect(email?.type).toBe('regex') + if (email?.type === 'regex') { + expect(email.value.source).toBe('^[a-z@.]+$') + expect(email.value.ignoreCase).toBe(true) + } + + const phone = result.value.get('phone') + expect(phone?.type).toBe('regex') + if (phone?.type === 'regex') { + expect(phone.value.source).toBe('\\d{3}-\\d{4}') + } + } + }) + + test("retrieve from dict", async () => { + const str = ` + PUSH 'pattern' + PUSH /test/gim + MAKE_DICT #1 + PUSH 'pattern' + DICT_GET + ` + const result = await run(toBytecode(str)) + expect(result.type).toBe('regex') + if (result.type === 'regex') { + expect(result.value.source).toBe('test') + expect(result.value.global).toBe(true) + expect(result.value.ignoreCase).toBe(true) + expect(result.value.multiline).toBe(true) + } + }) + + test("with STR_CONCAT converts to string", async () => { + const str = ` + PUSH "Pattern: " + PUSH /test/gi + STR_CONCAT #2 + ` + const result = await run(toBytecode(str)) + expect(result.type).toBe('string') + if (result.type === 'string') { + expect(result.value).toBe('Pattern: /test/gi') + } + }) + + test("multiple regex in STR_CONCAT", async () => { + const str = ` + PUSH /foo/ + PUSH " and " + PUSH /bar/i + STR_CONCAT #3 + ` + expect(await run(toBytecode(str))).toEqual({ type: 'string', value: '/foo/ and /bar/i' }) + }) + + test("DUP with regex", async () => { + const str = ` + PUSH /pattern/i + DUP + EQ + ` + // Same regex duplicated should be equal + expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) + }) + + test("empty pattern", async () => { + const str = ` + PUSH // + ` + const result = await run(toBytecode(str)) + expect(result.type).toBe('regex') + if (result.type === 'regex') { + expect(result.value.source).toBe('(?:)') + } + }) + + test("pattern with forward slashes escaped", async () => { + const str = ` + PUSH /https:\\/\\// + ` + const result = await run(toBytecode(str)) + expect(result.type).toBe('regex') + if (result.type === 'regex') { + expect(result.value.source).toBe('https:\\/\\/') + } + }) + + test("unicode patterns", async () => { + const str = ` + PUSH /こんにけは/ + ` + const result = await run(toBytecode(str)) + expect(result.type).toBe('regex') + if (result.type === 'regex') { + expect(result.value.source).toBe('こんにけは') + } + }) + + test("emoji in pattern", async () => { + const str = ` + PUSH /πŸŽ‰+/ + ` + const result = await run(toBytecode(str)) + expect(result.type).toBe('regex') + if (result.type === 'regex') { + expect(result.value.source).toBe('πŸŽ‰+') + } + }) + + test("comparing different regex types", async () => { + // Different patterns + const str1 = ` + PUSH /abc/ + PUSH /xyz/ + EQ + ` + expect(await run(toBytecode(str1))).toEqual({ type: 'boolean', value: false }) + + // Same pattern, different flags + const str2 = ` + PUSH /test/ + PUSH /test/i + EQ + ` + expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false }) + + // Different order of flags (should be equal) + const str3 = ` + PUSH /test/ig + PUSH /test/gi + EQ + ` + expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true }) + }) + + test("with native functions", async () => { + const { VM } = await import("#vm") + const bytecode = toBytecode(` + PUSH "hello world" + PUSH /world/ + CALL_NATIVE match + HALT + `) + + const vm = new VM(bytecode) + + // Register a native function that takes a string and regex + vm.registerFunction('match', (str: string, pattern: RegExp) => { + return pattern.test(str) + }) + + const result = await vm.run() + expect(result).toEqual({ type: 'boolean', value: true }) + }) + + test("native function with regex replacement", async () => { + const { VM } = await import("#vm") + const bytecode = toBytecode(` + PUSH "hello world" + PUSH /o/g + PUSH "0" + CALL_NATIVE replace + HALT + `) + + const vm = new VM(bytecode) + + vm.registerFunction('replace', (str: string, pattern: RegExp, replacement: string) => { + return str.replace(pattern, replacement) + }) + + const result = await vm.run() + expect(result).toEqual({ type: 'string', value: 'hell0 w0rld' }) + }) + + test("native function extracting matches", async () => { + const { VM } = await import("#vm") + const bytecode = toBytecode(` + PUSH "test123abc456" + PUSH /\\d+/g + CALL_NATIVE extractNumbers + HALT + `) + + const vm = new VM(bytecode) + + vm.registerFunction('extractNumbers', (str: string, pattern: RegExp) => { + return str.match(pattern) || [] + }) + + const result = await vm.run() + expect(result.type).toBe('array') + if (result.type === 'array') { + expect(result.value).toHaveLength(2) + expect(result.value[0]).toEqual({ type: 'string', value: '123' }) + expect(result.value[1]).toEqual({ type: 'string', value: '456' }) + } + }) +}) diff --git a/tests/unicode.test.ts b/tests/unicode.test.ts new file mode 100644 index 0000000..28dd933 --- /dev/null +++ b/tests/unicode.test.ts @@ -0,0 +1,119 @@ +import { test, expect, describe } from "bun:test" +import { run } from "#index" +import { toBytecode } from "#bytecode" + +describe("Unicode and Emoji", () => { + 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 }) + }) +})