522 lines
11 KiB
TypeScript
522 lines
11 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"], ".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 inside function should be null
|
|
// Then we add 5 to null (which coerces 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 })
|
|
})
|