Browse Source

feat: Adding exception handling

Matthias Ladkau 3 years ago
parent
commit
dc8dfe88f7

File diff suppressed because it is too large
+ 33 - 6
ecal.md


+ 27 - 18
interpreter/func_provider.go

@@ -32,7 +32,7 @@ var inbuildFuncMap = map[string]util.ECALFunction{
 	"add":             &addFunc{&inbuildBaseFunc{}},
 	"concat":          &concatFunc{&inbuildBaseFunc{}},
 	"dumpenv":         &dumpenvFunc{&inbuildBaseFunc{}},
-	"sinkError":       &sinkerror{&inbuildBaseFunc{}},
+	"raise":           &raise{&inbuildBaseFunc{}},
 	"addEvent":        &addevent{&inbuildBaseFunc{}},
 	"addEventAndWait": &addeventandwait{&addevent{&inbuildBaseFunc{}}},
 }
@@ -477,38 +477,45 @@ func (rf *dumpenvFunc) DocString() (string, error) {
 	return "Dumpenv returns the current variable environment as a string.", nil
 }
 
-// sinkerror
-// =========
+// raise
+// =====
 
 /*
-sinkerror returns a sink error object which indicates that the sink execution failed.
-This error can be used to break trigger sequences of sinks if
+raise returns an error. Outside of sinks this will stop the code execution
+if the error is not handled by try / except. Inside a sink only the specific sink
+will fail. This error can be used to break trigger sequences of sinks if
 FailOnFirstErrorInTriggerSequence is set.
 */
