native functions

This commit is contained in:
Chris Wanstrath 2025-10-05 18:01:19 -07:00
parent 0a4e6ceef6
commit e16a8104c7
3 changed files with 215 additions and 4 deletions

View File

@ -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)

View File

@ -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}`
} }

View 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 })
})