diff --git a/SPEC.md b/SPEC.md index 430314e..4262d6d 100644 --- a/SPEC.md +++ b/SPEC.md @@ -184,7 +184,8 @@ Performs different operations depending on operand types: - If either operand is a string, converts both to strings and concatenates - Else if both operands are arrays, concatenates the arrays - Else if both operands are dicts, merges them (b's keys overwrite a's keys on conflict) -- Otherwise, converts both to numbers and performs numeric addition +- Else if both operands are numbers, performs numeric addition +- Otherwise, throws an error **Examples**: - `5 + 3` → `8` (numeric addition) @@ -196,6 +197,12 @@ Performs different operations depending on operand types: - `{a: 1} + {b: 2}` → `{a: 1, b: 2}` (dict merge) - `{a: 1, b: 2} + {b: 99}` → `{a: 1, b: 99}` (dict merge, b overwrites) +**Invalid operations** (throw errors): +- `true + false` → Error +- `null + 5` → Error +- `[1] + 5` → Error +- `{a: 1} + 5` → Error + #### SUB **Stack**: [a, b] → [a - b] diff --git a/src/vm.ts b/src/vm.ts index a5f5e76..d55eb8c 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -148,22 +148,20 @@ export class VM { const b = this.stack.pop()! const a = this.stack.pop()! - // If either operand is a string, do string concatenation if (a.type === 'string' || b.type === 'string') { this.stack.push(toValue(toString(a) + toString(b))) } else if (a.type === 'array' && b.type === 'array') { - // two arrays, concatenate them this.stack.push({ type: 'array', value: [...a.value, ...b.value] }) } else if (a.type === 'dict' && b.type === 'dict') { - // two dicts, merge them (b's keys overwrite a's keys) const merged = new Map(a.value) for (const [key, value] of b.value) { merged.set(key, value) } this.stack.push({ type: 'dict', value: merged }) + } else if (a.type === 'number' && b.type === 'number') { + this.stack.push(toValue(a.value + b.value)) } else { - // Otherwise do numeric addition - this.stack.push(toValue(toNumber(a) + toNumber(b))) + throw new Error(`ADD: Cannot add ${a.type} and ${b.type}`) } break diff --git a/tests/functions.test.ts b/tests/functions.test.ts index fc35e78..5a8bbf4 100644 --- a/tests/functions.test.ts +++ b/tests/functions.test.ts @@ -487,7 +487,7 @@ test("TRY_CALL - handles null values", async () => { test("TRY_CALL - function can access its parameters", async () => { const bytecode = toBytecode([ - ["MAKE_FUNCTION", ["x"], ".body"], + ["MAKE_FUNCTION", ["x=0"], ".body"], ["STORE", "addFive"], ["PUSH", 10], ["STORE", "x"], @@ -501,8 +501,8 @@ test("TRY_CALL - function can access its parameters", async () => { ]) const result = await run(bytecode) - // Function is called with 0 args, so x inside function should be null - // Then we add 5 to null (which coerces to 0) + // Function is called with 0 args, so x defaults to 0 + // Then we add 5 to 0 expect(result).toEqual({ type: 'number', value: 5 }) }) diff --git a/tests/opcodes.test.ts b/tests/opcodes.test.ts index 7cc1322..394771f 100644 --- a/tests/opcodes.test.ts +++ b/tests/opcodes.test.ts @@ -373,6 +373,87 @@ describe("ADD", () => { } } }) + + test("cannot add boolean + boolean", async () => { + const bytecode = toBytecode([ + ["PUSH", true], + ["PUSH", false], + ["ADD"], + ["HALT"] + ]) + + await expect(run(bytecode)).rejects.toThrow('ADD: Cannot add boolean and boolean') + }) + + test("cannot add null + number", async () => { + const bytecode = toBytecode([ + ["PUSH", null], + ["PUSH", 5], + ["ADD"], + ["HALT"] + ]) + + await expect(run(bytecode)).rejects.toThrow('ADD: Cannot add null and number') + }) + + test("cannot add array + dict", async () => { + const bytecode = toBytecode([ + ["PUSH", 1], + ["MAKE_ARRAY", 1], + ["MAKE_DICT", 0], + ["ADD"], + ["HALT"] + ]) + + await expect(run(bytecode)).rejects.toThrow('ADD: Cannot add array and dict') + }) + + test("cannot add array + number", async () => { + const bytecode = toBytecode([ + ["PUSH", 1], + ["MAKE_ARRAY", 1], + ["PUSH", 5], + ["ADD"], + ["HALT"] + ]) + + await expect(run(bytecode)).rejects.toThrow('ADD: Cannot add array and number') + }) + + test("cannot add dict + number", async () => { + const bytecode = toBytecode([ + ["MAKE_DICT", 0], + ["PUSH", 5], + ["ADD"], + ["HALT"] + ]) + + await expect(run(bytecode)).rejects.toThrow('ADD: Cannot add dict and number') + }) + + test("cannot add function + number", async () => { + const bytecode = toBytecode([ + ["MAKE_FUNCTION", [], ".body"], + ["PUSH", 5], + ["ADD"], + ["HALT"], + [".body:"], + ["RETURN"] + ]) + + await expect(run(bytecode)).rejects.toThrow('ADD: Cannot add function and number') + }) + + test("cannot add boolean + null", async () => { + const bytecode = toBytecode([ + ["PUSH", true], + ["PUSH", null], + ["ADD"], + ["HALT"] + ]) + + await expect(run(bytecode)).rejects.toThrow('ADD: Cannot add boolean and null') + }) }) describe("SUB", () => {