forked from defunkt/ReefVM
native functions
This commit is contained in:
parent
0a4e6ceef6
commit
e16a8104c7
|
|
@ -70,14 +70,14 @@ It's where Shrimp live.
|
|||
- [x] DICT_HAS
|
||||
|
||||
### TypeScript Interop
|
||||
- [ ] CALL_TYPESCRIPT
|
||||
- [x] CALL_TYPESCRIPT
|
||||
|
||||
### Special
|
||||
- [x] HALT
|
||||
|
||||
## Test Status
|
||||
|
||||
✅ **58 tests passing** covering:
|
||||
✅ **66 tests passing** covering:
|
||||
- All stack operations (PUSH, POP, DUP)
|
||||
- All arithmetic operations (ADD, SUB, MUL, DIV, MOD)
|
||||
- All comparison operations (EQ, NEQ, LT, GT, LTE, GTE)
|
||||
|
|
@ -88,6 +88,7 @@ It's where Shrimp live.
|
|||
- All dictionary operations (MAKE_DICT, DICT_GET, DICT_SET, DICT_HAS)
|
||||
- Basic function operations (MAKE_FUNCTION, CALL, RETURN) with parameter binding
|
||||
- Exception handling (PUSH_TRY, PUSH_FINALLY, POP_TRY, THROW) with nested try/finally blocks and call stack unwinding
|
||||
- TypeScript interop (CALL_TYPESCRIPT) with sync and async functions
|
||||
- HALT instruction
|
||||
|
||||
## Design Decisions
|
||||
|
|
@ -98,4 +99,3 @@ It's where Shrimp live.
|
|||
|
||||
🚧 **Still TODO**:
|
||||
- Advanced function features (TAIL_CALL, variadic params, kwargs, default parameters)
|
||||
- TypeScript interop (CALL_TYPESCRIPT)
|
||||
29
src/vm.ts
29
src/vm.ts
|
|
@ -5,6 +5,8 @@ import { OpCode } from "./opcode"
|
|||
import { Scope } from "./scope"
|
||||
import { type Value, toValue, toNumber, isTrue, isEqual, toString } from "./value"
|
||||
|
||||
type TypeScriptFunction = (...args: Value[]) => Promise<Value> | Value
|
||||
|
||||
export class VM {
|
||||
pc = 0
|
||||
stopped = false
|
||||
|
|
@ -14,6 +16,7 @@ export class VM {
|
|||
scope: Scope
|
||||
constants: Constant[] = []
|
||||
instructions: Instruction[] = []
|
||||
typescriptFunctions: Map<string, TypeScriptFunction> = new Map()
|
||||
|
||||
constructor(bytecode: Bytecode) {
|
||||
this.instructions = bytecode.instructions
|
||||
|
|
@ -21,6 +24,10 @@ export class VM {
|
|||
this.scope = new Scope()
|
||||
}
|
||||
|
||||
registerFunction(name: string, fn: TypeScriptFunction) {
|
||||
this.typescriptFunctions.set(name, fn)
|
||||
}
|
||||
|
||||
async run(): Promise<Value> {
|
||||
this.pc = 0
|
||||
this.stopped = false
|
||||
|
|
@ -405,6 +412,28 @@ export class VM {
|
|||
this.stack.push(returnValue)
|
||||
break
|
||||
|
||||
case OpCode.CALL_TYPESCRIPT:
|
||||
const functionName = instruction.operand as string
|
||||
const tsFunction = this.typescriptFunctions.get(functionName)
|
||||
|
||||
if (!tsFunction)
|
||||
throw new Error(`CALL_TYPESCRIPT: function not found: ${functionName}`)
|
||||
|
||||
// Mark current frame as break target (like CALL does)
|
||||
if (this.callStack.length > 0)
|
||||
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
|
||||
|
||||
// Pop all arguments from stack (TypeScript function consumes entire stack)
|
||||
const tsArgs = [...this.stack]
|
||||
this.stack = []
|
||||
|
||||
// Call the TypeScript function and await if necessary
|
||||
const tsResult = await tsFunction(...tsArgs)
|
||||
|
||||
// Push result back onto stack
|
||||
this.stack.push(tsResult)
|
||||
break
|
||||
|
||||
default:
|
||||
throw `Unknown op: ${instruction.op}`
|
||||
}
|
||||
|
|
|
|||
182
tests/typescript-interop.test.ts
Normal file
182
tests/typescript-interop.test.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { test, expect } from "bun:test"
|
||||
import { VM } from "#vm"
|
||||
import { OpCode } from "#opcode"
|
||||
import { toValue, toNumber } from "#value"
|
||||
|
||||
test("CALL_TYPESCRIPT - basic function call", async () => {
|
||||
const vm = new VM({
|
||||
instructions: [
|
||||
{ op: OpCode.PUSH, operand: 0 }, // push 5
|
||||
{ op: OpCode.PUSH, operand: 1 }, // push 10
|
||||
{ op: OpCode.CALL_TYPESCRIPT, operand: 'add' }, // call TypeScript 'add'
|
||||
{ op: OpCode.HALT }
|
||||
],
|
||||
constants: [
|
||||
toValue(5),
|
||||
toValue(10)
|
||||
]
|
||||
})
|
||||
|
||||
// Register a TypeScript function
|
||||
vm.registerFunction('add', (a, b) => {
|
||||
return toValue(toNumber(a) + toNumber(b))
|
||||
})
|
||||
|
||||
const result = await vm.run()
|
||||
expect(result).toEqual({ type: 'number', value: 15 })
|
||||
})
|
||||
|
||||
test("CALL_TYPESCRIPT - function with string manipulation", async () => {
|
||||
const vm = new VM({
|
||||
instructions: [
|
||||
{ op: OpCode.PUSH, operand: 0 }, // push "hello"
|
||||
{ op: OpCode.PUSH, operand: 1 }, // push "world"
|
||||
{ op: OpCode.CALL_TYPESCRIPT, operand: 'concat' }, // call TypeScript 'concat'
|
||||
{ op: OpCode.HALT }
|
||||
],
|
||||
constants: [
|
||||
toValue("hello"),
|
||||
toValue("world")
|
||||
]
|
||||
})
|
||||
|
||||
vm.registerFunction('concat', (a, b) => {
|
||||
const aStr = a.type === 'string' ? a.value : String(a.value)
|
||||
const bStr = b.type === 'string' ? b.value : String(b.value)
|
||||
return toValue(aStr + ' ' + bStr)
|
||||
})
|
||||
|
||||
const result = await vm.run()
|
||||
expect(result).toEqual({ type: 'string', value: 'hello world' })
|
||||
})
|
||||
|
||||
test("CALL_TYPESCRIPT - async function", async () => {
|
||||
const vm = new VM({
|
||||
instructions: [
|
||||
{ op: OpCode.PUSH, operand: 0 }, // push 42
|
||||
{ op: OpCode.CALL_TYPESCRIPT, operand: 'asyncDouble' }, // call async TypeScript function
|
||||
{ op: OpCode.HALT }
|
||||
],
|
||||
constants: [
|
||||
toValue(42)
|
||||
]
|
||||
})
|
||||
|
||||
vm.registerFunction('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("CALL_TYPESCRIPT - function with no arguments", async () => {
|
||||
const vm = new VM({
|
||||
instructions: [
|
||||
{ op: OpCode.CALL_TYPESCRIPT, operand: 'getAnswer' }, // call with empty stack
|
||||
{ op: OpCode.HALT }
|
||||
],
|
||||
constants: []
|
||||
})
|
||||
|
||||
vm.registerFunction('getAnswer', () => {
|
||||
return toValue(42)
|
||||
})
|
||||
|
||||
const result = await vm.run()
|
||||
expect(result).toEqual({ type: 'number', value: 42 })
|
||||
})
|
||||
|
||||
test("CALL_TYPESCRIPT - function with multiple arguments", async () => {
|
||||
const vm = new VM({
|
||||
instructions: [
|
||||
{ op: OpCode.PUSH, operand: 0 }, // push 2
|
||||
{ op: OpCode.PUSH, operand: 1 }, // push 3
|
||||
{ op: OpCode.PUSH, operand: 2 }, // push 4
|
||||
{ op: OpCode.CALL_TYPESCRIPT, operand: 'sum' }, // call TypeScript 'sum'
|
||||
{ op: OpCode.HALT }
|
||||
],
|
||||
constants: [
|
||||
toValue(2),
|
||||
toValue(3),
|
||||
toValue(4)
|
||||
]
|
||||
})
|
||||
|
||||
vm.registerFunction('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("CALL_TYPESCRIPT - function returns array", async () => {
|
||||
const vm = new VM({
|
||||
instructions: [
|
||||
{ op: OpCode.PUSH, operand: 0 }, // push 3
|
||||
{ op: OpCode.CALL_TYPESCRIPT, operand: 'makeRange' }, // call TypeScript 'makeRange'
|
||||
{ op: OpCode.HALT }
|
||||
],
|
||||
constants: [
|
||||
toValue(3)
|
||||
]
|
||||
})
|
||||
|
||||
vm.registerFunction('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("CALL_TYPESCRIPT - function not found", async () => {
|
||||
const vm = new VM({
|
||||
instructions: [
|
||||
{ op: OpCode.CALL_TYPESCRIPT, operand: 'nonexistent' }
|
||||
],
|
||||
constants: []
|
||||
})
|
||||
|
||||
await expect(vm.run()).rejects.toThrow('CALL_TYPESCRIPT: function not found: nonexistent')
|
||||
})
|
||||
|
||||
test("CALL_TYPESCRIPT - using result in subsequent operations", async () => {
|
||||
const vm = new VM({
|
||||
instructions: [
|
||||
{ op: OpCode.PUSH, operand: 0 }, // push 5
|
||||
{ op: OpCode.CALL_TYPESCRIPT, operand: 'triple' }, // call TypeScript 'triple' -> 15
|
||||
{ op: OpCode.PUSH, operand: 1 }, // push 10
|
||||
{ op: OpCode.ADD }, // 15 + 10 = 25
|
||||
{ op: OpCode.HALT }
|
||||
],
|
||||
constants: [
|
||||
toValue(5),
|
||||
toValue(10)
|
||||
]
|
||||
})
|
||||
|
||||
vm.registerFunction('triple', (n) => {
|
||||
return toValue(toNumber(n) * 3)
|
||||
})
|
||||
|
||||
const result = await vm.run()
|
||||
expect(result).toEqual({ type: 'number', value: 25 })
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user