ReefVM/tests/native.test.ts

624 lines
14 KiB
TypeScript

import { test, expect } from "bun:test"
import { VM } from "#vm"
import { toBytecode } from "#bytecode"
import { toValue, toNumber, toString } from "#value"
test("LOAD - basic function call", async () => {
const bytecode = toBytecode(`
LOAD add
PUSH 5
PUSH 10
PUSH 2
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Register a Value-based function
vm.registerValueFunction('add', (a, b) => {
return toValue(toNumber(a) + toNumber(b))
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 15 })
})
test("LOAD - function with string manipulation", async () => {
const bytecode = toBytecode(`
LOAD concat
PUSH "hello"
PUSH "world"
PUSH 2
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerValueFunction('concat', (a, b) => {
const aStr = a.type === 'string' ? a.value : toString(a)
const bStr = b.type === 'string' ? b.value : toString(b)
return toValue(aStr + ' ' + bStr)
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'hello world' })
})
test("LOAD - async function", async () => {
const bytecode = toBytecode(`
LOAD asyncDouble
PUSH 42
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerValueFunction('asyncDouble', async (a) => {
// Simulate async operation
await new Promise(resolve => setTimeout(resolve, 1))
return toValue(toNumber(a) * 2)
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 84 })
})
test("LOAD - function with no arguments", async () => {
const bytecode = toBytecode(`
LOAD getAnswer
PUSH 0
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerValueFunction('getAnswer', () => {
return toValue(42)
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 42 })
})
test("LOAD - function with multiple arguments", async () => {
const bytecode = toBytecode(`
LOAD sum
PUSH 2
PUSH 3
PUSH 4
PUSH 3
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerValueFunction('sum', (...args) => {
const total = args.reduce((acc, val) => acc + toNumber(val), 0)
return toValue(total)
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 9 })
})
test("LOAD - function returns array", async () => {
const bytecode = toBytecode(`
LOAD makeRange
PUSH 3
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerValueFunction('makeRange', (n) => {
const count = toNumber(n)
const arr = []
for (let i = 0; i < count; i++) {
arr.push(toValue(i))
}
return { type: 'array', value: arr }
})
const result = await vm.run()
expect(result.type).toBe('array')
if (result.type === 'array') {
expect(result.value.length).toBe(3)
expect(result.value).toEqual([
toValue(0),
toValue(1),
toValue(2)
])
}
})
test("LOAD - function not found", async () => {
const bytecode = toBytecode(`
LOAD nonexistent
PUSH 0
PUSH 0
CALL
`)
const vm = new VM(bytecode)
expect(vm.run()).rejects.toThrow('Undefined variable: nonexistent')
})
test("LOAD - using result in subsequent operations", async () => {
const bytecode = toBytecode(`
LOAD triple
PUSH 5
PUSH 1
PUSH 0
CALL
PUSH 10
ADD
`)
const vm = new VM(bytecode)
vm.registerValueFunction('triple', (n) => {
return toValue(toNumber(n) * 3)
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 25 })
})
test("Native function wrapping - basic sync function with native types", async () => {
const bytecode = toBytecode(`
LOAD add
PUSH 5
PUSH 10
PUSH 2
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Register with native TypeScript types - auto-wraps!
vm.registerFunction('add', (a: number, b: number) => {
return a + b
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 15 })
})
test("Native function wrapping - async function with native types", async () => {
const bytecode = toBytecode(`
LOAD asyncDouble
PUSH 42
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Async native function
vm.registerFunction('asyncDouble', async (n: number) => {
await new Promise(resolve => setTimeout(resolve, 1))
return n * 2
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 84 })
})
test("Native function wrapping - string manipulation", async () => {
const bytecode = toBytecode(`
LOAD concat
PUSH "hello"
PUSH "world"
PUSH 2
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Native string function
vm.registerFunction('concat', (a: string, b: string) => {
return a + ' ' + b
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'hello world' })
})
test("Native function wrapping - with default parameters", async () => {
const bytecode = toBytecode(`
LOAD ls
PUSH "/home/user"
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Function with default parameter (like NOSE commands)
vm.registerFunction('ls', (path: string, link = false) => {
return link ? `listing ${path} with links` : `listing ${path}`
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'listing /home/user' })
})
test("Native function wrapping - returns array", async () => {
const bytecode = toBytecode(`
LOAD makeRange
PUSH 3
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Return native array - auto-converts to Value array
vm.registerFunction('makeRange', (n: number) => {
return Array.from({ length: n }, (_, i) => i)
})
const result = await vm.run()
expect(result.type).toBe('array')
if (result.type === 'array') {
expect(result.value.length).toBe(3)
expect(result.value).toEqual([
toValue(0),
toValue(1),
toValue(2)
])
}
})
test("Native function wrapping - returns object (becomes dict)", async () => {
const bytecode = toBytecode(`
LOAD makeUser
PUSH "Alice"
PUSH 30
PUSH 2
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Return plain object - auto-converts to dict
vm.registerFunction('makeUser', (name: string, age: number) => {
return { name, age }
})
const result = await vm.run()
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.get('name')).toEqual(toValue('Alice'))
expect(result.value.get('age')).toEqual(toValue(30))
}
})
test("Native function wrapping - mixed with manual Value functions", async () => {
const bytecode = toBytecode(`
LOAD nativeAdd
PUSH 5
PUSH 1
PUSH 0
CALL
STORE sum
LOAD manualDouble
LOAD sum
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
// Native function (auto-wrapped by registerFunction)
vm.registerFunction('nativeAdd', (n: number) => n + 10)
// Manual Value function (use registerValueFunction)
vm.registerValueFunction('manualDouble', (v) => {
return toValue(toNumber(v) * 2)
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 30 })
})
test("Named arguments - basic named arg", async () => {
const bytecode = toBytecode(`
LOAD greet
PUSH "name"
PUSH "Alice"
PUSH 0
PUSH 1
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('greet', (name: string) => `Hello, ${name}!`)
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'Hello, Alice!' })
})
test("Named arguments - mixed positional and named", async () => {
const bytecode = toBytecode(`
LOAD makeUser
PUSH "Alice"
PUSH "age"
PUSH 30
PUSH 1
PUSH 1
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('makeUser', (name: string, age: number) => {
return { name, age }
})
const result = await vm.run()
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.get('name')).toEqual(toValue('Alice'))
expect(result.value.get('age')).toEqual(toValue(30))
}
})
test("Named arguments - named takes priority over positional", async () => {
const bytecode = toBytecode(`
LOAD add
PUSH 100
PUSH "a"
PUSH 5
PUSH "b"
PUSH 10
PUSH 1
PUSH 2
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('add', (a: number, b: number) => a + b)
const result = await vm.run()
// Named args should be: a=5, b=10
// Positional arg (100) is provided but named args take priority
expect(result).toEqual({ type: 'number', value: 15 })
})
test("Named arguments - with defaults", async () => {
const bytecode = toBytecode(`
LOAD greet
PUSH "name"
PUSH "Bob"
PUSH 0
PUSH 1
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('greet', (name: string, greeting = 'Hello') => {
return `${greeting}, ${name}!`
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'Hello, Bob!' })
})
test("Named arguments - override defaults with named args", async () => {
const bytecode = toBytecode(`
LOAD greet
PUSH "name"
PUSH "Bob"
PUSH "greeting"
PUSH "Hi"
PUSH 0
PUSH 2
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('greet', (name: string, greeting = 'Hello') => {
return `${greeting}, ${name}!`
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'Hi, Bob!' })
})
test("Named arguments - with variadic function", async () => {
const bytecode = toBytecode(`
LOAD sum
PUSH 1
PUSH 2
PUSH 3
PUSH "multiplier"
PUSH 2
PUSH 3
PUSH 1
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('sum', (multiplier: number, ...nums: number[]) => {
const total = nums.reduce((acc, n) => acc + n, 0)
return total * multiplier
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 12 }) // (1 + 2 + 3) * 2
})
test("Named arguments - works with both wrapped and non-wrapped functions", async () => {
const bytecode = toBytecode(`
; Test wrapped function (registerFunction)
LOAD wrappedAdd
PUSH "a"
PUSH 5
PUSH "b"
PUSH 10
PUSH 0
PUSH 2
CALL
STORE result1
; Test non-wrapped function (registerValueFunction)
LOAD valueAdd
PUSH "a"
PUSH 3
PUSH "b"
PUSH 7
PUSH 0
PUSH 2
CALL
STORE result2
; Return both results
LOAD result1
LOAD result2
ADD
`)
const vm = new VM(bytecode)
// Wrapped function - auto-converts types
vm.registerFunction('wrappedAdd', (a: number, b: number) => a + b)
// Non-wrapped function - works directly with Values
vm.registerValueFunction('valueAdd', (a, b) => {
return toValue(toNumber(a) + toNumber(b))
})
const result = await vm.run()
expect(result).toEqual({ type: 'number', value: 25 }) // 15 + 10
})
test("@named pattern - basic atNamed parameter", async () => {
const bytecode = toBytecode(`
LOAD greet
PUSH "name"
PUSH "Alice"
PUSH "greeting"
PUSH "Hi"
PUSH "extra"
PUSH "value"
PUSH 0
PUSH 3
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('greet', (atNamed: any = {}) => {
const name = atNamed.name || 'Unknown'
const greeting = atNamed.greeting || 'Hello'
const extra = atNamed.extra || ''
return `${greeting}, ${name}! Extra: ${extra}`
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'Hi, Alice! Extra: value' })
})
test("@named pattern - mixed positional and atOptions", async () => {
const bytecode = toBytecode(`
LOAD configure
PUSH "app"
PUSH "debug"
PUSH true
PUSH "port"
PUSH 8080
PUSH 1
PUSH 2
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('configure', (name: string, atOptions: any = {}) => {
return {
name,
debug: atOptions.debug || false,
port: atOptions.port || 3000
}
})
const result = await vm.run()
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.get('name')).toEqual(toValue('app'))
expect(result.value.get('debug')).toEqual(toValue(true))
expect(result.value.get('port')).toEqual(toValue(8080))
}
})
test("@named pattern - with default empty object", async () => {
const bytecode = toBytecode(`
LOAD build
PUSH "project"
PUSH 1
PUSH 0
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('build', (name: string, atConfig: any = {}) => {
return `Building ${name} with ${Object.keys(atConfig).length} options`
})
const result = await vm.run()
expect(result).toEqual({ type: 'string', value: 'Building project with 0 options' })
})
test("@named pattern - collects only unmatched named args", async () => {
const bytecode = toBytecode(`
LOAD process
PUSH "file"
PUSH "test.txt"
PUSH "mode"
PUSH "read"
PUSH "extra1"
PUSH "value1"
PUSH "extra2"
PUSH "value2"
PUSH 0
PUSH 4
CALL
`)
const vm = new VM(bytecode)
vm.registerFunction('process', (file: string, mode: string, atOptions: any = {}) => {
// file and mode are matched, extra1 and extra2 go to atOptions
return {
file,
mode,
optionCount: Object.keys(atOptions).length,
extra1: atOptions.extra1,
extra2: atOptions.extra2
}
})
const result = await vm.run()
expect(result.type).toBe('dict')
if (result.type === 'dict') {
expect(result.value.get('file')).toEqual(toValue('test.txt'))
expect(result.value.get('mode')).toEqual(toValue('read'))
expect(result.value.get('optionCount')).toEqual(toValue(2))
expect(result.value.get('extra1')).toEqual(toValue('value1'))
expect(result.value.get('extra2')).toEqual(toValue('value2'))
}
})