Repository: skx/monkey (go)

Issue:
Ability to cancel a running Monkey Script
Was trying to find a way to cancel a running Monkey script (in code directly, not command line based) and I don't see any method to do this properly.

Could there be a possibility of using a Context or something to stop execution in progress?
A change to the [Eval function](https://github.com/skx/monkey/blob/29357e427d852c5c8ef2c628796f35a77d4d3cf8/evaluator/evaluator.go#L30) like below would solve some of the issue (The function would bail after the current operation completes)

```
func Eval(x context.Context, node ast.Node, env *object.Environment) object.Object {
        switch node := node.(type) {
                select {
                        case <-x.Done():
                                return &object.Error{Message: x.Err().Error()}
                        default:
               }
        //Statements
```

Thanks

Pull request:
79 timeout
This pull-request closes #79, by adding the ability to set a context prior to execution of a script.

* Added the global context, and test it in the evaluators main-loop.
* Added a simple test-case to ensure an infinite loop can be caught with a timeout.

Patch:
diff --git a/evaluator/evaluator.go b/evaluator/evaluator.go
index d1b50c6..f501f06 100644
--- a/evaluator/evaluator.go
+++ b/evaluator/evaluator.go
@@ -4,6 +4,7 @@ package evaluator
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"math"
 	"os"
@@ -21,6 +22,7 @@ var (
 	TRUE    = &object.Boolean{Value: true}
 	FALSE   = &object.Boolean{Value: false}
 	PRAGMAS = make(map[string]int)
+	CTX     = context.Background()
 )
 
 // The built-in functions / standard-library methods are stored here.
@@ -28,6 +30,17 @@ var builtins = map[string]*object.Builtin{}
 
 // Eval is our core function for evaluating nodes.
 func Eval(node ast.Node, env *object.Environment) object.Object {
+
+	//
+	// We test our context at every iteration of our main-loop.
+	//
+	select {
+	case <-CTX.Done():
+		return &object.Error{Message: CTX.Err().Error()}
+	default:
+		// nop
+	}
+
 	switch node := node.(type) {
 
 	//Statements
@@ -1134,6 +1147,12 @@ func RegisterBuiltin(name string, fun object.BuiltinFunction) {
 	builtins[name] = &object.Builtin{Fn: fun}
 }
 
+// SetContext lets you configure a context, which is helpful if you wish to
+// cause execution to timeout after a given period, for example.
+func SetContext(ctx context.Context) {
+	CTX = ctx
+}
+
 // evalObjectCallExpression invokes methods against objects.
 func evalObjectCallExpression(call *ast.ObjectCallExpression, env *object.Environment) object.Object {
 


Test patch:
diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go
index 78d66f1..62afd12 100644
--- a/evaluator/evaluator_test.go
+++ b/evaluator/evaluator_test.go
@@ -1,8 +1,11 @@
 package evaluator
 
 import (
+	"context"
 	"math"
+	"strings"
 	"testing"
+	"time"
 
 	"github.com/skx/monkey/lexer"
 	"github.com/skx/monkey/object"
@@ -622,3 +625,32 @@ func TestTypeBuiltin(t *testing.T) {
 		}
 	}
 }
+
+func TestTimeout(t *testing.T) {
+	input := `
+i = 1;
+for ( true ) {
+  i++;
+}
+`
+	ctx, cancel := context.WithTimeout(context.Background(), 350*time.Millisecond)
+	defer cancel()
+
+	l := lexer.New(input)
+	p := parser.New(l)
+	program := p.ParseProgram()
+	env := object.NewEnvironment()
+	SetContext(ctx)
+	evaluated := Eval(program, env)
+
+	errObj, ok := evaluated.(*object.Error)
+	if !ok {
+		t.Errorf("no error object returned. got=%T(%+v)",
+			evaluated, evaluated)
+	}
+
+	if !strings.Contains(errObj.Message, "deadline") {
+		t.Errorf("got error, but wasn't timeout: %s", errObj.Message)
+	}
+
+}