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
|
- [x] DICT_HAS
|
||||||
|
|
||||||
### TypeScript Interop
|
### TypeScript Interop
|
||||||
- [ ] CALL_TYPESCRIPT
|
- [x] CALL_TYPESCRIPT
|
||||||
|
|
||||||
### Special
|
### Special
|
||||||
- [x] HALT
|
- [x] HALT
|
||||||
|
|
||||||
## Test Status
|
## Test Status
|
||||||
|
|
||||||
✅ **58 tests passing** covering:
|
✅ **66 tests passing** covering:
|
||||||
- All stack operations (PUSH, POP, DUP)
|
- All stack operations (PUSH, POP, DUP)
|
||||||
- All arithmetic operations (ADD, SUB, MUL, DIV, MOD)
|
- All arithmetic operations (ADD, SUB, MUL, DIV, MOD)
|
||||||
- All comparison operations (EQ, NEQ, LT, GT, LTE, GTE)
|
- 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)
|
- All dictionary operations (MAKE_DICT, DICT_GET, DICT_SET, DICT_HAS)
|
||||||
- Basic function operations (MAKE_FUNCTION, CALL, RETURN) with parameter binding
|
- 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
|
- 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
|
- HALT instruction
|
||||||
|
|
||||||
## Design Decisions
|
## Design Decisions
|
||||||
|
|
@ -97,5 +98,4 @@ It's where Shrimp live.
|
||||||
- **Short-circuiting via compiler**: No AND/OR opcodes—compilers use JUMP patterns for proper short-circuit evaluation
|
- **Short-circuiting via compiler**: No AND/OR opcodes—compilers use JUMP patterns for proper short-circuit evaluation
|
||||||
|
|
||||||
🚧 **Still TODO**:
|
🚧 **Still TODO**:
|
||||||
- Advanced function features (TAIL_CALL, variadic params, kwargs, default parameters)
|
- 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 { Scope } from "./scope"
|
||||||
import { type Value, toValue, toNumber, isTrue, isEqual, toString } from "./value"
|
import { type Value, toValue, toNumber, isTrue, isEqual, toString } from "./value"
|
||||||
|
|
||||||
|
type TypeScriptFunction = (...args: Value[]) => Promise<Value> | Value
|
||||||
|
|
||||||
export class VM {
|
export class VM {
|
||||||
pc = 0
|
pc = 0
|
||||||
stopped = false
|
stopped = false
|
||||||
|
|
@ -14,6 +16,7 @@ export class VM {
|
||||||
scope: Scope
|
scope: Scope
|
||||||
constants: Constant[] = []
|
constants: Constant[] = []
|
||||||
instructions: Instruction[] = []
|
instructions: Instruction[] = []
|
||||||
|
typescriptFunctions: Map<string, TypeScriptFunction> = new Map()
|
||||||
|
|
||||||
constructor(bytecode: Bytecode) {
|
constructor(bytecode: Bytecode) {
|
||||||
this.instructions = bytecode.instructions
|
this.instructions = bytecode.instructions
|
||||||
|
|
@ -21,6 +24,10 @@ export class VM {
|
||||||
this.scope = new Scope()
|
this.scope = new Scope()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerFunction(name: string, fn: TypeScriptFunction) {
|
||||||
|
this.typescriptFunctions.set(name, fn)
|
||||||
|
}
|
||||||
|
|
||||||
async run(): Promise<Value> {
|
async run(): Promise<Value> {
|
||||||
this.pc = 0
|
this.pc = 0
|
||||||
this.stopped = false
|
this.stopped = false
|
||||||
|
|
@ -405,6 +412,28 @@ export class VM {
|
||||||
this.stack.push(returnValue)
|
this.stack.push(returnValue)
|
||||||
break
|
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:
|
default:
|
||||||
throw `Unknown op: ${instruction.op}`
|
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