ReefVM/tests/opcodes.test.ts

2093 lines
41 KiB
TypeScript

import { test, expect, describe } from "bun:test"
import { run } from "#index"
import { toBytecode } from "#bytecode"
describe("ADD", () => {
test("add two numbers", async () => {
await expect(`
PUSH 1
PUSH 5
ADD
`).toEqualValue(6)
await expect(`
PUSH 100
PUSH 500
ADD
`).toBeNumber(600)
})
test("concatenate two strings", async () => {
await expect(`
PUSH "hello"
PUSH " world"
ADD
`).toEqualValue('hello world')
await expect(`
PUSH "foo"
PUSH "bar"
ADD
`).toBeString('foobar')
})
test("concatenate string with number", async () => {
await expect(`
PUSH "count: "
PUSH 42
ADD
`).toEqualValue('count: 42')
await expect(`
PUSH 100
PUSH " items"
ADD
`).toBeString('100 items')
})
test("concatenate string with boolean", async () => {
await expect(`
PUSH "result: "
PUSH true
ADD
`).toBeString('result: true')
await expect(`
PUSH false
PUSH " value"
ADD
`).toEqualValue('false value')
})
test("concatenate string with null", async () => {
await expect(`
PUSH "value: "
PUSH null
ADD
`).toBeString('value: null')
})
test("concatenate multiple strings in sequence", async () => {
const str = `
PUSH "hello"
PUSH " "
ADD
PUSH "world"
ADD
PUSH "!"
ADD
`
await expect(str).toBeString('hello world!')
})
test("mixed arithmetic and string concatenation", async () => {
const str = `
PUSH "Result: "
PUSH 10
PUSH 5
ADD
ADD
`
await expect(str).toBeString('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
`
await expect(str).toBeNumber(3)
})
})
describe("MUL", () => {
test("multiply two numbers", async () => {
const str = `
PUSH 5
PUSH 2
MUL
`
await expect(str).toBeNumber(10)
})
})
describe("DIV", () => {
test("divide two numbers", async () => {
const str = `
PUSH 10
PUSH 2
DIV
`
await expect(str).toBeNumber(5)
const str2 = `
PUSH 10
PUSH 0
DIV
`
await expect(str2).toBeNumber(Infinity)
})
})
describe("MOD", () => {
test("modulo two numbers", async () => {
const str = `
PUSH 17
PUSH 5
MOD
`
await expect(str).toBeNumber(2)
})
})
describe("PUSH", () => {
test("pushes value onto stack", async () => {
const str = `
PUSH 42
`
await expect(str).toBeNumber(42)
})
})
describe("POP", () => {
test("removes top value", async () => {
const str = `
PUSH 10
PUSH 20
POP
`
await expect(str).toBeNumber(10)
})
})
describe("DUP", () => {
test("duplicates top value", async () => {
const str = `
PUSH 5
DUP
ADD
`
await expect(str).toBeNumber(10)
})
})
describe("SWAP", () => {
test("swaps two numbers", async () => {
const str = `
PUSH 10
PUSH 20
SWAP
`
await expect(str).toBeNumber(10)
})
test("swap and use in subtraction", async () => {
const str = `
PUSH 5
PUSH 10
SWAP
SUB
`
await expect(str).toBeNumber(5)
})
test("swap different types", async () => {
const str = `
PUSH "hello"
PUSH 42
SWAP
`
await expect(str).toBeString('hello')
})
})
describe("EQ", () => {
test("equality comparison", async () => {
const str = `
PUSH 5
PUSH 5
EQ
`
await expect(str).toBeBoolean(true)
const str2 = `
PUSH 5
PUSH 10
EQ
`
await expect(str2).toBeBoolean(false)
})
test('equality with regexes', async () => {
const str = `
PUSH /cool/i
PUSH /cool/i
EQ
`
await expect(str).toBeBoolean(true)
const str2 = `
PUSH /cool/
PUSH /cool/i
EQ
`
await expect(str2).toBeBoolean(false)
const str3 = `
PUSH /not-cool/
PUSH /cool/
EQ
`
await expect(str3).toBeBoolean(false)
})
})
describe("NEQ", () => {
test("not equal comparison", async () => {
const str = `
PUSH 5
PUSH 10
NEQ
`
await expect(str).toBeBoolean(true)
})
})
describe("LT", () => {
test("less than", async () => {
const str = `
PUSH 5
PUSH 10
LT
`
await expect(str).toBeBoolean(true)
})
})
describe("GT", () => {
test("greater than", async () => {
const str = `
PUSH 10
PUSH 5
GT
`
await expect(str).toBeBoolean(true)
})
})
describe("LTE", () => {
test("less than or equal", async () => {
// equal case
const str = `
PUSH 5
PUSH 5
LTE
`
await expect(str).toBeBoolean(true)
// less than case
const str2 = `
PUSH 3
PUSH 5
LTE
`
await expect(str2).toBeBoolean(true)
// greater than case (false)
const str3 = `
PUSH 10
PUSH 5
LTE
`
await expect(str3).toBeBoolean(false)
})
})
describe("GTE", () => {
test("greater than or equal", async () => {
// equal case
const str = `
PUSH 5
PUSH 5
GTE
`
await expect(str).toBeBoolean(true)
// greater than case
const str2 = `
PUSH 10
PUSH 5
GTE
`
await expect(str2).toBeBoolean(true)
// less than case (false)
const str3 = `
PUSH 3
PUSH 5
GTE
`
await expect(str3).toBeBoolean(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:
`
await expect(str).toBeNumber(2)
})
test("OR pattern - short circuits when true", async () => {
const str = `
PUSH 1
DUP
JUMP_IF_TRUE .end
POP
PUSH 2
.end:
`
await expect(str).toBeNumber(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:
`
await expect(str).toBeNumber(2)
})
})
describe("NOT", () => {
test("logical not", async () => {
// number is truthy, so NOT returns false
const str = `
PUSH 1
NOT
`
await expect(str).toBeBoolean(false)
// 0 is truthy in this language, so NOT returns false
const str2 = `
PUSH 0
NOT
`
await expect(str2).toBeBoolean(false)
// boolean false is falsy, so NOT returns true
const str3 = `
PUSH 1
PUSH 0
EQ
NOT
`
await expect(str3).toBeBoolean(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:
`
await expect(str1).toBeNumber(1)
// empty string is truthy (unlike JS)
const str2 = `
PUSH ''
JUMP_IF_FALSE .end
PUSH 1
.end:
`
await expect(str2).toBeNumber(1)
// false is falsy
const str3 = `
PUSH 0
PUSH 0
EQ
JUMP_IF_FALSE .end
PUSH 999
.end:
`
await expect(str3).toBeNumber(999)
})
})
describe("HALT", () => {
test("stops execution", async () => {
const str = `
PUSH 42
HALT
PUSH 100
`
await expect(str).toBeNumber(42)
})
})
describe("TYPE", () => {
test("null type", async () => {
const bytecode = toBytecode([
["PUSH", null],
["TYPE"],
["HALT"]
])
await expect(bytecode).toBeString('null')
})
test("boolean type", async () => {
const bytecode1 = toBytecode([
["PUSH", true],
["TYPE"],
["HALT"]
])
await expect(bytecode1).toBeString('boolean')
const bytecode2 = toBytecode([
["PUSH", false],
["TYPE"],
["HALT"]
])
await expect(bytecode2).toBeString('boolean')
})
test("number type", async () => {
const bytecode1 = toBytecode([
["PUSH", 42],
["TYPE"],
["HALT"]
])
await expect(bytecode1).toBeString('number')
const bytecode2 = toBytecode([
["PUSH", 0],
["TYPE"],
["HALT"]
])
await expect(bytecode2).toBeString('number')
const bytecode3 = toBytecode([
["PUSH", -3.14],
["TYPE"],
["HALT"]
])
await expect(bytecode3).toBeString('number')
})
test("string type", async () => {
const bytecode1 = toBytecode([
["PUSH", "hello"],
["TYPE"],
["HALT"]
])
await expect(bytecode1).toBeString('string')
const bytecode2 = toBytecode([
["PUSH", ""],
["TYPE"],
["HALT"]
])
await expect(bytecode2).toBeString('string')
})
test("array type", async () => {
const bytecode = toBytecode([
["PUSH", 1],
["PUSH", 2],
["PUSH", 3],
["MAKE_ARRAY", 3],
["TYPE"],
["HALT"]
])
await expect(bytecode).toBeString('array')
})
test("empty array type", async () => {
const bytecode = toBytecode([
["MAKE_ARRAY", 0],
["TYPE"],
["HALT"]
])
await expect(bytecode).toBeString('array')
})
test("dict type", async () => {
const bytecode = toBytecode([
["PUSH", "name"],
["PUSH", "Alice"],
["PUSH", "age"],
["PUSH", 30],
["MAKE_DICT", 2],
["TYPE"],
["HALT"]
])
await expect(bytecode).toBeString('dict')
})
test("empty dict type", async () => {
const bytecode = toBytecode([
["MAKE_DICT", 0],
["TYPE"],
["HALT"]
])
await expect(bytecode).toBeString('dict')
})
test("function type", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x"], ".body"],
["TYPE"],
["HALT"],
[".body:"],
["LOAD", "x"],
["RETURN"]
])
await expect(bytecode).toBeString('function')
})
test("native function type", async () => {
const bytecode = toBytecode([
["LOAD", "add"],
["TYPE"],
["HALT"]
])
const result = await run(bytecode, {
add: (a: number, b: number) => a + b
})
expect(result).toEqual({ type: 'string', value: 'native' })
})
test("regex type", async () => {
const bytecode = toBytecode([
["PUSH", /test/i],
["TYPE"],
["HALT"]
])
await expect(bytecode).toBeString('regex')
})
test("TYPE with stored result", async () => {
const bytecode = toBytecode([
["PUSH", 100],
["TYPE"],
["STORE", "myType"],
["LOAD", "myType"],
["HALT"]
])
await expect(bytecode).toBeString('number')
})
test("TYPE comparison - type guards", async () => {
const bytecode = toBytecode([
["PUSH", "hello world"],
["DUP"],
["TYPE"],
["PUSH", "string"],
["EQ"],
["HALT"]
])
await expect(bytecode).toBeBoolean(true)
})
})
describe("LOAD / STORE", () => {
test("variables", async () => {
const str = `
PUSH 42
STORE x
PUSH 21
LOAD x
`
await expect(str).toBeNumber(42)
})
test("multiple variables", async () => {
const str = `
PUSH 10
STORE a
PUSH 20
STORE b
PUSH 44
LOAD a
LOAD b
ADD
`
await expect(str).toBeNumber(30)
})
})
describe("TRY_LOAD", () => {
test("variable found", async () => {
const str = `
PUSH 100
STORE count
TRY_LOAD count
`
await expect(str).toBeNumber(100)
const str2 = `
PUSH 'Bobby'
STORE name
TRY_LOAD name
`
await expect(str2).toBeString('Bobby')
})
test("variable missing", async () => {
const str = `
PUSH 100
STORE count
TRY_LOAD count1
`
await expect(str).toBeString('count1')
const str2 = `
PUSH 'Bobby'
STORE name
TRY_LOAD full-name
`
await expect(str2).toBeString('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
`
await expect(str3).toBeBoolean(true)
// Null
const str4 = `
PUSH null
STORE empty
TRY_LOAD empty
`
await expect(str4).toBeNull()
})
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
`
await expect(str).toBeNumber(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
`
await expect(str).toBeString('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'
await expect(str).toBeBoolean(false)
const str2 = `
PUSH 100
STORE count
TRY_LOAD missing
PUSH 'missing'
EQ
`
// Variable missing, so TRY_LOAD returns 'missing', which == 'missing'
await expect(str2).toBeBoolean(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
`
await expect(str).toBeNumber(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
`
await expect(str).toBeNumber(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
`
await expect(str).toBeNumber(42)
})
test("no jump when true", async () => {
const str = `
PUSH 1
JUMP_IF_FALSE .skip
PUSH 100
.skip:
`
await expect(str).toBeNumber(100)
})
})
describe("JUMP_IF_TRUE", () => {
test("conditional jump when true", async () => {
const str = `
PUSH 1
JUMP_IF_TRUE .skip
PUSH 100
.skip:
PUSH 42
`
await expect(str).toBeNumber(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
`
await expect(str).toBeNumber(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
`
await expect(str).toBeNumber(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
`
await expect(str).toBeNumber(3)
})
test("mutates original array", async () => {
const str = `
PUSH 10
PUSH 20
MAKE_ARRAY #2
DUP
PUSH 30
ARRAY_PUSH
PUSH 2
ARRAY_GET
`
await expect(str).toBeNumber(30)
})
})
describe("ARRAY_LEN", () => {
test("gets length", async () => {
const str = `
PUSH 10
PUSH 20
PUSH 30
MAKE_ARRAY #3
ARRAY_LEN
`
await expect(str).toBeNumber(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
`
await expect(str).toBeString('Bob')
})
})
describe("DICT_SET", () => {
test("sets value", async () => {
const str = `
MAKE_DICT #0
DUP
PUSH 'key'
PUSH 'value'
DICT_SET
PUSH 'key'
DICT_GET
`
await expect(str).toBeString('value')
})
})
describe("DICT_HAS", () => {
test("checks key exists", async () => {
const str = `
PUSH 'key'
PUSH 'value'
MAKE_DICT #1
PUSH 'key'
DICT_HAS
`
await expect(str).toBeBoolean(true)
})
test("checks key missing", async () => {
const str = `
MAKE_DICT #0
PUSH 'missing'
DICT_HAS
`
await expect(str).toBeBoolean(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
`)
await expect(bytecode).toBeNumber(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
`)
await expect(bytecode).toBeString('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
`)
await expect(bytecode).toBeNull()
})
test("array - negative index", async () => {
const bytecode = toBytecode(`
PUSH 10
PUSH 20
PUSH 30
MAKE_ARRAY #3
PUSH -1
DOT_GET
`)
await expect(bytecode).toBeNull()
})
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
`)
await expect(bytecode).toBeNumber(400)
})
test("dict - returns null for missing key", async () => {
const bytecode = toBytecode(`
PUSH 'name'
PUSH 'Alice'
MAKE_DICT #1
PUSH 'age'
DOT_GET
`)
await expect(bytecode).toBeNull()
})
test("dict - with numeric key", async () => {
const bytecode = toBytecode(`
PUSH '123'
PUSH 'value'
MAKE_DICT #1
PUSH 123
DOT_GET
`)
await expect(bytecode).toBeString('value')
})
test("array - with string value", async () => {
const bytecode = toBytecode(`
PUSH 'foo'
PUSH 'bar'
PUSH 'baz'
MAKE_ARRAY #3
PUSH 1
DOT_GET
`)
await expect(bytecode).toBeString('bar')
})
test("array - with boolean values", async () => {
const bytecode = toBytecode(`
PUSH true
PUSH false
PUSH true
MAKE_ARRAY #3
PUSH 1
DOT_GET
`)
await expect(bytecode).toBeBoolean(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
`)
await expect(bytecode).toBeBoolean(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"]
])
await expect(bytecode).toBeString('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
`)
await expect(bytecode).toBeNumber(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
`)
await expect(bytecode).toBeNumber(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
`)
await expect(bytecode).toBeNumber(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
`)
await expect(bytecode).toBeString('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
`)
await expect(bytecode).toBeNumber(2)
})
test("with null value in array", async () => {
const bytecode = toBytecode(`
PUSH 10
PUSH null
PUSH 30
MAKE_ARRAY #3
PUSH 1
DOT_GET
`)
await expect(bytecode).toBeNull()
})
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
`)
await expect(bytecode).toBeNull()
})
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
`)
await expect(bytecode).toBeString('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
`)
await expect(bytecode).toBeNull()
})
test("empty dict access", async () => {
const bytecode = toBytecode(`
MAKE_DICT #0
PUSH 'key'
DOT_GET
`)
await expect(bytecode).toBeNull()
})
})
describe("STR_CONCAT", () => {
test("concats together strings", async () => {
const str = `
PUSH "Hi "
PUSH "friend"
PUSH "!"
STR_CONCAT #3
`
await expect(str).toBeString("Hi friend!")
const str2 = `
PUSH "Holy smokes!"
PUSH "It's "
PUSH "alive!"
STR_CONCAT #2
`
await expect(str2).toBeString("It's alive!")
const str3 = `
PUSH 1
PUSH " + "
PUSH 1
PUSH " = "
PUSH 1
PUSH 1
ADD
STR_CONCAT #5
`
await expect(str3).toBeString("1 + 1 = 2")
})
test("empty concat (count=0)", async () => {
const str = `
PUSH "leftover"
STR_CONCAT #0
`
await expect(str).toBeString("")
})
test("single string", async () => {
const str = `
PUSH "hello"
STR_CONCAT #1
`
await expect(str).toBeString("hello")
})
test("converts numbers to strings", async () => {
const str = `
PUSH 42
PUSH 100
PUSH 7
STR_CONCAT #3
`
await expect(str).toBeString("421007")
})
test("converts booleans to strings", async () => {
const str = `
PUSH "Result: "
PUSH true
STR_CONCAT #2
`
await expect(str).toBeString("Result: true")
const str2 = `
PUSH false
PUSH " is false"
STR_CONCAT #2
`
await expect(str2).toBeString("false is false")
})
test("converts null to strings", async () => {
const str = `
PUSH "Value: "
PUSH null
STR_CONCAT #2
`
await expect(str).toBeString("Value: null")
})
test("mixed types", async () => {
const str = `
PUSH "Count: "
PUSH 42
PUSH ", Active: "
PUSH true
PUSH ", Total: "
PUSH null
STR_CONCAT #6
`
await expect(str).toBeString("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
`
await expect(str).toBeString("Hello, Alice!")
})
test("composable (multiple concatenations)", async () => {
const str = `
PUSH "Hello"
PUSH " "
PUSH "World"
STR_CONCAT #3
PUSH "!"
STR_CONCAT #2
`
await expect(str).toBeString("Hello World!")
})
test("with emoji and unicode", async () => {
const str = `
PUSH "Hello "
PUSH "🌍"
PUSH "!"
STR_CONCAT #3
`
await expect(str).toBeString("Hello 🌍!")
const str2 = `
PUSH "こんにちは"
PUSH "世界"
STR_CONCAT #2
`
await expect(str2).toBeString("こんにちは世界")
})
test("with expressions", async () => {
const str = `
PUSH "Result: "
PUSH 10
PUSH 5
ADD
STR_CONCAT #2
`
await expect(str).toBeString("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
`
await expect(str).toBeString("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 })
})
})