diff --git a/builtins.go b/builtins.go index b954d072..aa254785 100644 --- a/builtins.go +++ b/builtins.go @@ -1,130 +1,44 @@ package tengo -var builtinFuncs = []*BuiltinFunction{ - { - Name: "len", - Value: builtinLen, - }, - { - Name: "copy", - Value: builtinCopy, - }, - { - Name: "append", - Value: builtinAppend, - }, - { - Name: "delete", - Value: builtinDelete, - }, - { - Name: "splice", - Value: builtinSplice, - }, - { - Name: "string", - Value: builtinString, - }, - { - Name: "int", - Value: builtinInt, - }, - { - Name: "bool", - Value: builtinBool, - }, - { - Name: "float", - Value: builtinFloat, - }, - { - Name: "char", - Value: builtinChar, - }, - { - Name: "bytes", - Value: builtinBytes, - }, - { - Name: "time", - Value: builtinTime, - }, - { - Name: "is_int", - Value: builtinIsInt, - }, - { - Name: "is_float", - Value: builtinIsFloat, - }, - { - Name: "is_string", - Value: builtinIsString, - }, - { - Name: "is_bool", - Value: builtinIsBool, - }, - { - Name: "is_char", - Value: builtinIsChar, - }, - { - Name: "is_bytes", - Value: builtinIsBytes, - }, - { - Name: "is_array", - Value: builtinIsArray, - }, - { - Name: "is_immutable_array", - Value: builtinIsImmutableArray, - }, - { - Name: "is_map", - Value: builtinIsMap, - }, - { - Name: "is_immutable_map", - Value: builtinIsImmutableMap, - }, - { - Name: "is_iterable", - Value: builtinIsIterable, - }, - { - Name: "is_time", - Value: builtinIsTime, - }, - { - Name: "is_error", - Value: builtinIsError, - }, - { - Name: "is_undefined", - Value: builtinIsUndefined, - }, - { - Name: "is_function", - Value: builtinIsFunction, - }, - { - Name: "is_callable", - Value: builtinIsCallable, - }, - { - Name: "type_name", - Value: builtinTypeName, - }, - { - Name: "format", - Value: builtinFormat, - }, - { - Name: "range", - Value: builtinRange, - }, +var builtinFuncs []*BuiltinFunction + +// if needVMObj is true, VM will pass [VMObj, args...] to fn when calling it. +func addBuiltinFunction(name string, fn CallableFunc, needVMObj bool) { + builtinFuncs = append(builtinFuncs, &BuiltinFunction{Name: name, Value: fn, NeedVMObj: needVMObj}) +} + +func init() { + addBuiltinFunction("len", builtinLen, false) + addBuiltinFunction("copy", builtinCopy, false) + addBuiltinFunction("append", builtinAppend, false) + addBuiltinFunction("delete", builtinDelete, false) + addBuiltinFunction("splice", builtinSplice, false) + addBuiltinFunction("string", builtinString, false) + addBuiltinFunction("int", builtinInt, false) + addBuiltinFunction("bool", builtinBool, false) + addBuiltinFunction("float", builtinFloat, false) + addBuiltinFunction("char", builtinChar, false) + addBuiltinFunction("bytes", builtinBytes, false) + addBuiltinFunction("time", builtinTime, false) + addBuiltinFunction("is_int", builtinIsInt, false) + addBuiltinFunction("is_float", builtinIsFloat, false) + addBuiltinFunction("is_string", builtinIsString, false) + addBuiltinFunction("is_bool", builtinIsBool, false) + addBuiltinFunction("is_char", builtinIsChar, false) + addBuiltinFunction("is_bytes", builtinIsBytes, false) + addBuiltinFunction("is_array", builtinIsArray, false) + addBuiltinFunction("is_immutable_array", builtinIsImmutableArray, false) + addBuiltinFunction("is_map", builtinIsMap, false) + addBuiltinFunction("is_immutable_map", builtinIsImmutableMap, false) + addBuiltinFunction("is_iterable", builtinIsIterable, false) + addBuiltinFunction("is_time", builtinIsTime, false) + addBuiltinFunction("is_error", builtinIsError, false) + addBuiltinFunction("is_undefined", builtinIsUndefined, false) + addBuiltinFunction("is_function", builtinIsFunction, false) + addBuiltinFunction("is_callable", builtinIsCallable, false) + addBuiltinFunction("type_name", builtinTypeName, false) + addBuiltinFunction("format", builtinFormat, false) + addBuiltinFunction("range", builtinRange, false) } // GetAllBuiltinFunctions returns all builtin function objects. diff --git a/bytecode.go b/bytecode.go index f3049cee..02a822bd 100644 --- a/bytecode.go +++ b/bytecode.go @@ -295,4 +295,5 @@ func init() { gob.Register(&Time{}) gob.Register(&Undefined{}) gob.Register(&UserFunction{}) + gob.Register(&BuiltinFunction{}) } diff --git a/docs/builtins.md b/docs/builtins.md index 940a215a..63aae5d8 100644 --- a/docs/builtins.md +++ b/docs/builtins.md @@ -126,6 +126,240 @@ v := ["a", "b", "c"] items := splice(v, 1, 1, "d", "e") // items == ["b"], v == ["a", "d", "e", "c"] ``` +## go + +Starts an independent concurrent goroutine which runs fn(arg1, arg2, ...) + +If fn is CompiledFunction, the current running VM will be cloned to create +a new VM in which the CompiledFunction will be running. +The fn can also be any object that has Call() method, such as BuiltinFunction, +in which case no cloned VM will be created. +Returns a goroutineVM object that has wait, result, abort methods. + +The goroutineVM will not exit unless: +1. All its descendant goroutineVMs exit +2. It calls abort() +3. Its goroutineVM object abort() is called on behalf of its parent VM +The latter 2 cases will trigger aborting procedure of all the descendant +goroutineVMs, which will further result in #1 above. + +```golang +var := 0 + +f1 := func(a,b) { var = 10; return a+b } +f2 := func(a,b,c) { var = 11; return a+b+c } + +gvm1 := go(f1,1,2) +gvm2 := go(f2,1,2,5) + +fmt.println(gvm1.result()) // 3 +fmt.println(gvm2.result()) // 8 +fmt.println(var) // 10 or 11 +``` + +* wait() waits for the goroutineVM to complete up to timeout seconds and +returns true if the goroutineVM exited(successfully or not) within the +timeout. It waits forever if the optional timeout not specified, +or timeout < 0. +* abort() triggers the termination process of the goroutineVM and all +its descendant VMs. +* result() waits the goroutineVM to complete, returns Error object if +any runtime error occurred during the execution, otherwise returns the +result value of fn(arg1, arg2, ...) + +### 1 client 1 server + +Below is a simple client server example: + +```golang +reqChan := makechan(8) +repChan := makechan(8) + +client := func(interval) { + reqChan.send("hello") + for i := 0; true; i++ { + fmt.println(repChan.recv()) + times.sleep(interval*times.second) + reqChan.send(i) + } +} + +server := func() { + for { + req := reqChan.recv() + if req == "hello" { + fmt.println(req) + repChan.send("world") + } else { + repChan.send(req+100) + } + } +} + +gClient := go(client, 2) +gServer := go(server) + +if ok := gClient.wait(5); !ok { + gClient.abort() +} +gServer.abort() + +//output: +//hello +//world +//100 +//101 +``` + +### n client n server, channel in channel + +```golang +sharedReqChan := makechan(128) + +client = func(name, interval, timeout) { + print := func(s) { + fmt.println(name, s) + } + print("started") + + repChan := makechan(1) + msg := {chan:repChan} + + msg.data = "hello" + sharedReqChan.send(msg) + print(repChan.recv()) + + for i := 0; i * interval < timeout; i++ { + msg.data = i + sharedReqChan.send(msg) + print(repChan.recv()) + times.sleep(interval*times.second) + } +} + +server = func(name) { + print := func(s) { + fmt.println(name, s) + } + print("started") + + for { + req := sharedReqChan.recv() + if req.data == "hello" { + req.chan.send("world") + } else { + req.chan.send(req.data+100) + } + } +} + +clients := func() { + for i :=0; i < 5; i++ { + go(client, format("client %d: ", i), 1, 4) + } +} + +servers := func() { + for i :=0; i < 2; i++ { + go(server, format("server %d: ", i)) + } +} + +// After 4 seconds, all clients should have exited normally +gclts := go(clients) +// If servers exit earlier than clients, then clients may be +// blocked forever waiting for the reply chan, because servers +// were aborted with the req fetched from sharedReqChan before +// sending back the reply. +// In such case, do below to abort() the clients manually +//go(func(){times.sleep(6*times.second); gclts.abort()}) + +// Servers are infinite loop, abort() them after 5 seconds +gsrvs := go(servers) +if ok := gsrvs.wait(5); !ok { + gsrvs.abort() +} + +// Main VM waits here until all the child "go" finish + +// If somehow the main VM is stuck, that is because there is +// at least one child VM that has not exited as expected, we +// can do abort() to force exit. +abort() + +//output: +//3 +//8 +//hello +//world +//100 +//101 + +//unordered output: +//client 4: started +//server 0: started +//client 4: world +//client 4: 100 +//client 3: started +//client 3: world +//client 3: 100 +//client 2: started +//client 2: world +//client 2: 100 +//client 0: started +//client 0: world +//client 0: 100 +//client 1: started +//client 1: world +//client 1: 100 +//server 1: started +//client 1: 101 +//client 2: 101 +//client 4: 101 +//client 0: 101 +//client 3: 101 +//client 3: 102 +//client 0: 102 +//client 2: 102 +//client 1: 102 +//client 4: 102 +//client 0: 103 +//client 3: 103 +//client 2: 103 +//client 1: 103 +//client 4: 103 + +``` + +## abort +Triggers the termination process of the current VM and all its descendant VMs. + +## makechan + +Makes a channel to send/receive object and returns a chan object that has +send, recv, close methods. + +```golang +unbufferedChan := makechan() +bufferedChan := makechan(128) + +// Send will block if the channel is full. +bufferedChan.send("hello") // send string +bufferedChan.send(55) // send int +bufferedChan.send([66, makechan(1)]) // channel in channel + +// Receive will block if the channel is empty. +obj := bufferedChan.recv() + +// Send to a closed channel causes panic. +// Receive from a closed channel returns undefined value. +unbufferedChan.close() +bufferedChan.close() +``` + +On the time the VM that the chan is running in is aborted, the sending +or receiving call returns immediately. + ## type_name Returns the type_name of an object. diff --git a/errors.go b/errors.go index 8ef610a3..5c66cc08 100644 --- a/errors.go +++ b/errors.go @@ -52,6 +52,9 @@ var ( // ErrInvalidRangeStep is an error where the step parameter is less than or equal to 0 when using builtin range function. ErrInvalidRangeStep = errors.New("range step must be greater than 0") + + // ErrVMAborted is an error to denote the VM was forcibly terminated without proper exit. + ErrVMAborted = errors.New("virtual machine aborted") ) // ErrInvalidArgumentType represents an invalid argument value type error. diff --git a/goroutinevm.go b/goroutinevm.go new file mode 100644 index 00000000..993abb4f --- /dev/null +++ b/goroutinevm.go @@ -0,0 +1,272 @@ +package tengo + +import ( + "fmt" + "runtime/debug" + "sync/atomic" + "time" +) + +func init() { + addBuiltinFunction("go", builtinGovm, true) + addBuiltinFunction("abort", builtinAbort, true) + addBuiltinFunction("makechan", builtinMakechan, false) +} + +type ret struct { + val Object + err error +} + +type goroutineVM struct { + *VM // if not nil, run CompiledFunction in VM + ret // return value + waitChan chan ret + done int64 +} + +// Starts a independent concurrent goroutine which runs fn(arg1, arg2, ...) +// +// If fn is CompiledFunction, the current running VM will be cloned to create +// a new VM in which the CompiledFunction will be running. +// +// The fn can also be any object that has Call() method, such as BuiltinFunction, +// in which case no cloned VM will be created. +// +// Returns a goroutineVM object that has wait, result, abort methods. +// +// The goroutineVM will not exit unless: +// 1. All its descendant goroutineVMs exit +// 2. It calls abort() +// 3. Its goroutineVM object abort() is called on behalf of its parent VM +// The latter 2 cases will trigger aborting procedure of all the descendant goroutineVMs, +// which will further result in #1 above. +func builtinGovm(args ...Object) (Object, error) { + vm := args[0].(*VMObj).Value + args = args[1:] // the first arg is VMObj inserted by VM + if len(args) == 0 { + return nil, ErrWrongNumArguments + } + + fn := args[0] + if !fn.CanCall() { + return nil, ErrInvalidArgumentType{ + Name: "first", + Expected: "callable function", + Found: fn.TypeName(), + } + } + + gvm := &goroutineVM{ + waitChan: make(chan ret, 1), + } + + var callers []frame + cfn, compiled := fn.(*CompiledFunction) + if compiled { + gvm.VM = vm.ShallowClone() + } else { + callers = vm.callers() + } + + if err := vm.addChild(gvm.VM); err != nil { + return nil, err + } + go func() { + var val Object + var err error + defer func() { + if perr := recover(); perr != nil { + if callers == nil { + panic("callers not saved") + } + err = fmt.Errorf("\nRuntime Panic: %v%s\n%s", perr, vm.callStack(callers), debug.Stack()) + } + if err != nil { + vm.addError(err) + } + gvm.waitChan <- ret{val, err} + vm.delChild(gvm.VM) + gvm.VM = nil + }() + + if cfn != nil { + val, err = gvm.RunCompiled(cfn, args[1:]...) + } else { + var nargs []Object + if bltnfn, ok := fn.(*BuiltinFunction); ok { + if bltnfn.NeedVMObj { + // pass VM as the first para to builtin functions + nargs = append(nargs, vm.selfObject()) + } + } + nargs = append(nargs, args[1:]...) + val, err = fn.Call(nargs...) + } + }() + + obj := map[string]Object{ + "result": &BuiltinFunction{Value: gvm.getRet}, + "wait": &BuiltinFunction{Value: gvm.waitTimeout}, + "abort": &BuiltinFunction{Value: gvm.abort}, + } + return &Map{Value: obj}, nil +} + +// Triggers the termination process of the current VM and all its descendant VMs. +func builtinAbort(args ...Object) (Object, error) { + vm := args[0].(*VMObj).Value + args = args[1:] // the first arg is VMObj inserted by VM + if len(args) != 0 { + return nil, ErrWrongNumArguments + } + vm.Abort() // aborts self and all descendant VMs + return nil, nil +} + +// Returns true if the goroutineVM is done +func (gvm *goroutineVM) wait(seconds int64) bool { + if atomic.LoadInt64(&gvm.done) == 1 { + return true + } + + if seconds < 0 { + seconds = 3153600000 // 100 years + } + + select { + case gvm.ret = <-gvm.waitChan: + atomic.StoreInt64(&gvm.done, 1) + case <-time.After(time.Duration(seconds) * time.Second): + return false + } + + return true +} + +// Waits for the goroutineVM to complete up to timeout seconds. +// Returns true if the goroutineVM exited(successfully or not) within the timeout. +// Waits forever if the optional timeout not specified, or timeout < 0. +func (gvm *goroutineVM) waitTimeout(args ...Object) (Object, error) { + if len(args) > 1 { + return nil, ErrWrongNumArguments + } + timeOut := -1 + if len(args) == 1 { + t, ok := ToInt(args[0]) + if !ok { + return nil, ErrInvalidArgumentType{ + Name: "first", + Expected: "int(compatible)", + Found: args[0].TypeName(), + } + } + timeOut = t + } + + if gvm.wait(int64(timeOut)) { + return TrueValue, nil + } + return FalseValue, nil +} + +// Triggers the termination process of the goroutineVM and all its descendant VMs. +func (gvm *goroutineVM) abort(args ...Object) (Object, error) { + if len(args) != 0 { + return nil, ErrWrongNumArguments + } + if gvm.VM != nil { + gvm.Abort() + } + return nil, nil +} + +// Waits the goroutineVM to complete, return Error object if any runtime error occurred +// during the execution, otherwise return the result value of fn(arg1, arg2, ...) +func (gvm *goroutineVM) getRet(args ...Object) (Object, error) { + if len(args) != 0 { + return nil, ErrWrongNumArguments + } + + gvm.wait(-1) + if gvm.ret.err != nil { + return &Error{Value: &String{Value: gvm.ret.err.Error()}}, nil + } + + return gvm.ret.val, nil +} + +type objchan chan Object + +// Makes a channel to send/receive object +// Returns a chan object that has send, recv, close methods. +func builtinMakechan(args ...Object) (Object, error) { + var size int + switch len(args) { + case 0: + case 1: + n, ok := ToInt(args[0]) + if !ok { + return nil, ErrInvalidArgumentType{ + Name: "first", + Expected: "int(compatible)", + Found: args[0].TypeName(), + } + } + size = n + default: + return nil, ErrWrongNumArguments + } + + oc := make(objchan, size) + obj := map[string]Object{ + "send": &BuiltinFunction{Value: oc.send, NeedVMObj: true}, + "recv": &BuiltinFunction{Value: oc.recv, NeedVMObj: true}, + "close": &BuiltinFunction{Value: oc.close}, + } + return &Map{Value: obj}, nil +} + +// Sends an obj to the channel, will block if channel is full and the VM has not been aborted. +// Sends to a closed channel causes panic. +func (oc objchan) send(args ...Object) (Object, error) { + vm := args[0].(*VMObj).Value + args = args[1:] // the first arg is VMObj inserted by VM + if len(args) != 1 { + return nil, ErrWrongNumArguments + } + select { + case <-vm.AbortChan: + return nil, ErrVMAborted + case oc <- args[0]: + } + return nil, nil +} + +// Receives an obj from the channel, will block if channel is empty and the VM has not been aborted. +// Receives from a closed channel returns undefined value. +func (oc objchan) recv(args ...Object) (Object, error) { + vm := args[0].(*VMObj).Value + args = args[1:] // the first arg is VMObj inserted by VM + if len(args) != 0 { + return nil, ErrWrongNumArguments + } + select { + case <-vm.AbortChan: + return nil, ErrVMAborted + case obj, ok := <-oc: + if ok { + return obj, nil + } + } + return nil, nil +} + +// Closes the channel. +func (oc objchan) close(args ...Object) (Object, error) { + if len(args) != 0 { + return nil, ErrWrongNumArguments + } + close(oc) + return nil, nil +} diff --git a/objects.go b/objects.go index 30913db5..c4fcda7a 100644 --- a/objects.go +++ b/objects.go @@ -320,8 +320,9 @@ func (o *Bool) GobEncode() (b []byte, err error) { // BuiltinFunction represents a builtin function. type BuiltinFunction struct { ObjectImpl - Name string - Value CallableFunc + Name string + Value CallableFunc + NeedVMObj bool } // TypeName returns the name of the type. @@ -335,7 +336,7 @@ func (o *BuiltinFunction) String() string { // Copy returns a copy of the type. func (o *BuiltinFunction) Copy() Object { - return &BuiltinFunction{Value: o.Value} + return &BuiltinFunction{Value: o.Value, NeedVMObj: o.NeedVMObj} } // Equals returns true if the value of the type is equal to the value of @@ -594,6 +595,7 @@ func (o *CompiledFunction) Copy() Object { NumLocals: o.NumLocals, NumParameters: o.NumParameters, VarArgs: o.VarArgs, + SourceMap: o.SourceMap, Free: append([]*ObjectPtr{}, o.Free...), // DO NOT Copy() of elements; these are variable pointers } } diff --git a/stdlib/fmt.go b/stdlib/fmt.go index 9945277f..352c427a 100644 --- a/stdlib/fmt.go +++ b/stdlib/fmt.go @@ -7,22 +7,26 @@ import ( ) var fmtModule = map[string]tengo.Object{ - "print": &tengo.UserFunction{Name: "print", Value: fmtPrint}, - "printf": &tengo.UserFunction{Name: "printf", Value: fmtPrintf}, - "println": &tengo.UserFunction{Name: "println", Value: fmtPrintln}, + "print": &tengo.BuiltinFunction{Value: fmtPrint, NeedVMObj: true}, + "printf": &tengo.BuiltinFunction{Value: fmtPrintf, NeedVMObj: true}, + "println": &tengo.BuiltinFunction{Value: fmtPrintln, NeedVMObj: true}, "sprintf": &tengo.UserFunction{Name: "sprintf", Value: fmtSprintf}, } func fmtPrint(args ...tengo.Object) (ret tengo.Object, err error) { + vm := args[0].(*tengo.VMObj).Value + args = args[1:] // the first arg is VMObj inserted by VM printArgs, err := getPrintArgs(args...) if err != nil { return nil, err } - _, _ = fmt.Print(printArgs...) + fmt.Fprint(vm.Out, printArgs...) return nil, nil } func fmtPrintf(args ...tengo.Object) (ret tengo.Object, err error) { + vm := args[0].(*tengo.VMObj).Value + args = args[1:] // the first arg is VMObj inserted by VM numArgs := len(args) if numArgs == 0 { return nil, tengo.ErrWrongNumArguments @@ -37,7 +41,7 @@ func fmtPrintf(args ...tengo.Object) (ret tengo.Object, err error) { } } if numArgs == 1 { - fmt.Print(format) + fmt.Fprint(vm.Out, format) return nil, nil } @@ -45,17 +49,19 @@ func fmtPrintf(args ...tengo.Object) (ret tengo.Object, err error) { if err != nil { return nil, err } - fmt.Print(s) + fmt.Fprint(vm.Out, s) return nil, nil } func fmtPrintln(args ...tengo.Object) (ret tengo.Object, err error) { + vm := args[0].(*tengo.VMObj).Value + args = args[1:] // the first arg is VMObj inserted by VM printArgs, err := getPrintArgs(args...) if err != nil { return nil, err } printArgs = append(printArgs, "\n") - _, _ = fmt.Print(printArgs...) + fmt.Fprint(vm.Out, printArgs...) return nil, nil } diff --git a/stdlib/os.go b/stdlib/os.go index 576bc94b..cf3da15b 100644 --- a/stdlib/os.go +++ b/stdlib/os.go @@ -39,9 +39,10 @@ var osModule = map[string]tengo.Object{ "seek_set": &tengo.Int{Value: int64(io.SeekStart)}, "seek_cur": &tengo.Int{Value: int64(io.SeekCurrent)}, "seek_end": &tengo.Int{Value: int64(io.SeekEnd)}, - "args": &tengo.UserFunction{ - Name: "args", - Value: osArgs, + "args": &tengo.BuiltinFunction{ + Name: "args", + Value: osArgs, + NeedVMObj: true, }, // args() => array(string) "chdir": &tengo.UserFunction{ Name: "chdir", @@ -328,11 +329,13 @@ func osOpenFile(args ...tengo.Object) (tengo.Object, error) { } func osArgs(args ...tengo.Object) (tengo.Object, error) { + vm := args[0].(*tengo.VMObj).Value + args = args[1:] // the first arg is VMObj inserted by VM if len(args) != 0 { return nil, tengo.ErrWrongNumArguments } arr := &tengo.Array{} - for _, osArg := range os.Args { + for _, osArg := range vm.Args { if len(osArg) > tengo.MaxStringLen { return nil, tengo.ErrStringLimit } diff --git a/stdlib/times.go b/stdlib/times.go index 0b6f7bd4..a82ff964 100644 --- a/stdlib/times.go +++ b/stdlib/times.go @@ -40,9 +40,10 @@ var timesModule = map[string]tengo.Object{ "october": &tengo.Int{Value: int64(time.October)}, "november": &tengo.Int{Value: int64(time.November)}, "december": &tengo.Int{Value: int64(time.December)}, - "sleep": &tengo.UserFunction{ - Name: "sleep", - Value: timesSleep, + "sleep": &tengo.BuiltinFunction{ + Name: "sleep", + Value: timesSleep, + NeedVMObj: true, }, // sleep(int) "parse_duration": &tengo.UserFunction{ Name: "parse_duration", @@ -183,6 +184,8 @@ var timesModule = map[string]tengo.Object{ } func timesSleep(args ...tengo.Object) (ret tengo.Object, err error) { + vm := args[0].(*tengo.VMObj).Value + args = args[1:] // the first arg is VMObj inserted by VM if len(args) != 1 { err = tengo.ErrWrongNumArguments return @@ -197,10 +200,26 @@ func timesSleep(args ...tengo.Object) (ret tengo.Object, err error) { } return } - - time.Sleep(time.Duration(i1)) ret = tengo.UndefinedValue + if time.Duration(i1) <= time.Second { + time.Sleep(time.Duration(i1)) + return + } + done := make(chan struct{}) + go func() { + time.Sleep(time.Duration(i1)) + select { + case <-vm.AbortChan: + case done <- struct{}{}: + } + }() + + select { + case <-vm.AbortChan: + return nil, tengo.ErrVMAborted + case <-done: + } return } diff --git a/vm.go b/vm.go index 811ecef9..3557fdbb 100644 --- a/vm.go +++ b/vm.go @@ -1,7 +1,13 @@ package tengo import ( + "errors" "fmt" + "io" + "os" + "runtime/debug" + "strings" + "sync" "sync/atomic" "github.com/d5/tengo/v2/parser" @@ -16,14 +22,21 @@ type frame struct { basePointer int } +type vmChildCtl struct { + sync.WaitGroup + sync.Mutex + vmMap map[*VM]struct{} + errors []error +} + // VM is a virtual machine that executes the bytecode compiled by Compiler. type VM struct { constants []Object - stack [StackSize]Object + stack []Object sp int globals []Object fileSet *parser.SourceFileSet - frames [MaxFrames]frame + frames []*frame framesIndex int curFrame *frame curInsts []byte @@ -32,8 +45,18 @@ type VM struct { maxAllocs int64 allocs int64 err error + AbortChan chan struct{} + childCtl vmChildCtl + In io.Reader + Out io.Writer + Args []string } +const ( + initialStackSize = 64 + initialFrames = 16 +) + // NewVM creates a VM. func NewVM( bytecode *Bytecode, @@ -48,50 +71,252 @@ func NewVM( sp: 0, globals: globals, fileSet: bytecode.FileSet, + frames: make([]*frame, 0, initialFrames), framesIndex: 1, ip: -1, maxAllocs: maxAllocs, + AbortChan: make(chan struct{}), + childCtl: vmChildCtl{vmMap: make(map[*VM]struct{})}, + In: os.Stdin, + Out: os.Stdout, + Args: os.Args, } - v.frames[0].fn = bytecode.MainFunction - v.frames[0].ip = -1 - v.curFrame = &v.frames[0] - v.curInsts = v.curFrame.fn.Instructions + frame := &frame{ + fn: bytecode.MainFunction, + ip: -1, + } + v.frames = append(v.frames, frame) return v } -// Abort aborts the execution. -func (v *VM) Abort() { - atomic.StoreInt64(&v.aborting, 1) -} - // Run starts the execution. func (v *VM) Run() (err error) { - // reset VM states - v.sp = 0 - v.curFrame = &(v.frames[0]) + atomic.StoreInt64(&v.aborting, 0) + _, err = v.RunCompiled(nil) + if err == nil && atomic.LoadInt64(&v.aborting) == 1 { + err = ErrVMAborted // root VM was aborted + } + return +} + +func concatInsts(instructions ...[]byte) []byte { + var concat []byte + for _, i := range instructions { + concat = append(concat, i...) + } + return concat +} + +var emptyEntry = &CompiledFunction{ + Instructions: MakeInstruction(parser.OpSuspend), +} + +// ShallowClone creates a shallow copy of the current VM, with separate stack and frame. +// The copy shares the underlying globals, constants with the original. +// ShallowClone is typically followed by RunCompiled to run user supplied compiled function. +func (v *VM) ShallowClone() *VM { + vClone := &VM{ + constants: v.constants, + sp: 0, + globals: v.globals, + fileSet: v.fileSet, + frames: make([]*frame, 0, initialFrames), + framesIndex: 1, + ip: -1, + maxAllocs: v.maxAllocs, + AbortChan: make(chan struct{}), + childCtl: vmChildCtl{vmMap: make(map[*VM]struct{})}, + In: v.In, + Out: v.Out, + Args: v.Args, + } + frame := &frame{ + fn: emptyEntry, + ip: -1, + } + vClone.frames = append(vClone.frames, frame) + return vClone +} + +// constract wrapper function func(fn, ...args){ return fn(args...) } +var funcWrapper = &CompiledFunction{ + Instructions: concatInsts( + MakeInstruction(parser.OpGetLocal, 0), + MakeInstruction(parser.OpGetLocal, 1), + MakeInstruction(parser.OpCall, 1, 1), + MakeInstruction(parser.OpReturn, 1), + ), + NumLocals: 2, + NumParameters: 2, + VarArgs: true, +} + +func (v *VM) releaseSpace() { + v.stack = nil + v.frames = append(make([]*frame, 0, initialFrames), v.frames[0]) +} + +// RunCompiled run the VM with user supplied function fn. +func (v *VM) RunCompiled(fn *CompiledFunction, args ...Object) (val Object, err error) { + v.stack = make([]Object, initialStackSize) + if fn == nil { // normal Run + // reset VM states + v.sp = 0 + } else { // run user supplied function + entry := &CompiledFunction{ + Instructions: concatInsts( + MakeInstruction(parser.OpCall, 1+len(args), 0), + MakeInstruction(parser.OpSuspend), + ), + } + v.stack[0] = funcWrapper + v.stack[1] = fn + for i, arg := range args { + v.stack[i+2] = arg + } + v.sp = 2 + len(args) + v.frames[0].fn = entry + } + + v.curFrame = v.frames[0] v.curInsts = v.curFrame.fn.Instructions v.framesIndex = 1 v.ip = -1 v.allocs = v.maxAllocs + 1 + defer func() { + if perr := recover(); perr != nil { + v.err = ErrPanic{perr, debug.Stack()} + v.Abort() // run time panic should trigger abort chain + } + v.childCtl.Wait() // waits for all child VMs to exit + err = v.postRun() + if fn != nil && atomic.LoadInt64(&v.aborting) == 0 { + val = v.stack[v.sp-1] + } + v.releaseSpace() + }() + + val = UndefinedValue v.run() - atomic.StoreInt64(&v.aborting, 0) + return +} + +// ErrPanic is an error where panic happended in the VM. +type ErrPanic struct { + perr interface{} + stack []byte +} + +func (e ErrPanic) Error() string { + return fmt.Sprintf("panic: %v\n%s", e.perr, e.stack) +} + +func (v *VM) addError(err error) { + v.childCtl.Lock() + v.childCtl.errors = append(v.childCtl.errors, err) + v.childCtl.Unlock() +} + +// Abort aborts the execution of current VM and all its descendant VMs. +func (v *VM) Abort() { + if atomic.LoadInt64(&v.aborting) != 0 { + return + } + v.childCtl.Lock() + atomic.StoreInt64(&v.aborting, 1) + close(v.AbortChan) // broadcast to all receivers + for cvm := range v.childCtl.vmMap { + cvm.Abort() + } + v.childCtl.Unlock() +} + +func (v *VM) addChild(cvm *VM) error { + v.childCtl.Lock() + defer v.childCtl.Unlock() + if atomic.LoadInt64(&v.aborting) != 0 { + return ErrVMAborted + } + v.childCtl.Add(1) + if cvm != nil { + v.childCtl.vmMap[cvm] = struct{}{} + } + return nil +} + +func (v *VM) delChild(cvm *VM) { + if cvm != nil { + v.childCtl.Lock() + delete(v.childCtl.vmMap, cvm) + v.childCtl.Unlock() + } + v.childCtl.Done() +} + +func (v *VM) callers() (frames []frame) { + curFrame := *v.curFrame + curFrame.ip = v.ip - 1 + frames = append(frames, curFrame) + for i := v.framesIndex - 1; i >= 1; i-- { + curFrame = *v.frames[i-1] + frames = append(frames, curFrame) + } + return frames +} + +func (v *VM) callStack(frames []frame) string { + if frames == nil { + frames = v.callers() + } + + var sb strings.Builder + for _, f := range frames { + filePos := v.fileSet.Position(f.fn.SourcePos(f.ip)) + fmt.Fprintf(&sb, "\n\tat %s", filePos) + } + return sb.String() +} + +func (v *VM) postRun() (err error) { err = v.err + // ErrVMAborted is user behavior thus it is not an actual runtime error + if errors.Is(err, ErrVMAborted) { + err = nil + } if err != nil { - filePos := v.fileSet.Position( - v.curFrame.fn.SourcePos(v.ip - 1)) - err = fmt.Errorf("Runtime Error: %w\n\tat %s", - err, filePos) - for v.framesIndex > 1 { - v.framesIndex-- - v.curFrame = &v.frames[v.framesIndex-1] - filePos = v.fileSet.Position( - v.curFrame.fn.SourcePos(v.curFrame.ip - 1)) - err = fmt.Errorf("%w\n\tat %s", err, filePos) + var e ErrPanic + if errors.As(err, &e) { + err = fmt.Errorf("\nRuntime Panic: %v%s\n%s", e.perr, v.callStack(nil), e.stack) + } else { + err = fmt.Errorf("\nRuntime Error: %w%s", err, v.callStack(nil)) } - return err } - return nil + + var sb strings.Builder + for _, cerr := range v.childCtl.errors { + fmt.Fprintf(&sb, "%v\n", cerr) + } + cerrs := sb.String() + + if err != nil && len(cerrs) != 0 { + err = fmt.Errorf("%w\n%s", err, cerrs) + return + } + if len(cerrs) != 0 { + err = fmt.Errorf("%s", cerrs) + } + return +} + +// VMObj exports VM +type VMObj struct { + ObjectImpl + Value *VM +} + +func (v *VM) selfObject() Object { + return &VMObj{Value: v} } func (v *VM) run() { @@ -550,12 +775,14 @@ func (v *VM) run() { v.sp-- switch arr := v.stack[v.sp].(type) { case *Array: + v.checkGrowStack(len(arr.Value)) for _, item := range arr.Value { v.stack[v.sp] = item v.sp++ } numArgs += len(arr.Value) - 1 case *ImmutableArray: + v.checkGrowStack(len(arr.Value)) for _, item := range arr.Value { v.stack[v.sp] = item v.sp++ @@ -619,7 +846,10 @@ func (v *VM) run() { // update call frame v.curFrame.ip = v.ip // store current ip before call - v.curFrame = &(v.frames[v.framesIndex]) + if v.framesIndex >= len(v.frames) { + v.frames = append(v.frames, &frame{}) + } + v.curFrame = v.frames[v.framesIndex] v.curFrame.fn = callee v.curFrame.freeVars = callee.Free v.curFrame.basePointer = v.sp - numArgs @@ -629,6 +859,12 @@ func (v *VM) run() { v.sp = v.sp - numArgs + callee.NumLocals } else { var args []Object + if bltnfn, ok := value.(*BuiltinFunction); ok { + if bltnfn.NeedVMObj { + // pass VM as the first para to builtin functions + args = append(args, v.selfObject()) + } + } args = append(args, v.stack[v.sp-numArgs:v.sp]...) ret, e := value.Call(args...) v.sp -= numArgs + 1 @@ -674,7 +910,7 @@ func (v *VM) run() { } //v.sp-- v.framesIndex-- - v.curFrame = &v.frames[v.framesIndex-1] + v.curFrame = v.frames[v.framesIndex-1] v.curInsts = v.curFrame.fn.Instructions v.ip = v.curFrame.ip //v.sp = lastFrame.basePointer - 1 @@ -767,6 +1003,7 @@ func (v *VM) run() { NumLocals: fn.NumLocals, NumParameters: fn.NumParameters, VarArgs: fn.VarArgs, + SourceMap: fn.SourceMap, Free: free, } v.allocs-- @@ -869,7 +1106,27 @@ func (v *VM) run() { v.err = fmt.Errorf("unknown opcode: %d", v.curInsts[v.ip]) return } + v.checkGrowStack(0) + } +} + +func (v *VM) checkGrowStack(added int) { + should := v.sp + added + if should < len(v.stack) { + return + } + if should >= StackSize { + v.err = ErrStackOverflow + return + } + roundup := initialStackSize + newSize := len(v.stack) * 2 + if should > newSize { + newSize = (should + roundup) / roundup * roundup } + new := make([]Object, newSize) + copy(new, v.stack) + v.stack = new } // IsStackEmpty tests if the stack is empty or not.