-type sinkerror struct {
+type raise struct {
 	*inbuildBaseFunc
 }
 
 /*
 Run executes this function.
 */
-func (rf *sinkerror) Run(instanceID string, vs parser.Scope, is map[string]interface{}, args []interface{}) (interface{}, error) {
-	var msg string
+func (rf *raise) Run(instanceID string, vs parser.Scope, is map[string]interface{}, args []interface{}) (interface{}, error) {
+	var err error
+	var detailMsg string
 	var detail interface{}
 
 	if len(args) > 0 {
-		msg = fmt.Sprint(args[0])
+		err = fmt.Errorf("%v", args[0])
 		if len(args) > 1 {
-			detail = args[1]
+			if args[1] != nil {
+				detailMsg = fmt.Sprint(args[1])
+			}
+			if len(args) > 2 {
+				detail = args[2]
+			}
 		}
 	}
 
 	erp := is["erp"].(*ECALRuntimeProvider)
 	node := is["astnode"].(*parser.ASTNode)
 
-	return nil, &SinkRuntimeError{
-		erp.NewRuntimeError(util.ErrSink, msg, node).(*util.RuntimeError),
-		nil,
+	return nil, &util.RuntimeErrorWithDetail{
+		erp.NewRuntimeError(err, detailMsg, node).(*util.RuntimeError),
+		vs,
 		detail,
 	}
 
@@ -517,8 +524,8 @@ func (rf *sinkerror) Run(instanceID string, vs parser.Scope, is map[string]inter
 /*
 DocString returns a descriptive string.
 */
-func (rf *sinkerror) DocString() (string, error) {
-	return "Sinkerror returns a sink error object which indicates that the sink execution failed.", nil
+func (rf *raise) DocString() (string, error) {
+	return "Raise returns an error object.", nil
 }
 
 // addEvent
@@ -639,15 +646,17 @@ func (rf *addeventandwait) Run(instanceID string, vs parser.Scope, is map[string
 
 				errors := map[interface{}]interface{}{}
 				for k, v := range e.ErrorMap {
-					se := v.(*SinkRuntimeError)
+					se := v.(*util.RuntimeErrorWithDetail)
 
 					// Note: The variable scope of the sink (se.environment)
 					// was also captured - for now it is not exposed to the
 					// language environment
 
 					errors[k] = map[interface{}]interface{}{
-						"message": se.Error(),
-						"detail":  se.detail,
+						"error":  se.Error(),
+						"type":   se.Type.Error(),
+						"detail": se.Detail,
+						"data":   se.Data,
 					}
 				}
 

+ 7 - 0
interpreter/provider.go

@@ -81,6 +81,7 @@ var providerMap = map[string]ecalRuntimeNew{
 	// Import statement
 
 	parser.NodeIMPORT: importRuntimeInst,
+	parser.NodeAS:     voidRuntimeInst,
 
 	// Sink definition
 
@@ -125,6 +126,12 @@ var providerMap = map[string]ecalRuntimeNew{
 	parser.NodeLOOP:     loopRuntimeInst,
 	parser.NodeBREAK:    breakRuntimeInst,
 	parser.NodeCONTINUE: continueRuntimeInst,
+
+	// Try statement
+
+	parser.NodeTRY:     tryRuntimeInst,
+	parser.NodeEXCEPT:  voidRuntimeInst,
+	parser.NodeFINALLY: voidRuntimeInst,
 }
 
 /*

+ 2 - 2
interpreter/rt_identifier.go

@@ -200,8 +200,8 @@ func (rt *identifierRuntime) resolveFunction(astring string, vs parser.Scope, is
 
 						result, err = funcObj.Run(rt.instanceID, vs, is, args)
 
-						_, ok1 := err.(*util.RuntimeError)
-						_, ok2 := err.(*SinkRuntimeError)
+						_, ok1 := err.(*util.RuntimeErrorWithDetail)
+						_, ok2 := err.(*util.RuntimeErrorWithDetail)
 
 						if err != nil && !ok1 && !ok2 {
 

+ 3 - 12
interpreter/rt_sink.go

@@ -204,14 +204,14 @@ func (rt *sinkRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interfa
 
 						if _, err = statements.Runtime.Eval(sinkVS, sinkIs); err != nil {
 
-							if sre, ok := err.(*SinkRuntimeError); ok {
-								sre.environment = sinkVS
+							if sre, ok := err.(*util.RuntimeErrorWithDetail); ok {
+								sre.Environment = sinkVS
 
 							} else {
 
 								// Provide additional information for unexpected errors
 
-								err = &SinkRuntimeError{
+								err = &util.RuntimeErrorWithDetail{
 									err.(*util.RuntimeError),
 									sinkVS,
 									nil,
@@ -233,15 +233,6 @@ func (rt *sinkRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interfa
 	return nil, err
 }
 
-/*
-SinkRuntimeError is a sink specific error with additional environment information.
-*/
-type SinkRuntimeError struct {
-	*util.RuntimeError
-	environment parser.Scope
-	detail      interface{}
-}
-
 // Sink child nodes
 // ================
 

+ 9 - 5
interpreter/rt_sink_test.go

@@ -104,7 +104,7 @@ sink rule2
 	{
         log("rule2 - Tracking user:", event.state.user)
         if event.state.user == "bar" {
-            sinkError("User bar was here", [123])
+            raise("UserBarWasHere", "User bar was seen", [123])
         }
 	}
 
@@ -145,10 +145,12 @@ ErrorResult:[
   {
     "errors": {
       "rule2": {
-        "detail": [
+        "data": [
           123
         ],
-        "message": "ECAL error in ECALTestRuntime: Error in sink (User bar was here) (Line:17 Pos:13)"
+        "detail": "User bar was seen",
+        "error": "ECAL error in ECALTestRuntime: UserBarWasHere (User bar was seen) (Line:17 Pos:13)",
+        "type": "UserBarWasHere"
       }
     },
     "event": {
@@ -191,8 +193,10 @@ if err != null {
 rule1 - test
 error: {
   "rule1": {
-    "detail": null,
-    "message": "ECAL error in ECALTestRuntime: Unknown construct (Unknown function: noexitingfunctioncall) (Line:6 Pos:9)"
+    "data": null,
+    "detail": "Unknown function: noexitingfunctioncall",
+    "error": "ECAL error in ECALTestRuntime: Unknown construct (Unknown function: noexitingfunctioncall) (Line:6 Pos:9)",
+    "type": "Unknown construct"
   }
 }`[1:] {
 		t.Error("Unexpected result:", testlogger.String())

+ 135 - 0
interpreter/rt_statements.go

@@ -13,6 +13,7 @@ package interpreter
 import (
 	"fmt"
 
+	"devt.de/krotik/common/errorutil"
 	"devt.de/krotik/common/sortutil"
 	"devt.de/krotik/ecal/parser"
 	"devt.de/krotik/ecal/scope"
@@ -462,3 +463,137 @@ func (rt *continueRuntime) Eval(vs parser.Scope, is map[string]interface{}) (int
 
 	return nil, err
 }
+
+// Try Runtime
+// ===========
+
+/*
+tryRuntime is the runtime for try blocks.
+*/
+type tryRuntime struct {
+	*baseRuntime
+}
+
+/*
+tryRuntimeInst returns a new runtime component instance.
+*/
+func tryRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &tryRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *tryRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	evalExcept := func(errObj map[interface{}]interface{}, except *parser.ASTNode) bool {
+		ret := false
+
+		if len(except.Children) == 1 {
+
+			// We only have statements - any exception is handled here
+
+			evs := vs.NewChild(scope.NameFromASTNode(except))
+
+			except.Children[0].Runtime.Eval(evs, is)
+
+			ret = true
+
+		} else if len(except.Children) == 2 {
+
+			// We have statements and the error object is available - any exception is handled here
+
+			evs := vs.NewChild(scope.NameFromASTNode(except))
+			evs.SetValue(except.Children[0].Token.Val, errObj)
+
+			except.Children[1].Runtime.Eval(evs, is)
+
+			ret = true
+
+		} else {
+			errorVar := ""
+
+			for i := 0; i < len(except.Children); i++ {
+				child := except.Children[i]
+
+				if !ret && child.Name == parser.NodeSTRING {
+					exceptError, evalErr := child.Runtime.Eval(vs, is)
+
+					// If we fail evaluating the string we panic as otherwise
+					// we would need to generate a new error while trying to handle another error
+					errorutil.AssertOk(evalErr)
+
+					ret = exceptError == fmt.Sprint(errObj["type"])
+
+				} else if ret && child.Name == parser.NodeAS {
+					errorVar = child.Children[0].Token.Val
+
+				} else if ret && child.Name == parser.NodeSTATEMENTS {
+					evs := vs.NewChild(scope.NameFromASTNode(except))
+
+					if errorVar != "" {
+						evs.SetValue(errorVar, errObj)
+					}
+
+					child.Runtime.Eval(evs, is)
+				}
+			}
+		}
+
+		return ret
+	}
+
+	// Make sure the finally block is executed in any case
+
+	if finally := rt.node.Children[len(rt.node.Children)-1]; finally.Name == parser.NodeFINALLY {
+		fvs := vs.NewChild(scope.NameFromASTNode(finally))
+		defer finally.Children[0].Runtime.Eval(fvs, is)
+	}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		tvs := vs.NewChild(scope.NameFromASTNode(rt.node))
+
+		res, err = rt.node.Children[0].Runtime.Eval(tvs, is)
+
+		// Evaluate except clauses
+
+		if err != nil {
+			errObj := map[interface{}]interface{}{
+				"type":  "UnexpectedError",
+				"error": err.Error(),
+			}
+
+			if rtError, ok := err.(*util.RuntimeError); ok {
+				errObj["type"] = rtError.Type.Error()
+				errObj["detail"] = rtError.Detail
+				errObj["pos"] = rtError.Pos
+				errObj["line"] = rtError.Line
+				errObj["source"] = rtError.Source
+
+			} else if rtError, ok := err.(*util.RuntimeErrorWithDetail); ok {
+				errObj["type"] = rtError.Type.Error()
+				errObj["detail"] = rtError.Detail
+				errObj["pos"] = rtError.Pos
+				errObj["line"] = rtError.Line
+				errObj["source"] = rtError.Source
+				errObj["data"] = rtError.Data
+			}
+
+			res = nil
+
+			for i := 1; i < len(rt.node.Children); i++ {
+				if child := rt.node.Children[i]; child.Name == parser.NodeEXCEPT {
+					if evalExcept(errObj, child) {
+						err = nil
+						break
+					}
+				}
+			}
+		}
+	}
+
+	return res, err
+}

+ 145 - 0
interpreter/rt_statements_test.go

@@ -858,3 +858,148 @@ for a[t] in 1 {
 		return
 	}
 }
+
+func TestTryStatements(t *testing.T) {
+
+	vs := scope.NewScope(scope.GlobalScope)
+
+	_, err := UnitTestEvalAndAST(
+		`
+try {
+	debug("Raising custom error")
+    raise("test 12", null, [1,2,3])
+} except "test 12" as e {
+	error("Something happened: ", e)
+} finally {
+	log("Cleanup")
+}
+`, vs,
+		`
+try
+  statements
+    identifier: debug
+      funccall
+        string: 'Raising custom error'
+    identifier: raise
+      funccall
+        string: 'test 12'
+        null
+        list
+          number: 1
+          number: 2
+          number: 3
+  except
+    string: 'test 12'
+    as
+      identifier: e
+    statements
+      identifier: error
+        funccall
+          string: 'Something happened: '
+          identifier: e
+  finally
+    statements
+      identifier: log
+        funccall
+          string: 'Cleanup'
+`[1:])
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if testlogger.String() != `
+debug: Raising custom error
+error: Something happened: {
+  "data": [
+    1,
+    2,
+    3
+  ],
+  "detail": "",
+  "error": "ECAL error in ECALTestRuntime: test 12 () (Line:4 Pos:5)",
+  "line": 4,
+  "pos": 5,
+  "source": "ECALTestRuntime",
+  "type": "test 12"
+}
+Cleanup`[1:] {
+		t.Error("Unexpected result:", testlogger.String())
+		return
+	}
+
+	_, err = UnitTestEval(
+		`
+try {
+	debug("Raising custom error")
+    raise("test 13", null, [1,2,3])
+} except "test 12" as e {
+	error("Something happened: ", e)
+} except e {
+	error("Something else happened: ", e)
+
+	try {
+		x := 1 + a
+	} except e {
+		log("Runtime error: ", e)
+	}
+
+} finally {
+	log("Cleanup")
+}
+`, vs)
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if testlogger.String() != `
+debug: Raising custom error
+error: Something else happened: {
+  "data": [
+    1,
+    2,
+    3
+  ],
+  "detail": "",
+  "error": "ECAL error in ECALTestRuntime: test 13 () (Line:4 Pos:5)",
+  "line": 4,
+  "pos": 5,
+  "source": "ECALTestRuntime",
+  "type": "test 13"
+}
+Runtime error: {
+  "detail": "a",
+  "error": "ECAL error in ECALTestRuntime: Operand is not a number (a) (Line:11 Pos:12)",
+  "line": 11,
+  "pos": 12,
+  "source": "ECALTestRuntime",
+  "type": "Operand is not a number"
+}
+Cleanup`[1:] {
+		t.Error("Unexpected result:", testlogger.String())
+		return
+	}
+
+	_, err = UnitTestEval(
+		`
+try {
+	x := 1 + "a"
+} except {
+	error("This did not work")
+}
+`, vs)
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if testlogger.String() != `
+error: This did not work`[1:] {
+		t.Error("Unexpected result:", testlogger.String())
+		return
+	}
+}

+ 13 - 0
parser/const.go

@@ -179,6 +179,12 @@ const (
 	TokenBREAK
 	TokenCONTINUE
 
+	// Try block
+
+	TokenTRY
+	TokenEXCEPT
+	TokenFINALLY
+
 	TokenENDLIST
 )
 
@@ -283,4 +289,11 @@ const (
 	NodeLOOP     = "loop"
 	NodeBREAK    = "break"
 	NodeCONTINUE = "continue"
+
+	// Try block
+
+	NodeTRY     = "try"
+	NodeEXCEPT  = "except"
+	NodeAS      = "as"
+	NodeFINALLY = "finally"
 )

+ 6 - 0
parser/lexer.go

@@ -228,6 +228,12 @@ var KeywordMap = map[string]LexTokenID{
 	"for":      TokenFOR,
 	"break":    TokenBREAK,
 	"continue": TokenCONTINUE,
+
+	// Try block
+
+	"try":     TokenTRY,
+	"except":  TokenEXCEPT,
+	"finally": TokenFINALLY,
 }
 
 /*

+ 74 - 2
parser/parser.go

@@ -85,7 +85,7 @@ func init() {
 		// Import statement
 
 		TokenIMPORT: {NodeIMPORT, nil, nil, nil, nil, 0, ndImport, nil},
-		TokenAS:     {"", nil, nil, nil, nil, 0, ndImport, nil},
+		TokenAS:     {NodeAS, nil, nil, nil, nil, 0, nil, nil},
 
 		// Sink definition
 
@@ -127,11 +127,17 @@ func init() {
 		TokenELIF: {"", nil, nil, nil, nil, 0, nil, nil},
 		TokenELSE: {"", nil, nil, nil, nil, 0, nil, nil},
 
-		// Loop statements
+		// Loop statement
 
 		TokenFOR:      {NodeLOOP, nil, nil, nil, nil, 0, ndLoop, nil},
 		TokenBREAK:    {NodeBREAK, nil, nil, nil, nil, 0, ndTerm, nil},
 		TokenCONTINUE: {NodeCONTINUE, nil, nil, nil, nil, 0, ndTerm, nil},
+
+		// Try statement
+
+		TokenTRY:     {NodeTRY, nil, nil, nil, nil, 0, ndTry, nil},
+		TokenEXCEPT:  {NodeEXCEPT, nil, nil, nil, nil, 0, nil, nil},
+		TokenFINALLY: {NodeFINALLY, nil, nil, nil, nil, 0, nil, nil},
 	}
 }
 
@@ -758,6 +764,72 @@ func ndLoop(p *parser, self *ASTNode) (*ASTNode, error) {
 	return self, err
 }
 
+/*
+ndTry is used to parse a try block.
+*/
+func ndTry(p *parser, self *ASTNode) (*ASTNode, error) {
+
+	try, err := parseInnerStatements(p, self)
+
+	if p.node.Token.ID != TokenFINALLY {
+
+		for err == nil && IsNotEndAndToken(p, TokenEXCEPT) {
+
+			except := p.node
+
+			err = acceptChild(p, try, TokenEXCEPT)
+
+			if err == nil {
+				for err == nil &&
+					IsNotEndAndNotToken(p, TokenAS) &&
+					IsNotEndAndNotToken(p, TokenIDENTIFIER) &&
+					IsNotEndAndNotToken(p, TokenLBRACE) {
+
+					if err = acceptChild(p, except, TokenSTRING); err == nil {
+
+						// Skip commas
+
+						if p.node.Token.ID == TokenCOMMA {
+							err = skipToken(p, TokenCOMMA)
+						}
+					}
+				}
+
+				if err == nil {
+
+					if p.node.Token.ID == TokenAS {
+						as := p.node
+
+						err = acceptChild(p, except, TokenAS)
+
+						if err == nil {
+							err = acceptChild(p, as, TokenIDENTIFIER)
+						}
+
+					} else if p.node.Token.ID == TokenIDENTIFIER {
+
+						err = acceptChild(p, except, TokenIDENTIFIER)
+					}
+				}
+
+				if err == nil {
+					_, err = parseInnerStatements(p, except)
+				}
+			}
+		}
+	}
+
+	if err == nil && p.node.Token.ID == TokenFINALLY {
+		finally := p.node
+
+		if err = acceptChild(p, try, TokenFINALLY); err == nil {
+			_, err = parseInnerStatements(p, finally)
+		}
+	}
+
+	return try, err
+}
+
 // Standard left denotation functions
 // ==================================
 

+ 105 - 0
parser/parser_statement_test.go

@@ -49,6 +49,111 @@ statements
 
 }
 
+func TestTryContext(t *testing.T) {
+
+	input := `
+try {
+	raise("test", [1,2,3])
+} except "test", "bla" as e {
+	print(1)
+} except e {
+	print(1)
+} except {
+	print(1)
+} finally {
+	print(2)
+}
+`
+	expectedOutput := `
+try
+  statements
+    identifier: raise
+      funccall
+        string: 'test'
+        list
+          number: 1
+          number: 2
+          number: 3
+  except
+    string: 'test'
+    string: 'bla'
+    as
+      identifier: e
+    statements
+      identifier: print
+        funccall
+          number: 1
+  except
+    identifier: e
+    statements
+      identifier: print
+        funccall
+          number: 1
+  except
+    statements
+      identifier: print
+        funccall
+          number: 1
+  finally
+    statements
+      identifier: print
+        funccall
+          number: 2
+`[1:]
+
+	if res, err := UnitTestParse("mytest", input); err != nil || fmt.Sprint(res) != expectedOutput {
+		t.Error("Unexpected parser output:\n", res, "expected was:\n", expectedOutput, "Error:", err)
+		return
+	}
+
+	input = `
+try {
+	raise("test", [1,2,3])
+}
+`
+	expectedOutput = `
+try
+  statements
+    identifier: raise
+      funccall
+        string: 'test'
+        list
+          number: 1
+          number: 2
+          number: 3
+`[1:]
+
+	if res, err := UnitTestParse("mytest", input); err != nil || fmt.Sprint(res) != expectedOutput {
+		t.Error("Unexpected parser output:\n", res, "expected was:\n", expectedOutput, "Error:", err)
+		return
+	}
+
+	input = `
+try {
+	raise("test", [1,2,3])
+} finally {
+}
+`
+	expectedOutput = `
+try
+  statements
+    identifier: raise
+      funccall
+        string: 'test'
+        list
+          number: 1
+          number: 2
+          number: 3
+  finally
+    statements
+`[1:]
+
+	if res, err := UnitTestParse("mytest", input); err != nil || fmt.Sprint(res) != expectedOutput {
+		t.Error("Unexpected parser output:\n", res, "expected was:\n", expectedOutput, "Error:", err)
+		return
+	}
+}
+
 func TestLoopParsing(t *testing.T) {
 
 	input := `

+ 43 - 1
parser/prettyprinter.go

@@ -79,6 +79,7 @@ func init() {
 		// Import statement
 
 		NodeIMPORT + "_2": template.Must(template.New(NodeIMPORT).Parse("import {{.c1}} as {{.c2}}")),
+		NodeAS + "_1":     template.Must(template.New(NodeRETURN).Parse("as {{.c1}}")),
 
 		// Sink definition
 
@@ -122,11 +123,17 @@ func init() {
 		// TokenELIF - Special case (handled in code)
 		// TokenELSE - Special case (handled in code)
 
-		// Loop statements
+		// Loop statement
 
 		NodeLOOP + "_2": template.Must(template.New(NodeLOOP).Parse("for {{.c1}} {\n{{.c2}}}\n")),
 		NodeBREAK:       template.Must(template.New(NodeBREAK).Parse("break")),
 		NodeCONTINUE:    template.Must(template.New(NodeCONTINUE).Parse("continue")),
+
+		// Try statement
+
+		// TokenTRY - Special case (handled in code)
+		// TokenEXCEPT - Special case (handled in code)
+		NodeFINALLY + "_1": template.Must(template.New(NodeFINALLY).Parse(" finally {\n{{.c1}}}\n")),
 	}
 
 	bracketPrecedenceMap = map[string]bool{
@@ -326,6 +333,41 @@ func PrettyPrint(ast *ASTNode) (string, error) {
 
 			buf.WriteString("\n")
 
+			return ppMetaData(ast, buf.String()), nil
+
+		} else if ast.Name == NodeTRY {
+
+			buf.WriteString("try {\n")
+			buf.WriteString(tempParam[fmt.Sprint("c1")])
+
+			buf.WriteString("}")
+
+			for i := 1; i < len(ast.Children); i++ {
+				buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
+			}
+
+			buf.WriteString("\n")
+
+			return ppMetaData(ast, buf.String()), nil
+
+		} else if ast.Name == NodeEXCEPT {
+			buf.WriteString(" except ")
+
+			for i := 0; i < len(ast.Children)-1; i++ {
+				buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
+
+				if ast.Children[i+1].Name != NodeAS && i < len(ast.Children)-2 {
+					buf.WriteString(",")
+				}
+				buf.WriteString(" ")
+			}
+
+			buf.WriteString("{\n")
+
+			buf.WriteString(tempParam[fmt.Sprint("c", len(ast.Children))])
+
+			buf.WriteString("}")
+
 			return ppMetaData(ast, buf.String()), nil
 		}
 

+ 9 - 0
util/error.go

@@ -82,3 +82,12 @@ func (re *RuntimeError) Error() string {
 
 	return ret
 }
+
+/*
+RuntimeErrorWithDetail is a runtime error with additional environment information.
+*/
+type RuntimeErrorWithDetail struct {
+	*RuntimeError
+	Environment parser.Scope
+	Data        interface{}
+}