340 lines
7.2 KiB
TypeScript
340 lines
7.2 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 })
|
|
})
|