vm.go
// Copyright (c) 2020-2023 Ozan Hacıbekiroğlu.
// Use of this source code is governed by a MIT License
// that can be found in the LICENSE file.
package ugo
import (
"errors"
"fmt"
"runtime"
"strconv"
"sync"
"sync/atomic"
"github.com/ozanh/ugo/parser"
"github.com/ozanh/ugo/token"
)
const (
stackSize = 2048
frameSize = 1024
)
// VM executes the instructions in Bytecode.
type VM struct {
abort int64
sp int
ip int
curInsts []byte
constants []Object
stack [stackSize]Object
frames [frameSize]frame
curFrame *frame
frameIndex int
bytecode *Bytecode
modulesCache []Object
globals Object
pool vmPool
mu sync.Mutex
err error
noPanic bool
}
// NewVM creates a VM object.
func NewVM(bc *Bytecode) *VM {
var constants []Object
if bc != nil {
constants = bc.Constants
}
vm := &VM{
bytecode: bc,
constants: constants,
}
vm.pool.root = vm
return vm
}
// SetRecover recovers panic when Run panics and returns panic as an error.
// If error handler is present `try-catch-finally`, VM continues to run from catch/finally.
func (vm *VM) SetRecover(v bool) *VM {
vm.mu.Lock()
defer vm.mu.Unlock()
vm.noPanic = v
return vm
}
// SetBytecode enables to set a new Bytecode.
func (vm *VM) SetBytecode(bc *Bytecode) *VM {
vm.mu.Lock()
defer vm.mu.Unlock()
vm.bytecode = bc
vm.constants = bc.Constants
vm.modulesCache = nil
return vm
}
// Clear clears stack by setting nil to stack indexes and removes modules cache.
func (vm *VM) Clear() *VM {
vm.mu.Lock()
defer vm.mu.Unlock()
for i := range vm.stack {
vm.stack[i] = nil
}
vm.pool.clear()
vm.modulesCache = nil
vm.globals = nil
return vm
}
// GetGlobals returns global variables.
func (vm *VM) GetGlobals() Object {
return vm.globals
}
// GetLocals returns variables from stack up to the NumLocals of given Bytecode.
// This must be called after Run() before Clear().
func (vm *VM) GetLocals(locals []Object) []Object {
vm.mu.Lock()
defer vm.mu.Unlock()
if locals != nil {
locals = locals[:0]
} else {
locals = make([]Object, 0, vm.bytecode.Main.NumLocals)
}
for i := range vm.stack[:vm.bytecode.Main.NumLocals] {
locals = append(locals, vm.stack[i])
}
return locals
}
// RunCompiledFunction runs given CompiledFunction as if it is Main function.
// Bytecode must be set before calling this method, because Fileset and Constants are copied.
func (vm *VM) RunCompiledFunction(
f *CompiledFunction,
globals Object,
args ...Object,
) (Object, error) {
vm.mu.Lock()
defer vm.mu.Unlock()
if vm.bytecode == nil {
return nil, errors.New("invalid Bytecode")
}
vm.bytecode = &Bytecode{
FileSet: vm.bytecode.FileSet,
Constants: vm.constants,
Main: f,
NumModules: vm.bytecode.NumModules,
}
for i := range vm.stack {
vm.stack[i] = nil
}
return vm.init(globals, args...)
}
// Abort aborts the VM execution. It is safe to call this method from another
// goroutine.
func (vm *VM) Abort() {
vm.pool.abort(vm)
atomic.StoreInt64(&vm.abort, 1)
}
// Aborted reports whether VM is aborted. It is safe to call this method from
// another goroutine.
func (vm *VM) Aborted() bool {
return atomic.LoadInt64(&vm.abort) == 1
}
// Run runs VM and executes the instructions until the OpReturn Opcode or Abort call.
func (vm *VM) Run(globals Object, args ...Object) (Object, error) {
vm.mu.Lock()
defer vm.mu.Unlock()
return vm.init(globals, args...)
}
func (vm *VM) init(globals Object, args ...Object) (Object, error) {
if vm.bytecode == nil || vm.bytecode.Main == nil {
return nil, errors.New("invalid Bytecode")
}
vm.err = nil
atomic.StoreInt64(&vm.abort, 0)
vm.initGlobals(globals)
vm.initLocals(args)
vm.initCurrentFrame()
vm.frameIndex = 1
vm.ip = -1
vm.sp = vm.curFrame.fn.NumLocals
// Resize modules cache or create it if not exists.
// Note that REPL can set module cache before running, don't recreate it add missing indexes.
if diff := vm.bytecode.NumModules - len(vm.modulesCache); diff > 0 {
for i := 0; i < diff; i++ {
vm.modulesCache = append(vm.modulesCache, nil)
}
}
for run := true; run; {
run = vm.run()
}
if vm.err != nil {
return nil, vm.err
}
if vm.sp < stackSize {
if vv, ok := vm.stack[vm.sp-1].(*ObjectPtr); ok {
return *vv.Value, nil
}
return vm.stack[vm.sp-1], nil
}
return nil, ErrStackOverflow
}
func (vm *VM) run() (rerun bool) {
defer func() {
if vm.noPanic {
if r := recover(); r != nil {
vm.handlePanic(r)
rerun = vm.err == nil
return
}
}
vm.clearCurrentFrame()
}()
vm.loop()
return
}
func (vm *VM) loop() {
VMLoop:
for atomic.LoadInt64(&vm.abort) == 0 {
vm.ip++
switch vm.curInsts[vm.ip] {
case OpConstant:
cidx := int(vm.curInsts[vm.ip+2]) | int(vm.curInsts[vm.ip+1])<<8
obj := vm.constants[cidx]
vm.stack[vm.sp] = obj
vm.sp++
vm.ip += 2
case OpGetLocal:
localIdx := int(vm.curInsts[vm.ip+1])
value := vm.stack[vm.curFrame.basePointer+localIdx]
if v, ok := value.(*ObjectPtr); ok {
value = *v.Value
}
vm.stack[vm.sp] = value
vm.sp++
vm.ip++
case OpSetLocal:
localIndex := int(vm.curInsts[vm.ip+1])
value := vm.stack[vm.sp-1]
index := vm.curFrame.basePointer + localIndex
if v, ok := vm.stack[index].(*ObjectPtr); ok {
*v.Value = value
} else {
vm.stack[index] = value
}
vm.sp--
vm.stack[vm.sp] = nil
vm.ip++
case OpBinaryOp:
tok := token.Token(vm.curInsts[vm.ip+1])
left, right := vm.stack[vm.sp-2], vm.stack[vm.sp-1]
var value Object
var err error
switch left := left.(type) {
case Int:
value, err = left.BinaryOp(tok, right)
case String:
value, err = left.BinaryOp(tok, right)
case Float:
value, err = left.BinaryOp(tok, right)
case Uint:
value, err = left.BinaryOp(tok, right)
case Char:
value, err = left.BinaryOp(tok, right)
case Bool:
value, err = left.BinaryOp(tok, right)
default:
value, err = left.BinaryOp(tok, right)
}
if err == nil {
vm.stack[vm.sp-2] = value
vm.sp--
vm.stack[vm.sp] = nil
vm.ip++
continue
}
if err == ErrInvalidOperator {
err = ErrInvalidOperator.NewError(tok.String())
}
if err = vm.throwGenErr(err); err != nil {
vm.err = err
return
}
case OpAndJump:
if vm.stack[vm.sp-1].IsFalsy() {
pos := int(vm.curInsts[vm.ip+2]) | int(vm.curInsts[vm.ip+1])<<8
vm.ip = pos - 1
continue
}
vm.stack[vm.sp-1] = nil
vm.sp--
vm.ip += 2
case OpOrJump:
if vm.stack[vm.sp-1].IsFalsy() {
vm.stack[vm.sp-1] = nil
vm.sp--
vm.ip += 2
continue
}
pos := int(vm.curInsts[vm.ip+2]) | int(vm.curInsts[vm.ip+1])<<8
vm.ip = pos - 1
case OpEqual:
left, right := vm.stack[vm.sp-2], vm.stack[vm.sp-1]
switch left := left.(type) {
case Int:
vm.stack[vm.sp-2] = Bool(left.Equal(right))
case String:
vm.stack[vm.sp-2] = Bool(left.Equal(right))
case Float:
vm.stack[vm.sp-2] = Bool(left.Equal(right))
case Bool:
vm.stack[vm.sp-2] = Bool(left.Equal(right))
case Uint:
vm.stack[vm.sp-2] = Bool(left.Equal(right))
case Char:
vm.stack[vm.sp-2] = Bool(left.Equal(right))
default:
vm.stack[vm.sp-2] = Bool(left.Equal(right))
}
vm.sp--
vm.stack[vm.sp] = nil
case OpNotEqual:
left, right := vm.stack[vm.sp-2], vm.stack[vm.sp-1]
switch left := left.(type) {
case Int:
vm.stack[vm.sp-2] = Bool(!left.Equal(right))
case String:
vm.stack[vm.sp-2] = Bool(!left.Equal(right))
case Float:
vm.stack[vm.sp-2] = Bool(!left.Equal(right))
case Bool:
vm.stack[vm.sp-2] = Bool(!left.Equal(right))
case Uint:
vm.stack[vm.sp-2] = Bool(!left.Equal(right))
case Char:
vm.stack[vm.sp-2] = Bool(!left.Equal(right))
default:
vm.stack[vm.sp-2] = Bool(!left.Equal(right))
}
vm.sp--
vm.stack[vm.sp] = nil
case OpTrue:
vm.stack[vm.sp] = True
vm.sp++
case OpFalse:
vm.stack[vm.sp] = False
vm.sp++
case OpCall:
err := vm.xOpCall()
if err == nil {
continue
}
if err = vm.throwGenErr(err); err != nil {
vm.err = err
return
}
case OpCallName:
err := vm.xOpCallName()
if err == nil {
continue
}
if err = vm.throwGenErr(err); err != nil {
vm.err = err
return
}
case OpReturn:
numRet := vm.curInsts[vm.ip+1]
bp := vm.curFrame.basePointer
if bp == 0 {
bp = vm.curFrame.fn.NumLocals + 1
}
if numRet == 1 {
vm.stack[bp-1] = vm.stack[vm.sp-1]
} else {
vm.stack[bp-1] = Undefined
}
for i := vm.sp - 1; i >= bp; i-- {
vm.stack[i] = nil
}
vm.sp = bp
if vm.frameIndex == 1 {
return
}
vm.clearCurrentFrame()
parent := &(vm.frames[vm.frameIndex-2])
vm.frameIndex--
vm.ip = parent.ip
vm.curFrame = parent
vm.curInsts = vm.curFrame.fn.Instructions
case OpGetBuiltin:
builtinIndex := BuiltinType(int(vm.curInsts[vm.ip+1]))
vm.stack[vm.sp] = BuiltinObjects[builtinIndex]
vm.sp++
vm.ip++
case OpClosure:
constIdx := int(vm.curInsts[vm.ip+2]) | int(vm.curInsts[vm.ip+1])<<8
fn := vm.constants[constIdx].(*CompiledFunction)
numFree := int(vm.curInsts[vm.ip+3])
free := make([]*ObjectPtr, numFree)
for i := 0; i < numFree; i++ {
switch freeVar := (vm.stack[vm.sp-numFree+i]).(type) {
case *ObjectPtr:
free[i] = freeVar
default:
temp := vm.stack[vm.sp-numFree+i]
free[i] = &ObjectPtr{
Value: &temp,
}
}
vm.stack[vm.sp-numFree+i] = nil
}
vm.sp -= numFree
newFn := &CompiledFunction{
Instructions: fn.Instructions,
NumParams: fn.NumParams,
NumLocals: fn.NumLocals,
Variadic: fn.Variadic,
SourceMap: fn.SourceMap,
Free: free,
}
vm.stack[vm.sp] = newFn
vm.sp++
vm.ip += 3
case OpJump:
vm.ip = (int(vm.curInsts[vm.ip+2]) | int(vm.curInsts[vm.ip+1])<<8) - 1
case OpJumpFalsy:
vm.sp--
obj := vm.stack[vm.sp]
vm.stack[vm.sp] = nil
var falsy bool
switch obj := obj.(type) {
case Bool:
falsy = obj.IsFalsy()
case Int:
falsy = obj.IsFalsy()
case Uint:
falsy = obj.IsFalsy()
case Float:
falsy = obj.IsFalsy()
case String:
falsy = obj.IsFalsy()
default:
falsy = obj.IsFalsy()
}
if falsy {
vm.ip = (int(vm.curInsts[vm.ip+2]) | int(vm.curInsts[vm.ip+1])<<8) - 1
continue
}
vm.ip += 2
case OpGetGlobal:
cidx := int(vm.curInsts[vm.ip+2]) | int(vm.curInsts[vm.ip+1])<<8
index := vm.constants[cidx]
var ret Object
var err error
ret, err = vm.globals.IndexGet(index)
if err != nil {
if err := vm.throwGenErr(err); err != nil {
vm.err = err
return
}
continue
}
if ret == nil {
vm.stack[vm.sp] = Undefined
} else {
vm.stack[vm.sp] = ret
}
vm.ip += 2
vm.sp++
case OpSetGlobal:
cidx := int(vm.curInsts[vm.ip+2]) | int(vm.curInsts[vm.ip+1])<<8
index := vm.constants[cidx]
value := vm.stack[vm.sp-1]
if v, ok := value.(*ObjectPtr); ok {
value = *v.Value
}
if err := vm.globals.IndexSet(index, value); err != nil {
if err := vm.throwGenErr(err); err != nil {
vm.err = err
return
}
continue
}
vm.ip += 2
vm.sp--
vm.stack[vm.sp] = nil
case OpArray:
numItems := int(vm.curInsts[vm.ip+2]) | int(vm.curInsts[vm.ip+1])<<8
arr := make(Array, numItems)
copy(arr, vm.stack[vm.sp-numItems:vm.sp])
vm.sp -= numItems
vm.stack[vm.sp] = arr
for i := vm.sp + 1; i < vm.sp+numItems+1; i++ {
vm.stack[i] = nil
}
vm.sp++
vm.ip += 2
case OpMap:
numItems := int(vm.curInsts[vm.ip+2]) | int(vm.curInsts[vm.ip+1])<<8
kv := make(Map)
for i := vm.sp - numItems; i < vm.sp; i += 2 {
key := vm.stack[i]
value := vm.stack[i+1]
kv[key.String()] = value
vm.stack[i] = nil
vm.stack[i+1] = nil
}
vm.sp -= numItems
vm.stack[vm.sp] = kv
vm.sp++
vm.ip += 2
case OpGetIndex:
numSel := int(vm.curInsts[vm.ip+1])
tp := vm.sp - 1 - numSel
target := vm.stack[tp]
value := Undefined
for ; numSel > 0; numSel-- {
ptr := vm.sp - numSel
index := vm.stack[ptr]
vm.stack[ptr] = nil
v, err := target.IndexGet(index)
if err != nil {
switch err {
case ErrNotIndexable:
err = ErrNotIndexable.NewError(target.TypeName())
case ErrIndexOutOfBounds:
err = ErrIndexOutOfBounds.NewError(index.String())
}
if err = vm.throwGenErr(err); err != nil {
vm.err = err
return
}
continue VMLoop
}
target = v
value = v
}
vm.stack[tp] = value
vm.sp = tp + 1
vm.ip++
case OpSetIndex:
value := vm.stack[vm.sp-3]
target := vm.stack[vm.sp-2]
index := vm.stack[vm.sp-1]
err := target.IndexSet(index, value)
if err != nil {
switch err {
case ErrNotIndexAssignable:
err = ErrNotIndexAssignable.NewError(target.TypeName())
case ErrIndexOutOfBounds:
err = ErrIndexOutOfBounds.NewError(index.String())
}
if err = vm.throwGenErr(err); err != nil {
vm.err = err
return
}
continue
}
vm.stack[vm.sp-3] = nil
vm.stack[vm.sp-2] = nil
vm.stack[vm.sp-1] = nil
vm.sp -= 3
case OpSliceIndex:
err := vm.xOpSliceIndex()
if err == nil {
continue
}
if err = vm.throwGenErr(err); err != nil {
vm.err = err
return
}
case OpGetFree:
freeIndex := int(vm.curInsts[vm.ip+1])
vm.stack[vm.sp] = *vm.curFrame.freeVars[freeIndex].Value
vm.sp++
vm.ip++
case OpSetFree:
freeIndex := int(vm.curInsts[vm.ip+1])
*vm.curFrame.freeVars[freeIndex].Value = vm.stack[vm.sp-1]
vm.sp--
vm.stack[vm.sp] = nil
vm.ip++
case OpGetLocalPtr:
localIndex := int(vm.curInsts[vm.ip+1])
var freeVar *ObjectPtr
value := vm.stack[vm.curFrame.basePointer+localIndex]
if obj, ok := value.(*ObjectPtr); ok {
freeVar = obj
} else {
freeVar = &ObjectPtr{Value: &value}
vm.stack[vm.curFrame.basePointer+localIndex] = freeVar
}
vm.stack[vm.sp] = freeVar
vm.sp++
vm.ip++
case OpGetFreePtr:
freeIndex := int(vm.curInsts[vm.ip+1])
value := vm.curFrame.freeVars[freeIndex]
vm.stack[vm.sp] = value
vm.sp++
vm.ip++
case OpDefineLocal:
localIndex := int(vm.curInsts[vm.ip+1])
vm.stack[vm.curFrame.basePointer+localIndex] = vm.stack[vm.sp-1]
vm.sp--
vm.stack[vm.sp] = nil
vm.ip++
case OpNull:
vm.stack[vm.sp] = Undefined
vm.sp++
case OpPop:
vm.sp--
vm.stack[vm.sp] = nil
case OpIterInit:
dst := vm.stack[vm.sp-1]
if dst.CanIterate() {
it := dst.Iterate()
vm.stack[vm.sp-1] = &iteratorObject{Iterator: it}
continue
}
var err error = ErrNotIterable.NewError(dst.TypeName())
if err = vm.throwGenErr(err); err != nil {
vm.err = err
return
}
case OpIterNext:
iterator := vm.stack[vm.sp-1]
hasMore := iterator.(Iterator).Next()
vm.stack[vm.sp-1] = Bool(hasMore)
case OpIterKey:
iterator := vm.stack[vm.sp-1]
val := iterator.(Iterator).Key()
vm.stack[vm.sp-1] = val
case OpIterValue:
iterator := vm.stack[vm.sp-1]
val := iterator.(Iterator).Value()
vm.stack[vm.sp-1] = val
case OpLoadModule:
cidx := int(vm.curInsts[vm.ip+2]) | int(vm.curInsts[vm.ip+1])<<8
midx := int(vm.curInsts[vm.ip+4]) | int(vm.curInsts[vm.ip+3])<<8
value := vm.modulesCache[midx]
if value == nil {
// module cache is empty, load the object from constants
vm.stack[vm.sp] = vm.constants[cidx]
vm.sp++
// load module by putting true for subsequent OpJumpFalsy
// if module is a compiledFunction it will be called and result will be stored in module cache
// if module is not a compiledFunction, copy of object will be stored in module cache
vm.stack[vm.sp] = True
vm.sp++
} else {
vm.stack[vm.sp] = value
vm.sp++
// no need to load the module, put false for subsequent OpJumpFalsy
vm.stack[vm.sp] = False
vm.sp++
}
vm.ip += 4
case OpStoreModule:
midx := int(vm.curInsts[vm.ip+2]) | int(vm.curInsts[vm.ip+1])<<8
value := vm.stack[vm.sp-1]
if v, ok := value.(Copier); ok {
// store deep copy of the module if supported
value = v.Copy()
vm.stack[vm.sp-1] = value
}
vm.modulesCache[midx] = value
vm.ip += 2
case OpSetupTry:
vm.xOpSetupTry()
case OpSetupCatch:
vm.xOpSetupCatch()
case OpSetupFinally:
vm.xOpSetupFinally()
case OpThrow:
err := vm.xOpThrow()
if err != nil {
vm.err = err
return
}
case OpFinalizer:
upto := int(vm.curInsts[vm.ip+1])
pos := vm.curFrame.errHandlers.findFinally(upto)
if pos <= 0 {
vm.ip++
continue
}
// go to finally if set
handler := vm.curFrame.errHandlers.last()
// save current ip to come back to same position
handler.returnTo = vm.ip
// save current sp to come back to same position
handler.sp = vm.sp
// remove current error if any
vm.curFrame.errHandlers.err = nil
// set ip to finally's position
vm.ip = pos - 1
case OpUnary:
err := vm.xOpUnary()
if err == nil {
continue
}
if err = vm.throwGenErr(err); err != nil {
vm.err = err
return
}
case OpNoOp:
default:
vm.err = fmt.Errorf("unknown opcode %d", vm.curInsts[vm.ip])
return
}
}
vm.err = ErrVMAborted
}
func (vm *VM) initGlobals(globals Object) {
if globals == nil {
globals = Map{}
}
vm.globals = globals
}
func (vm *VM) initLocals(args []Object) {
// init all params as undefined
numParams := vm.bytecode.Main.NumParams
locals := vm.stack[:vm.bytecode.Main.NumLocals]
// TODO (ozan): check why setting numParams fails some tests!
for i := 0; i < vm.bytecode.Main.NumLocals; i++ {
locals[i] = Undefined
}
if numParams <= 0 {
return
}
if len(args) < numParams {
if vm.bytecode.Main.Variadic {
locals[numParams-1] = Array{}
}
copy(locals, args)
return
}
if vm.bytecode.Main.Variadic {
vargs := args[numParams-1:]
arr := make(Array, 0, len(vargs))
locals[numParams-1] = append(arr, vargs...)
} else {
locals[numParams-1] = args[numParams-1]
}
copy(locals, args[:numParams-1])
}
func (vm *VM) initCurrentFrame() {
// initialize frame and pointers
vm.curInsts = vm.bytecode.Main.Instructions
vm.curFrame = &(vm.frames[0])
vm.curFrame.fn = vm.bytecode.Main
if vm.curFrame.fn.Free != nil {
// Assign free variables if exists in compiled function.
// This is required to run compiled functions returned from VM using RunCompiledFunction().
vm.curFrame.freeVars = vm.curFrame.fn.Free
}
vm.curFrame.errHandlers = nil
vm.curFrame.basePointer = 0
}
func (vm *VM) clearCurrentFrame() {
vm.curFrame.freeVars = nil
vm.curFrame.fn = nil
vm.curFrame.errHandlers = nil
}
func (vm *VM) handlePanic(r interface{}) {
if vm.sp < stackSize && vm.frameIndex <= frameSize && vm.err == nil {
if err := vm.throwGenErr(fmt.Errorf("%v", r)); err != nil {
vm.err = err
gostack := debugStack()
if vm.err != nil {
vm.err = fmt.Errorf("panic: %v %w\nGo Stack:\n%s",
r, vm.err, gostack)
}
}
return
}
gostack := debugStack()
if vm.err != nil {
vm.err = fmt.Errorf("panic: %v error: %w\nGo Stack:\n%s",
r, vm.err, gostack)
return
}
vm.err = fmt.Errorf("panic: %v\nGo Stack:\n%s", r, gostack)
}
func (vm *VM) xOpSetupTry() {
catch := int(vm.curInsts[vm.ip+2]) | int(vm.curInsts[vm.ip+1])<<8
finally := int(vm.curInsts[vm.ip+4]) | int(vm.curInsts[vm.ip+3])<<8
ptrs := errHandler{
sp: vm.sp,
catch: catch,
finally: finally,
}
if vm.curFrame.errHandlers == nil {
vm.curFrame.errHandlers = &errHandlers{
handlers: []errHandler{ptrs},
}
} else {
vm.curFrame.errHandlers.handlers = append(
vm.curFrame.errHandlers.handlers, ptrs)
}
vm.ip += 4
}
func (vm *VM) xOpSetupCatch() {
value := Undefined
errHandlers := vm.curFrame.errHandlers
if errHandlers.hasHandler() {
// set 0 to last catch position
hdl := errHandlers.last()
hdl.catch = 0
if errHandlers.err != nil {
value = errHandlers.err
errHandlers.err = nil
}
}
vm.stack[vm.sp] = value
vm.sp++
//Either OpSetLocal or OpPop is generated by compiler to handle error
}
func (vm *VM) xOpSetupFinally() {
errHandlers := vm.curFrame.errHandlers
if errHandlers.hasHandler() {
hdl := errHandlers.last()
hdl.catch = 0
hdl.finally = 0
}
}
func (vm *VM) xOpThrow() error {
op := vm.curInsts[vm.ip+1]
vm.ip++
switch op {
case 0: // system
errHandlers := vm.curFrame.errHandlers
if errHandlers.hasError() {
errHandlers.pop()
// do not put position info to error for re-throw after finally.
if err := vm.throw(errHandlers.err, true); err != nil {
return err
}
} else if pos := errHandlers.hasReturnTo(); pos > 0 {
// go to OpReturn if it is set
handler := errHandlers.last()
errHandlers.pop()
handler.returnTo = 0
if vm.sp >= handler.sp {
for i := vm.sp; i >= handler.sp; i-- {
vm.stack[i] = nil
}
}
vm.sp = handler.sp
vm.ip = pos - 1
}
case 1: // user
obj := vm.stack[vm.sp-1]
vm.stack[vm.sp-1] = nil
vm.sp--
if err := vm.throw(vm.newErrorFromObject(obj), false); err != nil {
return err
}
default:
return fmt.Errorf("wrong operand for OpThrow:%d", op)
}
return nil
}
func (vm *VM) throwGenErr(err error) error {
if e, ok := err.(*RuntimeError); ok {
if e.fileSet == nil {
e.fileSet = vm.bytecode.FileSet
}
return vm.throw(e, true)
} else if e, ok := err.(*Error); ok {
return vm.throw(vm.newError(e), false)
}
return vm.throw(vm.newErrorFromError(err), false)
}
func (vm *VM) throw(err *RuntimeError, noTrace bool) error {
if !noTrace {
err.addTrace(vm.getSourcePos())
}
// firstly check our frame has error handler
if vm.curFrame.errHandlers.hasHandler() {
return vm.handleThrownError(vm.curFrame, err)
}
// find previous frames having error handler
var frame *frame
index := vm.frameIndex - 2
for index >= 0 {
f := &(vm.frames[index])
err.addTrace(getFrameSourcePos(f))
if f.errHandlers.hasHandler() {
frame = f
break
}
f.freeVars = nil
f.fn = nil
index--
}
if frame == nil || index < 0 {
// not handled, exit
return err
}
vm.frameIndex = index + 1
if e := vm.handleThrownError(frame, err); e != nil {
return e
}
vm.curFrame = frame
vm.curFrame.fn = frame.fn
vm.curInsts = frame.fn.Instructions
return nil
}
func (vm *VM) handleThrownError(frame *frame, err *RuntimeError) error {
frame.errHandlers.err = err
handler := frame.errHandlers.last()
// if we have catch>0 goto catch else follow finally (one of them must be set)
if handler.catch > 0 {
vm.ip = handler.catch - 1
} else if handler.finally > 0 {
vm.ip = handler.finally - 1
} else {
frame.errHandlers.pop()
return vm.throw(err, false)
}
if vm.sp >= handler.sp {
for i := vm.sp; i >= handler.sp; i-- {
vm.stack[i] = nil
}
}
vm.sp = handler.sp
return nil
}
func (vm *VM) xOpCallName() error {
numArgs := int(vm.curInsts[vm.ip+1])
flags := int(vm.curInsts[vm.ip+2]) // 0 or 1
obj := vm.stack[vm.sp-numArgs-2]
name := vm.stack[vm.sp-1]
vm.sp--
vm.stack[vm.sp] = nil
var err error
if nameCaller, ok := obj.(NameCallerObject); ok {
var vargs []Object
if flags > 0 {
vargs, err = lastAsSlice(vm)
if err != nil {
return err
}
}
c := Call{
vm: vm,
args: vm.stack[vm.sp-numArgs : vm.sp-flags],
vargs: vargs,
}
ret, err := nameCaller.CallName(name.String(), c)
for i := 0; i < numArgs; i++ {
vm.sp--
vm.stack[vm.sp] = nil
}
if err != nil {
return err
}
vm.stack[vm.sp-1] = ret
//vm.sp++
//vm.sp = vm.sp - numArgs
vm.ip += 2
return nil
}
v, err := obj.IndexGet(name)
if err != nil {
return err
}
vm.stack[vm.sp-numArgs-1] = v
return vm.xOpCallAny(v, numArgs, flags)
}
func (vm *VM) xOpCall() error {
numArgs := int(vm.curInsts[vm.ip+1])
flags := int(vm.curInsts[vm.ip+2]) // 0 or 1
callee := vm.stack[vm.sp-numArgs-1]
return vm.xOpCallAny(callee, numArgs, flags)
}
func (vm *VM) xOpCallAny(callee Object, numArgs, flags int) error {
if cfunc, ok := callee.(*CompiledFunction); ok {
return vm.xOpCallCompiled(cfunc, numArgs, flags)
}
return vm.xOpCallObject(callee, numArgs, flags)
}
func (vm *VM) xOpCallCompiled(cfunc *CompiledFunction, numArgs, flags int) error {
basePointer := vm.sp - numArgs
numLocals := cfunc.NumLocals
numParams := cfunc.NumParams
if flags == 0 {
if !cfunc.Variadic {
if numArgs != numParams {
return ErrWrongNumArguments.NewError(
wantEqXGotY(numParams, numArgs),
)
}
} else {
if numArgs < numParams-1 {
// f := func(a, ...b) {}
// f()
return ErrWrongNumArguments.NewError(
wantGEqXGotY(numParams-1, numArgs),
)
}
if numArgs == numParams-1 {
// f := func(a, ...b) {} // a==1 b==[]
// f(1)
vm.stack[basePointer+numArgs] = Array{}
} else {
// f := func(a, ...b) {} // a == 1 b == [] // a == 1 b == [2, 3]
// f(1, 2) // f(1, 2, 3)
arr := vm.stack[basePointer+numParams-1 : basePointer+numArgs]
vm.stack[basePointer+numParams-1] = append(Array{}, arr...)
}
}
} else {
var arrSize int
if arr, ok := vm.stack[basePointer+numArgs-1].(Array); ok {
arrSize = len(arr)
} else {
return NewArgumentTypeError("last", "array",
vm.stack[basePointer+numArgs-1].TypeName())
}
if cfunc.Variadic {
if numArgs < numParams {
// f := func(a, ...b) {}
// f(...[1]) // f(...[1, 2])
if arrSize+numArgs < numParams {
// f := func(a, ...b) {}
// f(...[])
return ErrWrongNumArguments.NewError(
wantGEqXGotY(numParams-1, arrSize+numArgs-1),
)
}
tempBuf := make(Array, 0, arrSize+numArgs)
tempBuf = append(tempBuf,
vm.stack[basePointer:basePointer+numArgs-1]...)
tempBuf = append(tempBuf,
vm.stack[basePointer+numArgs-1].(Array)...)
copy(vm.stack[basePointer:], tempBuf[:numParams-1])
arr := tempBuf[numParams-1:]
vm.stack[basePointer+numParams-1] = append(Array{}, arr...)
} else if numArgs > numParams {
// f := func(a, ...b) {} // a == 1 b == [2, 3]
// f(1, 2, ...[3])
arr := append(Array{},
vm.stack[basePointer+numParams-1:basePointer+numArgs-1]...)
arr = append(arr, vm.stack[basePointer+numArgs-1].(Array)...)
vm.stack[basePointer+numParams-1] = arr
}
} else {
if arrSize+numArgs-1 != numParams {
// f := func(a, b) {}
// f(1, ...[2, 3, 4])
return ErrWrongNumArguments.NewError(
wantEqXGotY(numParams, arrSize+numArgs-1),
)
}
// f := func(a, b) {}
// f(...[1, 2])
arr := vm.stack[basePointer+numArgs-1].(Array)
copy(vm.stack[basePointer+numArgs-1:], arr)
}
}
for i := numParams; i < numLocals; i++ {
vm.stack[basePointer+i] = Undefined
}
// test if it's tail-call
if cfunc == vm.curFrame.fn { // recursion
nextOp := vm.curInsts[vm.ip+2+1]
if nextOp == OpReturn ||
(nextOp == OpPop && OpReturn == vm.curInsts[vm.ip+2+2]) {
curBp := vm.curFrame.basePointer
copy(vm.stack[curBp:curBp+numLocals], vm.stack[basePointer:])
newSp := vm.sp - numArgs - 1
for i := vm.sp; i >= newSp; i-- {
vm.stack[i] = nil
}
vm.sp = newSp
vm.ip = -1 // reset ip to beginning of the frame
vm.curFrame.errHandlers = nil // reset error handlers if any set
return nil
}
}
frame := &(vm.frames[vm.frameIndex])
vm.frameIndex++
if vm.frameIndex > frameSize-1 {
return ErrStackOverflow
}
frame.fn = cfunc
frame.freeVars = cfunc.Free
frame.errHandlers = nil
frame.basePointer = basePointer
vm.curFrame.ip = vm.ip + 2
vm.curInsts = cfunc.Instructions
vm.curFrame = frame
vm.sp = basePointer + numLocals
vm.ip = -1
return nil
}
func (vm *VM) xOpCallObject(callee Object, numArgs, flags int) error {
if !callee.CanCall() {
return ErrNotCallable.NewError(callee.TypeName())
}
if c, ok := callee.(ExCallerObject); ok {
return vm.xOpCallExCaller(c, numArgs, flags)
}
var args []Object
if flags > 0 {
last, err := lastAsSlice(vm)
if err != nil {
return err
}
args = make([]Object, 0, numArgs-1+len(last))
args = append(args, vm.stack[vm.sp-numArgs:vm.sp-1]...)
args = append(args, last...)
} else {
args = make([]Object, 0, numArgs)
args = append(args, vm.stack[vm.sp-numArgs:vm.sp]...)
}
for i := 0; i < numArgs; i++ {
vm.sp--
vm.stack[vm.sp] = nil
}
result, err := callee.Call(args...)
if err != nil {
return err
}
vm.stack[vm.sp-1] = result
vm.ip += 2
return nil
}
func (vm *VM) xOpCallExCaller(callee ExCallerObject, numArgs, flags int) error {
var err error
var vargs []Object
var args = vm.stack[vm.sp-numArgs : vm.sp-flags]
if flags > 0 {
vargs, err = lastAsSlice(vm)
if err != nil {
return err
}
}
c := Call{
vm: vm,
args: args,
vargs: vargs,
}
result, err := callee.CallEx(c)
for i := 0; i < numArgs; i++ {
vm.sp--
vm.stack[vm.sp] = nil
}
if err != nil {
return err
}
vm.stack[vm.sp-1] = result
vm.ip += 2
return nil
}
func lastAsSlice(vm *VM) ([]Object, error) {
switch arr := vm.stack[vm.sp-1].(type) {
case Array:
return arr, nil
default:
return nil, NewArgumentTypeError("last", "array", arr.TypeName())
}
}
func (vm *VM) xOpUnary() error {
tok := token.Token(vm.curInsts[vm.ip+1])
right := vm.stack[vm.sp-1]
var value Object
switch tok {
case token.Not:
vm.stack[vm.sp-1] = Bool(right.IsFalsy())
vm.ip++
return nil
case token.Sub:
switch o := right.(type) {
case Int:
value = -o
case Float:
value = -o
case Char:
value = Int(-o)
case Uint:
value = -o
case Bool:
if o {
value = Int(-1)
} else {
value = Int(0)
}
default:
goto invalidType
}
case token.Xor:
switch o := right.(type) {
case Int:
value = ^o
case Uint:
value = ^o
case Char:
value = ^Int(o)
case Bool:
if o {
value = ^Int(1)
} else {
value = ^Int(0)
}
default:
goto invalidType
}
case token.Add:
switch o := right.(type) {
case Int, Uint, Float, Char:
value = right
case Bool:
if o {
value = Int(1)
} else {
value = Int(0)
}
default:
goto invalidType
}
default:
return ErrInvalidOperator.NewError(
fmt.Sprintf("invalid for '%s': '%s'",
tok.String(), right.TypeName()))
}
vm.stack[vm.sp-1] = value
vm.ip++
return nil
invalidType:
return ErrType.NewError(
fmt.Sprintf("invalid type for unary '%s': '%s'",
tok.String(), right.TypeName()))
}
func (vm *VM) xOpSliceIndex() error {
obj := vm.stack[vm.sp-3]
left := vm.stack[vm.sp-2]
right := vm.stack[vm.sp-1]
vm.stack[vm.sp-3] = nil
vm.stack[vm.sp-2] = nil
vm.stack[vm.sp-1] = nil
vm.sp -= 3
var objlen int
var isbytes bool
switch obj := obj.(type) {
case Array:
objlen = len(obj)
case String:
objlen = len(obj)
case Bytes:
isbytes = true
objlen = len(obj)
default:
return ErrType.NewError(obj.TypeName(), "cannot be sliced")
}
var low int
switch v := left.(type) {
case *UndefinedType:
low = 0
case Int:
low = int(v)
case Uint:
low = int(v)
case Char:
low = int(v)
default:
return ErrType.NewError("invalid first index type", left.TypeName())
}
var high int
switch v := right.(type) {
case *UndefinedType:
high = objlen
case Int:
high = int(v)
case Uint:
high = int(v)
case Char:
high = int(v)
default:
return ErrType.NewError("invalid second index type", right.TypeName())
}
if low > high {
return ErrInvalidIndex.NewError(fmt.Sprintf("[%d:%d]", low, high))
}
if isbytes {
objlen = cap(obj.(Bytes))
}
if low < 0 || high < 0 || high > objlen {
return ErrIndexOutOfBounds.NewError(fmt.Sprintf("[%d:%d]", low, high))
}
switch obj := obj.(type) {
case Array:
vm.stack[vm.sp] = obj[low:high]
case String:
vm.stack[vm.sp] = obj[low:high]
case Bytes:
vm.stack[vm.sp] = obj[low:high]
}
vm.sp++
return nil
}
func (vm *VM) newError(err *Error) *RuntimeError {
var fileset *parser.SourceFileSet
if vm.bytecode != nil {
fileset = vm.bytecode.FileSet
}
return &RuntimeError{Err: err, fileSet: fileset}
}
func (vm *VM) newErrorFromObject(object Object) *RuntimeError {
switch v := object.(type) {
case *RuntimeError:
return v
case *Error:
return vm.newError(v)
default:
return vm.newError(&Error{Message: v.String()})
}
}
func (vm *VM) newErrorFromError(err error) *RuntimeError {
if v, ok := err.(Object); ok {
return vm.newErrorFromObject(v)
}
return vm.newError(&Error{Message: err.Error(), Cause: err})
}
func (vm *VM) getSourcePos() parser.Pos {
if vm.curFrame == nil || vm.curFrame.fn == nil {
return parser.NoPos
}
return vm.curFrame.fn.SourcePos(vm.ip)
}
type errHandler struct {
sp int
catch int
finally int
returnTo int
}
type errHandlers struct {
handlers []errHandler
err *RuntimeError
}
func (t *errHandlers) hasError() bool {
return t != nil && t.err != nil
}
func (t *errHandlers) pop() bool {
if t == nil || len(t.handlers) == 0 {
return false
}
t.handlers = t.handlers[:len(t.handlers)-1]
return true
}
func (t *errHandlers) last() *errHandler {
if t == nil || len(t.handlers) == 0 {
return nil
}
return &t.handlers[len(t.handlers)-1]
}
func (t *errHandlers) hasHandler() bool {
return t != nil && len(t.handlers) > 0
}
func (t *errHandlers) findFinally(upto int) int {
if t == nil {
return 0
}
start:
index := len(t.handlers) - 1
if index < upto || index < 0 {
return 0
}
p := t.handlers[index].finally
if p == 0 {
t.pop()
goto start
}
return p
}
func (t *errHandlers) hasReturnTo() int {
if t.hasHandler() {
return t.handlers[len(t.handlers)-1].returnTo
}
return 0
}
type frame struct {
fn *CompiledFunction
freeVars []*ObjectPtr
ip int
basePointer int
errHandlers *errHandlers
}
func getFrameSourcePos(frame *frame) parser.Pos {
if frame == nil || frame.fn == nil {
return parser.NoPos
}
return frame.fn.SourcePos(frame.ip + 1)
}
func wantEqXGotY(x, y int) string {
buf := make([]byte, 0, 20)
buf = append(buf, "want="...)
buf = strconv.AppendInt(buf, int64(x), 10)
buf = append(buf, " got="...)
buf = strconv.AppendInt(buf, int64(y), 10)
return string(buf)
}
func wantGEqXGotY(x, y int) string {
buf := make([]byte, 0, 20)
buf = append(buf, "want>="...)
buf = strconv.AppendInt(buf, int64(x), 10)
buf = append(buf, " got="...)
buf = strconv.AppendInt(buf, int64(y), 10)
return string(buf)
}
// Ported from runtime/debug.Stack
func debugStack() []byte {
buf := make([]byte, 1024)
for {
n := runtime.Stack(buf, false)
if n < len(buf) {
return buf[:n]
}
buf = make([]byte, 2*len(buf))
}
}
// Invoker invokes a given callee object (either a CompiledFunction or any other
// callable object) with the given arguments.
//
// Invoker creates a new VM instance if the callee is a CompiledFunction,
// otherwise it runs the callee directly. Every Invoker call checks if the VM is
// aborted. If it is, it returns ErrVMAborted.
//
// Invoker is not safe for concurrent use by multiple goroutines.
//
// Acquire and Release methods are used to acquire and release a VM from the
// pool. So it is possible to reuse a VM instance for multiple Invoke calls.
// This is useful when you want to execute multiple functions in a single VM.
// For example, you can use Acquire and Release to execute multiple functions
// in a single VM instance.
// Note that you should call Release after Acquire, if you want to reuse the VM.
// If you don't want to use the pool, you can just call Invoke method.
// It is unsafe to hold a reference to the VM after Release is called.
// Using VM pool is about three times faster than creating a new VM for each
// Invoke call.
type Invoker struct {
vm *VM
child *VM
callee Object
isCompiled bool
dorelease bool
}
// NewInvoker creates a new Invoker object.
func NewInvoker(vm *VM, callee Object) *Invoker {
inv := &Invoker{vm: vm, callee: callee}
_, inv.isCompiled = inv.callee.(*CompiledFunction)
return inv
}
// Acquire acquires a VM from the pool.
func (inv *Invoker) Acquire() {
inv.acquire(true)
}
func (inv *Invoker) acquire(usePool bool) {
if !inv.isCompiled {
inv.child = inv.vm
}
if inv.child != nil {
return
}
inv.child = inv.vm.pool.acquire(
inv.callee.(*CompiledFunction),
usePool,
)
if usePool {
inv.dorelease = true
}
}
// Release releases the VM back to the pool if it was acquired from the pool.
func (inv *Invoker) Release() {
if inv.child != nil && inv.dorelease {
inv.child.pool.release(inv.child)
}
inv.child = nil
inv.dorelease = false
}
// Invoke invokes the callee object with the given arguments.
func (inv *Invoker) Invoke(args ...Object) (Object, error) {
if inv.child == nil {
inv.acquire(false)
}
if inv.child.Aborted() {
return Undefined, ErrVMAborted
}
if inv.isCompiled {
return inv.child.Run(inv.vm.globals, args...)
}
return inv.invokeObject(inv.callee, args...)
}
func (inv *Invoker) invokeObject(callee Object, args ...Object) (Object, error) {
if !callee.CanCall() {
return Undefined, ErrNotCallable.NewError(callee.TypeName())
}
if c, ok := callee.(ExCallerObject); ok {
return c.CallEx(
Call{
vm: inv.vm,
vargs: args,
},
)
}
return callee.Call(args...)
}
type vmPool struct {
mu sync.Mutex
root *VM
vms map[*VM]struct{}
}
func (v *vmPool) abort(vm *VM) {
v.mu.Lock()
defer v.mu.Unlock()
for vm := range v.vms {
vm.Abort()
}
}
func (v *vmPool) acquire(cf *CompiledFunction, usePool bool) *VM {
var vm *VM
if usePool {
vm = vmSyncPool.Get().(*VM)
} else {
vm = &VM{bytecode: &Bytecode{}}
}
return v.root.pool._acquire(vm, cf)
}
func (v *vmPool) _acquire(vm *VM, cf *CompiledFunction) *VM {
v.mu.Lock()
defer v.mu.Unlock()
vm.bytecode.FileSet = v.root.bytecode.FileSet
vm.bytecode.Constants = v.root.bytecode.Constants
vm.bytecode.NumModules = v.root.bytecode.NumModules
vm.bytecode.Main = cf
vm.constants = v.root.bytecode.Constants
vm.modulesCache = v.root.modulesCache
vm.pool = vmPool{
root: v.root,
}
vm.noPanic = v.root.noPanic
if v.vms == nil {
v.vms = make(map[*VM]struct{})
}
v.vms[vm] = struct{}{}
return vm
}
func (v *vmPool) release(vm *VM) {
v.root.pool._release(vm)
}
func (v *vmPool) _release(vm *VM) {
v.mu.Lock()
delete(v.vms, vm)
v.mu.Unlock()
bc := vm.bytecode
*bc = Bytecode{}
*vm = VM{bytecode: bc}
vmSyncPool.Put(vm)
}
func (v *vmPool) clear() {
v.mu.Lock()
defer v.mu.Unlock()
for vm := range v.vms {
delete(v.vms, vm)
}
}
var vmSyncPool = sync.Pool{
New: func() interface{} {
return &VM{
bytecode: &Bytecode{},
}
},
}