Merge pull request from d5/scriptctx

Add Context to script run
This commit is contained in:
Daniel Kang 2019-01-20 08:48:10 -08:00 committed by GitHub
commit 00168726d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 156 additions and 96 deletions

View file

@ -22,23 +22,25 @@ Tengo is [fast](#benchmark) as it's compiled to bytecode and executed on stack-b
- Garbage collected _(thanks to Go runtime)_
- Easily extensible using customizable types
- Written in pure Go _(no CGO, no external dependencies)_
- Executable as a [standalone language](https://github.com/d5/tengo#tengo-as-a-standalone-language) _(without writing any Go code)_
- Executable as a standalone language
## Benchmark
| | fib(35) | fibt(35) | Type |
| :--- | ---: | ---: | :---: |
| Go | `59ms` | `4ms` | Go (native) |
| [**Tengo**](https://github.com/d5/tengo) | `4,809ms` | `5ms` | VM on Go |
| Lua | `1,752ms` | `3ms` | Lua (native) |
| [go-lua](https://github.com/Shopify/go-lua) | `5,236ms` | `5ms` | Lua VM on Go |
| [GopherLua](https://github.com/yuin/gopher-lua) | `5,558ms` | `5ms` | Lua VM on Go |
| Python | `3,132ms` | `28ms` | Python (native) |
| [starlark-go](https://github.com/google/starlark-go) | `16,789ms` | `5ms` | Python-like Interpreter on Go |
| [otto](https://github.com/robertkrimen/otto) | `85,765ms` | `22ms` | JS Interpreter on Go |
| [Anko](https://github.com/mattn/anko) | `99,235ms` | `24ms` | Interpreter on Go |
| Go | `67ms` | `4ms` | Go (native) |
| [**Tengo**](https://github.com/d5/tengo) | `4,390ms` | `5ms` | VM on Go |
| Lua | `1,804ms` | `3ms` | Lua (native) |
| [go-lua](https://github.com/Shopify/go-lua) | `5,114ms` | `4ms` | Lua VM on Go |
| [GopherLua](https://github.com/yuin/gopher-lua) | `5,679ms` | `5ms` | Lua VM on Go |
| Python | `2,853ms` | `25ms` | Python (native) |
| [starlark-go](https://github.com/google/starlark-go) | `16,725ms` | `5ms` | Python-like Interpreter on Go |
| [otto](https://github.com/robertkrimen/otto) | `88,148ms` | `21ms` | JS Interpreter on Go |
| [Anko](https://github.com/mattn/anko) | `107,968ms` | `22ms` | Interpreter on Go |
[fib(35)](https://github.com/d5/tengobench/blob/master/code/fib.tengo) is a function to compute 35th Fibonacci number, and, [fibt(35)](https://github.com/d5/tengobench/blob/master/code/fibtc.tengo) is the [tail-call](https://en.wikipedia.org/wiki/Tail_call) version of the same function. You can see all the code used for this test in [tengobench](https://github.com/d5/tengobench).
[fib(35)](https://github.com/d5/tengobench/blob/master/code/fib.tengo) is a function to compute 35th Fibonacci number, and, [fibt(35)](https://github.com/d5/tengobench/blob/master/code/fibtc.tengo) is the [tail-call](https://en.wikipedia.org/wiki/Tail_call) version of the same function.
_Please note that **Go** case does not read the source code from a local file, while all other cases do. All shell commands and the source code used in this benchmarking is available [here](https://github.com/d5/tengobench)._
## Tengo Syntax in 5 Minutes

View file

@ -176,9 +176,9 @@ func ReadOperands(numOperands []int, ins []byte) (operands []int, offset int) {
for _, width := range numOperands {
switch width {
case 1:
operands = append(operands, int(ReadUint8(ins[offset:])))
operands = append(operands, int(ins[offset]))
case 2:
operands = append(operands, int(ReadUint16(ins[offset:])))
operands = append(operands, int(ins[offset+1])|int(ins[offset])<<8)
}
offset += width
@ -186,13 +186,3 @@ func ReadOperands(numOperands []int, ins []byte) (operands []int, offset int) {
return
}
// ReadUint16 reads uint16 from the byte slice.
func ReadUint16(b []byte) uint16 {
return uint16(b[1]) | uint16(b[0])<<8
}
// ReadUint8 reads uint8 from the byte slice.
func ReadUint8(b []byte) uint8 {
return uint8(b[0])
}

View file

@ -3,6 +3,7 @@ package runtime
import (
"errors"
"fmt"
"sync/atomic"
"github.com/d5/tengo/compiler"
"github.com/d5/tengo/compiler/token"
@ -38,7 +39,8 @@ type VM struct {
curFrame *Frame
curInsts []byte
curIPLimit int
aborting bool
ip int
aborting int64
}
// NewVM creates a VM.
@ -67,27 +69,24 @@ func NewVM(bytecode *compiler.Bytecode, globals []*objects.Object) *VM {
curFrame: &(frames[0]),
curInsts: frames[0].fn.Instructions,
curIPLimit: len(frames[0].fn.Instructions) - 1,
ip: -1,
}
}
// Abort aborts the execution.
func (v *VM) Abort() {
v.aborting = true
atomic.StoreInt64(&v.aborting, 1)
}
// Run starts the execution.
func (v *VM) Run() error {
var ip int
for v.ip < v.curIPLimit && (atomic.LoadInt64(&v.aborting) == 0) {
v.ip++
for v.curFrame.ip < v.curIPLimit && !v.aborting {
v.curFrame.ip++
ip = v.curFrame.ip
switch compiler.Opcode(v.curInsts[ip]) {
switch compiler.Opcode(v.curInsts[v.ip]) {
case compiler.OpConstant:
cidx := compiler.ReadUint16(v.curInsts[ip+1:])
v.curFrame.ip += 2
cidx := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8
v.ip += 2
if v.sp >= StackSize {
return ErrStackOverflow
@ -437,54 +436,54 @@ func (v *VM) Run() error {
}
case compiler.OpJumpFalsy:
pos := int(compiler.ReadUint16(v.curInsts[ip+1:]))
v.curFrame.ip += 2
pos := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8
v.ip += 2
condition := v.stack[v.sp-1]
v.sp--
if (*condition).IsFalsy() {
v.curFrame.ip = pos - 1
v.ip = pos - 1
}
case compiler.OpAndJump:
pos := int(compiler.ReadUint16(v.curInsts[ip+1:]))
v.curFrame.ip += 2
pos := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8
v.ip += 2
condition := *v.stack[v.sp-1]
if condition.IsFalsy() {
v.curFrame.ip = pos - 1
v.ip = pos - 1
} else {
v.sp--
}
case compiler.OpOrJump:
pos := int(compiler.ReadUint16(v.curInsts[ip+1:]))
v.curFrame.ip += 2
pos := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8
v.ip += 2
condition := *v.stack[v.sp-1]
if !condition.IsFalsy() {
v.curFrame.ip = pos - 1
v.ip = pos - 1
} else {
v.sp--
}
case compiler.OpJump:
pos := int(compiler.ReadUint16(v.curInsts[ip+1:]))
v.curFrame.ip = pos - 1
pos := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8
v.ip = pos - 1
case compiler.OpSetGlobal:
globalIndex := compiler.ReadUint16(v.curInsts[ip+1:])
v.curFrame.ip += 2
globalIndex := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8
v.ip += 2
v.sp--
v.globals[globalIndex] = v.stack[v.sp]
case compiler.OpSetSelGlobal:
globalIndex := compiler.ReadUint16(v.curInsts[ip+1:])
numSelectors := int(compiler.ReadUint8(v.curInsts[ip+3:]))
v.curFrame.ip += 3
globalIndex := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8
numSelectors := int(v.curInsts[v.ip+3])
v.ip += 3
// pop selector outcomes (left to right)
selectors := make([]interface{}, numSelectors, numSelectors)
@ -511,8 +510,8 @@ func (v *VM) Run() error {
}
case compiler.OpGetGlobal:
globalIndex := compiler.ReadUint16(v.curInsts[ip+1:])
v.curFrame.ip += 2
globalIndex := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8
v.ip += 2
val := v.globals[globalIndex]
@ -524,8 +523,8 @@ func (v *VM) Run() error {
v.sp++
case compiler.OpArray:
numElements := int(compiler.ReadUint16(v.curInsts[ip+1:]))
v.curFrame.ip += 2
numElements := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8
v.ip += 2
var elements []objects.Object
for i := v.sp - numElements; i < v.sp; i++ {
@ -543,8 +542,8 @@ func (v *VM) Run() error {
v.sp++
case compiler.OpMap:
numElements := int(compiler.ReadUint16(v.curInsts[ip+1:]))
v.curFrame.ip += 2
numElements := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8
v.ip += 2
kv := make(map[string]objects.Object)
for i := v.sp - numElements; i < v.sp; i += 2 {
@ -803,8 +802,8 @@ func (v *VM) Run() error {
}
case compiler.OpCall:
numArgs := int(compiler.ReadUint8(v.curInsts[ip+1:]))
v.curFrame.ip++
numArgs := int(v.curInsts[v.ip+1])
v.ip++
callee := *v.stack[v.sp-1-numArgs]
@ -847,9 +846,9 @@ func (v *VM) Run() error {
}
case compiler.OpReturnValue:
//numRets := int(compiler.ReadUint8(v.curInsts[ip+1:]))
_ = int(compiler.ReadUint8(v.curInsts[ip+1:]))
v.curFrame.ip++
//numRets := int(compiler.ReadUint8(v.curInsts[v.ip+1:]))
//_ = int64(compiler.ReadUint8(v.curInsts[v.ip+1:]))
v.ip++
// TODO: multi-value return is not fully implemented yet
//var rets []*objects.Object
@ -865,6 +864,7 @@ func (v *VM) Run() error {
v.curFrame = &v.frames[v.framesIndex-1]
v.curInsts = v.curFrame.fn.Instructions
v.curIPLimit = len(v.curInsts) - 1
v.ip = v.curFrame.ip
//v.sp = lastFrame.basePointer - 1
v.sp = lastFrame.basePointer
@ -887,6 +887,7 @@ func (v *VM) Run() error {
v.curFrame = &v.frames[v.framesIndex-1]
v.curInsts = v.curFrame.fn.Instructions
v.curIPLimit = len(v.curInsts) - 1
v.ip = v.curFrame.ip
v.sp = lastFrame.basePointer - 1
@ -898,10 +899,10 @@ func (v *VM) Run() error {
v.sp++
case compiler.OpDefineLocal:
localIndex := compiler.ReadUint8(v.curInsts[ip+1:])
v.curFrame.ip++
localIndex := int(v.curInsts[v.ip+1])
v.ip++
sp := v.curFrame.basePointer + int(localIndex)
sp := v.curFrame.basePointer + localIndex
// local variables can be mutated by other actions
// so always store the copy of popped value
@ -911,10 +912,10 @@ func (v *VM) Run() error {
v.stack[sp] = &val
case compiler.OpSetLocal:
localIndex := compiler.ReadUint8(v.curInsts[ip+1:])
v.curFrame.ip++
localIndex := int(v.curInsts[v.ip+1])
v.ip++
sp := v.curFrame.basePointer + int(localIndex)
sp := v.curFrame.basePointer + localIndex
// update pointee of v.stack[sp] instead of replacing the pointer itself.
// this is needed because there can be free variables referencing the same local variables.
@ -924,10 +925,10 @@ func (v *VM) Run() error {
*v.stack[sp] = *val // also use a copy of popped value
case compiler.OpSetSelLocal:
localIndex := compiler.ReadUint8(v.curInsts[ip+1:])
numSelectors := int(compiler.ReadUint8(v.curInsts[ip+2:]))
v.curFrame.ip += 2
localIndex := int(v.curInsts[v.ip+1])
numSelectors := int(v.curInsts[v.ip+2])
v.ip += 2
// pop selector outcomes (left to right)
selectors := make([]interface{}, numSelectors, numSelectors)
for i := 0; i < numSelectors; i++ {
@ -948,17 +949,17 @@ func (v *VM) Run() error {
val := v.stack[v.sp-1] // no need to copy value here; selectorAssign uses copy of value
v.sp--
sp := v.curFrame.basePointer + int(localIndex)
sp := v.curFrame.basePointer + localIndex
if err := selectorAssign(v.stack[sp], val, selectors); err != nil {
return err
}
case compiler.OpGetLocal:
localIndex := compiler.ReadUint8(v.curInsts[ip+1:])
v.curFrame.ip++
localIndex := int(v.curInsts[v.ip+1])
v.ip++
val := v.stack[v.curFrame.basePointer+int(localIndex)]
val := v.stack[v.curFrame.basePointer+localIndex]
if v.sp >= StackSize {
return ErrStackOverflow
@ -968,8 +969,8 @@ func (v *VM) Run() error {
v.sp++
case compiler.OpGetBuiltin:
builtinIndex := compiler.ReadUint8(v.curInsts[ip+1:])
v.curFrame.ip++
builtinIndex := int(v.curInsts[v.ip+1])
v.ip++
if v.sp >= StackSize {
return ErrStackOverflow
@ -979,17 +980,17 @@ func (v *VM) Run() error {
v.sp++
case compiler.OpClosure:
constIndex := compiler.ReadUint16(v.curInsts[ip+1:])
numFree := compiler.ReadUint8(v.curInsts[ip+3:])
v.curFrame.ip += 3
constIndex := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8
numFree := int(v.curInsts[v.ip+3])
v.ip += 3
if err := v.pushClosure(int(constIndex), int(numFree)); err != nil {
if err := v.pushClosure(constIndex, numFree); err != nil {
return err
}
case compiler.OpGetFree:
freeIndex := compiler.ReadUint8(v.curInsts[ip+1:])
v.curFrame.ip++
freeIndex := int(v.curInsts[v.ip+1])
v.ip++
val := v.curFrame.freeVars[freeIndex]
@ -1001,9 +1002,9 @@ func (v *VM) Run() error {
v.sp++
case compiler.OpSetSelFree:
freeIndex := compiler.ReadUint8(v.curInsts[ip+1:])
numSelectors := int(compiler.ReadUint8(v.curInsts[ip+2:]))
v.curFrame.ip += 2
freeIndex := int(v.curInsts[v.ip+1])
numSelectors := int(v.curInsts[v.ip+2])
v.ip += 2
// pop selector outcomes (left to right)
selectors := make([]interface{}, numSelectors, numSelectors)
@ -1030,8 +1031,8 @@ func (v *VM) Run() error {
}
case compiler.OpSetFree:
freeIndex := compiler.ReadUint8(v.curInsts[ip+1:])
v.curFrame.ip++
freeIndex := int(v.curInsts[v.ip+1])
v.ip++
val := v.stack[v.sp-1]
v.sp--
@ -1107,20 +1108,20 @@ func (v *VM) Run() error {
v.sp++
case compiler.OpModule:
cidx := compiler.ReadUint16(v.curInsts[ip+1:])
v.curFrame.ip += 2
cidx := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8
v.ip += 2
if err := v.importModule(v.constants[cidx].(*objects.CompiledModule)); err != nil {
return err
}
default:
return fmt.Errorf("unknown opcode: %d", v.curInsts[ip])
return fmt.Errorf("unknown opcode: %d", v.curInsts[v.ip])
}
}
// check if stack still has some objects left
if v.sp > 0 && !v.aborting {
if v.sp > 0 && atomic.LoadInt64(&v.aborting) == 0 {
return fmt.Errorf("non empty stack after execution")
}
@ -1133,7 +1134,7 @@ func (v *VM) Globals() []*objects.Object {
}
// FrameInfo returns the current function call frame information.
func (v *VM) FrameInfo() (frameIndex int, ip int) {
func (v *VM) FrameInfo() (frameIndex, ip int) {
return v.framesIndex - 1, v.frames[v.framesIndex-1].ip
}
@ -1174,10 +1175,10 @@ func (v *VM) callFunction(fn *objects.CompiledFunction, freeVars []*objects.Obje
// check if this is a tail-call (recursive call right before return)
if fn == v.curFrame.fn { // recursion
nextOp := compiler.Opcode(v.curInsts[v.curFrame.ip+1])
nextOp := compiler.Opcode(v.curInsts[v.ip+1])
if nextOp == compiler.OpReturnValue || // tail call
(nextOp == compiler.OpPop &&
compiler.OpReturn == compiler.Opcode(v.curInsts[v.curFrame.ip+2])) {
compiler.OpReturn == compiler.Opcode(v.curInsts[v.ip+2])) {
// stack before tail-call
//
@ -1203,7 +1204,8 @@ func (v *VM) callFunction(fn *objects.CompiledFunction, freeVars []*objects.Obje
v.stack[v.curFrame.basePointer+p] = v.stack[v.sp-numArgs+p]
}
v.sp -= numArgs + 1
v.curFrame.ip = -1 // reset IP to beginning of the frame
v.ip = -1
//v.curFrame.ip = -1 // reset IP to beginning of the frame
// stack after tail-call
//
@ -1229,12 +1231,17 @@ func (v *VM) callFunction(fn *objects.CompiledFunction, freeVars []*objects.Obje
}
}
// store current ip before call
v.curFrame.ip = v.ip
// update call frame
v.curFrame = &(v.frames[v.framesIndex])
v.curFrame.fn = fn
v.curFrame.freeVars = freeVars
v.curFrame.ip = -1
//v.curFrame.ip = -1
v.curFrame.basePointer = v.sp - numArgs
v.curInsts = fn.Instructions
v.ip = -1
v.curIPLimit = len(v.curInsts) - 1
v.framesIndex++

View file

@ -1,6 +1,8 @@
package script
import (
"context"
"github.com/d5/tengo/compiler"
"github.com/d5/tengo/objects"
"github.com/d5/tengo/runtime"
@ -20,6 +22,25 @@ func (c *Compiled) Run() error {
return c.machine.Run()
}
// RunContext is like Run but includes a context.
func (c *Compiled) RunContext(ctx context.Context) (err error) {
ch := make(chan error, 1)
go func() {
ch <- c.machine.Run()
}()
select {
case <-ctx.Done():
c.machine.Abort()
<-ch
err = ctx.Err()
case err = <-ch:
}
return
}
// IsDefined returns true if the variable name is defined (has value) before or after the execution.
func (c *Compiled) IsDefined(name string) bool {
symbol, _, ok := c.symbolTable.Resolve(name)

View file

@ -1,7 +1,9 @@
package script_test
import (
"context"
"testing"
"time"
"github.com/d5/tengo/assert"
"github.com/d5/tengo/script"
@ -45,6 +47,31 @@ func TestCompiled_IsDefined(t *testing.T) {
compiledIsDefined(t, c, "b", false)
}
func TestCompiled_RunContext(t *testing.T) {
// machine completes normally
c := compile(t, `a := 5`, nil)
err := c.RunContext(context.Background())
assert.NoError(t, err)
compiledGet(t, c, "a", int64(5))
// cancelled
c = compile(t, `for true {}`, nil)
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(1 * time.Millisecond)
cancel()
}()
err = c.RunContext(ctx)
assert.Equal(t, context.Canceled, err)
// timeout
c = compile(t, `for true {}`, nil)
ctx, cancel = context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel()
err = c.RunContext(ctx)
assert.Equal(t, context.DeadlineExceeded, err)
}
func compile(t *testing.T, input string, vars M) *script.Compiled {
s := script.New([]byte(input))
for vn, vv := range vars {

View file

@ -1,6 +1,7 @@
package script
import (
"context"
"fmt"
"github.com/d5/tengo/compiler"
@ -87,6 +88,18 @@ func (s *Script) Run() (compiled *Compiled, err error) {
return
}
// RunContext is like Run but includes a context.
func (s *Script) RunContext(ctx context.Context) (compiled *Compiled, err error) {
compiled, err = s.Compile()
if err != nil {
return
}
err = compiled.RunContext(ctx)
return
}
func (s *Script) prepCompile() (symbolTable *compiler.SymbolTable, globals []*objects.Object) {
var names []string
for name := range s.variables {