ReefVM/tests/functions.test.ts

725 lines
15 KiB
TypeScript

import { test, expect } from "bun:test"
import { toBytecode } from "#bytecode"
import { toValue, run } from "#reef"
test("MAKE_FUNCTION - creates function with captured scope", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION () #999
`)
const result = await run(bytecode)
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 run(bytecode)
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 run(bytecode)
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 run(bytecode)
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 run(bytecode)
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 run(bytecode)
// 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 run(bytecode)
// 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 run(bytecode)
// 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 run(bytecode)
// 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 run(bytecode)
// 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 run(bytecode)
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 run(bytecode)
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 run(bytecode)
// 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 run(bytecode)
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 run(bytecode)
// 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 run(bytecode)
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 run(bytecode)
// 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 run(bytecode)
// x should use default value 5
expect(result).toEqual({ type: 'number', value: 5 })
})
test("CALL - fixed params can be named", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION (a b) .func_0
STORE minus
TRY_LOAD minus
PUSH 200
PUSH 'a'
PUSH 900
PUSH 1
PUSH 1
CALL
HALT
.func_0:
TRY_LOAD a
TRY_LOAD b
SUB
RETURN
`)
const result = await run(bytecode)
expect(result).toEqual(toValue(700))
})
test("TRY_CALL - calls function if found", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", [], ".body"],
["STORE", "myFunc"],
["TRY_CALL", "myFunc"],
["HALT"],
[".body:"],
["PUSH", 42],
["RETURN"]
])
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 42 })
})
test("TRY_CALL - pushes value if variable exists but is not a function", async () => {
const bytecode = toBytecode([
["PUSH", 99],
["STORE", "myVar"],
["TRY_CALL", "myVar"],
["HALT"]
])
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 99 })
})
test("TRY_CALL - pushes string if variable not found", async () => {
const bytecode = toBytecode([
["TRY_CALL", "unknownVar"],
["HALT"]
])
const result = await run(bytecode)
expect(result).toEqual({ type: 'string', value: 'unknownVar' })
})
test("TRY_CALL - handles arrays", async () => {
const bytecode = toBytecode([
["PUSH", 1],
["PUSH", 2],
["MAKE_ARRAY", 2],
["STORE", "myArray"],
["TRY_CALL", "myArray"],
["HALT"]
])
const result = await run(bytecode)
expect(result.type).toBe('array')
if (result.type === 'array') {
expect(result.value).toEqual([
{ type: 'number', value: 1 },
{ type: 'number', value: 2 }
])
}
})
test("TRY_CALL - handles dicts", async () => {
const bytecode = toBytecode([
["PUSH", "key"],
["PUSH", "value"],
["MAKE_DICT", 1],
["STORE", "myDict"],
["TRY_CALL", "myDict"],
["HALT"]
])
const result = await run(bytecode)
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.get('key')).toEqual({ type: 'string', value: 'value' })
}
})
test("TRY_CALL - handles null values", async () => {
const bytecode = toBytecode([
["PUSH", null],
["STORE", "myNull"],
["TRY_CALL", "myNull"],
["HALT"]
])
const result = await run(bytecode)
expect(result).toEqual({ type: 'null', value: null })
})
test("TRY_CALL - function can access its parameters", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x=0"], ".body"],
["STORE", "addFive"],
["PUSH", 10],
["STORE", "x"],
["TRY_CALL", "addFive"],
["HALT"],
[".body:"],
["LOAD", "x"],
["PUSH", 5],
["ADD"],
["RETURN"]
])
const result = await run(bytecode)
// Function is called with 0 args, so x defaults to 0
// Then we add 5 to 0
expect(result).toEqual({ type: 'number', value: 5 })
})
test("TRY_CALL - with string format", async () => {
const bytecode = toBytecode(`
MAKE_FUNCTION () #4
STORE myFunc
TRY_CALL myFunc
HALT
PUSH 100
RETURN
`)
const result = await run(bytecode)
expect(result).toEqual({ type: 'number', value: 100 })
})
test("CALL - passing null triggers default value for single parameter", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x=42"], ".body"],
["STORE", "func"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["RETURN"],
[".end:"],
["LOAD", "func"],
["PUSH", null],
["PUSH", 1],
["PUSH", 0],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// Passing null should trigger the default value of 42
expect(result).toEqual({ type: 'number', value: 42 })
})
test("CALL - passing null triggers default value for multiple parameters", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["a=10", "b=20", "c=30"], ".body"],
["STORE", "func"],
["JUMP", ".end"],
[".body:"],
["LOAD", "a"],
["LOAD", "b"],
["ADD"],
["LOAD", "c"],
["ADD"],
["RETURN"],
[".end:"],
["LOAD", "func"],
["PUSH", 5],
["PUSH", null],
["PUSH", null],
["PUSH", 3],
["PUSH", 0],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// a=5 (provided), b=20 (null triggers default), c=30 (null triggers default)
// Result: 5 + 20 + 30 = 55
expect(result).toEqual({ type: 'number', value: 55 })
})
test("CALL - null in middle parameter triggers default", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x=100", "y=200", "z=300"], ".body"],
["STORE", "func"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["LOAD", "y"],
["ADD"],
["LOAD", "z"],
["ADD"],
["RETURN"],
[".end:"],
["LOAD", "func"],
["PUSH", 1],
["PUSH", null],
["PUSH", 3],
["PUSH", 3],
["PUSH", 0],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// x=1, y=200 (null triggers default), z=3
// Result: 1 + 200 + 3 = 204
expect(result).toEqual({ type: 'number', value: 204 })
})
test("CALL - null with named arguments triggers default", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x=50", "y=75"], ".body"],
["STORE", "func"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["LOAD", "y"],
["ADD"],
["RETURN"],
[".end:"],
["LOAD", "func"],
["PUSH", "x"],
["PUSH", null],
["PUSH", "y"],
["PUSH", 25],
["PUSH", 0],
["PUSH", 2],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// x=50 (null triggers default), y=25 (provided via named arg)
// Result: 50 + 25 = 75
expect(result).toEqual({ type: 'number', value: 75 })
})
test("CALL - null with string default value", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["name='Guest'"], ".body"],
["STORE", "greet"],
["JUMP", ".end"],
[".body:"],
["PUSH", "Hello, "],
["LOAD", "name"],
["ADD"],
["RETURN"],
[".end:"],
["LOAD", "greet"],
["PUSH", null],
["PUSH", 1],
["PUSH", 0],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// Passing null should trigger the default value 'Guest'
expect(result).toEqual({ type: 'string', value: 'Hello, Guest' })
})
test("CALL - null with no default still results in null", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x"], ".body"],
["STORE", "func"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["RETURN"],
[".end:"],
["LOAD", "func"],
["PUSH", null],
["PUSH", 1],
["PUSH", 0],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// No default value, so null should be returned
expect(result).toEqual({ type: 'null', value: null })
})
test("CALL - null triggers default with variadic parameters", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x=99", "...rest"], ".body"],
["STORE", "func"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["RETURN"],
[".end:"],
["LOAD", "func"],
["PUSH", null],
["PUSH", 1],
["PUSH", 2],
["PUSH", 3],
["PUSH", 4],
["PUSH", 0],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// x should be 99 (null triggers default), rest gets [1, 2, 3]
expect(result).toEqual({ type: 'number', value: 99 })
})
test("CALL - null triggers default with @named parameter", async () => {
const bytecode = toBytecode([
["MAKE_FUNCTION", ["x=777", "@named"], ".body"],
["STORE", "func"],
["JUMP", ".end"],
[".body:"],
["LOAD", "x"],
["RETURN"],
[".end:"],
["LOAD", "func"],
["PUSH", null],
["PUSH", "foo"],
["PUSH", "bar"],
["PUSH", 1],
["PUSH", 1],
["CALL"],
["HALT"]
])
const result = await run(bytecode)
// x should be 777 (null triggers default)
expect(result).toEqual({ type: 'number', value: 777 })
})