forked from defunkt/ReefVM
238 lines
7.4 KiB
TypeScript
238 lines
7.4 KiB
TypeScript
import { test, expect } from "bun:test"
|
|
import { isValue, toValue } from "#reef"
|
|
|
|
test("isValue - recognizes valid Value objects", () => {
|
|
expect(isValue({ type: 'number', value: 42 })).toBe(true)
|
|
expect(isValue({ type: 'number', value: 0 })).toBe(true)
|
|
expect(isValue({ type: 'string', value: 'hello' })).toBe(true)
|
|
expect(isValue({ type: 'string', value: '' })).toBe(true)
|
|
expect(isValue({ type: 'boolean', value: true })).toBe(true)
|
|
expect(isValue({ type: 'boolean', value: false })).toBe(true)
|
|
expect(isValue({ type: 'null', value: null })).toBe(true)
|
|
expect(isValue({ type: 'array', value: [] })).toBe(true)
|
|
expect(isValue({ type: 'array', value: [toValue(1), toValue(2)] })).toBe(true)
|
|
expect(isValue({ type: 'dict', value: new Map() })).toBe(true)
|
|
expect(isValue({ type: 'regex', value: /test/ })).toBe(true)
|
|
expect(isValue({
|
|
type: 'function',
|
|
value: '<function>',
|
|
params: [],
|
|
defaults: {},
|
|
body: 0,
|
|
variadic: false,
|
|
named: false,
|
|
parentScope: null as any
|
|
})).toBe(true)
|
|
expect(isValue({
|
|
type: 'native',
|
|
value: '<function>',
|
|
fn: (() => { }) as any
|
|
})).toBe(true)
|
|
})
|
|
|
|
test("isValue - rejects primitives", () => {
|
|
expect(isValue(42)).toBe(false)
|
|
expect(isValue(0)).toBe(false)
|
|
expect(isValue('hello')).toBe(false)
|
|
expect(isValue('')).toBe(false)
|
|
expect(isValue(true)).toBe(false)
|
|
expect(isValue(false)).toBe(false)
|
|
expect(isValue(null)).toBe(false)
|
|
expect(isValue(undefined)).toBe(false)
|
|
})
|
|
|
|
test("isValue - rejects plain objects", () => {
|
|
expect(isValue({})).toBe(false)
|
|
expect(isValue({ foo: 'bar' })).toBe(false)
|
|
expect(isValue({ type: 'number' })).toBe(false)
|
|
expect(isValue({ value: 42 })).toBe(false)
|
|
})
|
|
|
|
test("isValue - rejects arrays and functions", () => {
|
|
expect(isValue([])).toBe(false)
|
|
expect(isValue([1, 2, 3])).toBe(false)
|
|
expect(isValue(() => { })).toBe(false)
|
|
expect(isValue(function () { })).toBe(false)
|
|
})
|
|
|
|
test("isValue - rejects other object types", () => {
|
|
expect(isValue(new Date())).toBe(false)
|
|
expect(isValue(/regex/)).toBe(false)
|
|
expect(isValue(new Map())).toBe(false)
|
|
expect(isValue(new Set())).toBe(false)
|
|
})
|
|
|
|
test("isValue - used by toValue to detect already-converted values", () => {
|
|
const value = toValue(42)
|
|
expect(isValue(value)).toBe(true)
|
|
|
|
const result = toValue(value)
|
|
expect(result).toBe(value)
|
|
})
|
|
|
|
test("isValue - edge cases with type and value properties", () => {
|
|
// extra properties
|
|
expect(isValue({ type: 'number', value: 42, extra: 'data' })).toBe(true)
|
|
|
|
expect(isValue({ type: null, value: 42 })).toBe(false)
|
|
expect(isValue({ type: 'number', value: undefined })).toBe(true)
|
|
|
|
expect(isValue({ type: 'number', val: 42 })).toBe(false)
|
|
expect(isValue({ typ: 'number', value: 42 })).toBe(false)
|
|
})
|
|
|
|
test("toValue - converts wrapped Reef functions back to original Value", async () => {
|
|
const { VM, toBytecode, fnFromValue } = await import("#reef")
|
|
|
|
// Create a Reef function
|
|
const bytecode = toBytecode([
|
|
["MAKE_FUNCTION", ["x"], ".body"],
|
|
["STORE", "add1"],
|
|
["JUMP", ".end"],
|
|
[".body:"],
|
|
["LOAD", "x"],
|
|
["PUSH", 1],
|
|
["ADD"],
|
|
["RETURN"],
|
|
[".end:"],
|
|
["HALT"]
|
|
])
|
|
|
|
const vm = new VM(bytecode)
|
|
await vm.run()
|
|
|
|
const reefFunction = vm.scope.get("add1")!
|
|
expect(reefFunction.type).toBe("function")
|
|
|
|
// Convert to JS function
|
|
const jsFunction = fnFromValue(reefFunction, vm)
|
|
expect(typeof jsFunction).toBe("function")
|
|
|
|
// Convert back to Value - should return the original Reef function
|
|
const backToValue = toValue(jsFunction)
|
|
expect(backToValue).toBe(reefFunction) // Same reference
|
|
expect(backToValue.type).toBe("function")
|
|
})
|
|
|
|
test("fromValue - converts native function back to original JS function", async () => {
|
|
const { VM, toBytecode, toValue, fromValue } = await import("#reef")
|
|
|
|
const bytecode = toBytecode([["HALT"]])
|
|
const vm = new VM(bytecode)
|
|
|
|
// Create a native JS function
|
|
const originalFn = (x: number, y: number) => x * y
|
|
|
|
// Convert to Value (wraps it as a native function)
|
|
const nativeValue = toValue(originalFn, vm)
|
|
expect(nativeValue.type).toBe("native")
|
|
|
|
// Convert back to JS - should get the original function
|
|
const convertedFn = fromValue(nativeValue, vm)
|
|
expect(typeof convertedFn).toBe("function")
|
|
|
|
// Verify it's the same function
|
|
expect(convertedFn).toBe(originalFn)
|
|
|
|
// Verify it works correctly
|
|
expect(convertedFn(3, 4)).toBe(12)
|
|
})
|
|
|
|
test("fromValue - native function roundtrip preserves functionality", async () => {
|
|
const { VM, toBytecode, toValue, fromValue } = await import("#reef")
|
|
|
|
const bytecode = toBytecode([["HALT"]])
|
|
const vm = new VM(bytecode)
|
|
|
|
// Create a native function with state
|
|
let callCount = 0
|
|
const countingFn = (n: number) => {
|
|
callCount++
|
|
return n * callCount
|
|
}
|
|
|
|
// Roundtrip through Value
|
|
const nativeValue = toValue(countingFn, vm)
|
|
const roundtrippedFn = fromValue(nativeValue, vm)
|
|
|
|
// Verify it maintains state across calls
|
|
expect(roundtrippedFn(10)).toBe(10) // 10 * 1
|
|
expect(roundtrippedFn(10)).toBe(20) // 10 * 2
|
|
expect(roundtrippedFn(10)).toBe(30) // 10 * 3
|
|
expect(callCount).toBe(3)
|
|
})
|
|
|
|
test("fromValue - async native function roundtrip", async () => {
|
|
const { VM, toBytecode, toValue, fromValue } = await import("#reef")
|
|
|
|
const bytecode = toBytecode([["HALT"]])
|
|
const vm = new VM(bytecode)
|
|
|
|
const asyncFn = async (x: number, y: number) => {
|
|
await new Promise(resolve => setTimeout(resolve, 1))
|
|
return x + y
|
|
}
|
|
|
|
const nativeValue = toValue(asyncFn, vm)
|
|
expect(nativeValue.type).toBe("native")
|
|
|
|
const roundtrippedFn = fromValue(nativeValue, vm)
|
|
|
|
const result = await roundtrippedFn(5, 7)
|
|
expect(result).toBe(12)
|
|
})
|
|
|
|
test("toValue - throws helpful error when converting function without VM", () => {
|
|
function myFunction(x: number) {
|
|
return x * 2
|
|
}
|
|
|
|
expect(() => toValue(myFunction)).toThrow(/can't toValue\(\) function without a vm/)
|
|
expect(() => toValue(myFunction)).toThrow(/Function: myFunction/)
|
|
expect(() => toValue(myFunction)).toThrow(/Source:/)
|
|
expect(() => toValue(myFunction)).toThrow(/Called from:/)
|
|
})
|
|
|
|
test("toValue - error message shows function name from binding", () => {
|
|
const anonymousFn = (x: number) => x * 2
|
|
|
|
expect(() => toValue(anonymousFn)).toThrow(/Function: anonymousFn/)
|
|
expect(() => toValue(anonymousFn)).toThrow(/Source:/)
|
|
})
|
|
|
|
test("toValue - error when function is nested in object without VM", () => {
|
|
const obj = {
|
|
name: "test",
|
|
handler: (x: number) => x * 2
|
|
}
|
|
|
|
expect(() => toValue(obj)).toThrow(/can't toValue\(\) function without a vm/)
|
|
expect(() => toValue(obj)).toThrow(/Function: handler/)
|
|
})
|
|
|
|
test("toValue - error when function is nested in array without VM", () => {
|
|
const arr = [1, 2, (x: number) => x * 2]
|
|
|
|
expect(() => toValue(arr)).toThrow(/can't toValue\(\) function without a vm/)
|
|
})
|
|
|
|
test("fromValue - throws helpful error when converting function without VM", async () => {
|
|
const { Scope, fromValue } = await import("#reef")
|
|
|
|
const reefFunction = {
|
|
type: 'function' as const,
|
|
params: ['x', 'y'],
|
|
defaults: {},
|
|
body: 10,
|
|
parentScope: new Scope(),
|
|
variadic: false,
|
|
named: false,
|
|
value: '<function>' as const
|
|
}
|
|
|
|
expect(() => fromValue(reefFunction)).toThrow(/VM is required for function conversion/)
|
|
expect(() => fromValue(reefFunction)).toThrow(/Function params: \[x, y\]/)
|
|
expect(() => fromValue(reefFunction)).toThrow(/Function body at instruction: 10/)
|
|
expect(() => fromValue(reefFunction)).toThrow(/Called from:/)
|
|
})
|