dicts, too

This commit is contained in:
Chris Wanstrath 2025-10-29 15:28:34 -07:00
parent 956fd576f8
commit ba8376e2c3
4 changed files with 326 additions and 0 deletions

View File

@ -183,6 +183,7 @@ All arithmetic operations pop two values, perform operation, push result as numb
Performs different operations depending on operand types: Performs different operations depending on operand types:
- If either operand is a string, converts both to strings and concatenates - 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 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 - Otherwise, converts both to numbers and performs numeric addition
**Examples**: **Examples**:
@ -192,6 +193,8 @@ Performs different operations depending on operand types:
- `100 + " items"``"100 items"` (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)
- `[1, 2] + [3, 4]``[1, 2, 3, 4]` (array concatenation) - `[1, 2] + [3, 4]``[1, 2, 3, 4]` (array concatenation)
- `{a: 1} + {b: 2}``{a: 1, b: 2}` (dict merge)
- `{a: 1, b: 2} + {b: 99}``{a: 1, b: 99}` (dict merge, b overwrites)
#### SUB #### SUB
**Stack**: [a, b] → [a - b] **Stack**: [a, b] → [a - b]

158
examples/add-with-dicts.ts Normal file
View File

@ -0,0 +1,158 @@
/**
* Demonstrates the ADD opcode working with dicts
*
* ADD now handles dict merging:
* - {a: 1} + {b: 2} === {a: 1, b: 2}
* - If both operands are dicts, they are merged
* - Keys from the second dict overwrite keys from the first on conflict
*/
import { toBytecode, run } from "#reef"
// Basic dict merge
const basicMerge = toBytecode([
["PUSH", "a"],
["PUSH", 1],
["MAKE_DICT", 1],
["PUSH", "b"],
["PUSH", 2],
["MAKE_DICT", 1],
["ADD"],
["HALT"]
])
console.log('Basic dict merge ({a: 1} + {b: 2}):')
const result1 = await run(basicMerge)
console.log(result1)
// Output: { type: 'dict', value: Map { a: 1, b: 2 } }
// Merge with overlapping keys
const overlapMerge = toBytecode([
["PUSH", "a"],
["PUSH", 1],
["PUSH", "b"],
["PUSH", 2],
["MAKE_DICT", 2],
["PUSH", "b"],
["PUSH", 99],
["PUSH", "c"],
["PUSH", 3],
["MAKE_DICT", 2],
["ADD"],
["HALT"]
])
console.log('\nMerge with overlapping keys ({a: 1, b: 2} + {b: 99, c: 3}):')
const result2 = await run(overlapMerge)
console.log(result2)
console.log('Note: b is overwritten from 2 to 99')
// Output: { type: 'dict', value: Map { a: 1, b: 99, c: 3 } }
// Merge multiple dicts in sequence
const multipleMerge = toBytecode([
["PUSH", "a"],
["PUSH", 1],
["MAKE_DICT", 1],
["PUSH", "b"],
["PUSH", 2],
["MAKE_DICT", 1],
["ADD"],
["PUSH", "c"],
["PUSH", 3],
["MAKE_DICT", 1],
["ADD"],
["PUSH", "d"],
["PUSH", 4],
["MAKE_DICT", 1],
["ADD"],
["HALT"]
])
console.log('\nMultiple merges ({a: 1} + {b: 2} + {c: 3} + {d: 4}):')
const result3 = await run(multipleMerge)
console.log(result3)
// Output: { type: 'dict', value: Map { a: 1, b: 2, c: 3, d: 4 } }
// Merge dicts with different value types
const mixedTypes = toBytecode([
["PUSH", "num"],
["PUSH", 42],
["PUSH", "str"],
["PUSH", "hello"],
["MAKE_DICT", 2],
["PUSH", "bool"],
["PUSH", true],
["PUSH", "null"],
["PUSH", null],
["MAKE_DICT", 2],
["ADD"],
["HALT"]
])
console.log('\nMerge dicts with different types ({num: 42, str: "hello"} + {bool: true, null: null}):')
const result4 = await run(mixedTypes)
console.log(result4)
// Output: { type: 'dict', value: Map { num: 42, str: 'hello', bool: true, null: null } }
// Merge empty dict with non-empty
const emptyMerge = toBytecode([
["MAKE_DICT", 0],
["PUSH", "x"],
["PUSH", 100],
["PUSH", "y"],
["PUSH", 200],
["MAKE_DICT", 2],
["ADD"],
["HALT"]
])
console.log('\nMerge empty dict with {x: 100, y: 200} ({} + {x: 100, y: 200}):')
const result5 = await run(emptyMerge)
console.log(result5)
// Output: { type: 'dict', value: Map { x: 100, y: 200 } }
// Merge dicts with nested structures
const nestedMerge = toBytecode([
["PUSH", "data"],
["PUSH", 1],
["PUSH", 2],
["MAKE_ARRAY", 2],
["MAKE_DICT", 1],
["PUSH", "config"],
["PUSH", "debug"],
["PUSH", true],
["MAKE_DICT", 1],
["MAKE_DICT", 1],
["ADD"],
["HALT"]
])
console.log('\nMerge dicts with nested structures:')
const result6 = await run(nestedMerge)
console.log(result6)
// Output: { type: 'dict', value: Map { data: [1, 2], config: { debug: true } } }
// Building configuration objects
const configBuild = toBytecode([
// Default config
["PUSH", "debug"],
["PUSH", false],
["PUSH", "port"],
["PUSH", 3000],
["PUSH", "host"],
["PUSH", "localhost"],
["MAKE_DICT", 3],
// Override with user config
["PUSH", "debug"],
["PUSH", true],
["PUSH", "port"],
["PUSH", 8080],
["MAKE_DICT", 2],
["ADD"],
["HALT"]
])
console.log('\nBuilding config (defaults + overrides):')
const result7 = await run(configBuild)
console.log(result7)
// Output: { type: 'dict', value: Map { debug: true, port: 8080, host: 'localhost' } }

