forked from defunkt/ReefVM
1937 lines
42 KiB
TypeScript
1937 lines
42 KiB
TypeScript
import { test, expect, describe } from "bun:test"
|
|
import { run } from "#index"
|
|
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 })
|
|
|
|
const str2 = `
|
|
PUSH 100
|
|
PUSH 500
|
|
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' })
|
|
})
|
|
|
|
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 })
|
|
}
|
|
})
|
|
|
|
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 })
|
|
}
|
|
}
|
|
})
|
|
|
|
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", () => {
|
|
test("subtract two numbers", async () => {
|
|
const str = `
|
|
PUSH 5
|
|
PUSH 2
|
|
SUB
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
|
|
})
|
|
})
|
|
|
|
describe("MUL", () => {
|
|
test("multiply two numbers", async () => {
|
|
const str = `
|
|
PUSH 5
|
|
PUSH 2
|
|
MUL
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
|
|
})
|
|
})
|
|
|
|
describe("DIV", () => {
|
|
test("divide two numbers", async () => {
|
|
const str = `
|
|
PUSH 10
|
|
PUSH 2
|
|
DIV
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 5 })
|
|
|
|
const str2 = `
|
|
PUSH 10
|
|
PUSH 0
|
|
DIV
|
|
`
|
|
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: Infinity })
|
|
})
|
|
})
|
|
|
|
describe("MOD", () => {
|
|
test("modulo two numbers", async () => {
|
|
const str = `
|
|
PUSH 17
|
|
PUSH 5
|
|
MOD
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
|
|
})
|
|
})
|
|
|
|
describe("PUSH", () => {
|
|
test("pushes value onto stack", async () => {
|
|
const str = `
|
|
PUSH 42
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
|
})
|
|
})
|
|
|
|
describe("POP", () => {
|
|
test("removes top value", async () => {
|
|
const str = `
|
|
PUSH 10
|
|
PUSH 20
|
|
POP
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
|
|
})
|
|
})
|
|
|
|
describe("DUP", () => {
|
|
test("duplicates top value", async () => {
|
|
const str = `
|
|
PUSH 5
|
|
DUP
|
|
ADD
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
|
|
})
|
|
})
|
|
|
|
describe("SWAP", () => {
|
|
test("swaps two numbers", async () => {
|
|
const str = `
|
|
PUSH 10
|
|
PUSH 20
|
|
SWAP
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
|
|
})
|
|
|
|
test("swap and use in subtraction", async () => {
|
|
const str = `
|
|
PUSH 5
|
|
PUSH 10
|
|
SWAP
|
|
SUB
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 5 })
|
|
})
|
|
|
|
test("swap different types", async () => {
|
|
const str = `
|
|
PUSH "hello"
|
|
PUSH 42
|
|
SWAP
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'hello' })
|
|
})
|
|
})
|
|
|
|
describe("EQ", () => {
|
|
test("equality comparison", async () => {
|
|
const str = `
|
|
PUSH 5
|
|
PUSH 5
|
|
EQ
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
|
|
|
const str2 = `
|
|
PUSH 5
|
|
PUSH 10
|
|
EQ
|
|
`
|
|
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
|
})
|
|
|
|
test('equality with regexes', async () => {
|
|
const str = `
|
|
PUSH /cool/i
|
|
PUSH /cool/i
|
|
EQ
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
|
|
|
const str2 = `
|
|
PUSH /cool/
|
|
PUSH /cool/i
|
|
EQ
|
|
`
|
|
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: false })
|
|
|
|
const str3 = `
|
|
PUSH /not-cool/
|
|
PUSH /cool/
|
|
EQ
|
|
`
|
|
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false })
|
|
})
|
|
})
|
|
|
|
describe("NEQ", () => {
|
|
test("not equal comparison", async () => {
|
|
const str = `
|
|
PUSH 5
|
|
PUSH 10
|
|
NEQ
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
|
})
|
|
})
|
|
|
|
describe("LT", () => {
|
|
test("less than", async () => {
|
|
const str = `
|
|
PUSH 5
|
|
PUSH 10
|
|
LT
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
|
})
|
|
})
|
|
|
|
describe("GT", () => {
|
|
test("greater than", async () => {
|
|
const str = `
|
|
PUSH 10
|
|
PUSH 5
|
|
GT
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
|
})
|
|
})
|
|
|
|
describe("LTE", () => {
|
|
test("less than or equal", async () => {
|
|
// equal case
|
|
const str = `
|
|
PUSH 5
|
|
PUSH 5
|
|
LTE
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
|
|
|
// less than case
|
|
const str2 = `
|
|
PUSH 3
|
|
PUSH 5
|
|
LTE
|
|
`
|
|
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true })
|
|
|
|
// greater than case (false)
|
|
const str3 = `
|
|
PUSH 10
|
|
PUSH 5
|
|
LTE
|
|
`
|
|
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false })
|
|
})
|
|
})
|
|
|
|
describe("GTE", () => {
|
|
test("greater than or equal", async () => {
|
|
// equal case
|
|
const str = `
|
|
PUSH 5
|
|
PUSH 5
|
|
GTE
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
|
|
|
// greater than case
|
|
const str2 = `
|
|
PUSH 10
|
|
PUSH 5
|
|
GTE
|
|
`
|
|
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true })
|
|
|
|
// less than case (false)
|
|
const str3 = `
|
|
PUSH 3
|
|
PUSH 5
|
|
GTE
|
|
`
|
|
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: false })
|
|
})
|
|
})
|
|
|
|
describe("Logical patterns", () => {
|
|
test("AND pattern - short circuits when false", async () => {
|
|
// false && <anything> should short-circuit and return false
|
|
const str = `
|
|
PUSH 1
|
|
PUSH 0
|
|
EQ
|
|
DUP
|
|
JUMP_IF_FALSE .end
|
|
POP
|
|
PUSH 999
|
|
.end:
|
|
`
|
|
const result = await run(toBytecode(str))
|
|
expect(result.type).toBe('boolean')
|
|
if (result.type === 'boolean') {
|
|
expect(result.value).toBe(false)
|
|
}
|
|
})
|
|
|
|
test("AND pattern - evaluates both when true", async () => {
|
|
const str = `
|
|
PUSH 1
|
|
DUP
|
|
JUMP_IF_FALSE .end
|
|
POP
|
|
PUSH 2
|
|
.end:
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
|
|
})
|
|
|
|
test("OR pattern - short circuits when true", async () => {
|
|
const str = `
|
|
PUSH 1
|
|
DUP
|
|
JUMP_IF_TRUE .end
|
|
POP
|
|
PUSH 2
|
|
.end:
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 1 })
|
|
})
|
|
|
|
test("OR pattern - evaluates second when false", async () => {
|
|
const str = `
|
|
PUSH 1
|
|
PUSH 0
|
|
EQ
|
|
DUP
|
|
JUMP_IF_TRUE .end
|
|
POP
|
|
PUSH 2
|
|
.end:
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
|
|
})
|
|
})
|
|
|
|
describe("NOT", () => {
|
|
test("logical not", async () => {
|
|
// number is truthy, so NOT returns false
|
|
const str = `
|
|
PUSH 1
|
|
NOT
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: 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 })
|
|
|
|
// boolean false is falsy, so NOT returns true
|
|
const str3 = `
|
|
PUSH 1
|
|
PUSH 0
|
|
EQ
|
|
NOT
|
|
`
|
|
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true })
|
|
})
|
|
})
|
|
|
|
describe("Truthiness", () => {
|
|
test("only null and false are falsy", async () => {
|
|
// 0 is truthy (unlike JS)
|
|
const str1 = `
|
|
PUSH 0
|
|
JUMP_IF_FALSE .end
|
|
PUSH 1
|
|
.end:
|
|
`
|
|
expect(await run(toBytecode(str1))).toEqual({ type: 'number', value: 1 })
|
|
|
|
// empty string is truthy (unlike JS)
|
|
const str2 = `
|
|
PUSH ''
|
|
JUMP_IF_FALSE .end
|
|
PUSH 1
|
|
.end:
|
|
`
|
|
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 1 })
|
|
|
|
// false is falsy
|
|
const str3 = `
|
|
PUSH 0
|
|
PUSH 0
|
|
EQ
|
|
JUMP_IF_FALSE .end
|
|
PUSH 999
|
|
.end:
|
|
`
|
|
expect(await run(toBytecode(str3))).toEqual({ type: 'number', value: 999 })
|
|
})
|
|
})
|
|
|
|
describe("HALT", () => {
|
|
test("stops execution", async () => {
|
|
const str = `
|
|
PUSH 42
|
|
HALT
|
|
PUSH 100
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
|
})
|
|
})
|
|
|
|
describe("LOAD / STORE", () => {
|
|
test("variables", async () => {
|
|
const str = `
|
|
PUSH 42
|
|
STORE x
|
|
PUSH 21
|
|
LOAD x
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
|
})
|
|
|
|
test("multiple variables", async () => {
|
|
const str = `
|
|
PUSH 10
|
|
STORE a
|
|
PUSH 20
|
|
STORE b
|
|
PUSH 44
|
|
LOAD a
|
|
LOAD b
|
|
ADD
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 30 })
|
|
})
|
|
})
|
|
|
|
describe("TRY_LOAD", () => {
|
|
test("variable found", async () => {
|
|
const str = `
|
|
PUSH 100
|
|
STORE count
|
|
TRY_LOAD count
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 })
|
|
|
|
const str2 = `
|
|
PUSH 'Bobby'
|
|
STORE name
|
|
TRY_LOAD name
|
|
`
|
|
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'Bobby' })
|
|
})
|
|
|
|
test("variable missing", async () => {
|
|
const str = `
|
|
PUSH 100
|
|
STORE count
|
|
TRY_LOAD count1
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'count1' })
|
|
|
|
const str2 = `
|
|
PUSH 'Bobby'
|
|
STORE name
|
|
TRY_LOAD full-name
|
|
`
|
|
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: 'full-name' })
|
|
})
|
|
|
|
test("with different value types", async () => {
|
|
// Array
|
|
const str1 = `
|
|
PUSH 1
|
|
PUSH 2
|
|
PUSH 3
|
|
MAKE_ARRAY #3
|
|
STORE arr
|
|
TRY_LOAD arr
|
|
`
|
|
const result1 = await run(toBytecode(str1))
|
|
expect(result1.type).toBe('array')
|
|
|
|
// Dict
|
|
const str2 = `
|
|
PUSH 'key'
|
|
PUSH 'value'
|
|
MAKE_DICT #1
|
|
STORE dict
|
|
TRY_LOAD dict
|
|
`
|
|
const result2 = await run(toBytecode(str2))
|
|
expect(result2.type).toBe('dict')
|
|
|
|
// Boolean
|
|
const str3 = `
|
|
PUSH true
|
|
STORE flag
|
|
TRY_LOAD flag
|
|
`
|
|
expect(await run(toBytecode(str3))).toEqual({ type: 'boolean', value: true })
|
|
|
|
// Null
|
|
const str4 = `
|
|
PUSH null
|
|
STORE empty
|
|
TRY_LOAD empty
|
|
`
|
|
expect(await run(toBytecode(str4))).toEqual({ type: 'null', value: null })
|
|
})
|
|
|
|
test("in nested scope", async () => {
|
|
// Function should be able to TRY_LOAD variable from parent scope
|
|
const str = `
|
|
PUSH 42
|
|
STORE outer
|
|
MAKE_FUNCTION () .fn
|
|
PUSH 0
|
|
PUSH 0
|
|
CALL
|
|
HALT
|
|
.fn:
|
|
TRY_LOAD outer
|
|
RETURN
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
|
})
|
|
|
|
test("missing variable in nested scope returns name", async () => {
|
|
// If variable doesn't exist in any scope, should return name as string
|
|
const str = `
|
|
PUSH 42
|
|
STORE outer
|
|
MAKE_FUNCTION () .fn
|
|
PUSH 0
|
|
PUSH 0
|
|
CALL
|
|
HALT
|
|
.fn:
|
|
TRY_LOAD inner
|
|
RETURN
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'inner' })
|
|
})
|
|
|
|
test("used for conditional variable existence check", async () => {
|
|
// Pattern: use TRY_LOAD to check if variable exists and get its value or name
|
|
const str = `
|
|
PUSH 100
|
|
STORE count
|
|
TRY_LOAD count
|
|
PUSH 'count'
|
|
EQ
|
|
`
|
|
// Variable exists, so TRY_LOAD returns 100, which != 'count'
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
|
|
|
|
const str2 = `
|
|
PUSH 100
|
|
STORE count
|
|
TRY_LOAD missing
|
|
PUSH 'missing'
|
|
EQ
|
|
`
|
|
// Variable missing, so TRY_LOAD returns 'missing', which == 'missing'
|
|
expect(await run(toBytecode(str2))).toEqual({ type: 'boolean', value: true })
|
|
})
|
|
|
|
test("with function value", async () => {
|
|
const str = `
|
|
MAKE_FUNCTION () .fn
|
|
STORE myFunc
|
|
JUMP .skip
|
|
.fn:
|
|
PUSH 99
|
|
RETURN
|
|
.skip:
|
|
TRY_LOAD myFunc
|
|
`
|
|
const result = await run(toBytecode(str))
|
|
expect(result.type).toBe('function')
|
|
})
|
|
})
|
|
|
|
describe("JUMP", () => {
|
|
test("relative jump forward", async () => {
|
|
const str = `
|
|
PUSH 1
|
|
JUMP .skip
|
|
PUSH 100
|
|
.skip:
|
|
PUSH 2
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
|
|
})
|
|
|
|
test("forward jump skips instructions", async () => {
|
|
// Use forward jump to skip, demonstrating relative addressing
|
|
const str = `
|
|
PUSH 100
|
|
JUMP .end
|
|
PUSH 200
|
|
PUSH 300
|
|
.end:
|
|
PUSH 400
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 400 })
|
|
})
|
|
|
|
test("backward - simple loop", async () => {
|
|
// Very simple: counter starts at 0, loops 3 times incrementing
|
|
// On 3rd iteration (counter==3), exits and returns counter
|
|
const bytecode = toBytecode(`
|
|
PUSH 0
|
|
STORE counter
|
|
.loop:
|
|
LOAD counter
|
|
PUSH 3
|
|
EQ
|
|
JUMP_IF_FALSE .body
|
|
LOAD counter
|
|
HALT
|
|
.body:
|
|
LOAD counter
|
|
PUSH 1
|
|
ADD
|
|
STORE counter
|
|
JUMP .loop
|
|
`)
|
|
|
|
const result = await run(bytecode)
|
|
expect(result).toEqual({ type: 'number', value: 3 })
|
|
})
|
|
})
|
|
|
|
describe("JUMP_IF_FALSE", () => {
|
|
test("conditional jump when false", async () => {
|
|
const str = `
|
|
PUSH 1
|
|
PUSH 0
|
|
EQ
|
|
JUMP_IF_FALSE .skip
|
|
PUSH 100
|
|
.skip:
|
|
PUSH 42
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
|
})
|
|
|
|
test("no jump when true", async () => {
|
|
const str = `
|
|
PUSH 1
|
|
JUMP_IF_FALSE .skip
|
|
PUSH 100
|
|
.skip:
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 })
|
|
})
|
|
})
|
|
|
|
describe("JUMP_IF_TRUE", () => {
|
|
test("conditional jump when true", async () => {
|
|
const str = `
|
|
PUSH 1
|
|
JUMP_IF_TRUE .skip
|
|
PUSH 100
|
|
.skip:
|
|
PUSH 42
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
|
|
})
|
|
})
|
|
|
|
describe("MAKE_ARRAY", () => {
|
|
test("creates array", async () => {
|
|
const str = `
|
|
PUSH 10
|
|
PUSH 20
|
|
PUSH 30
|
|
MAKE_ARRAY #3
|
|
`
|
|
const result = await run(toBytecode(str))
|
|
expect(result.type).toBe('array')
|
|
if (result.type === 'array') {
|
|
expect(result.value).toHaveLength(3)
|
|
expect(result.value[0]).toEqual({ type: 'number', value: 10 })
|
|
expect(result.value[1]).toEqual({ type: 'number', value: 20 })
|
|
expect(result.value[2]).toEqual({ type: 'number', value: 30 })
|
|
}
|
|
})
|
|
})
|
|
|
|
describe("ARRAY_GET", () => {
|
|
test("gets element", async () => {
|
|
const str = `
|
|
PUSH 10
|
|
PUSH 20
|
|
PUSH 30
|
|
MAKE_ARRAY #3
|
|
PUSH 1
|
|
ARRAY_GET
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 20 })
|
|
})
|
|
})
|
|
|
|
describe("ARRAY_SET", () => {
|
|
test("sets element", async () => {
|
|
const str = `
|
|
PUSH 10
|
|
PUSH 20
|
|
PUSH 30
|
|
MAKE_ARRAY #3
|
|
DUP
|
|
PUSH 1
|
|
PUSH 99
|
|
ARRAY_SET
|
|
PUSH 1
|
|
ARRAY_GET
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 99 })
|
|
})
|
|
})
|
|
|
|
describe("ARRAY_PUSH", () => {
|
|
test("appends to array", async () => {
|
|
const str = `
|
|
PUSH 10
|
|
PUSH 20
|
|
MAKE_ARRAY #2
|
|
DUP
|
|
PUSH 30
|
|
ARRAY_PUSH
|
|
ARRAY_LEN
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
|
|
})
|
|
|
|
test("mutates original array", async () => {
|
|
const str = `
|
|
PUSH 10
|
|
PUSH 20
|
|
MAKE_ARRAY #2
|
|
DUP
|
|
PUSH 30
|
|
ARRAY_PUSH
|
|
PUSH 2
|
|
ARRAY_GET
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 30 })
|
|
})
|
|
})
|
|
|
|
describe("ARRAY_LEN", () => {
|
|
test("gets length", async () => {
|
|
const str = `
|
|
PUSH 10
|
|
PUSH 20
|
|
PUSH 30
|
|
MAKE_ARRAY #3
|
|
ARRAY_LEN
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
|
|
})
|
|
})
|
|
|
|
describe("MAKE_DICT", () => {
|
|
test("creates dict", async () => {
|
|
const str = `
|
|
PUSH 'name'
|
|
PUSH 'Alice'
|
|
PUSH 'age'
|
|
PUSH 30
|
|
MAKE_DICT #2
|
|
`
|
|
const result = await run(toBytecode(str))
|
|
expect(result.type).toBe('dict')
|
|
if (result.type === 'dict') {
|
|
expect(result.value.size).toBe(2)
|
|
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Alice' })
|
|
expect(result.value.get('age')).toEqual({ type: 'number', value: 30 })
|
|
}
|
|
})
|
|
})
|
|
|
|
describe("DICT_GET", () => {
|
|
test("gets value", async () => {
|
|
const str = `
|
|
PUSH 'name'
|
|
PUSH 'Bob'
|
|
MAKE_DICT #1
|
|
PUSH 'name'
|
|
DICT_GET
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'Bob' })
|
|
})
|
|
})
|
|
|
|
describe("DICT_SET", () => {
|
|
test("sets value", async () => {
|
|
const str = `
|
|
MAKE_DICT #0
|
|
DUP
|
|
PUSH 'key'
|
|
PUSH 'value'
|
|
DICT_SET
|
|
PUSH 'key'
|
|
DICT_GET
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: 'value' })
|
|
})
|
|
})
|
|
|
|
describe("DICT_HAS", () => {
|
|
test("checks key exists", async () => {
|
|
const str = `
|
|
PUSH 'key'
|
|
PUSH 'value'
|
|
MAKE_DICT #1
|
|
PUSH 'key'
|
|
DICT_HAS
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
|
|
})
|
|
|
|
test("checks key missing", async () => {
|
|
const str = `
|
|
MAKE_DICT #0
|
|
PUSH 'missing'
|
|
DICT_HAS
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
|
|
})
|
|
})
|
|
|
|
describe("DOT_GET", () => {
|
|
test("gets an ARRAY index", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 100
|
|
PUSH 200
|
|
PUSH 300
|
|
PUSH 400
|
|
MAKE_ARRAY #4
|
|
PUSH 2
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'number', value: 300 })
|
|
})
|
|
|
|
test("gets a DICT value", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 'name'
|
|
PUSH 'Bob'
|
|
PUSH 'age'
|
|
PUSH 106
|
|
MAKE_DICT #2
|
|
PUSH 'name'
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'string', value: 'Bob' })
|
|
})
|
|
|
|
test("fails to work on NUMBER", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 500
|
|
PUSH 0
|
|
DOT_GET
|
|
`)
|
|
|
|
try {
|
|
await run(bytecode)
|
|
expect(true).toBe(false) // Should not reach here
|
|
} catch (e: any) {
|
|
expect(e.message).toContain('DOT_GET: number not supported')
|
|
}
|
|
})
|
|
|
|
test("fails to work on STRING", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 'ShrimpVM?'
|
|
PUSH 1
|
|
DOT_GET
|
|
`)
|
|
|
|
try {
|
|
await run(bytecode)
|
|
expect(true).toBe(false) // Should not reach here
|
|
} catch (e: any) {
|
|
expect(e.message).toContain('DOT_GET: string not supported')
|
|
}
|
|
})
|
|
|
|
test("array - returns null for out of bounds", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 10
|
|
PUSH 20
|
|
MAKE_ARRAY #2
|
|
PUSH 5
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'null', value: null })
|
|
})
|
|
|
|
test("array - negative index", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 10
|
|
PUSH 20
|
|
PUSH 30
|
|
MAKE_ARRAY #3
|
|
PUSH -1
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'null', value: null })
|
|
})
|
|
|
|
test("array - first and last element", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 100
|
|
PUSH 200
|
|
PUSH 300
|
|
MAKE_ARRAY #3
|
|
DUP
|
|
PUSH 0
|
|
DOT_GET
|
|
STORE first
|
|
PUSH 2
|
|
DOT_GET
|
|
STORE last
|
|
LOAD first
|
|
LOAD last
|
|
ADD
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'number', value: 400 })
|
|
})
|
|
|
|
test("dict - returns null for missing key", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 'name'
|
|
PUSH 'Alice'
|
|
MAKE_DICT #1
|
|
PUSH 'age'
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'null', value: null })
|
|
})
|
|
|
|
test("dict - with numeric key", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH '123'
|
|
PUSH 'value'
|
|
MAKE_DICT #1
|
|
PUSH 123
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'string', value: 'value' })
|
|
})
|
|
|
|
test("array - with string value", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 'foo'
|
|
PUSH 'bar'
|
|
PUSH 'baz'
|
|
MAKE_ARRAY #3
|
|
PUSH 1
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'string', value: 'bar' })
|
|
})
|
|
|
|
test("array - with boolean values", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH true
|
|
PUSH false
|
|
PUSH true
|
|
MAKE_ARRAY #3
|
|
PUSH 1
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'boolean', value: false })
|
|
})
|
|
|
|
test("dict - with boolean value", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 'active'
|
|
PUSH true
|
|
PUSH 'visible'
|
|
PUSH false
|
|
MAKE_DICT #2
|
|
PUSH 'active'
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'boolean', value: true })
|
|
})
|
|
|
|
test("array - nested arrays", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 1
|
|
PUSH 2
|
|
MAKE_ARRAY #2
|
|
PUSH 3
|
|
PUSH 4
|
|
MAKE_ARRAY #2
|
|
MAKE_ARRAY #2
|
|
PUSH 1
|
|
DOT_GET
|
|
`)
|
|
|
|
const result = await run(bytecode)
|
|
expect(result.type).toBe('array')
|
|
if (result.type === 'array') {
|
|
expect(result.value).toHaveLength(2)
|
|
expect(result.value[0]).toEqual({ type: 'number', value: 3 })
|
|
expect(result.value[1]).toEqual({ type: 'number', value: 4 })
|
|
}
|
|
})
|
|
|
|
test("dict - nested dicts", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 'user'
|
|
PUSH 'name'
|
|
PUSH 'Alice'
|
|
MAKE_DICT #1
|
|
MAKE_DICT #1
|
|
PUSH 'user'
|
|
DOT_GET
|
|
`)
|
|
|
|
const result = await run(bytecode)
|
|
expect(result.type).toBe('dict')
|
|
if (result.type === 'dict') {
|
|
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Alice' })
|
|
}
|
|
})
|
|
|
|
test("array format", async () => {
|
|
const bytecode = toBytecode([
|
|
["PUSH", "apple"],
|
|
["PUSH", "banana"],
|
|
["PUSH", "cherry"],
|
|
["MAKE_ARRAY", 3],
|
|
["PUSH", 1],
|
|
["DOT_GET"],
|
|
["HALT"]
|
|
])
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'string', value: 'banana' })
|
|
})
|
|
|
|
test("with variables - array", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 10
|
|
PUSH 20
|
|
PUSH 30
|
|
MAKE_ARRAY #3
|
|
STORE arr
|
|
PUSH 1
|
|
STORE idx
|
|
LOAD arr
|
|
LOAD idx
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'number', value: 20 })
|
|
})
|
|
|
|
test("with variables - dict", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 'x'
|
|
PUSH 100
|
|
PUSH 'y'
|
|
PUSH 200
|
|
MAKE_DICT #2
|
|
STORE point
|
|
PUSH 'y'
|
|
STORE key
|
|
LOAD point
|
|
LOAD key
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'number', value: 200 })
|
|
})
|
|
|
|
test("with expression as index", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 10
|
|
PUSH 20
|
|
PUSH 30
|
|
PUSH 40
|
|
MAKE_ARRAY #4
|
|
PUSH 1
|
|
PUSH 2
|
|
ADD
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'number', value: 40 })
|
|
})
|
|
|
|
test("chained access - array of dicts", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 'name'
|
|
PUSH 'Alice'
|
|
MAKE_DICT #1
|
|
PUSH 'name'
|
|
PUSH 'Bob'
|
|
MAKE_DICT #1
|
|
MAKE_ARRAY #2
|
|
PUSH 1
|
|
DOT_GET
|
|
PUSH 'name'
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'string', value: 'Bob' })
|
|
})
|
|
|
|
test("chained access - dict of arrays", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 'nums'
|
|
PUSH 1
|
|
PUSH 2
|
|
PUSH 3
|
|
MAKE_ARRAY #3
|
|
MAKE_DICT #1
|
|
PUSH 'nums'
|
|
DOT_GET
|
|
PUSH 1
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'number', value: 2 })
|
|
})
|
|
|
|
test("with null value in array", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 10
|
|
PUSH null
|
|
PUSH 30
|
|
MAKE_ARRAY #3
|
|
PUSH 1
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'null', value: null })
|
|
})
|
|
|
|
test("with null value in dict", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 'name'
|
|
PUSH 'Alice'
|
|
PUSH 'middle'
|
|
PUSH null
|
|
MAKE_DICT #2
|
|
PUSH 'middle'
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'null', value: null })
|
|
})
|
|
|
|
test("array with regex values", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH /foo/
|
|
PUSH /bar/i
|
|
PUSH /baz/g
|
|
MAKE_ARRAY #3
|
|
PUSH 1
|
|
DOT_GET
|
|
`)
|
|
|
|
const result = await run(bytecode)
|
|
expect(result.type).toBe('regex')
|
|
if (result.type === 'regex') {
|
|
expect(result.value.source).toBe('bar')
|
|
expect(result.value.ignoreCase).toBe(true)
|
|
}
|
|
})
|
|
|
|
test("dict with regex values", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 'email'
|
|
PUSH /^[a-z@.]+$/i
|
|
PUSH 'url'
|
|
PUSH /^https?:\\/\\//
|
|
MAKE_DICT #2
|
|
PUSH 'email'
|
|
DOT_GET
|
|
`)
|
|
|
|
const result = await run(bytecode)
|
|
expect(result.type).toBe('regex')
|
|
if (result.type === 'regex') {
|
|
expect(result.value.source).toBe('^[a-z@.]+$')
|
|
expect(result.value.ignoreCase).toBe(true)
|
|
}
|
|
})
|
|
|
|
test("dict - key coercion from number", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH '0'
|
|
PUSH 'zero'
|
|
PUSH '1'
|
|
PUSH 'one'
|
|
MAKE_DICT #2
|
|
PUSH 0
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'string', value: 'zero' })
|
|
})
|
|
|
|
test("fails on boolean", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH true
|
|
PUSH 0
|
|
DOT_GET
|
|
`)
|
|
|
|
try {
|
|
await run(bytecode)
|
|
expect(true).toBe(false)
|
|
} catch (e: any) {
|
|
expect(e.message).toContain('DOT_GET: boolean not supported')
|
|
}
|
|
})
|
|
|
|
test("fails on null", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH null
|
|
PUSH 0
|
|
DOT_GET
|
|
`)
|
|
|
|
try {
|
|
await run(bytecode)
|
|
expect(true).toBe(false)
|
|
} catch (e: any) {
|
|
expect(e.message).toContain('DOT_GET: null not supported')
|
|
}
|
|
})
|
|
|
|
test("fails on regex", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH /test/
|
|
PUSH 0
|
|
DOT_GET
|
|
`)
|
|
|
|
try {
|
|
await run(bytecode)
|
|
expect(true).toBe(false)
|
|
} catch (e: any) {
|
|
expect(e.message).toContain('DOT_GET: regex not supported')
|
|
}
|
|
})
|
|
|
|
test("empty array access", async () => {
|
|
const bytecode = toBytecode(`
|
|
MAKE_ARRAY #0
|
|
PUSH 0
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'null', value: null })
|
|
})
|
|
|
|
test("empty dict access", async () => {
|
|
const bytecode = toBytecode(`
|
|
MAKE_DICT #0
|
|
PUSH 'key'
|
|
DOT_GET
|
|
`)
|
|
|
|
expect(await run(bytecode)).toEqual({ type: 'null', value: null })
|
|
})
|
|
})
|
|
|
|
describe("STR_CONCAT", () => {
|
|
test("concats together strings", async () => {
|
|
const str = `
|
|
PUSH "Hi "
|
|
PUSH "friend"
|
|
PUSH "!"
|
|
STR_CONCAT #3
|
|
`
|
|
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hi friend!" })
|
|
|
|
const str2 = `
|
|
PUSH "Holy smokes!"
|
|
PUSH "It's "
|
|
PUSH "alive!"
|
|
STR_CONCAT #2
|
|
`
|
|
|
|
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "It's alive!" })
|
|
|
|
|
|
const str3 = `
|
|
PUSH 1
|
|
PUSH " + "
|
|
PUSH 1
|
|
PUSH " = "
|
|
PUSH 1
|
|
PUSH 1
|
|
ADD
|
|
STR_CONCAT #5
|
|
`
|
|
|
|
expect(await run(toBytecode(str3))).toEqual({ type: 'string', value: "1 + 1 = 2" })
|
|
})
|
|
|
|
test("empty concat (count=0)", async () => {
|
|
const str = `
|
|
PUSH "leftover"
|
|
STR_CONCAT #0
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "" })
|
|
})
|
|
|
|
test("single string", async () => {
|
|
const str = `
|
|
PUSH "hello"
|
|
STR_CONCAT #1
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "hello" })
|
|
})
|
|
|
|
test("converts numbers to strings", async () => {
|
|
const str = `
|
|
PUSH 42
|
|
PUSH 100
|
|
PUSH 7
|
|
STR_CONCAT #3
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "421007" })
|
|
})
|
|
|
|
test("converts booleans to strings", async () => {
|
|
const str = `
|
|
PUSH "Result: "
|
|
PUSH true
|
|
STR_CONCAT #2
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Result: true" })
|
|
|
|
const str2 = `
|
|
PUSH false
|
|
PUSH " is false"
|
|
STR_CONCAT #2
|
|
`
|
|
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "false is false" })
|
|
})
|
|
|
|
test("converts null to strings", async () => {
|
|
const str = `
|
|
PUSH "Value: "
|
|
PUSH null
|
|
STR_CONCAT #2
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Value: null" })
|
|
})
|
|
|
|
test("mixed types", async () => {
|
|
const str = `
|
|
PUSH "Count: "
|
|
PUSH 42
|
|
PUSH ", Active: "
|
|
PUSH true
|
|
PUSH ", Total: "
|
|
PUSH null
|
|
STR_CONCAT #6
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Count: 42, Active: true, Total: null" })
|
|
})
|
|
|
|
test("array format", async () => {
|
|
const bytecode = toBytecode([
|
|
["PUSH", "Hello"],
|
|
["PUSH", " "],
|
|
["PUSH", "World"],
|
|
["STR_CONCAT", 3],
|
|
["HALT"]
|
|
])
|
|
|
|
const result = await run(bytecode)
|
|
expect(result).toEqual({ type: 'string', value: "Hello World" })
|
|
})
|
|
|
|
test("with variables", async () => {
|
|
const str = `
|
|
PUSH "Alice"
|
|
STORE name
|
|
PUSH "Hello, "
|
|
LOAD name
|
|
PUSH "!"
|
|
STR_CONCAT #3
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello, Alice!" })
|
|
})
|
|
|
|
test("composable (multiple concatenations)", async () => {
|
|
const str = `
|
|
PUSH "Hello"
|
|
PUSH " "
|
|
PUSH "World"
|
|
STR_CONCAT #3
|
|
PUSH "!"
|
|
STR_CONCAT #2
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello World!" })
|
|
})
|
|
|
|
test("with emoji and unicode", async () => {
|
|
const str = `
|
|
PUSH "Hello "
|
|
PUSH "🌍"
|
|
PUSH "!"
|
|
STR_CONCAT #3
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Hello 🌍!" })
|
|
|
|
const str2 = `
|
|
PUSH "こんにちは"
|
|
PUSH "世界"
|
|
STR_CONCAT #2
|
|
`
|
|
expect(await run(toBytecode(str2))).toEqual({ type: 'string', value: "こんにちは世界" })
|
|
})
|
|
|
|
test("with expressions", async () => {
|
|
const str = `
|
|
PUSH "Result: "
|
|
PUSH 10
|
|
PUSH 5
|
|
ADD
|
|
STR_CONCAT #2
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "Result: 15" })
|
|
})
|
|
|
|
test("large concat", async () => {
|
|
const str = `
|
|
PUSH "a"
|
|
PUSH "b"
|
|
PUSH "c"
|
|
PUSH "d"
|
|
PUSH "e"
|
|
PUSH "f"
|
|
PUSH "g"
|
|
PUSH "h"
|
|
PUSH "i"
|
|
PUSH "j"
|
|
STR_CONCAT #10
|
|
`
|
|
expect(await run(toBytecode(str))).toEqual({ type: 'string', value: "abcdefghij" })
|
|
})
|
|
})
|
|
|
|
describe("BREAK", () => {
|
|
test("throws error when no break target", async () => {
|
|
// BREAK requires a break target frame on the call stack
|
|
// A single function call has no previous frame to mark as break target
|
|
const bytecode = toBytecode(`
|
|
MAKE_FUNCTION () .fn
|
|
PUSH 0
|
|
PUSH 0
|
|
CALL
|
|
HALT
|
|
.fn:
|
|
BREAK
|
|
`)
|
|
|
|
try {
|
|
await run(bytecode)
|
|
expect(true).toBe(false) // Should not reach here
|
|
} catch (e: any) {
|
|
expect(e.message).toContain('no break target found')
|
|
}
|
|
})
|
|
|
|
test("exits from nested function call", async () => {
|
|
// BREAK unwinds to the break target (the outer function's frame)
|
|
// Main calls outer, outer calls inner, inner BREAKs back to outer's caller (main)
|
|
const bytecode = toBytecode(`
|
|
MAKE_FUNCTION () .outer
|
|
PUSH 0
|
|
PUSH 0
|
|
CALL
|
|
PUSH 42
|
|
HALT
|
|
.outer:
|
|
MAKE_FUNCTION () .inner
|
|
PUSH 0
|
|
PUSH 0
|
|
CALL
|
|
PUSH 99
|
|
RETURN
|
|
.inner:
|
|
BREAK
|
|
`)
|
|
|
|
const result = await run(bytecode)
|
|
expect(result).toEqual({ type: 'number', value: 42 })
|
|
})
|
|
})
|