ReefVM/tests/basic.test.ts
2025-10-05 20:08:00 -07:00

574 lines
11 KiB
TypeScript

import { test, expect } from "bun:test"
import { run } from "#index"
import { toBytecode } from "#bytecode"
test("ADD - 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("SUB - subtract two numbers", async () => {
const str = `
PUSH 5
PUSH 2
SUB
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 3 })
})
test("MUL - multiply two numbers", async () => {
const str = `
PUSH 5
PUSH 2
MUL
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
})
test("DIV - 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 })
})
test("MOD - modulo two numbers", async () => {
const str = `
PUSH 17
PUSH 5
MOD
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
})
test("PUSH - pushes value onto stack", async () => {
const str = `
PUSH 42
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
})
test("POP - removes top value", async () => {
const str = `
PUSH 10
PUSH 20
POP
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
})
test("DUP - duplicates top value", async () => {
const str = `
PUSH 5
DUP
ADD
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 10 })
})
test("EQ - 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("NEQ - not equal comparison", async () => {
const str = `
PUSH 5
PUSH 10
NEQ
`
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
})
test("LT - less than", async () => {
const str = `
PUSH 5
PUSH 10
LT
`
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
})
test("GT - greater than", async () => {
const str = `
PUSH 10
PUSH 5
GT
`
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: true })
})
test("LTE - 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 })
})
test("GTE - 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 })
})
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 #2
POP
PUSH 999
`
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 #2
POP
PUSH 2
`
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 #2
POP
PUSH 2
`
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 #2
POP
PUSH 2
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
})
test("NOT - 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 })
})
test("isTruthy - only null and false are falsy", async () => {
// 0 is truthy (unlike JS)
const str1 = `
PUSH 0
JUMP_IF_FALSE #1
PUSH 1
`
expect(await run(toBytecode(str1))).toEqual({ type: 'number', value: 1 })
// empty string is truthy (unlike JS)
const str2 = `
PUSH ''
JUMP_IF_FALSE #1
PUSH 1
`
expect(await run(toBytecode(str2))).toEqual({ type: 'number', value: 1 })
// false is falsy
const str3 = `
PUSH 0
PUSH 0
EQ
JUMP_IF_FALSE #1
PUSH 999
`
expect(await run(toBytecode(str3))).toEqual({ type: 'number', value: 999 })
})
test("HALT - stops execution", async () => {
const str = `
PUSH 42
HALT
PUSH 100
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
})
test("STORE and LOAD - variables", async () => {
const str = `
PUSH 42
STORE x
PUSH 21
LOAD x
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
})
test("STORE and LOAD - 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 })
})
test("JUMP - relative jump forward", async () => {
const str = `
PUSH 1
JUMP #1
PUSH 100
PUSH 2
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 2 })
})
test("JUMP - backward offset demonstrates relative jumps", async () => {
// Use forward jump to skip, demonstrating relative addressing
const str = `
PUSH 100
JUMP #2
PUSH 200
PUSH 300
PUSH 400
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 400 })
})
test("JUMP_IF_FALSE - conditional jump when false", async () => {
const str = `
PUSH 1
PUSH 0
EQ
JUMP_IF_FALSE #1
PUSH 100
PUSH 42
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
})
test("JUMP_IF_FALSE - no jump when true", async () => {
const str = `
PUSH 1
JUMP_IF_FALSE #1
PUSH 100
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 100 })
})
test("JUMP_IF_TRUE - conditional jump when true", async () => {
const str = `
PUSH 1
JUMP_IF_TRUE #1
PUSH 100
PUSH 42
`
expect(await run(toBytecode(str))).toEqual({ type: 'number', value: 42 })
})
test("MAKE_ARRAY - 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 })
}
})
test("ARRAY_GET - 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 })
})
test("ARRAY_SET - 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 })
})
test("ARRAY_PUSH - 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("ARRAY_PUSH - 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 })
})
test("ARRAY_LEN - 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 })
})
test("MAKE_DICT - 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 })
}
})
test("DICT_GET - 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' })
})
test("DICT_SET - 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' })
})
test("DICT_HAS - 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("DICT_HAS - checks key missing", async () => {
const str = `
MAKE_DICT #0
PUSH 'missing'
DICT_HAS
`
expect(await run(toBytecode(str))).toEqual({ type: 'boolean', value: false })
})
test("BREAK - 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 () #3
CALL #0
HALT
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("BREAK - 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 () #4
CALL #0
PUSH 42
HALT
MAKE_FUNCTION () #7
CALL #0
PUSH 99
RETURN
BREAK
`)
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 42 })
})
test("CONTINUE - throws error when no continue target", async () => {
// CONTINUE requires continueAddress to be set on a frame
// Since we have no instruction to set it yet, this should throw
const bytecode = toBytecode(`
MAKE_FUNCTION () #3
CALL #0
HALT
CONTINUE
`)
try {
await run(bytecode)
expect(true).toBe(false) // Should not reach here
} catch (e: any) {
expect(e.message).toContain('no continue target found')
}
})