From 614f5c4f9183eceba202ccd846bde11e1596a38a Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Sat, 8 Nov 2025 11:38:41 -0800 Subject: [PATCH] use fancy matchers, like Shrimp --- bunfig.toml | 2 + tests/bitwise.test.ts | 28 ++-- tests/exceptions.test.ts | 16 +- tests/opcodes.test.ts | 331 +++++++++++++++++++-------------------- tests/regex.test.ts | 18 +-- tests/setup.ts | 249 +++++++++++++++++++++++++++++ 6 files changed, 443 insertions(+), 201 deletions(-) create mode 100644 bunfig.toml create mode 100644 tests/setup.ts diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..8370a01 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = ["./tests/setup.ts"] diff --git a/tests/bitwise.test.ts b/tests/bitwise.test.ts index 66f80ec..910fad5 100644 --- a/tests/bitwise.test.ts +++ b/tests/bitwise.test.ts @@ -6,84 +6,84 @@ describe('bitwise operations', () => { const bytecode = toBytecode([ ["PUSH", 5], ["PUSH", 3], ["BIT_AND"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'number', value: 1 }) + await expect(bytecode).toBeNumber(1) }) test('BIT_AND with zero', async () => { const bytecode = toBytecode([ ["PUSH", 5], ["PUSH", 0], ["BIT_AND"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'number', value: 0 }) + await expect(bytecode).toBeNumber(0) }) test('BIT_OR', async () => { const bytecode = toBytecode([ ["PUSH", 5], ["PUSH", 3], ["BIT_OR"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'number', value: 7 }) + await expect(bytecode).toBeNumber(7) }) test('BIT_OR with zero', async () => { const bytecode = toBytecode([ ["PUSH", 5], ["PUSH", 0], ["BIT_OR"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'number', value: 5 }) + await expect(bytecode).toBeNumber(5) }) test('BIT_XOR', async () => { const bytecode = toBytecode([ ["PUSH", 5], ["PUSH", 3], ["BIT_XOR"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'number', value: 6 }) + await expect(bytecode).toBeNumber(6) }) test('BIT_XOR with itself returns zero', async () => { const bytecode = toBytecode([ ["PUSH", 5], ["PUSH", 5], ["BIT_XOR"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'number', value: 0 }) + await expect(bytecode).toBeNumber(0) }) test('BIT_SHL', async () => { const bytecode = toBytecode([ ["PUSH", 5], ["PUSH", 2], ["BIT_SHL"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'number', value: 20 }) + await expect(bytecode).toBeNumber(20) }) test('BIT_SHL by zero', async () => { const bytecode = toBytecode([ ["PUSH", 5], ["PUSH", 0], ["BIT_SHL"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'number', value: 5 }) + await expect(bytecode).toBeNumber(5) }) test('BIT_SHR', async () => { const bytecode = toBytecode([ ["PUSH", 20], ["PUSH", 2], ["BIT_SHR"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'number', value: 5 }) + await expect(bytecode).toBeNumber(5) }) test('BIT_SHR preserves sign for negative numbers', async () => { const bytecode = toBytecode([ ["PUSH", -20], ["PUSH", 2], ["BIT_SHR"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'number', value: -5 }) + await expect(bytecode).toBeNumber(-5) }) test('BIT_USHR', async () => { const bytecode = toBytecode([ ["PUSH", -1], ["PUSH", 1], ["BIT_USHR"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'number', value: 2147483647 }) + await expect(bytecode).toBeNumber(2147483647) }) test('BIT_USHR does not preserve sign', async () => { const bytecode = toBytecode([ ["PUSH", -8], ["PUSH", 1], ["BIT_USHR"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'number', value: 2147483644 }) + await expect(bytecode).toBeNumber(2147483644) }) test('compound bitwise operations', async () => { @@ -94,7 +94,7 @@ describe('bitwise operations', () => { ["BIT_OR"], // stack: [5] ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'number', value: 5 }) + await expect(bytecode).toBeNumber(5) }) test('shift with large shift amounts', async () => { @@ -102,6 +102,6 @@ describe('bitwise operations', () => { ["PUSH", 1], ["PUSH", 31], ["BIT_SHL"], ["HALT"] ]) // 1 << 31 = -2147483648 (most significant bit set) - expect(await run(bytecode)).toEqual({ type: 'number', value: -2147483648 }) + await expect(bytecode).toBeNumber(-2147483648) }) }) diff --git a/tests/exceptions.test.ts b/tests/exceptions.test.ts index 80c06c8..7f306e8 100644 --- a/tests/exceptions.test.ts +++ b/tests/exceptions.test.ts @@ -15,7 +15,7 @@ test("PUSH_TRY and POP_TRY - no exception thrown", async () => { PUSH 999 HALT ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 }) + await expect(str).toBeNumber(52) }) test("THROW - catch exception with error value", async () => { @@ -29,7 +29,7 @@ test("THROW - catch exception with error value", async () => { .catch: HALT ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error occurred' }) + await expect(str).toBeString('error occurred') }) test("THROW - uncaught exception throws JS error", async () => { @@ -58,7 +58,7 @@ test("THROW - exception with nested try blocks", async () => { PUSH "outer error" HALT ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'inner error' }) + await expect(str).toBeString('inner error') }) test("THROW - exception skips outer handler", async () => { @@ -75,7 +75,7 @@ test("THROW - exception skips outer handler", async () => { .outer_catch: HALT ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'error message' }) + await expect(str).toBeString('error message') }) test("THROW - exception unwinds call stack", async () => { @@ -150,7 +150,7 @@ test("PUSH_FINALLY - finally executes after successful try", async () => { ADD HALT ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 110 }) + await expect(str).toBeNumber(110) }) test("PUSH_FINALLY - finally executes after exception", async () => { @@ -169,7 +169,7 @@ test("PUSH_FINALLY - finally executes after exception", async () => { PUSH "finally ran" HALT ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'finally ran' }) + await expect(str).toBeString('finally ran') }) test("PUSH_FINALLY - finally without catch", async () => { @@ -189,7 +189,7 @@ test("PUSH_FINALLY - finally without catch", async () => { ADD HALT ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 52 }) + await expect(str).toBeNumber(52) }) test("PUSH_FINALLY - nested try-finally blocks", async () => { @@ -214,7 +214,7 @@ test("PUSH_FINALLY - nested try-finally blocks", async () => { ADD HALT ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 11 }) + await expect(str).toBeNumber(11) }) test("PUSH_FINALLY - error when no handler", async () => { diff --git a/tests/opcodes.test.ts b/tests/opcodes.test.ts index 8ecca3f..e6d4726 100644 --- a/tests/opcodes.test.ts +++ b/tests/opcodes.test.ts @@ -4,76 +4,67 @@ 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 }) + await expect(` + PUSH 1 + PUSH 5 + ADD + `).toEqualValue(6) - const str2 = ` - PUSH 100 - PUSH 500 - ADD - ` - expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 600 }) + await expect(` + PUSH 100 + PUSH 500 + ADD + `).toBeNumber(600) }) test("concatenate two strings", async () => { - const str = ` - PUSH "hello" - PUSH " world" - ADD - ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'hello world' }) + await expect(` + PUSH "hello" + PUSH " world" + ADD + `).toEqualValue('hello world') - const str2 = ` - PUSH "foo" - PUSH "bar" - ADD - ` - expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'foobar' }) + await expect(` + PUSH "foo" + PUSH "bar" + ADD + `).toBeString('foobar') }) test("concatenate string with number", async () => { - const str = ` - PUSH "count: " - PUSH 42 - ADD - ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'count: 42' }) + await expect(` + PUSH "count: " + PUSH 42 + ADD + `).toEqualValue('count: 42') - const str2 = ` - PUSH 100 - PUSH " items" - ADD - ` - expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: '100 items' }) + await expect(` + PUSH 100 + PUSH " items" + ADD + `).toBeString('100 items') }) test("concatenate string with boolean", async () => { - const str = ` - PUSH "result: " - PUSH true - ADD - ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'result: true' }) + await expect(` + PUSH "result: " + PUSH true + ADD + `).toBeString('result: true') - const str2 = ` - PUSH false - PUSH " value" - ADD - ` - expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'false value' }) + await expect(` + PUSH false + PUSH " value" + ADD + `).toEqualValue('false value') }) test("concatenate string with null", async () => { - const str = ` - PUSH "value: " - PUSH null - ADD - ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'value: null' }) + await expect(` + PUSH "value: " + PUSH null + ADD + `).toBeString('value: null') }) test("concatenate multiple strings in sequence", async () => { @@ -86,7 +77,7 @@ describe("ADD", () => { PUSH "!" ADD ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'hello world!' }) + await expect(str).toBeString('hello world!') }) test("mixed arithmetic and string concatenation", async () => { @@ -97,7 +88,7 @@ describe("ADD", () => { ADD ADD ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'Result: 15' }) + await expect(str).toBeString('Result: 15') }) test("concatenate two arrays", async () => { @@ -463,7 +454,7 @@ describe("SUB", () => { PUSH 2 SUB ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 }) + await expect(str).toBeNumber(3) }) }) @@ -474,7 +465,7 @@ describe("MUL", () => { PUSH 2 MUL ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 }) + await expect(str).toBeNumber(10) }) }) @@ -485,14 +476,14 @@ describe("DIV", () => { PUSH 2 DIV ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 5 }) + await expect(str).toBeNumber(5) const str2 = ` PUSH 10 PUSH 0 DIV ` - expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: Infinity }) + await expect(str2).toBeNumber(Infinity) }) }) @@ -503,7 +494,7 @@ describe("MOD", () => { PUSH 5 MOD ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 }) + await expect(str).toBeNumber(2) }) }) @@ -512,7 +503,7 @@ describe("PUSH", () => { const str = ` PUSH 42 ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) + await expect(str).toBeNumber(42) }) }) @@ -523,7 +514,7 @@ describe("POP", () => { PUSH 20 POP ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 }) + await expect(str).toBeNumber(10) }) }) @@ -534,7 +525,7 @@ describe("DUP", () => { DUP ADD ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 }) + await expect(str).toBeNumber(10) }) }) @@ -545,7 +536,7 @@ describe("SWAP", () => { PUSH 20 SWAP ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 }) + await expect(str).toBeNumber(10) }) test("swap and use in subtraction", async () => { @@ -555,7 +546,7 @@ describe("SWAP", () => { SWAP SUB ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 5 }) + await expect(str).toBeNumber(5) }) test("swap different types", async () => { @@ -564,7 +555,7 @@ describe("SWAP", () => { PUSH 42 SWAP ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'hello' }) + await expect(str).toBeString('hello') }) }) @@ -575,14 +566,14 @@ describe("EQ", () => { PUSH 5 EQ ` - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) + await expect(str).toBeBoolean(true) const str2 = ` PUSH 5 PUSH 10 EQ ` - expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false }) + await expect(str2).toBeBoolean(false) }) test('equality with regexes', async () => { @@ -591,21 +582,21 @@ describe("EQ", () => { PUSH /cool/i EQ ` - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) + await expect(str).toBeBoolean(true) const str2 = ` PUSH /cool/ PUSH /cool/i EQ ` - expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false }) + await expect(str2).toBeBoolean(false) const str3 = ` PUSH /not-cool/ PUSH /cool/ EQ ` - expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false }) + await expect(str3).toBeBoolean(false) }) }) @@ -616,7 +607,7 @@ describe("NEQ", () => { PUSH 10 NEQ ` - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) + await expect(str).toBeBoolean(true) }) }) @@ -627,7 +618,7 @@ describe("LT", () => { PUSH 10 LT ` - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) + await expect(str).toBeBoolean(true) }) }) @@ -638,7 +629,7 @@ describe("GT", () => { PUSH 5 GT ` - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) + await expect(str).toBeBoolean(true) }) }) @@ -650,7 +641,7 @@ describe("LTE", () => { PUSH 5 LTE ` - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) + await expect(str).toBeBoolean(true) // less than case const str2 = ` @@ -658,7 +649,7 @@ describe("LTE", () => { PUSH 5 LTE ` - expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true }) + await expect(str2).toBeBoolean(true) // greater than case (false) const str3 = ` @@ -666,7 +657,7 @@ describe("LTE", () => { PUSH 5 LTE ` - expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false }) + await expect(str3).toBeBoolean(false) }) }) @@ -678,7 +669,7 @@ describe("GTE", () => { PUSH 5 GTE ` - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) + await expect(str).toBeBoolean(true) // greater than case const str2 = ` @@ -686,7 +677,7 @@ describe("GTE", () => { PUSH 5 GTE ` - expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true }) + await expect(str2).toBeBoolean(true) // less than case (false) const str3 = ` @@ -694,7 +685,7 @@ describe("GTE", () => { PUSH 5 GTE ` - expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false }) + await expect(str3).toBeBoolean(false) }) }) @@ -727,7 +718,7 @@ describe("Logical patterns", () => { PUSH 2 .end: ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 }) + await expect(str).toBeNumber(2) }) test("OR pattern - short circuits when true", async () => { @@ -739,7 +730,7 @@ describe("Logical patterns", () => { PUSH 2 .end: ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 1 }) + await expect(str).toBeNumber(1) }) test("OR pattern - evaluates second when false", async () => { @@ -753,7 +744,7 @@ describe("Logical patterns", () => { PUSH 2 .end: ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 }) + await expect(str).toBeNumber(2) }) }) @@ -764,14 +755,14 @@ describe("NOT", () => { PUSH 1 NOT ` - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false }) + await expect(str).toBeBoolean(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 }) + await expect(str2).toBeBoolean(false) // boolean false is falsy, so NOT returns true const str3 = ` @@ -780,7 +771,7 @@ describe("NOT", () => { EQ NOT ` - expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true }) + await expect(str3).toBeBoolean(true) }) }) @@ -793,7 +784,7 @@ describe("Truthiness", () => { PUSH 1 .end: ` - expect(await run(toBytecode(str1))).toEqual({ type: 'number', value: 1 }) + await expect(str1).toBeNumber(1) // empty string is truthy (unlike JS) const str2 = ` @@ -802,7 +793,7 @@ describe("Truthiness", () => { PUSH 1 .end: ` - expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 1 }) + await expect(str2).toBeNumber(1) // false is falsy const str3 = ` @@ -813,7 +804,7 @@ describe("Truthiness", () => { PUSH 999 .end: ` - expect(await run(toBytecode(str3))).toEqual({ type: 'number', value: 999 }) + await expect(str3).toBeNumber(999) }) }) @@ -824,7 +815,7 @@ describe("HALT", () => { HALT PUSH 100 ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) + await expect(str).toBeNumber(42) }) }) @@ -835,7 +826,7 @@ describe("TYPE", () => { ["TYPE"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'string', value: 'null' }) + await expect(bytecode).toBeString('null') }) test("boolean type", async () => { @@ -844,14 +835,14 @@ describe("TYPE", () => { ["TYPE"], ["HALT"] ]) - expect(await run(bytecode1)).toEqual({ type: 'string', value: 'boolean' }) + await expect(bytecode1).toBeString('boolean') const bytecode2 = toBytecode([ ["PUSH", false], ["TYPE"], ["HALT"] ]) - expect(await run(bytecode2)).toEqual({ type: 'string', value: 'boolean' }) + await expect(bytecode2).toBeString('boolean') }) test("number type", async () => { @@ -860,21 +851,21 @@ describe("TYPE", () => { ["TYPE"], ["HALT"] ]) - expect(await run(bytecode1)).toEqual({ type: 'string', value: 'number' }) + await expect(bytecode1).toBeString('number') const bytecode2 = toBytecode([ ["PUSH", 0], ["TYPE"], ["HALT"] ]) - expect(await run(bytecode2)).toEqual({ type: 'string', value: 'number' }) + await expect(bytecode2).toBeString('number') const bytecode3 = toBytecode([ ["PUSH", -3.14], ["TYPE"], ["HALT"] ]) - expect(await run(bytecode3)).toEqual({ type: 'string', value: 'number' }) + await expect(bytecode3).toBeString('number') }) test("string type", async () => { @@ -883,14 +874,14 @@ describe("TYPE", () => { ["TYPE"], ["HALT"] ]) - expect(await run(bytecode1)).toEqual({ type: 'string', value: 'string' }) + await expect(bytecode1).toBeString('string') const bytecode2 = toBytecode([ ["PUSH", ""], ["TYPE"], ["HALT"] ]) - expect(await run(bytecode2)).toEqual({ type: 'string', value: 'string' }) + await expect(bytecode2).toBeString('string') }) test("array type", async () => { @@ -902,7 +893,7 @@ describe("TYPE", () => { ["TYPE"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'string', value: 'array' }) + await expect(bytecode).toBeString('array') }) test("empty array type", async () => { @@ -911,7 +902,7 @@ describe("TYPE", () => { ["TYPE"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'string', value: 'array' }) + await expect(bytecode).toBeString('array') }) test("dict type", async () => { @@ -924,7 +915,7 @@ describe("TYPE", () => { ["TYPE"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'string', value: 'dict' }) + await expect(bytecode).toBeString('dict') }) test("empty dict type", async () => { @@ -933,7 +924,7 @@ describe("TYPE", () => { ["TYPE"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'string', value: 'dict' }) + await expect(bytecode).toBeString('dict') }) test("function type", async () => { @@ -945,7 +936,7 @@ describe("TYPE", () => { ["LOAD", "x"], ["RETURN"] ]) - expect(await run(bytecode)).toEqual({ type: 'string', value: 'function' }) + await expect(bytecode).toBeString('function') }) test("native function type", async () => { @@ -966,7 +957,7 @@ describe("TYPE", () => { ["TYPE"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'string', value: 'regex' }) + await expect(bytecode).toBeString('regex') }) test("TYPE with stored result", async () => { @@ -977,7 +968,7 @@ describe("TYPE", () => { ["LOAD", "myType"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'string', value: 'number' }) + await expect(bytecode).toBeString('number') }) test("TYPE comparison - type guards", async () => { @@ -989,7 +980,7 @@ describe("TYPE", () => { ["EQ"], ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'boolean', value: true }) + await expect(bytecode).toBeBoolean(true) }) }) @@ -1001,7 +992,7 @@ describe("LOAD / STORE", () => { PUSH 21 LOAD x ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) + await expect(str).toBeNumber(42) }) test("multiple variables", async () => { @@ -1015,7 +1006,7 @@ describe("LOAD / STORE", () => { LOAD b ADD ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 30 }) + await expect(str).toBeNumber(30) }) }) @@ -1026,14 +1017,14 @@ describe("TRY_LOAD", () => { STORE count TRY_LOAD count ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 }) + await expect(str).toBeNumber(100) const str2 = ` PUSH 'Bobby' STORE name TRY_LOAD name ` - expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'Bobby' }) + await expect(str2).toBeString('Bobby') }) test("variable missing", async () => { @@ -1042,14 +1033,14 @@ describe("TRY_LOAD", () => { STORE count TRY_LOAD count1 ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'count1' }) + await expect(str).toBeString('count1') const str2 = ` PUSH 'Bobby' STORE name TRY_LOAD full-name ` - expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'full-name' }) + await expect(str2).toBeString('full-name') }) test("with different value types", async () => { @@ -1082,7 +1073,7 @@ describe("TRY_LOAD", () => { STORE flag TRY_LOAD flag ` - expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true }) + await expect(str3).toBeBoolean(true) // Null const str4 = ` @@ -1090,7 +1081,7 @@ describe("TRY_LOAD", () => { STORE empty TRY_LOAD empty ` - expect(await run(toBytecode(str4))).toEqual({ type: 'null', value: null }) + await expect(str4).toBeNull() }) test("in nested scope", async () => { @@ -1107,7 +1098,7 @@ describe("TRY_LOAD", () => { TRY_LOAD outer RETURN ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) + await expect(str).toBeNumber(42) }) test("missing variable in nested scope returns name", async () => { @@ -1124,7 +1115,7 @@ describe("TRY_LOAD", () => { TRY_LOAD inner RETURN ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'inner' }) + await expect(str).toBeString('inner') }) test("used for conditional variable existence check", async () => { @@ -1137,7 +1128,7 @@ describe("TRY_LOAD", () => { EQ ` // Variable exists, so TRY_LOAD returns 100, which != 'count' - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false }) + await expect(str).toBeBoolean(false) const str2 = ` PUSH 100 @@ -1147,7 +1138,7 @@ describe("TRY_LOAD", () => { EQ ` // Variable missing, so TRY_LOAD returns 'missing', which == 'missing' - expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true }) + await expect(str2).toBeBoolean(true) }) test("with function value", async () => { @@ -1175,7 +1166,7 @@ describe("JUMP", () => { .skip: PUSH 2 ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 }) + await expect(str).toBeNumber(2) }) test("forward jump skips instructions", async () => { @@ -1188,7 +1179,7 @@ describe("JUMP", () => { .end: PUSH 400 ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 400 }) + await expect(str).toBeNumber(400) }) test("backward - simple loop", async () => { @@ -1228,7 +1219,7 @@ describe("JUMP_IF_FALSE", () => { .skip: PUSH 42 ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) + await expect(str).toBeNumber(42) }) test("no jump when true", async () => { @@ -1238,7 +1229,7 @@ describe("JUMP_IF_FALSE", () => { PUSH 100 .skip: ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 }) + await expect(str).toBeNumber(100) }) }) @@ -1251,7 +1242,7 @@ describe("JUMP_IF_TRUE", () => { .skip: PUSH 42 ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) + await expect(str).toBeNumber(42) }) }) @@ -1284,7 +1275,7 @@ describe("ARRAY_GET", () => { PUSH 1 ARRAY_GET ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 20 }) + await expect(str).toBeNumber(20) }) }) @@ -1302,7 +1293,7 @@ describe("ARRAY_SET", () => { PUSH 1 ARRAY_GET ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 99 }) + await expect(str).toBeNumber(99) }) }) @@ -1317,7 +1308,7 @@ describe("ARRAY_PUSH", () => { ARRAY_PUSH ARRAY_LEN ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 }) + await expect(str).toBeNumber(3) }) test("mutates original array", async () => { @@ -1331,7 +1322,7 @@ describe("ARRAY_PUSH", () => { PUSH 2 ARRAY_GET ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 30 }) + await expect(str).toBeNumber(30) }) }) @@ -1344,7 +1335,7 @@ describe("ARRAY_LEN", () => { MAKE_ARRAY #3 ARRAY_LEN ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 }) + await expect(str).toBeNumber(3) }) }) @@ -1376,7 +1367,7 @@ describe("DICT_GET", () => { PUSH 'name' DICT_GET ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'Bob' }) + await expect(str).toBeString('Bob') }) }) @@ -1391,7 +1382,7 @@ describe("DICT_SET", () => { PUSH 'key' DICT_GET ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'value' }) + await expect(str).toBeString('value') }) }) @@ -1404,7 +1395,7 @@ describe("DICT_HAS", () => { PUSH 'key' DICT_HAS ` - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) + await expect(str).toBeBoolean(true) }) test("checks key missing", async () => { @@ -1413,7 +1404,7 @@ describe("DICT_HAS", () => { PUSH 'missing' DICT_HAS ` - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false }) + await expect(str).toBeBoolean(false) }) }) @@ -1429,7 +1420,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'number', value: 300 }) + await expect(bytecode).toBeNumber(300) }) test("gets a DICT value", async () => { @@ -1443,7 +1434,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'string', value: 'Bob' }) + await expect(bytecode).toBeString('Bob') }) test("fails to work on NUMBER", async () => { @@ -1485,7 +1476,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'null', value: null }) + await expect(bytecode).toBeNull() }) test("array - negative index", async () => { @@ -1498,7 +1489,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'null', value: null }) + await expect(bytecode).toBeNull() }) test("array - first and last element", async () => { @@ -1519,7 +1510,7 @@ describe("DOT_GET", () => { ADD `) - expect(await run(bytecode)).toEqual({ type: 'number', value: 400 }) + await expect(bytecode).toBeNumber(400) }) test("dict - returns null for missing key", async () => { @@ -1531,7 +1522,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'null', value: null }) + await expect(bytecode).toBeNull() }) test("dict - with numeric key", async () => { @@ -1543,7 +1534,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'string', value: 'value' }) + await expect(bytecode).toBeString('value') }) test("array - with string value", async () => { @@ -1556,7 +1547,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'string', value: 'bar' }) + await expect(bytecode).toBeString('bar') }) test("array - with boolean values", async () => { @@ -1569,7 +1560,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'boolean', value: false }) + await expect(bytecode).toBeBoolean(false) }) test("dict - with boolean value", async () => { @@ -1583,7 +1574,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'boolean', value: true }) + await expect(bytecode).toBeBoolean(true) }) test("array - nested arrays", async () => { @@ -1637,7 +1628,7 @@ describe("DOT_GET", () => { ["HALT"] ]) - expect(await run(bytecode)).toEqual({ type: 'string', value: 'banana' }) + await expect(bytecode).toBeString('banana') }) test("with variables - array", async () => { @@ -1654,7 +1645,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'number', value: 20 }) + await expect(bytecode).toBeNumber(20) }) test("with variables - dict", async () => { @@ -1672,7 +1663,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'number', value: 200 }) + await expect(bytecode).toBeNumber(200) }) test("with expression as index", async () => { @@ -1688,7 +1679,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'number', value: 40 }) + await expect(bytecode).toBeNumber(40) }) test("chained access - array of dicts", async () => { @@ -1706,7 +1697,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'string', value: 'Bob' }) + await expect(bytecode).toBeString('Bob') }) test("chained access - dict of arrays", async () => { @@ -1723,7 +1714,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'number', value: 2 }) + await expect(bytecode).toBeNumber(2) }) test("with null value in array", async () => { @@ -1736,7 +1727,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'null', value: null }) + await expect(bytecode).toBeNull() }) test("with null value in dict", async () => { @@ -1750,7 +1741,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'null', value: null }) + await expect(bytecode).toBeNull() }) test("array with regex values", async () => { @@ -1801,7 +1792,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'string', value: 'zero' }) + await expect(bytecode).toBeString('zero') }) test("fails on boolean", async () => { @@ -1856,7 +1847,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'null', value: null }) + await expect(bytecode).toBeNull() }) test("empty dict access", async () => { @@ -1866,7 +1857,7 @@ describe("DOT_GET", () => { DOT_GET `) - expect(await run(bytecode)).toEqual({ type: 'null', value: null }) + await expect(bytecode).toBeNull() }) }) @@ -1879,7 +1870,7 @@ describe("STR_CONCAT", () => { STR_CONCAT #3 ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hi friend!" }) + await expect(str).toBeString("Hi friend!") const str2 = ` PUSH "Holy smokes!" @@ -1888,7 +1879,7 @@ describe("STR_CONCAT", () => { STR_CONCAT #2 ` - expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "It's alive!" }) + await expect(str2).toBeString("It's alive!") const str3 = ` @@ -1902,7 +1893,7 @@ describe("STR_CONCAT", () => { STR_CONCAT #5 ` - expect(await run(toBytecode(str3))).toEqual({ type: 'string', value: "1 + 1 = 2" }) + await expect(str3).toBeString("1 + 1 = 2") }) test("empty concat (count=0)", async () => { @@ -1910,7 +1901,7 @@ describe("STR_CONCAT", () => { PUSH "leftover" STR_CONCAT #0 ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "" }) + await expect(str).toBeString("") }) test("single string", async () => { @@ -1918,7 +1909,7 @@ describe("STR_CONCAT", () => { PUSH "hello" STR_CONCAT #1 ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "hello" }) + await expect(str).toBeString("hello") }) test("converts numbers to strings", async () => { @@ -1928,7 +1919,7 @@ describe("STR_CONCAT", () => { PUSH 7 STR_CONCAT #3 ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "421007" }) + await expect(str).toBeString("421007") }) test("converts booleans to strings", async () => { @@ -1937,14 +1928,14 @@ describe("STR_CONCAT", () => { PUSH true STR_CONCAT #2 ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Result: true" }) + await expect(str).toBeString("Result: true") const str2 = ` PUSH false PUSH " is false" STR_CONCAT #2 ` - expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "false is false" }) + await expect(str2).toBeString("false is false") }) test("converts null to strings", async () => { @@ -1953,7 +1944,7 @@ describe("STR_CONCAT", () => { PUSH null STR_CONCAT #2 ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Value: null" }) + await expect(str).toBeString("Value: null") }) test("mixed types", async () => { @@ -1966,7 +1957,7 @@ describe("STR_CONCAT", () => { PUSH null STR_CONCAT #6 ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Count: 42, Active: true, Total: null" }) + await expect(str).toBeString("Count: 42, Active: true, Total: null") }) test("array format", async () => { @@ -1991,7 +1982,7 @@ describe("STR_CONCAT", () => { PUSH "!" STR_CONCAT #3 ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello, Alice!" }) + await expect(str).toBeString("Hello, Alice!") }) test("composable (multiple concatenations)", async () => { @@ -2003,7 +1994,7 @@ describe("STR_CONCAT", () => { PUSH "!" STR_CONCAT #2 ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello World!" }) + await expect(str).toBeString("Hello World!") }) test("with emoji and unicode", async () => { @@ -2013,14 +2004,14 @@ describe("STR_CONCAT", () => { PUSH "!" STR_CONCAT #3 ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello 🌍!" }) + await expect(str).toBeString("Hello 🌍!") const str2 = ` PUSH "こんにけは" PUSH "δΈ–η•Œ" STR_CONCAT #2 ` - expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "γ“γ‚“γ«γ‘γ―δΈ–η•Œ" }) + await expect(str2).toBeString("γ“γ‚“γ«γ‘γ―δΈ–η•Œ") }) test("with expressions", async () => { @@ -2031,7 +2022,7 @@ describe("STR_CONCAT", () => { ADD STR_CONCAT #2 ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Result: 15" }) + await expect(str).toBeString("Result: 15") }) test("large concat", async () => { @@ -2048,7 +2039,7 @@ describe("STR_CONCAT", () => { PUSH "j" STR_CONCAT #10 ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "abcdefghij" }) + await expect(str).toBeString("abcdefghij") }) }) diff --git a/tests/regex.test.ts b/tests/regex.test.ts index 2e2caa2..8664ce3 100644 --- a/tests/regex.test.ts +++ b/tests/regex.test.ts @@ -159,14 +159,14 @@ describe("RegExp", () => { PUSH /bar/ NEQ ` - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) + await expect(str).toBeBoolean(true) const str2 = ` PUSH /test/i PUSH /test/i NEQ ` - expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false }) + await expect(str2).toBeBoolean(false) }) test("is truthy", async () => { @@ -177,7 +177,7 @@ describe("RegExp", () => { PUSH 42 .end: ` - expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 }) + await expect(str).toBeNumber(42) }) test("NOT returns false (regex is truthy)", async () => { @@ -185,7 +185,7 @@ describe("RegExp", () => { PUSH /pattern/ NOT ` - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false }) + await expect(str).toBeBoolean(false) }) test("in arrays", async () => { @@ -301,7 +301,7 @@ describe("RegExp", () => { PUSH /bar/i STR_CONCAT #3 ` - expect(await run(toBytecode(str))).toEqual({ type: 'string', value: '/foo/ and /bar/i' }) + await expect(str).toBeString('/foo/ and /bar/i') }) test("DUP with regex", async () => { @@ -311,7 +311,7 @@ describe("RegExp", () => { EQ ` // Same regex duplicated should be equal - expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true }) + await expect(str).toBeBoolean(true) }) test("empty pattern", async () => { @@ -365,7 +365,7 @@ describe("RegExp", () => { PUSH /xyz/ EQ ` - expect(await run(toBytecode(str1))).toEqual({ type: 'boolean', value: false }) + await expect(str1).toBeBoolean(false) // Same pattern, different flags const str2 = ` @@ -373,7 +373,7 @@ describe("RegExp", () => { PUSH /test/i EQ ` - expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false }) + await expect(str2).toBeBoolean(false) // Different order of flags (should be equal) const str3 = ` @@ -381,7 +381,7 @@ describe("RegExp", () => { PUSH /test/gi EQ ` - expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true }) + await expect(str3).toBeBoolean(true) }) test("with native functions", async () => { diff --git a/tests/setup.ts b/tests/setup.ts new file mode 100644 index 0000000..2654285 --- /dev/null +++ b/tests/setup.ts @@ -0,0 +1,249 @@ +import { expect } from "bun:test" +import { toValue, fromValue, type Value, toBytecode, run, type Bytecode } from "#reef" +import { isEqual } from "../src/value" + +declare module "bun:test" { + interface Matchers { + /** + * Run bytecode and assert that the result equals a JavaScript value after conversion via toValue() + * @example expect(bytecode).toEqualValue(42) + * @example expect("PUSH 5\nPUSH 3\nADD").toEqualValue(8) + * @example expect([["PUSH", 42]]).toEqualValue(42) + */ + toEqualValue(expected: any): Promise + + /** + * Run bytecode and assert that the result is null + * @example expect(bytecode).toBeNull() + * @example expect("PUSH null").toBeNull() + */ + toBeNull(): Promise + + /** + * Run bytecode and assert that the result is a boolean with the expected value + * @example expect(bytecode).toBeBoolean(true) + * @example expect("PUSH true").toBeBoolean(true) + */ + toBeBoolean(expected: boolean): Promise + + /** + * Run bytecode and assert that the result is a number with the expected value + * @example expect(bytecode).toBeNumber(42) + * @example expect("PUSH 42").toBeNumber(42) + */ + toBeNumber(expected: number): Promise + + /** + * Run bytecode and assert that the result is a string with the expected value + * @example expect(bytecode).toBeString("hello") + * @example expect("PUSH \"hello\"").toBeString("hello") + */ + toBeString(expected: string): Promise + + /** + * Run bytecode and assert that the result is an array with the expected values + * @example expect(bytecode).toBeArray([1, 2, 3]) + */ + toBeArray(expected: any[]): Promise + + /** + * Run bytecode and assert that the result is a dict with the expected key-value pairs + * @example expect(bytecode).toBeDict({ x: 10, y: 20 }) + */ + toBeDict(expected: Record): Promise + + /** + * Run bytecode and assert that the result is a function (Reef or native) + * @example expect(bytecode).toBeFunction() + */ + toBeFunction(): Promise + + /** + * Run bytecode and assert that the result is truthy according to ReefVM semantics + * (only null and false are falsy) + * @example expect(bytecode).toBeTruthy() + */ + toBeTruthy(): Promise + + /** + * Run bytecode and assert that the result is falsy according to ReefVM semantics + * (only null and false are falsy) + * @example expect(bytecode).toBeFalsy() + */ + toBeFalsy(): Promise + } +} + +expect.extend({ + async toEqualValue(received: string | any[] | Bytecode, expected: any) { + const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received + const result = await run(bytecode) + const expectedValue = toValue(expected) + const pass = isEqual(result, expectedValue) + + return { + pass, + message: () => + pass + ? `Expected value NOT to equal ${formatValue(expectedValue)}, but it did` + : `Expected value to equal ${formatValue(expectedValue)}, but received ${formatValue(result)}`, + } + }, + + async toBeNull(received: string | any[] | Bytecode) { + const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received + const result = await run(bytecode) + const pass = result.type === "null" + + return { + pass, + message: () => + pass + ? `Expected value NOT to be null, but it was` + : `Expected value to be null, but received ${formatValue(result)}`, + } + }, + + async toBeBoolean(received: string | any[] | Bytecode, expected: boolean) { + const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received + const result = await run(bytecode) + const pass = result.type === "boolean" && result.value === expected + + return { + pass, + message: () => + pass + ? `Expected value NOT to be boolean ${expected}, but it was` + : `Expected value to be boolean ${expected}, but received ${formatValue(result)}`, + } + }, + + async toBeNumber(received: string | any[] | Bytecode, expected: number) { + const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received + const result = await run(bytecode) + const pass = result.type === "number" && result.value === expected + + return { + pass, + message: () => + pass + ? `Expected value NOT to be number ${expected}, but it was` + : `Expected value to be number ${expected}, but received ${formatValue(result)}`, + } + }, + + async toBeString(received: string | any[] | Bytecode, expected: string) { + const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received + const result = await run(bytecode) + const pass = result.type === "string" && result.value === expected + + return { + pass, + message: () => + pass + ? `Expected value NOT to be string "${expected}", but it was` + : `Expected value to be string "${expected}", but received ${formatValue(result)}`, + } + }, + + async toBeArray(received: string | any[] | Bytecode, expected: any[]) { + const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received + const result = await run(bytecode) + const expectedValue = toValue(expected) + const pass = result.type === "array" && isEqual(result, expectedValue) + + return { + pass, + message: () => + pass + ? `Expected value NOT to be array ${formatValue(expectedValue)}, but it was` + : `Expected value to be array ${formatValue(expectedValue)}, but received ${formatValue(result)}`, + } + }, + + async toBeDict(received: string | any[] | Bytecode, expected: Record) { + const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received + const result = await run(bytecode) + const expectedValue = toValue(expected) + const pass = result.type === "dict" && isEqual(result, expectedValue) + + return { + pass, + message: () => + pass + ? `Expected value NOT to be dict ${formatValue(expectedValue)}, but it was` + : `Expected value to be dict ${formatValue(expectedValue)}, but received ${formatValue(result)}`, + } + }, + + async toBeFunction(received: string | any[] | Bytecode) { + const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received + const result = await run(bytecode) + const pass = result.type === "function" || result.type === "native" + + return { + pass, + message: () => + pass + ? `Expected value NOT to be a function, but it was` + : `Expected value to be a function, but received ${formatValue(result)}`, + } + }, + + async toBeTruthy(received: string | any[] | Bytecode) { + const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received + const result = await run(bytecode) + // ReefVM semantics: only null and false are falsy + const pass = !(result.type === "null" || (result.type === "boolean" && !result.value)) + + return { + pass, + message: () => + pass + ? `Expected value NOT to be truthy, but it was: ${formatValue(result)}` + : `Expected value to be truthy, but received ${formatValue(result)}`, + } + }, + + async toBeFalsy(received: string | any[] | Bytecode) { + const bytecode = typeof received === "string" || Array.isArray(received) ? toBytecode(received) : received + const result = await run(bytecode) + // ReefVM semantics: only null and false are falsy + const pass = result.type === "null" || (result.type === "boolean" && !result.value) + + return { + pass, + message: () => + pass + ? `Expected value NOT to be falsy, but it was: ${formatValue(result)}` + : `Expected value to be falsy, but received ${formatValue(result)}`, + } + }, +}) + +function formatValue(value: Value): string { + switch (value.type) { + case "null": + return "null" + case "boolean": + case "number": + return String(value.value) + case "string": + return `"${value.value}"` + case "array": + return `[${value.value.map(formatValue).join(", ")}]` + case "dict": { + const entries = Array.from(value.value.entries()) + .map(([k, v]) => `${k}: ${formatValue(v)}`) + .join(", ") + return `{${entries}}` + } + case "regex": + return String(value.value) + case "function": + case "native": + return "" + default: + return String(value) + } +}