248 lines
4.8 KiB
TypeScript
248 lines
4.8 KiB
TypeScript
import { test, expect } from "bun:test"
|
|
import { toBytecode } from "#bytecode"
|
|
import { OpCode } from "#opcode"
|
|
import { VM } from "#vm"
|
|
|
|
test("string compilation", () => {
|
|
const str = `
|
|
PUSH 1
|
|
PUSH 5
|
|
ADD
|
|
`
|
|
expect(toBytecode(str)).toEqual({
|
|
instructions: [
|
|
{ op: OpCode.PUSH, operand: 0 },
|
|
{ op: OpCode.PUSH, operand: 1 },
|
|
{ op: OpCode.ADD },
|
|
],
|
|
constants: [
|
|
{ type: 'number', value: 1 },
|
|
{ type: 'number', value: 5 }
|
|
]
|
|
})
|
|
})
|
|
|
|
|
|
test("MAKE_FUNCTION - basic function", async () => {
|
|
const bytecode = toBytecode(`
|
|
MAKE_FUNCTION () .body
|
|
PUSH 0
|
|
PUSH 0
|
|
CALL
|
|
HALT
|
|
.body:
|
|
PUSH 42
|
|
RETURN
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 42 })
|
|
})
|
|
|
|
test("MAKE_FUNCTION - function with parameters", async () => {
|
|
const bytecode = toBytecode(`
|
|
MAKE_FUNCTION (x y) .add
|
|
PUSH 10
|
|
PUSH 20
|
|
PUSH 2
|
|
PUSH 0
|
|
CALL
|
|
HALT
|
|
.add:
|
|
LOAD x
|
|
LOAD y
|
|
ADD
|
|
RETURN
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 30 })
|
|
})
|
|
|
|
test("MAKE_FUNCTION - function with default parameters", async () => {
|
|
const bytecode = toBytecode(`
|
|
MAKE_FUNCTION (x y=100) .add
|
|
PUSH 10
|
|
PUSH 1
|
|
PUSH 0
|
|
CALL
|
|
HALT
|
|
.add:
|
|
LOAD x
|
|
LOAD y
|
|
ADD
|
|
RETURN
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 110 })
|
|
})
|
|
|
|
test("MAKE_FUNCTION - tail recursive countdown", async () => {
|
|
const bytecode = toBytecode(`
|
|
MAKE_FUNCTION (n) .countdown
|
|
STORE countdown
|
|
LOAD countdown
|
|
PUSH 5
|
|
PUSH 1
|
|
PUSH 0
|
|
CALL
|
|
HALT
|
|
.countdown:
|
|
LOAD n
|
|
PUSH 0
|
|
EQ
|
|
JUMP_IF_FALSE .recurse
|
|
PUSH "done"
|
|
RETURN
|
|
.recurse:
|
|
LOAD countdown
|
|
LOAD n
|
|
PUSH 1
|
|
SUB
|
|
PUSH 1
|
|
PUSH 0
|
|
TAIL_CALL
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'done' })
|
|
})
|
|
|
|
test("MAKE_FUNCTION - multiple default values", async () => {
|
|
const bytecode = toBytecode(`
|
|
MAKE_FUNCTION (a=1 b=2 c=3) .sum
|
|
PUSH 0
|
|
PUSH 0
|
|
CALL
|
|
HALT
|
|
.sum:
|
|
LOAD a
|
|
LOAD b
|
|
LOAD c
|
|
ADD
|
|
ADD
|
|
RETURN
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 6 })
|
|
})
|
|
|
|
test("MAKE_FUNCTION - default with string", async () => {
|
|
const bytecode = toBytecode(`
|
|
MAKE_FUNCTION (name="World") .greet
|
|
PUSH 0
|
|
PUSH 0
|
|
CALL
|
|
HALT
|
|
.greet:
|
|
LOAD name
|
|
RETURN
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'string', value: 'World' })
|
|
})
|
|
|
|
test("semicolon comments are ignored", () => {
|
|
const bytecode = toBytecode(`
|
|
; This is a comment
|
|
PUSH 1 ; push one
|
|
PUSH 5 ; push five
|
|
ADD ; add them
|
|
`)
|
|
|
|
expect(bytecode).toEqual({
|
|
instructions: [
|
|
{ op: OpCode.PUSH, operand: 0 },
|
|
{ op: OpCode.PUSH, operand: 1 },
|
|
{ op: OpCode.ADD },
|
|
],
|
|
constants: [
|
|
{ type: 'number', value: 1 },
|
|
{ type: 'number', value: 5 }
|
|
]
|
|
})
|
|
})
|
|
|
|
test("semicolon comments work with functions", async () => {
|
|
const bytecode = toBytecode(`
|
|
MAKE_FUNCTION (x y) .add ; function with two params
|
|
PUSH 10 ; first arg
|
|
PUSH 20 ; second arg
|
|
PUSH 2 ; positional count
|
|
PUSH 0 ; named count
|
|
CALL ; call the function
|
|
HALT
|
|
.add: ; Function body starts here
|
|
LOAD x ; load first param
|
|
LOAD y ; load second param
|
|
ADD ; add them
|
|
RETURN ; return result
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 30 })
|
|
})
|
|
|
|
test("labels - basic jump", async () => {
|
|
const bytecode = toBytecode(`
|
|
JUMP .end
|
|
PUSH 999
|
|
.end:
|
|
PUSH 42
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 42 })
|
|
})
|
|
|
|
test("labels - conditional jump", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 1
|
|
JUMP_IF_FALSE .else
|
|
PUSH 10
|
|
JUMP .end
|
|
.else:
|
|
PUSH 20
|
|
.end:
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 10 })
|
|
})
|
|
|
|
test("labels - loop", async () => {
|
|
const bytecode = toBytecode(`
|
|
PUSH 0
|
|
STORE i
|
|
.loop:
|
|
LOAD i
|
|
PUSH 3
|
|
LT
|
|
JUMP_IF_FALSE .end
|
|
LOAD i
|
|
PUSH 1
|
|
ADD
|
|
STORE i
|
|
JUMP .loop
|
|
.end:
|
|
LOAD i
|
|
`)
|
|
|
|
const vm = new VM(bytecode)
|
|
const result = await vm.run()
|
|
expect(result).toEqual({ type: 'number', value: 3 })
|
|
})
|
|
|