diff --git a/SPEC.md b/SPEC.md index 1aa2118..6c870ed 100644 --- a/SPEC.md +++ b/SPEC.md @@ -179,7 +179,16 @@ All arithmetic operations pop two values, perform operation, push result as numb #### ADD **Stack**: [a, b] → [a + b] -**Note**: Only for numbers (use separate string concat if needed) + +Performs addition or string concatenation depending on operand types: +- If either operand is a string, converts both to strings and concatenates +- Otherwise, converts both to numbers and performs numeric addition + +**Examples**: +- `5 + 3` → `8` (numeric addition) +- `"hello" + " world"` → `"hello world"` (string concatenation) +- `"count: " + 42` → `"count: 42"` (string concatenation) +- `100 + " items"` → `"100 items"` (string concatenation) #### SUB **Stack**: [a, b] → [a - b] diff --git a/examples/add-with-strings.ts b/examples/add-with-strings.ts new file mode 100644 index 0000000..ff6d969 --- /dev/null +++ b/examples/add-with-strings.ts @@ -0,0 +1,73 @@ +/** + * Demonstrates the ADD opcode working with both numbers and strings + * + * ADD now behaves like JavaScript's + operator: + * - If either operand is a string, it does string concatenation + * - Otherwise, it does numeric addition + */ + +import { toBytecode, run } from "#reef" + +// Numeric addition +const numericAdd = toBytecode(` + PUSH 10 + PUSH 5 + ADD + HALT +`) + +console.log('Numeric addition (10 + 5):') +console.log(await run(numericAdd)) +// Output: { type: 'number', value: 15 } + +// String concatenation +const stringConcat = toBytecode(` + PUSH "hello" + PUSH " world" + ADD + HALT +`) + +console.log('\nString concatenation ("hello" + " world"):') +console.log(await run(stringConcat)) +// Output: { type: 'string', value: 'hello world' } + +// Mixed: string + number +const mixedConcat = toBytecode(` + PUSH "count: " + PUSH 42 + ADD + HALT +`) + +console.log('\nMixed concatenation ("count: " + 42):') +console.log(await run(mixedConcat)) +// Output: { type: 'string', value: 'count: 42' } + +// Building a message +const buildMessage = toBytecode(` + PUSH "You have " + PUSH 3 + ADD + PUSH " new messages" + ADD + HALT +`) + +console.log('\nBuilding a message:') +console.log(await run(buildMessage)) +// Output: { type: 'string', value: 'You have 3 new messages' } + +// Computing then concatenating +const computeAndConcat = toBytecode(` + PUSH "Result: " + PUSH 10 + PUSH 5 + ADD + ADD + HALT +`) + +console.log('\nComputing then concatenating ("Result: " + (10 + 5)):') +console.log(await run(computeAndConcat)) +// Output: { type: 'string', value: 'Result: 15' } diff --git a/src/vm.ts b/src/vm.ts index 30e1267..a92dab9 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -145,7 +145,16 @@ export class VM { break case OpCode.ADD: - this.binaryOp((a, b) => toNumber(a) + toNumber(b)) + 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 { + // Otherwise do numeric addition + this.stack.push(toValue(toNumber(a) + toNumber(b))) + } break case OpCode.SUB: diff --git a/tests/opcodes.test.ts b/tests/opcodes.test.ts index 0336752..ec4d4c8 100644 --- a/tests/opcodes.test.ts +++ b/tests/opcodes.test.ts @@ -18,6 +18,87 @@ describe("ADD", () => { ` expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 600 }) }) + + test("concatenate two strings", async () => { + const str = ` + PUSH "hello" + PUSH " world" + ADD + ` + expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'hello world' }) + + const str2 = ` + PUSH "foo" + PUSH "bar" + ADD + ` + expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'foobar' }) + }) + + test("concatenate string with number", async () => { + const str = ` + PUSH "count: " + PUSH 42 + ADD + ` + expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'count: 42' }) + + const str2 = ` + PUSH 100 + PUSH " items" + ADD + ` + expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: '100 items' }) + }) + + test("concatenate string with boolean", async () => { + const str = ` + PUSH "result: " + PUSH true + ADD + ` + expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'result: true' }) + + const str2 = ` + PUSH false + PUSH " value" + ADD + ` + expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'false value' }) + }) + + test("concatenate string with null", async () => { + const str = ` + PUSH "value: " + PUSH null + ADD + ` + expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'value: null' }) + }) + + test("concatenate multiple strings in sequence", async () => { + const str = ` + PUSH "hello" + PUSH " " + ADD + PUSH "world" + ADD + PUSH "!" + ADD + ` + expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'hello world!' }) + }) + + test("mixed arithmetic and string concatenation", async () => { + const str = ` + PUSH "Result: " + PUSH 10 + PUSH 5 + ADD + ADD + ` + expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'Result: 15' }) + }) }) describe("SUB", () => {