From 956fd576f8ddb7642b75f3aafd54094c81f776b9 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 29 Oct 2025 15:25:37 -0700 Subject: [PATCH] ADD concats arrays too --- SPEC.md | 5 +- examples/add-with-arrays.ts | 116 ++++++++++++++++++++++++++++++++++++ src/vm.ts | 3 + tests/opcodes.test.ts | 116 ++++++++++++++++++++++++++++++++++++ 4 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 examples/add-with-arrays.ts diff --git a/SPEC.md b/SPEC.md index 6c870ed..26e70ef 100644 --- a/SPEC.md +++ b/SPEC.md @@ -180,8 +180,9 @@ All arithmetic operations pop two values, perform operation, push result as numb #### ADD **Stack**: [a, b] → [a + b] -Performs addition or string concatenation depending on operand types: +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 - Otherwise, converts both to numbers and performs numeric addition **Examples**: @@ -189,6 +190,8 @@ Performs addition or string concatenation depending on operand types: - `"hello" + " world"` → `"hello world"` (string concatenation) - `"count: " + 42` → `"count: 42"` (string concatenation) - `100 + " items"` → `"100 items"` (string concatenation) +- `[1, 2, 3] + [4]` → `[1, 2, 3, 4]` (array concatenation) +- `[1, 2] + [3, 4]` → `[1, 2, 3, 4]` (array concatenation) #### SUB **Stack**: [a, b] → [a - b] diff --git a/examples/add-with-arrays.ts b/examples/add-with-arrays.ts new file mode 100644 index 0000000..7ec2683 --- /dev/null +++ b/examples/add-with-arrays.ts @@ -0,0 +1,116 @@ +/** + * Demonstrates the ADD opcode working with arrays + * + * ADD now handles array concatenation: + * - [1, 2, 3] + [4] === [1, 2, 3, 4] + * - If both operands are arrays, they are concatenated + */ + +import { toBytecode, run } from "#reef" + +// Basic array concatenation +const basicConcat = toBytecode([ + ["PUSH", 1], + ["PUSH", 2], + ["PUSH", 3], + ["MAKE_ARRAY", 3], + ["PUSH", 4], + ["MAKE_ARRAY", 1], + ["ADD"], + ["HALT"] +]) + +console.log('Basic array concatenation ([1, 2, 3] + [4]):') +const result1 = await run(basicConcat) +console.log(result1) +// Output: { type: 'array', value: [1, 2, 3, 4] } + +// Concatenate two multi-element arrays +const multiConcat = toBytecode([ + ["PUSH", 1], + ["PUSH", 2], + ["MAKE_ARRAY", 2], + ["PUSH", 3], + ["PUSH", 4], + ["MAKE_ARRAY", 2], + ["ADD"], + ["HALT"] +]) + +console.log('\nConcatenate two arrays ([1, 2] + [3, 4]):') +const result2 = await run(multiConcat) +console.log(result2) +// Output: { type: 'array', value: [1, 2, 3, 4] } + +// Concatenate multiple arrays in sequence +const multipleConcat = toBytecode([ + ["PUSH", 1], + ["MAKE_ARRAY", 1], + ["PUSH", 2], + ["MAKE_ARRAY", 1], + ["ADD"], + ["PUSH", 3], + ["MAKE_ARRAY", 1], + ["ADD"], + ["PUSH", 4], + ["MAKE_ARRAY", 1], + ["ADD"], + ["HALT"] +]) + +console.log('\nMultiple concatenations ([1] + [2] + [3] + [4]):') +const result3 = await run(multipleConcat) +console.log(result3) +// Output: { type: 'array', value: [1, 2, 3, 4] } + +// Concatenate arrays with mixed types +const mixedTypes = toBytecode([ + ["PUSH", 1], + ["PUSH", "hello"], + ["MAKE_ARRAY", 2], + ["PUSH", true], + ["PUSH", null], + ["MAKE_ARRAY", 2], + ["ADD"], + ["HALT"] +]) + +console.log('\nConcatenate arrays with mixed types ([1, "hello"] + [true, null]):') +const result4 = await run(mixedTypes) +console.log(result4) +// Output: { type: 'array', value: [1, "hello", true, null] } + +// Concatenate empty array with non-empty +const emptyConcat = toBytecode([ + ["MAKE_ARRAY", 0], + ["PUSH", 1], + ["PUSH", 2], + ["PUSH", 3], + ["MAKE_ARRAY", 3], + ["ADD"], + ["HALT"] +]) + +console.log('\nConcatenate empty array with [1, 2, 3] ([] + [1, 2, 3]):') +const result5 = await run(emptyConcat) +console.log(result5) +// Output: { type: 'array', value: [1, 2, 3] } + +// Nested arrays +const nestedConcat = toBytecode([ + ["PUSH", 1], + ["PUSH", 2], + ["MAKE_ARRAY", 2], + ["MAKE_ARRAY", 1], + ["PUSH", 3], + ["PUSH", 4], + ["MAKE_ARRAY", 2], + ["MAKE_ARRAY", 1], + ["ADD"], + ["HALT"] +]) + +console.log('\nConcatenate nested arrays ([[1, 2]] + [[3, 4]]):') +const result6 = await run(nestedConcat) +console.log(result6) +// Output: { type: 'array', value: [[1, 2], [3, 4]] } diff --git a/src/vm.ts b/src/vm.ts index a92dab9..30fa3a9 100644 --- a/src/vm.ts +++ b/src/vm.ts @@ -151,6 +151,9 @@ export class VM { // 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 { // Otherwise do numeric addition this.stack.push(toValue(toNumber(a) + toNumber(b))) diff --git a/tests/opcodes.test.ts b/tests/opcodes.test.ts index ec4d4c8..a66b698 100644 --- a/tests/opcodes.test.ts +++ b/tests/opcodes.test.ts @@ -99,6 +99,122 @@ describe("ADD", () => { ` expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'Result: 15' }) }) + + test("concatenate two arrays", async () => { + const bytecode = toBytecode([ + ["PUSH", 1], + ["PUSH", 2], + ["PUSH", 3], + ["MAKE_ARRAY", 3], + ["PUSH", 4], + ["MAKE_ARRAY", 1], + ["ADD"], + ["HALT"] + ]) + + const result = await run(bytecode) + expect(result.type).toBe('array') + if (result.type === 'array') { + expect(result.value.length).toBe(4) + expect(result.value[0]).toEqual({ type: 'number', value: 1 }) + expect(result.value[1]).toEqual({ type: 'number', value: 2 }) + expect(result.value[2]).toEqual({ type: 'number', value: 3 }) + expect(result.value[3]).toEqual({ type: 'number', value: 4 }) + } + }) + + test("concatenate empty arrays", async () => { + const bytecode = toBytecode([ + ["MAKE_ARRAY", 0], + ["PUSH", 1], + ["PUSH", 2], + ["MAKE_ARRAY", 2], + ["ADD"], + ["HALT"] + ]) + + const result = await run(bytecode) + expect(result.type).toBe('array') + if (result.type === 'array') { + expect(result.value.length).toBe(2) + expect(result.value[0]).toEqual({ type: 'number', value: 1 }) + expect(result.value[1]).toEqual({ type: 'number', value: 2 }) + } + }) + + test("concatenate multiple arrays in sequence", async () => { + const bytecode = toBytecode([ + ["PUSH", 1], + ["MAKE_ARRAY", 1], + ["PUSH", 2], + ["MAKE_ARRAY", 1], + ["ADD"], + ["PUSH", 3], + ["MAKE_ARRAY", 1], + ["ADD"], + ["HALT"] + ]) + + const result = await run(bytecode) + expect(result.type).toBe('array') + if (result.type === 'array') { + expect(result.value.length).toBe(3) + expect(result.value[0]).toEqual({ type: 'number', value: 1 }) + expect(result.value[1]).toEqual({ type: 'number', value: 2 }) + expect(result.value[2]).toEqual({ type: 'number', value: 3 }) + } + }) + + test("concatenate arrays with different types", async () => { + const bytecode = toBytecode([ + ["PUSH", 1], + ["PUSH", "hello"], + ["MAKE_ARRAY", 2], + ["PUSH", true], + ["PUSH", null], + ["MAKE_ARRAY", 2], + ["ADD"], + ["HALT"] + ]) + + const result = await run(bytecode) + expect(result.type).toBe('array') + if (result.type === 'array') { + expect(result.value.length).toBe(4) + expect(result.value[0]).toEqual({ type: 'number', value: 1 }) + expect(result.value[1]).toEqual({ type: 'string', value: 'hello' }) + expect(result.value[2]).toEqual({ type: 'boolean', value: true }) + expect(result.value[3]).toEqual({ type: 'null', value: null }) + } + }) + + test("concatenate arrays containing nested arrays", async () => { + const bytecode = toBytecode([ + ["PUSH", 1], + ["PUSH", 2], + ["MAKE_ARRAY", 2], + ["MAKE_ARRAY", 1], + ["PUSH", 3], + ["MAKE_ARRAY", 1], + ["ADD"], + ["HALT"] + ]) + + const result = await run(bytecode) + expect(result.type).toBe('array') + if (result.type === 'array') { + expect(result.value.length).toBe(2) + // First element is nested array [1, 2] + expect(result.value[0]?.type).toBe('array') + if (result.value[0]?.type === 'array') { + expect(result.value[0].value.length).toBe(2) + expect(result.value[0].value[0]).toEqual({ type: 'number', value: 1 }) + expect(result.value[0].value[1]).toEqual({ type: 'number', value: 2 }) + } + // Second element is 3 + expect(result.value[1]).toEqual({ type: 'number', value: 3 }) + } + }) }) describe("SUB", () => {