View File

@ -154,6 +154,13 @@ export class VM {
} else if (a.type === 'array' && b.type === 'array') { } else if (a.type === 'array' && b.type === 'array') {
// two arrays, concatenate them // two arrays, concatenate them
this.stack.push({ type: 'array', value: [...a.value, ...b.value] }) 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 { } else {
// Otherwise do numeric addition // Otherwise do numeric addition
this.stack.push(toValue(toNumber(a) + toNumber(b))) this.stack.push(toValue(toNumber(a) + toNumber(b)))

View File

@ -215,6 +215,164 @@ describe("ADD", () => {
expect(result.value[1]).toEqual({ type: 'number', value: 3 }) expect(result.value[1]).toEqual({ type: 'number', value: 3 })
} }
}) })
test("merge two dicts", async () => {
const bytecode = toBytecode([
["PUSH", "a"],
["PUSH", 1],
["MAKE_DICT", 1],
["PUSH", "b"],
["PUSH", 2],
["MAKE_DICT", 1],
["ADD"],
["HALT"]
])
const result = await run(bytecode)
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.size).toBe(2)
expect(result.value.get('a')).toEqual({ type: 'number', value: 1 })
expect(result.value.get('b')).toEqual({ type: 'number', value: 2 })
}
})
test("merge dicts with overlapping keys (second overwrites)", async () => {
const bytecode = toBytecode([
["PUSH", "a"],
["PUSH", 1],
["PUSH", "b"],
["PUSH", 2],
["MAKE_DICT", 2],
["PUSH", "b"],
["PUSH", 99],
["PUSH", "c"],
["PUSH", 3],
["MAKE_DICT", 2],
["ADD"],
["HALT"]
])
const result = await run(bytecode)
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.size).toBe(3)
expect(result.value.get('a')).toEqual({ type: 'number', value: 1 })
expect(result.value.get('b')).toEqual({ type: 'number', value: 99 }) // overwritten
expect(result.value.get('c')).toEqual({ type: 'number', value: 3 })
}
})
test("merge empty dicts", async () => {
const bytecode = toBytecode([
["MAKE_DICT", 0],
["PUSH", "x"],
["PUSH", 42],
["MAKE_DICT", 1],
["ADD"],
["HALT"]
])
const result = await run(bytecode)
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.size).toBe(1)
expect(result.value.get('x')).toEqual({ type: 'number', value: 42 })
}
})
test("merge multiple dicts in sequence", async () => {
const bytecode = toBytecode([
["PUSH", "a"],
["PUSH", 1],
["MAKE_DICT", 1],
["PUSH", "b"],
["PUSH", 2],
["MAKE_DICT", 1],
["ADD"],
["PUSH", "c"],
["PUSH", 3],
["MAKE_DICT", 1],
["ADD"],
["HALT"]
])
const result = await run(bytecode)
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.size).toBe(3)
expect(result.value.get('a')).toEqual({ type: 'number', value: 1 })
expect(result.value.get('b')).toEqual({ type: 'number', value: 2 })
expect(result.value.get('c')).toEqual({ type: 'number', value: 3 })
}
})
test("merge dicts with different value types", async () => {
const bytecode = toBytecode([
["PUSH", "num"],
["PUSH", 42],
["PUSH", "str"],
["PUSH", "hello"],
["MAKE_DICT", 2],
["PUSH", "bool"],
["PUSH", true],
["PUSH", "null"],
["PUSH", null],
["MAKE_DICT", 2],
["ADD"],
["HALT"]
])
const result = await run(bytecode)
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.size).toBe(4)
expect(result.value.get('num')).toEqual({ type: 'number', value: 42 })
expect(result.value.get('str')).toEqual({ type: 'string', value: 'hello' })
expect(result.value.get('bool')).toEqual({ type: 'boolean', value: true })
expect(result.value.get('null')).toEqual({ type: 'null', value: null })
}
})
test("merge dicts with nested structures", async () => {
const bytecode = toBytecode([
["PUSH", "a"],
["PUSH", 1],
["PUSH", 2],
["MAKE_ARRAY", 2],
["MAKE_DICT", 1],
["PUSH", "b"],
["PUSH", "x"],
["PUSH", 99],
["MAKE_DICT", 1],
["MAKE_DICT", 1],
["ADD"],
["HALT"]
])
const result = await run(bytecode)
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.size).toBe(2)
// a contains an array [1, 2]
const aValue = result.value.get('a')
expect(aValue?.type).toBe('array')
if (aValue?.type === 'array') {
expect(aValue.value.length).toBe(2)
expect(aValue.value[0]).toEqual({ type: 'number', value: 1 })
expect(aValue.value[1]).toEqual({ type: 'number', value: 2 })
}
// b contains a nested dict {x: 99}
const bValue = result.value.get('b')
expect(bValue?.type).toBe('dict')
if (bValue?.type === 'dict') {
expect(bValue.value.size).toBe(1)
expect(bValue.value.get('x')).toEqual({ type: 'number', value: 99 })
}
}
})
}) })
describe("SUB", () => { describe("SUB", () => {