713 lines
23 KiB
TypeScript
713 lines
23 KiB
TypeScript
import type { Bytecode, Constant, Instruction } from "./bytecode"
|
|
import type { ExceptionHandler } from "./exception"
|
|
import { type Frame } from "./frame"
|
|
import { OpCode } from "./opcode"
|
|
import { Scope } from "./scope"
|
|
import type { Value, NativeFunction, TypeScriptFunction } from "./value"
|
|
import { toValue, toNumber, isTrue, isEqual, toString, fromValue, fnFromValue } from "./value"
|
|
import { extractParamInfo, wrapNative, isWrapped, getOriginalFunction } from "./function"
|
|
|
|
export class VM {
|
|
pc = 0
|
|
stopped = false
|
|
stack: Value[] = []
|
|
callStack: Frame[] = []
|
|
exceptionHandlers: ExceptionHandler[] = []
|
|
scope: Scope
|
|
constants: Constant[] = []
|
|
instructions: Instruction[] = []
|
|
labels: Map<number, string> = new Map()
|
|
nativeFunctions: Map<string, NativeFunction> = new Map()
|
|
|
|
constructor(bytecode: Bytecode, functions?: Record<string, TypeScriptFunction>) {
|
|
this.instructions = bytecode.instructions
|
|
this.constants = bytecode.constants
|
|
this.labels = bytecode.labels || new Map()
|
|
this.scope = new Scope()
|
|
|
|
if (functions)
|
|
for (const name of Object.keys(functions))
|
|
this.registerFunction(name, functions[name]!)
|
|
}
|
|
|
|
async call(name: string, ...args: any) {
|
|
const value = this.scope.get(name)
|
|
|
|
if (!value) throw new Error(`Can't find ${name}`)
|
|
if (value.type !== 'function' && value.type !== 'native') throw new Error(`Can't call ${name}`)
|
|
|
|
if (value.type === 'native') {
|
|
return await this.callNative(value.fn, args)
|
|
} else {
|
|
const fn = fnFromValue(value, this)
|
|
return await fn(...args)
|
|
}
|
|
}
|
|
|
|
registerFunction(name: string, fn: TypeScriptFunction) {
|
|
const wrapped = isWrapped(fn) ? fn as NativeFunction : wrapNative(this, fn)
|
|
this.scope.set(name, { type: 'native', fn: wrapped, value: '<function>' })
|
|
}
|
|
|
|
registerValueFunction(name: string, fn: NativeFunction) {
|
|
this.scope.set(name, { type: 'native', fn, value: '<function>' })
|
|
}
|
|
|
|
async run(): Promise<Value> {
|
|
this.pc = 0
|
|
this.stopped = false
|
|
|
|
while (!this.stopped && this.pc < this.instructions.length) {
|
|
const instruction = this.instructions[this.pc]!
|
|
await this.execute(instruction)
|
|
this.pc++
|
|
}
|
|
|
|
return this.stack[this.stack.length - 1] || toValue(null)
|
|
}
|
|
|
|
async execute(instruction: Instruction) /* throws */ {
|
|
switch (instruction.op) {
|
|
case OpCode.PUSH:
|
|
const constIdx = instruction.operand as number
|
|
const constant = this.constants[constIdx]
|
|
|
|
if (!constant || constant.type === 'function_def')
|
|
throw new Error(`Invalid constant index: ${constIdx}`)
|
|
|
|
this.stack.push(constant)
|
|
break
|
|
|
|
case OpCode.POP:
|
|
this.stack.pop()
|
|
break
|
|
|
|
case OpCode.DUP:
|
|
this.stack.push(this.stack[this.stack.length - 1]!)
|
|
break
|
|
|
|
case OpCode.ADD:
|
|
this.binaryOp((a, b) => toNumber(a) + toNumber(b))
|
|
break
|
|
|
|
case OpCode.SUB:
|
|
this.binaryOp((a, b) => toNumber(a) - toNumber(b))
|
|
break
|
|
|
|
case OpCode.MUL:
|
|
this.binaryOp((a, b) => toNumber(a) * toNumber(b))
|
|
break
|
|
|
|
case OpCode.DIV:
|
|
this.binaryOp((a, b) => toNumber(a) / toNumber(b))
|
|
break
|
|
|
|
case OpCode.MOD:
|
|
this.binaryOp((a, b) => toNumber(a) % toNumber(b))
|
|
break
|
|
|
|
case OpCode.EQ:
|
|
this.comparisonOp((a, b) => isEqual(a, b))
|
|
break
|
|
|
|
case OpCode.NEQ:
|
|
this.comparisonOp((a, b) => !isEqual(a, b))
|
|
break
|
|
|
|
case OpCode.LT:
|
|
this.comparisonOp((a, b) => toNumber(a) < toNumber(b))
|
|
break
|
|
|
|
case OpCode.GT:
|
|
this.comparisonOp((a, b) => toNumber(a) > toNumber(b))
|
|
break
|
|
|
|
case OpCode.LTE:
|
|
this.comparisonOp((a, b) => toNumber(a) <= toNumber(b))
|
|
break
|
|
|
|
case OpCode.GTE:
|
|
this.comparisonOp((a, b) => toNumber(a) >= toNumber(b))
|
|
break
|
|
|
|
case OpCode.NOT:
|
|
const val = this.stack.pop()!
|
|
this.stack.push({ type: 'boolean', value: !isTrue(val) })
|
|
break
|
|
|
|
case OpCode.HALT:
|
|
this.stopped = true
|
|
break
|
|
|
|
case OpCode.LOAD: {
|
|
const varName = instruction.operand as string
|
|
const value = this.scope.get(varName)
|
|
|
|
if (value === undefined)
|
|
throw new Error(`Undefined variable: ${varName}`)
|
|
|
|
this.stack.push(value)
|
|
break
|
|
}
|
|
|
|
case OpCode.TRY_LOAD: {
|
|
const varName = instruction.operand as string
|
|
const value = this.scope.get(varName)
|
|
|
|
if (value === undefined)
|
|
this.stack.push(toValue(varName))
|
|
else
|
|
this.stack.push(value)
|
|
|
|
break
|
|
}
|
|
|
|
case OpCode.STORE:
|
|
const name = instruction.operand as string
|
|
const toStore = this.stack.pop()!
|
|
this.scope.set(name, toStore)
|
|
break
|
|
|
|
case OpCode.JUMP:
|
|
this.pc += (instruction.operand as number)
|
|
break
|
|
|
|
case OpCode.JUMP_IF_FALSE:
|
|
const cond = this.stack.pop()!
|
|
if (!isTrue(cond))
|
|
this.pc += (instruction.operand as number)
|
|
break
|
|
|
|
case OpCode.JUMP_IF_TRUE:
|
|
const condTrue = this.stack.pop()!
|
|
if (isTrue(condTrue))
|
|
this.pc += (instruction.operand as number)
|
|
break
|
|
|
|
case OpCode.BREAK:
|
|
// Unwind call stack until we find a frame marked as break target
|
|
let foundBreakTarget = false
|
|
while (this.callStack.length) {
|
|
const frame = this.callStack.pop()!
|
|
this.scope = frame.returnScope
|
|
this.pc = frame.returnAddress
|
|
if (frame.isBreakTarget) {
|
|
foundBreakTarget = true
|
|
break
|
|
}
|
|
}
|
|
if (!foundBreakTarget)
|
|
throw new Error('BREAK: no break target found')
|
|
break
|
|
|
|
case OpCode.PUSH_TRY:
|
|
const catchAddress = instruction.operand as number
|
|
this.exceptionHandlers.push({
|
|
catchAddress,
|
|
callStackDepth: this.callStack.length,
|
|
scope: this.scope
|
|
})
|
|
break
|
|
|
|
case OpCode.PUSH_FINALLY:
|
|
const finallyAddress = instruction.operand as number
|
|
if (this.exceptionHandlers.length === 0)
|
|
throw new Error('PUSH_FINALLY: no exception handler to modify')
|
|
|
|
this.exceptionHandlers[this.exceptionHandlers.length - 1]!.finallyAddress = finallyAddress
|
|
break
|
|
|
|
case OpCode.POP_TRY:
|
|
if (this.exceptionHandlers.length === 0)
|
|
throw new Error('POP_TRY: no exception handler to pop')
|
|
|
|
this.exceptionHandlers.pop()
|
|
break
|
|
|
|
case OpCode.THROW:
|
|
const errorValue = this.stack.pop()!
|
|
|
|
if (this.exceptionHandlers.length === 0) {
|
|
const errorMsg = toString(errorValue)
|
|
throw new Error(`Uncaught exception: ${errorMsg}`)
|
|
}
|
|
|
|
const throwHandler = this.exceptionHandlers.pop()!
|
|
|
|
while (this.callStack.length > throwHandler.callStackDepth)
|
|
this.callStack.pop()
|
|
|
|
this.scope = throwHandler.scope
|
|
|
|
this.stack.push(errorValue)
|
|
|
|
// Jump to finally if present, otherwise jump to catch
|
|
const targetAddress = throwHandler.finallyAddress !== undefined
|
|
? throwHandler.finallyAddress
|
|
: throwHandler.catchAddress
|
|
|
|
// subtract 1 because pc will be incremented
|
|
this.pc = targetAddress - 1
|
|
break
|
|
|
|
case OpCode.MAKE_ARRAY:
|
|
const arraySize = instruction.operand as number
|
|
const items: Value[] = []
|
|
|
|
for (let i = 0; i < arraySize; i++)
|
|
items.unshift(this.stack.pop()!)
|
|
|
|
this.stack.push({ type: 'array', value: items })
|
|
break
|
|
|
|
case OpCode.ARRAY_GET:
|
|
const index = this.stack.pop()!
|
|
const array = this.stack.pop()!
|
|
|
|
if (array.type !== 'array')
|
|
throw new Error('ARRAY_GET: not an array')
|
|
|
|
const idx = Math.floor(toNumber(index))
|
|
if (idx < 0 || idx >= array.value.length)
|
|
throw new Error(`ARRAY_GET: index ${idx} out of bounds`)
|
|
|
|
this.stack.push(array.value[idx]!)
|
|
break
|
|
|
|
case OpCode.ARRAY_SET:
|
|
const setValue = this.stack.pop()!
|
|
const setIndex = this.stack.pop()!
|
|
const setArray = this.stack.pop()!
|
|
|
|
if (setArray.type !== 'array')
|
|
throw new Error('ARRAY_SET: not an array')
|
|
|
|
const setIdx = Math.floor(toNumber(setIndex))
|
|
if (setIdx < 0 || setIdx >= setArray.value.length)
|
|
throw new Error(`ARRAY_SET: index ${setIdx} out of bounds`)
|
|
|
|
setArray.value[setIdx] = setValue
|
|
break
|
|
|
|
case OpCode.ARRAY_PUSH:
|
|
const pushValue = this.stack.pop()!
|
|
const pushArray = this.stack.pop()!
|
|
|
|
if (pushArray.type !== 'array')
|
|
throw new Error('ARRAY_PUSH: not an array')
|
|
|
|
pushArray.value.push(pushValue)
|
|
break
|
|
|
|
case OpCode.ARRAY_LEN:
|
|
const lenArray = this.stack.pop()!
|
|
|
|
if (lenArray.type !== 'array')
|
|
throw new Error('ARRAY_LEN: not an array')
|
|
|
|
this.stack.push({ type: 'number', value: lenArray.value.length })
|
|
break
|
|
|
|
case OpCode.MAKE_DICT:
|
|
const dictPairs = instruction.operand as number
|
|
const dict = new Map<string, Value>()
|
|
|
|
for (let i = 0; i < dictPairs; i++) {
|
|
const value = this.stack.pop()!
|
|
const key = this.stack.pop()!
|
|
dict.set(toString(key), value)
|
|
}
|
|
|
|
this.stack.push({ type: 'dict', value: dict })
|
|
break
|
|
|
|
case OpCode.DICT_GET:
|
|
const getKey = this.stack.pop()!
|
|
const getDict = this.stack.pop()!
|
|
|
|
if (getDict.type !== 'dict')
|
|
throw new Error('DICT_GET: not a dict')
|
|
|
|
this.stack.push(getDict.value.get(toString(getKey)) || toValue(null))
|
|
break
|
|
|
|
case OpCode.DICT_SET:
|
|
const dictSetValue = this.stack.pop()!
|
|
const dictSetKey = this.stack.pop()!
|
|
const dictSet = this.stack.pop()!
|
|
|
|
if (dictSet.type !== 'dict')
|
|
throw new Error('DICT_SET: not a dict')
|
|
|
|
dictSet.value.set(toString(dictSetKey), dictSetValue)
|
|
break
|
|
|
|
case OpCode.DICT_HAS:
|
|
const hasKey = this.stack.pop()!
|
|
const hasDict = this.stack.pop()!
|
|
|
|
if (hasDict.type !== 'dict')
|
|
throw new Error('DICT_HAS: not a dict')
|
|
|
|
this.stack.push({ type: 'boolean', value: hasDict.value.has(toString(hasKey)) })
|
|
break
|
|
|
|
case OpCode.DOT_GET: {
|
|
const index = this.stack.pop()!
|
|
const target = this.stack.pop()!
|
|
|
|
if (target.type === 'array')
|
|
this.stack.push(toValue(target.value?.[Number(index.value)]))
|
|
|
|
else if (target.type === 'dict')
|
|
this.stack.push(toValue(target.value?.get(String(index.value))))
|
|
|
|
else
|
|
throw new Error(`DOT_GET: ${target.type} not supported`)
|
|
|
|
break
|
|
}
|
|
|
|
case OpCode.STR_CONCAT:
|
|
let count = instruction.operand as number
|
|
let parts = []
|
|
|
|
while (count-- > 0 && this.stack.length)
|
|
parts.unshift(toString(this.stack.pop()!))
|
|
|
|
this.stack.push(toValue(parts.join('')))
|
|
break
|
|
|
|
case OpCode.MAKE_FUNCTION:
|
|
const fnDefIdx = instruction.operand as number
|
|
const fnDef = this.constants[fnDefIdx]
|
|
|
|
if (!fnDef || fnDef.type !== 'function_def')
|
|
throw new Error(`Invalid function definition index: ${fnDefIdx}`)
|
|
|
|
this.stack.push({
|
|
type: 'function',
|
|
params: fnDef.params,
|
|
defaults: fnDef.defaults,
|
|
body: fnDef.body,
|
|
variadic: fnDef.variadic,
|
|
named: fnDef.named,
|
|
parentScope: this.scope,
|
|
value: '<function>'
|
|
})
|
|
break
|
|
|
|
case OpCode.TRY_CALL: {
|
|
const varName = instruction.operand as string
|
|
const value = this.scope.get(varName)
|
|
|
|
if (value?.type === 'function' || value?.type === 'native') {
|
|
this.stack.push(value)
|
|
this.stack.push(toValue(0))
|
|
this.stack.push(toValue(0))
|
|
this.instructions[this.pc] = { op: OpCode.CALL }
|
|
this.pc--
|
|
break
|
|
} else if (value) {
|
|
this.stack.push(value)
|
|
break
|
|
} else {
|
|
this.stack.push(toValue(varName))
|
|
break
|
|
}
|
|
}
|
|
|
|
case OpCode.CALL: {
|
|
// Pop named count from stack (top)
|
|
const namedCount = toNumber(this.stack.pop()!)
|
|
|
|
// Pop positional count from stack
|
|
const positionalCount = toNumber(this.stack.pop()!)
|
|
|
|
// Pop named arguments (name-value pairs) from stack
|
|
// Stack has: ... key1 value1 key2 value2 (top)
|
|
// So we pop value2, key2, value1, key1
|
|
const namedArgs = new Map<string, Value>()
|
|
const namedPairs: Array<{ key: string; value: Value }> = []
|
|
for (let i = 0; i < namedCount; i++) {
|
|
const value = this.stack.pop()!
|
|
const key = this.stack.pop()!
|
|
namedPairs.unshift({ key: toString(key), value })
|
|
}
|
|
for (const pair of namedPairs)
|
|
namedArgs.set(pair.key, pair.value)
|
|
|
|
// Pop positional arguments from stack
|
|
const positionalArgs: Value[] = []
|
|
for (let i = 0; i < positionalCount; i++)
|
|
positionalArgs.unshift(this.stack.pop()!)
|
|
|
|
const fn = this.stack.pop()!
|
|
|
|
// Handle native functions
|
|
if (fn.type === 'native') {
|
|
// Mark current frame as break target (like regular CALL does)
|
|
if (this.callStack.length > 0)
|
|
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
|
|
|
|
// Extract parameter info on-demand
|
|
const originalFn = getOriginalFunction(fn.fn)
|
|
const paramInfo = extractParamInfo(originalFn)
|
|
|
|
// Bind parameters using the same priority as Reef functions
|
|
const nativeArgs: Value[] = []
|
|
|
|
// Determine how many params are fixed (excluding variadic and named)
|
|
let nativeFixedParamCount = paramInfo.params.length
|
|
if (paramInfo.variadic) nativeFixedParamCount--
|
|
if (paramInfo.named) nativeFixedParamCount--
|
|
|
|
// Track which positional args have been consumed
|
|
let nativePositionalArgIndex = 0
|
|
|
|
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
|
for (let i = 0; i < nativeFixedParamCount; i++) {
|
|
const paramName = paramInfo.params[i]!
|
|
|
|
// Check if named argument was provided for this param
|
|
if (namedArgs.has(paramName)) {
|
|
nativeArgs.push(namedArgs.get(paramName)!)
|
|
namedArgs.delete(paramName) // Remove from named args so it won't go to @named
|
|
} else if (nativePositionalArgIndex < positionalArgs.length) {
|
|
nativeArgs.push(positionalArgs[nativePositionalArgIndex]!)
|
|
nativePositionalArgIndex++
|
|
} else if (paramInfo.defaults[paramName] !== undefined) {
|
|
nativeArgs.push(paramInfo.defaults[paramName]!)
|
|
} else {
|
|
nativeArgs.push(toValue(null))
|
|
}
|
|
}
|
|
|
|
// Handle named parameter (collect remaining unmatched named args)
|
|
// Parameter names matching atXxx pattern (e.g., atOptions, atNamed) collect extra named args
|
|
if (paramInfo.named) {
|
|
const namedDict = new Map<string, Value>()
|
|
for (const [key, value] of namedArgs) {
|
|
namedDict.set(key, value)
|
|
}
|
|
// Convert dict to plain JavaScript object for the native function
|
|
const namedObj = fromValue({ type: 'dict', value: namedDict }, this)
|
|
nativeArgs.push(toValue(namedObj))
|
|
}
|
|
|
|
// Handle variadic parameter (TypeScript rest parameters)
|
|
// For TypeScript functions with ...rest, we spread the remaining args
|
|
// rather than wrapping them in an array
|
|
if (paramInfo.variadic) {
|
|
const remainingArgs = positionalArgs.slice(nativePositionalArgIndex)
|
|
nativeArgs.push(...remainingArgs)
|
|
}
|
|
|
|
// Call the native function with bound args
|
|
const result = await fn.fn.call(this, ...nativeArgs)
|
|
this.stack.push(result)
|
|
break
|
|
}
|
|
|
|
if (fn.type !== 'function')
|
|
throw new Error('CALL: not a function')
|
|
|
|
if (this.callStack.length > 0)
|
|
this.callStack[this.callStack.length - 1]!.isBreakTarget = true
|
|
|
|
this.callStack.push({
|
|
returnAddress: this.pc,
|
|
returnScope: this.scope,
|
|
isBreakTarget: false
|
|
})
|
|
|
|
this.scope = new Scope(fn.parentScope)
|
|
|
|
// Determine how many params are fixed (excluding variadic and named)
|
|
let fixedParamCount = fn.params.length
|
|
if (fn.variadic) fixedParamCount--
|
|
if (fn.named) fixedParamCount--
|
|
|
|
// Track which positional args have been consumed
|
|
let positionalArgIndex = 0
|
|
|
|
// Bind fixed parameters using priority: named arg > positional arg > default > null
|
|
for (let i = 0; i < fixedParamCount; i++) {
|
|
const paramName = fn.params[i]!
|
|
|
|
// Check if named argument was provided for this param
|
|
if (namedArgs.has(paramName)) {
|
|
this.scope.set(paramName, namedArgs.get(paramName)!)
|
|
namedArgs.delete(paramName) // Remove from named args so it won't go to named
|
|
} else if (positionalArgIndex < positionalArgs.length) {
|
|
this.scope.set(paramName, positionalArgs[positionalArgIndex]!)
|
|
positionalArgIndex++
|
|
} else if (fn.defaults[paramName] !== undefined) {
|
|
const defaultIdx = fn.defaults[paramName]!
|
|
const defaultValue = this.constants[defaultIdx]!
|
|
if (defaultValue.type === 'function_def')
|
|
throw new Error('Default value cannot be a function definition')
|
|
this.scope.set(paramName, defaultValue)
|
|
} else {
|
|
this.scope.set(paramName, toValue(null))
|
|
}
|
|
}
|
|
|
|
// Handle variadic parameter (collect remaining positional args)
|
|
if (fn.variadic) {
|
|
const variadicParamName = fn.params[fn.params.length - (fn.named ? 2 : 1)]!
|
|
const remainingArgs = positionalArgs.slice(positionalArgIndex)
|
|
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs })
|
|
}
|
|
|
|
// Handle named parameter (collect remaining named args that didn't match params)
|
|
if (fn.named) {
|
|
const namedParamName = fn.params[fn.params.length - 1]!
|
|
const namedDict = new Map<string, Value>()
|
|
for (const [key, value] of namedArgs) {
|
|
namedDict.set(key, value)
|
|
}
|
|
this.scope.set(namedParamName, { type: 'dict', value: namedDict })
|
|
}
|
|
|
|
// subtract 1 because pc was incremented
|
|
this.pc = fn.body - 1
|
|
break
|
|
}
|
|
|
|
case OpCode.TAIL_CALL: {
|
|
// Pop named count from stack (top)
|
|
const tailNamedCount = toNumber(this.stack.pop()!)
|
|
|
|
// Pop positional count from stack
|
|
const tailPositionalCount = toNumber(this.stack.pop()!)
|
|
|
|
// Pop named arguments (name-value pairs) from stack
|
|
const tailNamedArgs = new Map<string, Value>()
|
|
const tailNamedPairs: Array<{ key: string; value: Value }> = []
|
|
for (let i = 0; i < tailNamedCount; i++) {
|
|
const value = this.stack.pop()!
|
|
const key = this.stack.pop()!
|
|
tailNamedPairs.unshift({ key: toString(key), value })
|
|
}
|
|
for (const pair of tailNamedPairs) {
|
|
tailNamedArgs.set(pair.key, pair.value)
|
|
}
|
|
|
|
// Pop positional arguments from stack
|
|
const tailPositionalArgs: Value[] = []
|
|
for (let i = 0; i < tailPositionalCount; i++)
|
|
tailPositionalArgs.unshift(this.stack.pop()!)
|
|
|
|
const tailFn = this.stack.pop()!
|
|
|
|
if (tailFn.type !== 'function')
|
|
throw new Error('TAIL_CALL: not a function')
|
|
|
|
this.scope = new Scope(tailFn.parentScope)
|
|
|
|
// Determine how many params are fixed (excluding variadic and named)
|
|
let tailFixedParamCount = tailFn.params.length
|
|
if (tailFn.variadic) tailFixedParamCount--
|
|
if (tailFn.named) tailFixedParamCount--
|
|
|
|
// Track which positional args have been consumed
|
|
let tailPositionalArgIndex = 0
|
|
|
|
// Bind fixed parameters
|
|
for (let i = 0; i < tailFixedParamCount; i++) {
|
|
const paramName = tailFn.params[i]!
|
|
|
|
if (tailNamedArgs.has(paramName)) {
|
|
this.scope.set(paramName, tailNamedArgs.get(paramName)!)
|
|
tailNamedArgs.delete(paramName)
|
|
} else if (tailPositionalArgIndex < tailPositionalArgs.length) {
|
|
this.scope.set(paramName, tailPositionalArgs[tailPositionalArgIndex]!)
|
|
tailPositionalArgIndex++
|
|
} else if (tailFn.defaults[paramName] !== undefined) {
|
|
const defaultIdx = tailFn.defaults[paramName]!
|
|
const defaultValue = this.constants[defaultIdx]!
|
|
if (defaultValue.type === 'function_def')
|
|
throw new Error('Default value cannot be a function definition')
|
|
this.scope.set(paramName, defaultValue)
|
|
} else {
|
|
this.scope.set(paramName, toValue(null))
|
|
}
|
|
}
|
|
|
|
// Handle variadic parameter
|
|
if (tailFn.variadic) {
|
|
const variadicParamName = tailFn.params[tailFn.params.length - (tailFn.named ? 2 : 1)]!
|
|
const remainingArgs = tailPositionalArgs.slice(tailPositionalArgIndex)
|
|
this.scope.set(variadicParamName, { type: 'array', value: remainingArgs })
|
|
}
|
|
|
|
// Handle named parameter
|
|
if (tailFn.named) {
|
|
const namedParamName = tailFn.params[tailFn.params.length - 1]!
|
|
const namedDict = new Map<string, Value>()
|
|
for (const [key, value] of tailNamedArgs) {
|
|
namedDict.set(key, value)
|
|
}
|
|
this.scope.set(namedParamName, { type: 'dict', value: namedDict })
|
|
}
|
|
|
|
// subtract 1 because PC was incremented
|
|
this.pc = tailFn.body - 1
|
|
break
|
|
}
|
|
|
|
case OpCode.RETURN:
|
|
const returnValue = this.stack.length ? this.stack.pop()! : toValue(null)
|
|
|
|
const frame = this.callStack.pop()
|
|
if (!frame)
|
|
throw new Error('RETURN: no call frame to return from')
|
|
|
|
this.scope = frame.returnScope
|
|
this.pc = frame.returnAddress
|
|
|
|
this.stack.push(returnValue)
|
|
break
|
|
|
|
default:
|
|
throw `Unknown op: ${instruction.op}`
|
|
}
|
|
}
|
|
|
|
binaryOp(fn: (a: Value, b: Value) => number) {
|
|
const b = this.stack.pop()!
|
|
const a = this.stack.pop()!
|
|
const result = fn(a, b)
|
|
this.stack.push({ type: 'number', value: result })
|
|
}
|
|
|
|
comparisonOp(fn: (a: Value, b: Value) => boolean) {
|
|
const b = this.stack.pop()!
|
|
const a = this.stack.pop()!
|
|
const result = fn(a, b)
|
|
this.stack.push({ type: 'boolean', value: result })
|
|
}
|
|
|
|
async callNative(nativeFn: NativeFunction, args: any[]): Promise<Value> {
|
|
const originalFn = getOriginalFunction(nativeFn)
|
|
|
|
const lastArg = args[args.length - 1]
|
|
if (lastArg && !Array.isArray(lastArg) && typeof lastArg === 'object') {
|
|
const paramInfo = extractParamInfo(originalFn)
|
|
const positional = args.slice(0, -1)
|
|
const named = lastArg
|
|
|
|
args = [...positional]
|
|
for (let i = positional.length; i < paramInfo.params.length; i++) {
|
|
const paramName = paramInfo.params[i]!
|
|
if (named[paramName] !== undefined) {
|
|
args[i] = named[paramName]
|
|
}
|
|
}
|
|
}
|
|
|
|
const result = await originalFn.call(this, ...args)
|
|
return toValue(result)
|
|
}
|
|
} |