ReefVM/tests/functions.test.ts
2025-10-05 22:34:07 -07:00

378 lines
7.6 KiB
TypeScript

import { test, expect } from "bun:test"
import { toBytecode } from "#bytecode"
import { VM } from "#vm"
test("MAKE_FUNCTION - creates function with captured scope", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION () #999
`)
const result = await new VM(bytecode).run()
expect(result.type).toBe('function')
if (result.type === 'function') {
expect(result.body).toBe(999)
expect(result.params).toEqual([])
}
})
test("CALL and RETURN - basic function call", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION () #5
PUSH 0
PUSH 0
CALL
HALT
PUSH 42
RETURN
`)
const result = await new VM(bytecode).run()
expect(result).toEqual({ type: 'number', value: 42 })
})
test("CALL and RETURN - function with one parameter", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (x) #6
PUSH 100
PUSH 1
PUSH 0
CALL
HALT
LOAD x
RETURN
`)
const result = await new VM(bytecode).run()
expect(result).toEqual({ type: 'number', value: 100 })
})
test("CALL and RETURN - function with two parameters", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (a b) #7
PUSH 10
PUSH 20
PUSH 2
PUSH 0
CALL
HALT
LOAD a
LOAD b
ADD
RETURN
`)
const result = await new VM(bytecode).run()
expect(result).toEqual({ type: 'number', value: 30 })
})
test("CALL - variadic function with no fixed params", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (...args) #8
PUSH 1
PUSH 2
PUSH 3
PUSH 3
PUSH 0
CALL
HALT
LOAD args
RETURN
`)
const result = await new VM(bytecode).run()
expect(result).toEqual({
type: 'array',
value: [
{ type: 'number', value: 1 },
{ type: 'number', value: 2 },
{ type: 'number', value: 3 }
]
})
})
test("CALL - variadic function with one fixed param", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (x ...rest) #8
PUSH 10
PUSH 20
PUSH 30
PUSH 3
PUSH 0
CALL
HALT
LOAD rest
RETURN
`)
const result = await new VM(bytecode).run()
// x should be 10, rest should be [20, 30]
expect(result).toEqual({
type: 'array',
value: [
{ type: 'number', value: 20 },
{ type: 'number', value: 30 }
]
})
})
test("CALL - variadic function with two fixed params", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (a b ...rest) #9
PUSH 1
PUSH 2
PUSH 3
PUSH 4
PUSH 4
PUSH 0
CALL
HALT
LOAD rest
RETURN
`)
const result = await new VM(bytecode).run()
// a=1, b=2, rest=[3, 4]
expect(result).toEqual({
type: 'array',
value: [
{ type: 'number', value: 3 },
{ type: 'number', value: 4 }
]
})
})
test("CALL - variadic function with no extra args", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (x ...rest) #6
PUSH 10
PUSH 1
PUSH 0
CALL
HALT
LOAD rest
RETURN
`)
const result = await new VM(bytecode).run()
// rest should be empty array
expect(result).toEqual({ type: 'array', value: [] })
})
test("CALL - variadic function with defaults on fixed params", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (x=5 ...rest) #5
PUSH 0
PUSH 0
CALL
HALT
LOAD x
RETURN
`)
const result = await new VM(bytecode).run()
// x should use default value 5
expect(result).toEqual({ type: 'number', value: 5 })
})
test("TAIL_CALL - variadic function", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (x ...rest) #8
PUSH 1
PUSH 2
PUSH 3
PUSH 3
PUSH 0
CALL
HALT
LOAD rest
RETURN
`)
const result = await new VM(bytecode).run()
// Should return the rest array [2, 3]
expect(result).toEqual({
type: 'array',
value: [
{ type: 'number', value: 2 },
{ type: 'number', value: 3 }
]
})
})
test("CALL - named args function with no fixed params", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (@named) #9
PUSH "name"
PUSH "Bob"
PUSH "age"
PUSH 50
PUSH 0
PUSH 2
CALL
HALT
LOAD named
RETURN
`)
const result = await new VM(bytecode).run()
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Bob' })
expect(result.value.get('age')).toEqual({ type: 'number', value: 50 })
}
})
test("CALL - named args function with one fixed param", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (x @named) #8
PUSH 10
PUSH "name"
PUSH "Alice"
PUSH 1
PUSH 1
CALL
HALT
LOAD named
RETURN
`)
const result = await new VM(bytecode).run()
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Alice' })
expect(result.value.size).toBe(1)
}
})
test("CALL - named args with matching param name should bind to param not named", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (name @named) #8
PUSH "Bob"
PUSH "age"
PUSH 50
PUSH 1
PUSH 1
CALL
HALT
LOAD name
RETURN
`)
const result = await new VM(bytecode).run()
// name should be bound as regular param, not collected in named
expect(result).toEqual({ type: 'string', value: 'Bob' })
})
test("CALL - named args that match param names should not be in named", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (name age @named) #9
PUSH "name"
PUSH "Bob"
PUSH "city"
PUSH "NYC"
PUSH 0
PUSH 2
CALL
HALT
LOAD named
RETURN
`)
const result = await new VM(bytecode).run()
expect(result.type).toBe('dict')
if (result.type === 'dict') {
// Only city should be in named, name should be bound to param
expect(result.value.get('city')).toEqual({ type: 'string', value: 'NYC' })
expect(result.value.has('name')).toBe(false)
expect(result.value.size).toBe(1)
}
})
test("CALL - mixed variadic and named args", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (x ...rest @named) #10
PUSH 1
PUSH 2
PUSH 3
PUSH "name"
PUSH "Bob"
PUSH 3
PUSH 1
CALL
HALT
LOAD rest
RETURN
`)
const result = await new VM(bytecode).run()
// rest should have [2, 3]
expect(result).toEqual({
type: 'array',
value: [
{ type: 'number', value: 2 },
{ type: 'number', value: 3 }
]
})
})
test("CALL - mixed variadic and named args, check named", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (x ...rest @named) #10
PUSH 1
PUSH 2
PUSH 3
PUSH "name"
PUSH "Bob"
PUSH 3
PUSH 1
CALL
HALT
LOAD named
RETURN
`)
const result = await new VM(bytecode).run()
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.get('name')).toEqual({ type: 'string', value: 'Bob' })
}
})
test("CALL - named args with no extra named args", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (x @named) #6
PUSH 10
PUSH 1
PUSH 0
CALL
HALT
LOAD named
RETURN
`)
const result = await new VM(bytecode).run()
// named should be empty dict
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.size).toBe(0)
}
})
test("CALL - named args with defaults on fixed params", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (x=5 @named) #7
PUSH "name"
PUSH "Alice"
PUSH 0
PUSH 1
CALL
HALT
LOAD x
RETURN
`)
const result = await new VM(bytecode).run()
// x should use default value 5
expect(result).toEqual({ type: 'number', value: 5 })
})