diff --git a/README.md b/README.md index fb07101..dd2b2c3 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 🚧 **Still TODO**: -- Advanced function features (TAIL_CALL, variadic params, kwargs, default parameters) -- TypeScript interop (CALL_TYPESCRIPT) \ No newline at end of file +- Advanced function features (TAIL_CALL, variadic params, kwargs, default parameters) \ No newline at end of file diff --git a/src/vm.ts b/src/vm.ts index 552a997..bf21539 100644 --- a/src/vm.ts +++ b/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 + export class VM { pc = 0 stopped = false @@ -14,6 +16,7 @@ export class VM { scope: Scope constants: Constant[] = [] instructions: Instruction[] = [] + typescriptFunctions: Map = 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 { 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}` } diff --git a/tests/typescript-interop.test.ts b/tests/typescript-interop.test.ts new file mode 100644 index 0000000..8067516 --- /dev/null +++ b/tests/typescript-interop.test.ts @@ -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 }) +})