Browse Source

feat: Adding initial interpreter and stdlib support

Matthias Ladkau 3 years ago
parent
commit
60b3888a69

File diff suppressed because it is too large
+ 380 - 0
ecal.md


+ 27 - 0
eval.go

@@ -0,0 +1,27 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+/*
+Package eval contains the main API for the event condition language ECAL.
+*/
+package eval
+
+import "devt.de/krotik/ecal/util"
+
+// TODO: Maybe API documentation - access comments during runtime
+
+/*
+processor is the main implementation for the Processor interface.
+*/
+type processor struct {
+	// TODO: GM GraphManager is part of initial values published in the global scope
+
+	util.Logger
+}

+ 16 - 0
interpreter/debug.go

@@ -0,0 +1,16 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+/*
+Package interpreter contains the ECAL interpreter.
+*/
+package interpreter
+
+// TODO: Stacktrace

+ 473 - 0
interpreter/func_provider.go

@@ -0,0 +1,473 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"fmt"
+	"strconv"
+
+	"devt.de/krotik/ecal/parser"
+	"devt.de/krotik/ecal/scope"
+	"devt.de/krotik/ecal/util"
+)
+
+/*
+inbuildFuncMap contains the mapping of inbuild functions.
+*/
+var inbuildFuncMap = map[string]util.ECALFunction{
+	"range":   &rangeFunc{&inbuildBaseFunc{}},
+	"new":     &newFunc{&inbuildBaseFunc{}},
+	"len":     &lenFunc{&inbuildBaseFunc{}},
+	"del":     &delFunc{&inbuildBaseFunc{}},
+	"add":     &addFunc{&inbuildBaseFunc{}},
+	"concat":  &concatFunc{&inbuildBaseFunc{}},
+	"dumpenv": &dumpenvFunc{&inbuildBaseFunc{}},
+}
+
+/*
+inbuildBaseFunc is the base structure for inbuild functions providing some
+utility functions.
+*/
+type inbuildBaseFunc struct {
+}
+
+/*
+AssertNumParam converts a general interface{} parameter into a number.
+*/
+func (ibf *inbuildBaseFunc) AssertNumParam(index int, val interface{}) (float64, error) {
+	var err error
+
+	resNum, ok := val.(float64)
+
+	if !ok {
+
+		resNum, err = strconv.ParseFloat(fmt.Sprint(val), 64)
+		if err != nil {
+			err = fmt.Errorf("Parameter %v should be a number", index)
+		}
+	}
+
+	return resNum, err
+}
+
+/*
+AssertMapParam converts a general interface{} parameter into a map.
+*/
+func (ibf *inbuildBaseFunc) AssertMapParam(index int, val interface{}) (map[interface{}]interface{}, error) {
+
+	valMap, ok := val.(map[interface{}]interface{})
+
+	if ok {
+		return valMap, nil
+	}
+
+	return nil, fmt.Errorf("Parameter %v should be a map", index)
+}
+
+/*
+AssertListParam converts a general interface{} parameter into a list.
+*/
+func (ibf *inbuildBaseFunc) AssertListParam(index int, val interface{}) ([]interface{}, error) {
+
+	valList, ok := val.([]interface{})
+
+	if ok {
+		return valList, nil
+	}
+
+	return nil, fmt.Errorf("Parameter %v should be a list", index)
+}
+
+// Range
+// =====
+
+/*
+rangeFunc is an interator function which returns a range of numbers.
+*/
+type rangeFunc struct {
+	*inbuildBaseFunc
+}
+
+/*
+Run executes this function.
+*/
+func (rf *rangeFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, args []interface{}) (interface{}, error) {
+	var currVal, to float64
+	var err error
+
+	lenargs := len(args)
+	from := 0.
+	step := 1.
+
+	if lenargs == 0 {
+		err = fmt.Errorf("Need at least an end range as first parameter")
+	}
+
+	if err == nil {
+
+		if stepVal, ok := is[instanceID+"step"]; ok {
+
+			step = stepVal.(float64)
+			from = is[instanceID+"from"].(float64)
+			to = is[instanceID+"to"].(float64)
+			currVal = is[instanceID+"currVal"].(float64)
+
+			is[instanceID+"currVal"] = currVal + step
+
+			// Check for end of iteration
+
+			if (from < to && currVal > to) || (from > to && currVal < to) || from == to {
+				err = util.ErrEndOfIteration
+			}
+
+		} else {
+
+			if lenargs == 1 {
+				to, err = rf.AssertNumParam(1, args[0])
+			} else {
+				from, err = rf.AssertNumParam(1, args[0])
+
+				if err == nil {
+					to, err = rf.AssertNumParam(2, args[1])
+				}
+
+				if err == nil && lenargs > 2 {
+					step, err = rf.AssertNumParam(3, args[2])
+				}
+			}
+
+			if err == nil {
+				is[instanceID+"from"] = from
+				is[instanceID+"to"] = to
+				is[instanceID+"step"] = step
+				is[instanceID+"currVal"] = from
+
+				currVal = from
+			}
+		}
+	}
+
+	if err == nil {
+		err = util.ErrIsIterator // Identify as iterator
+	}
+
+	return currVal, err
+}
+
+/*
+DocString returns a descriptive string.
+*/
+func (rf *rangeFunc) DocString() (string, error) {
+	return "Range function which can be used to iterate over number ranges. Parameters are start, end and step.", nil
+}
+
+// New
+// ===
+
+/*
+newFunc instantiates a new object.
+*/
+type newFunc struct {
+	*inbuildBaseFunc
+}
+
+/*
+Run executes this function.
+*/
+func (rf *newFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, args []interface{}) (interface{}, error) {
+	var res interface{}
+
+	err := fmt.Errorf("Need a map as first parameter")
+
+	if len(args) > 0 {
+		var argMap map[interface{}]interface{}
+		if argMap, err = rf.AssertMapParam(1, args[0]); err == nil {
+			obj := make(map[interface{}]interface{})
+			res = obj
+
+			_, err = rf.addSuperClasses(vs, is, obj, argMap)
+
+			if initObj, ok := obj["init"]; ok {
+				if initFunc, ok := initObj.(*function); ok {
+
+					initvs := scope.NewScope(fmt.Sprintf("newfunc: %v", instanceID))
+					initis := make(map[string]interface{})
+
+					_, err = initFunc.Run(instanceID, initvs, initis, args[1:])
+				}
+			}
+		}
+	}
+
+	return res, err
+}
+
+/*
+addSuperClasses adds super class functions to a given object.
+*/
+func (rf *newFunc) addSuperClasses(vs parser.Scope, is map[string]interface{},
+	obj map[interface{}]interface{}, template map[interface{}]interface{}) (interface{}, error) {
+
+	var err error
+
+	var initFunc interface{}
+	var initSuperList []interface{}
+
+	// First loop into the base classes (i.e. top-most classes)
+
+	if super, ok := template["super"]; ok {
+		if superList, ok := super.([]interface{}); ok {
+			for _, superObj := range superList {
+				var superInit interface{}
+
+				if superTemplate, ok := superObj.(map[interface{}]interface{}); ok {
+					superInit, err = rf.addSuperClasses(vs, is, obj, superTemplate)
+					initSuperList = append(initSuperList, superInit) // Build up the list of super functions
+				}
+			}
+		} else {
+			err = fmt.Errorf("Property _super must be a list of super classes")
+		}
+	}
+
+	// Copy all properties from template to obj
+
+	for k, v := range template {
+
+		// Save previous init function
+
+		if funcVal, ok := v.(*function); ok {
+			newFunction := &function{funcVal.name, nil, obj, funcVal.declaration}
+			if k == "init" {
+				newFunction.super = initSuperList
+				initFunc = newFunction
+			}
+			obj[k] = newFunction
+		} else {
+			obj[k] = v
+		}
+	}
+
+	return initFunc, err
+}
+
+/*
+DocString returns a descriptive string.
+*/
+func (rf *newFunc) DocString() (string, error) {
+	return "New creates a new object instance.", nil
+}
+
+// Len
+// ===
+
+/*
+lenFunc returns the size of a list or map.
+*/
+type lenFunc struct {
+	*inbuildBaseFunc
+}
+
+/*
+Run executes this function.
+*/
+func (rf *lenFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, args []interface{}) (interface{}, error) {
+	var res float64
+
+	err := fmt.Errorf("Need a list or a map as first parameter")
+
+	if len(args) > 0 {
+		argList, ok1 := args[0].([]interface{})
+		argMap, ok2 := args[0].(map[interface{}]interface{})
+
+		if ok1 {
+			res = float64(len(argList))
+			err = nil
+		} else if ok2 {
+			res = float64(len(argMap))
+			err = nil
+		}
+	}
+
+	return res, err
+}
+
+/*
+DocString returns a descriptive string.
+*/
+func (rf *lenFunc) DocString() (string, error) {
+	return "Len returns the size of a list or map.", nil
+}
+
+// Del
+// ===
+
+/*
+delFunc removes an element from a list or map.
+*/
+type delFunc struct {
+	*inbuildBaseFunc
+}
+
+/*
+Run executes this function.
+*/
+func (rf *delFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, args []interface{}) (interface{}, error) {
+	var res interface{}
+
+	err := fmt.Errorf("Need a list or a map as first parameter and an index or key as second parameter")
+
+	if len(args) == 2 {
+
+		if argList, ok1 := args[0].([]interface{}); ok1 {
+			var index float64
+
+			index, err = rf.AssertNumParam(2, args[1])
+			if err == nil {
+				res = append(argList[:int(index)], argList[int(index+1):]...)
+			}
+		}
+
+		if argMap, ok2 := args[0].(map[interface{}]interface{}); ok2 {
+			key := fmt.Sprint(args[1])
+			delete(argMap, key)
+			res = argMap
+			err = nil
+		}
+	}
+
+	return res, err
+}
+
+/*
+DocString returns a descriptive string.
+*/
+func (rf *delFunc) DocString() (string, error) {
+	return "Del removes an item from a list or map.", nil
+}
+
+// Add
+// ===
+
+/*
+addFunc adds an element to a list.
+*/
+type addFunc struct {
+	*inbuildBaseFunc
+}
+
+/*
+Run executes this function.
+*/
+func (rf *addFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, args []interface{}) (interface{}, error) {
+	var res interface{}
+
+	err := fmt.Errorf("Need a list as first parameter and a value as second parameter")
+
+	if len(args) > 1 {
+		var argList []interface{}
+
+		if argList, err = rf.AssertListParam(1, args[0]); err == nil {
+			if len(args) == 3 {
+				var index float64
+
+				if index, err = rf.AssertNumParam(3, args[2]); err == nil {
+					argList = append(argList, 0)
+					copy(argList[int(index+1):], argList[int(index):])
+					argList[int(index)] = args[1]
+					res = argList
+				}
+			} else {
+				res = append(argList, args[1])
+			}
+		}
+	}
+
+	return res, err
+}
+
+/*
+DocString returns a descriptive string.
+*/
+func (rf *addFunc) DocString() (string, error) {
+	return "Add adds an item to a list. The item is added at the optionally given index or at the end if no index is specified.", nil
+}
+
+// Concat
+// ======
+
+/*
+concatFunc joins one or more lists together.
+*/
+type concatFunc struct {
+	*inbuildBaseFunc
+}
+
+/*
+Run executes this function.
+*/
+func (rf *concatFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, args []interface{}) (interface{}, error) {
+	var res interface{}
+
+	err := fmt.Errorf("Need at least two lists as parameters")
+
+	if len(args) > 1 {
+		var argList []interface{}
+
+		resList := make([]interface{}, 0)
+		err = nil
+
+		for _, a := range args {
+			if err == nil {
+				if argList, err = rf.AssertListParam(1, a); err == nil {
+					resList = append(resList, argList...)
+				}
+			}
+		}
+
+		if err == nil {
+			res = resList
+		}
+	}
+
+	return res, err
+}
+
+/*
+DocString returns a descriptive string.
+*/
+func (rf *concatFunc) DocString() (string, error) {
+	return "Concat joins one or more lists together. The result is a new list.", nil
+}
+
+// dumpenv
+// =======
+
+/*
+dumpenvFunc returns the current variable environment as a string.
+*/
+type dumpenvFunc struct {
+	*inbuildBaseFunc
+}
+
+/*
+Run executes this function.
+*/
+func (rf *dumpenvFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, args []interface{}) (interface{}, error) {
+	return vs.String(), nil
+}
+
+/*
+DocString returns a descriptive string.
+*/
+func (rf *dumpenvFunc) DocString() (string, error) {
+	return "Dumpenv returns the current variable environment as a string.", nil
+}

+ 264 - 0
interpreter/func_provider_test.go

@@ -0,0 +1,264 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestStdlib(t *testing.T) {
+
+	res, err := UnitTestEvalAndAST(
+		`fmt.Sprint([1,2,3])`, nil,
+		`
+identifier: fmt
+  identifier: Sprint
+    funccall
+      list
+        number: 1
+        number: 2
+        number: 3
+`[1:])
+
+	if err != nil || res != "[1 2 3]" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`fmt.Sprint(math.Pi)`, nil,
+		`
+identifier: fmt
+  identifier: Sprint
+    funccall
+      identifier: math
+        identifier: Pi
+`[1:])
+
+	if err != nil || res != "3.141592653589793" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	// Negative case
+
+	res, err = UnitTestEvalAndAST(
+		`a.fmtSprint([1,2,3])`, nil,
+		`
+identifier: a
+  identifier: fmtSprint
+    funccall
+      list
+        number: 1
+        number: 2
+        number: 3
+`[1:])
+
+	if err == nil ||
+		err.Error() != "ECAL error in ECALTestRuntime: Unknown construct (Unknown function: fmtSprint) (Line:1 Pos:3)" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+}
+
+func TestSimpleFunctions(t *testing.T) {
+
+	res, err := UnitTestEvalAndAST(
+		`len([1,2,3])`, nil,
+		`
+identifier: len
+  funccall
+    list
+      number: 1
+      number: 2
+      number: 3
+`[1:])
+
+	if err != nil || res != 3. {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`len({"a":1, 2:"b"})`, nil,
+		`
+identifier: len
+  funccall
+    map
+      kvp
+        string: 'a'
+        number: 1
+      kvp
+        number: 2
+        string: 'b'
+`[1:])
+
+	if err != nil || res != 2. {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`del([1,2,3], 1)`, nil,
+		`
+identifier: del
+  funccall
+    list
+      number: 1
+      number: 2
+      number: 3
+    number: 1
+`[1:])
+
+	if err != nil || fmt.Sprint(res) != "[1 3]" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`del({
+  "a" : 1
+  "b" : 2
+  "c" : 3
+}, "b")`, nil,
+		`
+identifier: del
+  funccall
+    map
+      kvp
+        string: 'a'
+        number: 1
+      kvp
+        string: 'b'
+        number: 2
+      kvp
+        string: 'c'
+        number: 3
+    string: 'b'
+`[1:])
+
+	if err != nil || fmt.Sprint(res) != "map[a:1 c:3]" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`add([1,2,3], 4)`, nil,
+		`
+identifier: add
+  funccall
+    list
+      number: 1
+      number: 2
+      number: 3
+    number: 4
+`[1:])
+
+	if err != nil || fmt.Sprint(res) != "[1 2 3 4]" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`add([1,2,3], 4, 0)`, nil,
+		`
+identifier: add
+  funccall
+    list
+      number: 1
+      number: 2
+      number: 3
+    number: 4
+    number: 0
+`[1:])
+
+	if err != nil || fmt.Sprint(res) != "[4 1 2 3]" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`add([1,2,3], 4, 1)`, nil,
+		`
+identifier: add
+  funccall
+    list
+      number: 1
+      number: 2
+      number: 3
+    number: 4
+    number: 1
+`[1:])
+
+	if err != nil || fmt.Sprint(res) != "[1 4 2 3]" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`concat([1,2,3], [4,5,6], [7,8,9])`, nil,
+		`
+identifier: concat
+  funccall
+    list
+      number: 1
+      number: 2
+      number: 3
+    list
+      number: 4
+      number: 5
+      number: 6
+    list
+      number: 7
+      number: 8
+      number: 9
+`[1:])
+
+	if err != nil || fmt.Sprint(res) != "[1 2 3 4 5 6 7 8 9]" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`dumpenv()`, nil,
+		`
+identifier: dumpenv
+  funccall
+`[1:])
+
+	if err != nil || fmt.Sprint(res) != `GlobalScope {
+}` {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	// Negative case
+
+	res, err = UnitTestEvalAndAST(
+		`a.len([1,2,3])`, nil,
+		`
+identifier: a
+  identifier: len
+    funccall
+      list
+        number: 1
+        number: 2
+        number: 3
+`[1:])
+
+	if err == nil ||
+		err.Error() != "ECAL error in ECALTestRuntime: Unknown construct (Unknown function: len) (Line:1 Pos:3)" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+}

+ 126 - 0
interpreter/main_test.go

@@ -0,0 +1,126 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"testing"
+
+	"devt.de/krotik/common/datautil"
+	"devt.de/krotik/ecal/parser"
+	"devt.de/krotik/ecal/scope"
+)
+
+// Main function for all tests in this package
+
+func TestMain(m *testing.M) {
+	flag.Parse()
+
+	// Run the tests
+
+	res := m.Run()
+
+	// Check if all nodes have been tested
+
+	for n := range providerMap {
+		if _, ok := usedNodes[n]; !ok {
+			fmt.Println("Not tested node: ", n)
+		}
+	}
+
+	os.Exit(res)
+}
+
+// Used nodes map which is filled during unit testing. Prefilled only with nodes
+// which should not be encountered in ASTs.
+//
+var usedNodes = map[string]bool{
+	parser.NodeEOF: true,
+}
+
+func UnitTestEval(input string, vs parser.Scope) (interface{}, error) {
+	return UnitTestEvalAndAST(input, vs, "")
+}
+
+func UnitTestEvalAndAST(input string, vs parser.Scope, expectedAST string) (interface{}, error) {
+	var traverseAST func(n *parser.ASTNode)
+
+	traverseAST = func(n *parser.ASTNode) {
+		if n.Name == "" {
+			panic(fmt.Sprintf("Node found with empty string name: %s", n))
+		}
+
+		usedNodes[n.Name] = true
+		for _, cn := range n.Children {
+			traverseAST(cn)
+		}
+	}
+
+	// Parse the input
+
+	ast, err := parser.ParseWithRuntime("ECALEvalTest", input, NewECALRuntimeProvider("ECALTestRuntime"))
+	if err != nil {
+		return nil, err
+	}
+
+	traverseAST(ast)
+
+	if expectedAST != "" && ast.String() != expectedAST {
+		return nil, fmt.Errorf("Unexpected AST result:\n%v", ast.String())
+	}
+
+	// Validate input
+
+	if err := ast.Runtime.Validate(); err != nil {
+		return nil, err
+	}
+
+	if vs == nil {
+		vs = scope.NewScope(scope.GlobalScope)
+	}
+
+	return ast.Runtime.Eval(vs, make(map[string]interface{}))
+}
+
+/*
+addLogFunction adds a simple log function to a given Scope.
+*/
+func addLogFunction(vs parser.Scope) *datautil.RingBuffer {
+	buf := datautil.NewRingBuffer(20)
+	vs.SetValue("log", &TestLogger{buf})
+	return buf
+}
+
+/*
+TestLogger is a simple logger function which can be added to tess.
+*/
+type TestLogger struct {
+	buf *datautil.RingBuffer
+}
+
+func (tl *TestLogger) Run(instanceID string, vs parser.Scope, is map[string]interface{}, args []interface{}) (interface{}, error) {
+	tl.buf.Add(fmt.Sprint(args...))
+	return nil, nil
+}
+
+func (tl *TestLogger) DocString() (string, error) {
+	return "testlogger docstring", nil
+}
+
+func (tl *TestLogger) String() string {
+	return "TestLogger"
+}
+
+func (tl *TestLogger) MarshalJSON() ([]byte, error) {
+	return []byte(tl.String()), nil
+}

+ 159 - 0
interpreter/provider.go

@@ -0,0 +1,159 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+// TODO:
+// Import resolve
+// Event function: event
+// Context supporting final
+// Event handling
+
+package interpreter
+
+import (
+	"devt.de/krotik/ecal/parser"
+	"devt.de/krotik/ecal/util"
+)
+
+/*
+ecalRuntimeNew is used to instantiate ECAL runtime components.
+*/
+type ecalRuntimeNew func(*ECALRuntimeProvider, *parser.ASTNode) parser.Runtime
+
+/*
+providerMap contains the mapping of AST nodes to runtime components for ECAL ASTs.
+*/
+var providerMap = map[string]ecalRuntimeNew{
+
+	parser.NodeEOF: invalidRuntimeInst,
+
+	parser.NodeSTRING:     stringValueRuntimeInst, // String constant
+	parser.NodeNUMBER:     numberValueRuntimeInst, // Number constant
+	parser.NodeIDENTIFIER: identifierRuntimeInst,  // Idendifier
+
+	// Constructed tokens
+
+	parser.NodeSTATEMENTS: statementsRuntimeInst, // List of statements
+	parser.NodeFUNCCALL:   voidRuntimeInst,       // Function call
+	parser.NodeCOMPACCESS: voidRuntimeInst,       // Composition structure access
+	parser.NodeLIST:       listValueRuntimeInst,  // List value
+	parser.NodeMAP:        mapValueRuntimeInst,   // Map value
+	parser.NodePARAMS:     voidRuntimeInst,       // Function parameters
+	parser.NodeGUARD:      guardRuntimeInst,      // Guard expressions for conditional statements
+
+	// Condition operators
+
+	parser.NodeGEQ: greaterequalOpRuntimeInst,
+	parser.NodeLEQ: lessequalOpRuntimeInst,
+	parser.NodeNEQ: notequalOpRuntimeInst,
+	parser.NodeEQ:  equalOpRuntimeInst,
+	parser.NodeGT:  greaterOpRuntimeInst,
+	parser.NodeLT:  lessOpRuntimeInst,
+
+	// Separators
+
+	parser.NodeKVP:    voidRuntimeInst, // Key-value pair
+	parser.NodePRESET: voidRuntimeInst, // Preset value
+
+	// Arithmetic operators
+
+	parser.NodePLUS: plusOpRuntimeInst,
+
+	parser.NodeMINUS:  minusOpRuntimeInst,
+	parser.NodeTIMES:  timesOpRuntimeInst,
+	parser.NodeDIV:    divOpRuntimeInst,
+	parser.NodeMODINT: modintOpRuntimeInst,
+	parser.NodeDIVINT: divintOpRuntimeInst,
+
+	// Assignment statement
+
+	parser.NodeASSIGN: assignmentRuntimeInst,
+	/*
+
+		// Import statement
+
+		parser.NodeIMPORT
+
+		// Sink definition
+
+		parser.NodeSINK
+		parser.NodeKINDMATCH
+		parser.NodeSCOPEMATCH
+		parser.NodeSTATEMATCH
+		parser.NodePRIORITY
+		parser.NodeSUPPRESSES
+	*/
+	// Function definition
+
+	parser.NodeFUNC:   funcRuntimeInst,
+	parser.NodeRETURN: returnRuntimeInst,
+
+	// Boolean operators
+
+	parser.NodeOR:  orOpRuntimeInst,
+	parser.NodeAND: andOpRuntimeInst,
+	parser.NodeNOT: notOpRuntimeInst,
+
+	// Condition operators
+
+	parser.NodeLIKE:      likeOpRuntimeInst,
+	parser.NodeIN:        inOpRuntimeInst,
+	parser.NodeHASPREFIX: beginswithOpRuntimeInst,
+	parser.NodeHASSUFFIX: endswithOpRuntimeInst,
+	parser.NodeNOTIN:     notinOpRuntimeInst,
+
+	// Constant terminals
+
+	parser.NodeFALSE: falseRuntimeInst,
+	parser.NodeTRUE:  trueRuntimeInst,
+	parser.NodeNULL:  nullRuntimeInst,
+
+	// Conditional statements
+
+	parser.NodeIF: ifRuntimeInst,
+
+	// Loop statements
+
+	parser.NodeLOOP:     loopRuntimeInst,
+	parser.NodeBREAK:    breakRuntimeInst,
+	parser.NodeCONTINUE: continueRuntimeInst,
+}
+
+/*
+ECALRuntimeProvider is the factory object producing runtime objects for ECAL ASTs.
+*/
+type ECALRuntimeProvider struct {
+	Name string // Name to identify the input
+}
+
+/*
+NewECALRuntimeProvider returns a new instance of a ECAL runtime provider.
+*/
+func NewECALRuntimeProvider(name string) *ECALRuntimeProvider {
+	return &ECALRuntimeProvider{name}
+}
+
+/*
+Runtime returns a runtime component for a given ASTNode.
+*/
+func (erp *ECALRuntimeProvider) Runtime(node *parser.ASTNode) parser.Runtime {
+
+	if instFunc, ok := providerMap[node.Name]; ok {
+		return instFunc(erp, node)
+	}
+
+	return invalidRuntimeInst(erp, node)
+}
+
+/*
+NewRuntimeError creates a new RuntimeError object.
+*/
+func (erp *ECALRuntimeProvider) NewRuntimeError(t error, d string, node *parser.ASTNode) error {
+	return util.NewRuntimeError(erp.Name, t, d, node)
+}

+ 213 - 0
interpreter/rt_arithmetic.go

@@ -0,0 +1,213 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"math"
+
+	"devt.de/krotik/ecal/parser"
+)
+
+// Basic Arithmetic Operator Runtimes
+// ==================================
+
+type plusOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+plusOpRuntimeInst returns a new runtime component instance.
+*/
+func plusOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &plusOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *plusOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		// Use as prefix
+
+		if len(rt.node.Children) == 1 {
+			return rt.numVal(func(n float64) interface{} {
+				return n
+			}, vs, is)
+		}
+
+		// Use as operation
+
+		res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
+			return n1 + n2
+		}, vs, is)
+	}
+
+	return res, err
+}
+
+type minusOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+minusOpRuntimeInst returns a new runtime component instance.
+*/
+func minusOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &minusOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *minusOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		// Use as prefix
+
+		if len(rt.node.Children) == 1 {
+			return rt.numVal(func(n float64) interface{} {
+				return -n
+			}, vs, is)
+		}
+
+		// Use as operation
+
+		res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
+			return n1 - n2
+		}, vs, is)
+	}
+
+	return res, err
+}
+
+type timesOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+timesOpRuntimeInst returns a new runtime component instance.
+*/
+func timesOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &timesOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *timesOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
+			return n1 * n2
+		}, vs, is)
+	}
+
+	return res, err
+}
+
+type divOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+divOpRuntimeInst returns a new runtime component instance.
+*/
+func divOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &divOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *divOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
+			return n1 / n2
+		}, vs, is)
+	}
+
+	return res, err
+}
+
+type divintOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+divintOpRuntimeInst returns a new runtime component instance.
+*/
+func divintOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &divintOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *divintOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
+			return math.Floor(n1 / n2)
+		}, vs, is)
+	}
+
+	return res, err
+}
+
+type modintOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+divOpRuntimeInst returns a new runtime component instance.
+*/
+func modintOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &modintOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *modintOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
+			return float64(int64(n1) % int64(n2))
+		}, vs, is)
+	}
+
+	return res, err
+}

+ 141 - 0
interpreter/rt_arithmetic_test.go

@@ -0,0 +1,141 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"testing"
+)
+
+func TestSimpleArithmetics(t *testing.T) {
+
+	res, err := UnitTestEvalAndAST(
+		`1 + 2`, nil,
+		`
+plus
+  number: 1
+  number: 2
+`[1:])
+
+	if err != nil || res != 3. {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`1 + 2 + 3`, nil,
+		`
+plus
+  plus
+    number: 1
+    number: 2
+  number: 3
+`[1:])
+
+	if err != nil || res != 6. {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`1 - 2 + 3`, nil,
+		`
+plus
+  minus
+    number: 1
+    number: 2
+  number: 3
+`[1:])
+
+	if err != nil || res != 2. {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`1 - 2`, nil,
+		`
+minus
+  number: 1
+  number: 2
+`[1:])
+
+	if err != nil || res != -1. {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`-5.2 - 2.2`, nil,
+		`
+minus
+  minus
+    number: 5.2
+  number: 2.2
+`[1:])
+
+	if err != nil || res != -7.4 {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`+ 5.2 * 2`, nil,
+		`
+times
+  plus
+    number: 5.2
+  number: 2
+`[1:])
+
+	if err != nil || res != 10.4 {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`5.2 / 2`, nil,
+		`
+div
+  number: 5.2
+  number: 2
+`[1:])
+
+	if err != nil || res != 2.6 {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`5.2 // 2`, nil,
+		`
+divint
+  number: 5.2
+  number: 2
+`[1:])
+
+	if err != nil || res != 2. {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`5.2 % 2`, nil,
+		`
+modint
+  number: 5.2
+  number: 2
+`[1:])
+
+	if err != nil || res != 1. {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+}

+ 122 - 0
interpreter/rt_assign.go

@@ -0,0 +1,122 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"fmt"
+
+	"devt.de/krotik/ecal/parser"
+	"devt.de/krotik/ecal/util"
+)
+
+/*
+assignmentRuntime is the runtime component for assignment of values.
+*/
+type assignmentRuntime struct {
+	*baseRuntime
+	leftSide []*identifierRuntime
+}
+
+/*
+assignmentRuntimeInst returns a new runtime component instance.
+*/
+func assignmentRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &assignmentRuntime{newBaseRuntime(erp, node), nil}
+}
+
+/*
+Validate this node and all its child nodes.
+*/
+func (rt *assignmentRuntime) Validate() error {
+	err := rt.baseRuntime.Validate()
+
+	if err == nil {
+
+		leftVar := rt.node.Children[0]
+
+		if leftRuntime, ok := leftVar.Runtime.(*identifierRuntime); ok {
+
+			rt.leftSide = []*identifierRuntime{leftRuntime}
+
+		} else if leftVar.Name == parser.NodeLIST {
+
+			rt.leftSide = make([]*identifierRuntime, 0, len(leftVar.Children))
+
+			for _, child := range leftVar.Children {
+				childRuntime, ok := child.Runtime.(*identifierRuntime)
+
+				if !ok {
+					err = rt.erp.NewRuntimeError(util.ErrVarAccess,
+						"Must have a list of variables on the left side of the assignment", rt.node)
+					break
+				}
+
+				rt.leftSide = append(rt.leftSide, childRuntime)
+			}
+
+		} else {
+
+			err = rt.erp.NewRuntimeError(util.ErrVarAccess,
+				"Must have a variable or list of variables on the left side of the assignment", rt.node)
+		}
+	}
+
+	return err
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *assignmentRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		var val interface{}
+
+		val, err = rt.node.Children[1].Runtime.Eval(vs, is)
+
+		if err == nil {
+			if len(rt.leftSide) == 1 {
+
+				err = rt.leftSide[0].Set(vs, is, val)
+
+			} else if valList, ok := val.([]interface{}); ok {
+
+				if len(rt.leftSide) != len(valList) {
+
+					err = rt.erp.NewRuntimeError(util.ErrInvalidState,
+						fmt.Sprintf("Assigned number of variables is different to "+
+							"number of values (%v variables vs %v values)",
+							len(rt.leftSide), len(valList)), rt.node)
+
+				} else {
+
+					for i, v := range rt.leftSide {
+
+						if err = v.Set(vs, is, valList[i]); err != nil {
+							err = rt.erp.NewRuntimeError(util.ErrVarAccess,
+								err.Error(), rt.node)
+							break
+						}
+					}
+				}
+
+			} else {
+
+				err = rt.erp.NewRuntimeError(util.ErrInvalidState,
+					fmt.Sprintf("Result is not a list (value is %v)", val),
+					rt.node)
+			}
+		}
+	}
+
+	return nil, err
+}

+ 218 - 0
interpreter/rt_assign_test.go

@@ -0,0 +1,218 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"testing"
+
+	"devt.de/krotik/ecal/scope"
+)
+
+func TestSimpleAssignments(t *testing.T) {
+
+	vs := scope.NewScope(scope.GlobalScope)
+
+	res, err := UnitTestEvalAndAST(
+		`a := 42`, vs,
+		`
+:=
+  identifier: a
+  number: 42
+`[1:])
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    a (float64) : 42
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`a := "test"`, vs,
+		`
+:=
+  identifier: a
+  string: 'test'
+`[1:])
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    a (string) : test
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`a := [1,2,3]`, vs,
+		`
+:=
+  identifier: a
+  list
+    number: 1
+    number: 2
+    number: 3
+`[1:])
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    a ([]interface {}) : [1,2,3]
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`a := {
+			1 : "foo",
+			"2" : "bar",
+			"foobar" : 3,
+			x : 4,
+		}`, vs,
+		`
+:=
+  identifier: a
+  map
+    kvp
+      number: 1
+      string: 'foo'
+    kvp
+      string: '2'
+      string: 'bar'
+    kvp
+      string: 'foobar'
+      number: 3
+    kvp
+      identifier: x
+      number: 4
+`[1:])
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    a (map[interface {}]interface {}) : {"1":"foo","2":"bar","foobar":3,"null":4}
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+}
+
+func TestComplexAssignments(t *testing.T) {
+
+	vs := scope.NewScope(scope.GlobalScope)
+
+	res, err := UnitTestEvalAndAST(
+		`[a, b] := ["test", [1,2,3]]`, vs,
+		`
+:=
+  list
+    identifier: a
+    identifier: b
+  list
+    string: 'test'
+    list
+      number: 1
+      number: 2
+      number: 3
+`[1:])
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    a (string) : test
+    b ([]interface {}) : [1,2,3]
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(`
+a := {
+	"b" : [ 0, {
+		"c" : [ 0, 1, 2, {
+			"test" : 1
+		}]
+	}]
+}
+a.b[1].c["3"]["test"] := 3
+`, vs,
+		`
+statements
+  :=
+    identifier: a
+    map
+      kvp
+        string: 'b'
+        list
+          number: 0
+          map
+            kvp
+              string: 'c'
+              list
+                number: 0
+                number: 1
+                number: 2
+                map
+                  kvp
+                    string: 'test'
+                    number: 1
+  :=
+    identifier: a
+      identifier: b
+        compaccess
+          number: 1
+        identifier: c
+          compaccess
+            string: '3'
+          compaccess
+            string: 'test'
+    number: 3
+`[1:])
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    a (map[interface {}]interface {}) : {"b":[0,{"c":[0,1,2,{"test":3}]}]}
+    b ([]interface {}) : [1,2,3]
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(`
+z := a.b[1].c["3"]["test"]
+[x, y] := a.b
+`, vs,
+		`
+statements
+  :=
+    identifier: z
+    identifier: a
+      identifier: b
+        compaccess
+          number: 1
+        identifier: c
+          compaccess
+            string: '3'
+          compaccess
+            string: 'test'
+  :=
+    list
+      identifier: x
+      identifier: y
+    identifier: a
+      identifier: b
+`[1:])
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    a (map[interface {}]interface {}) : {"b":[0,{"c":[0,1,2,{"test":3}]}]}
+    b ([]interface {}) : [1,2,3]
+    x (float64) : 0
+    y (map[interface {}]interface {}) : {"c":[0,1,2,{"test":3}]}
+    z (float64) : 3
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+
+}

+ 474 - 0
interpreter/rt_boolean.go

@@ -0,0 +1,474 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"fmt"
+	"regexp"
+	"strings"
+
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/ecal/parser"
+)
+
+// Basic Boolean Operator Runtimes
+// ===============================
+
+type greaterequalOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+greaterequalOpRuntimeInst returns a new runtime component instance.
+*/
+func greaterequalOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &greaterequalOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *greaterequalOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
+			return n1 >= n2
+		}, vs, is)
+
+		if err != nil {
+			res, err = rt.strOp(func(n1 string, n2 string) interface{} {
+				return n1 >= n2
+			}, vs, is)
+		}
+	}
+
+	return res, err
+}
+
+type greaterOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+greaterOpRuntimeInst returns a new runtime component instance.
+*/
+func greaterOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &greaterOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *greaterOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
+			return n1 > n2
+		}, vs, is)
+
+		if err != nil {
+			res, err = rt.strOp(func(n1 string, n2 string) interface{} {
+				return n1 > n2
+			}, vs, is)
+		}
+	}
+
+	return res, err
+}
+
+type lessequalOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+lessequalOpRuntimeInst returns a new runtime component instance.
+*/
+func lessequalOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &lessequalOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *lessequalOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
+			return n1 <= n2
+		}, vs, is)
+
+		if err != nil {
+			res, err = rt.strOp(func(n1 string, n2 string) interface{} {
+				return n1 <= n2
+			}, vs, is)
+		}
+	}
+
+	return res, err
+}
+
+type lessOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+lessOpRuntimeInst returns a new runtime component instance.
+*/
+func lessOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &lessOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *lessOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
+			return n1 < n2
+		}, vs, is)
+
+		if err != nil {
+			res, err = rt.strOp(func(n1 string, n2 string) interface{} {
+				return n1 < n2
+			}, vs, is)
+		}
+	}
+
+	return res, err
+}
+
+type equalOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+equalOpRuntimeInst returns a new runtime component instance.
+*/
+func equalOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &equalOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *equalOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		res, err = rt.genOp(func(n1 interface{}, n2 interface{}) interface{} {
+			return n1 == n2
+		}, vs, is)
+	}
+
+	return res, err
+}
+
+type notequalOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+notequalOpRuntimeInst returns a new runtime component instance.
+*/
+func notequalOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &notequalOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *notequalOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		res, err = rt.genOp(func(n1 interface{}, n2 interface{}) interface{} {
+			return n1 != n2
+		}, vs, is)
+	}
+
+	return res, err
+}
+
+type andOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+andOpRuntimeInst returns a new runtime component instance.
+*/
+func andOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &andOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *andOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		res, err = rt.boolOp(func(b1 bool, b2 bool) interface{} {
+			return b1 && b2
+		}, vs, is)
+	}
+
+	return res, err
+}
+
+type orOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+orOpRuntimeInst returns a new runtime component instance.
+*/
+func orOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &orOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *orOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		res, err = rt.boolOp(func(b1 bool, b2 bool) interface{} {
+			return b1 || b2
+		}, vs, is)
+	}
+
+	return res, err
+}
+
+type notOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+notOpRuntimeInst returns a new runtime component instance.
+*/
+func notOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &notOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *notOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		res, err = rt.boolVal(func(b bool) interface{} {
+			return !b
+		}, vs, is)
+	}
+
+	return res, err
+}
+
+// In-build condition operators
+// ============================
+
+/*
+likeOpRuntime is the pattern matching operator. The syntax of the regular
+expressions accepted is the same general syntax used by Go, Perl, Python, and
+other languages.
+*/
+type likeOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+likeOpRuntimeInst returns a new runtime component instance.
+*/
+func likeOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &likeOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *likeOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		errorutil.AssertTrue(len(rt.node.Children) == 2,
+			fmt.Sprint("Operation requires 2 operands", rt.node))
+
+		str, err := rt.node.Children[0].Runtime.Eval(vs, is)
+		if err == nil {
+			var pattern interface{}
+
+			pattern, err = rt.node.Children[1].Runtime.Eval(vs, is)
+			if err == nil {
+				var re *regexp.Regexp
+
+				re, err = regexp.Compile(fmt.Sprint(pattern))
+				if err == nil {
+
+					return re.MatchString(fmt.Sprint(str)), nil
+				}
+			}
+		}
+	}
+
+	return nil, err
+}
+
+type beginswithOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+beginswithOpRuntimeInst returns a new runtime component instance.
+*/
+func beginswithOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &beginswithOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *beginswithOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		res, err = rt.strOp(func(s1 string, s2 string) interface{} {
+			return strings.HasPrefix(s1, s2)
+		}, vs, is)
+	}
+
+	return res, err
+}
+
+type endswithOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+endswithOpRuntimeInst returns a new runtime component instance.
+*/
+func endswithOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &endswithOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *endswithOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		res, err = rt.strOp(func(s1 string, s2 string) interface{} {
+			return strings.HasSuffix(s1, s2)
+		}, vs, is)
+	}
+
+	return res, err
+}
+
+type inOpRuntime struct {
+	*operatorRuntime
+}
+
+/*
+inOpRuntimeInst returns a new runtime component instance.
+*/
+func inOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &inOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *inOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		res, err = rt.listOp(func(val interface{}, list []interface{}) interface{} {
+			for _, i := range list {
+				if val == i {
+					return true
+				}
+			}
+			return false
+		}, vs, is)
+	}
+
+	return res, err
+}
+
+type notinOpRuntime struct {
+	*inOpRuntime
+}
+
+/*
+notinOpRuntimeInst returns a new runtime component instance.
+*/
+func notinOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &notinOpRuntime{&inOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *notinOpRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		if res, err = rt.inOpRuntime.Eval(vs, is); err == nil {
+			res = !res.(bool)
+		}
+	}
+
+	return res, err
+}

+ 334 - 0
interpreter/rt_boolean_test.go

@@ -0,0 +1,334 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestSimpleBoolean(t *testing.T) {
+
+	res, err := UnitTestEvalAndAST(
+		`2 >= 2`, nil,
+		`
+>=
+  number: 2
+  number: 2
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`"foo" >= "bar"`, nil,
+		`
+>=
+  string: 'foo'
+  string: 'bar'
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`3 > 2`, nil,
+		`
+>
+  number: 3
+  number: 2
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`"foo" > "bar"`, nil,
+		`
+>
+  string: 'foo'
+  string: 'bar'
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`2 <= 2`, nil,
+		`
+<=
+  number: 2
+  number: 2
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`"bar" <= "foo"`, nil,
+		`
+<=
+  string: 'bar'
+  string: 'foo'
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`2 < 3`, nil,
+		`
+<
+  number: 2
+  number: 3
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`"bar" < "foo"`, nil,
+		`
+<
+  string: 'bar'
+  string: 'foo'
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`3 == 3`, nil,
+		`
+==
+  number: 3
+  number: 3
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`3 == 3 == true`, nil,
+		`
+==
+  ==
+    number: 3
+    number: 3
+  true
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`2 != 3`, nil,
+		`
+!=
+  number: 2
+  number: 3
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`3 != 3 == false`, nil,
+		`
+==
+  !=
+    number: 3
+    number: 3
+  false
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`null == null`, nil,
+		`
+==
+  null
+  null
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`1 < 2 and 2 > 1`, nil,
+		`
+and
+  <
+    number: 1
+    number: 2
+  >
+    number: 2
+    number: 1
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`1 < 2 or 2 < 1`, nil,
+		`
+or
+  <
+    number: 1
+    number: 2
+  <
+    number: 2
+    number: 1
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`not (1 < 2 or 2 < 1)`, nil,
+		`
+not
+  or
+    <
+      number: 1
+      number: 2
+    <
+      number: 2
+      number: 1
+`[1:])
+
+	if fmt.Sprint(res) != "false" || err != nil {
+		t.Error(res, err)
+		return
+	}
+}
+
+func TestConditionOperators(t *testing.T) {
+
+	res, err := UnitTestEvalAndAST(
+		`"Hans" like "Ha*"`, nil,
+		`
+like
+  string: 'Hans'
+  string: 'Ha*'
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`"Hans" hasprefix "Ha"`, nil,
+		`
+hasprefix
+  string: 'Hans'
+  string: 'Ha'
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`"Hans" hassuffix "ns"`, nil,
+		`
+hassuffix
+  string: 'Hans'
+  string: 'ns'
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`2 in [1,2,3]`, nil,
+		`
+in
+  number: 2
+  list
+    number: 1
+    number: 2
+    number: 3
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`"Hans" in [1,2,"Hans"]`, nil,
+		`
+in
+  string: 'Hans'
+  list
+    number: 1
+    number: 2
+    string: 'Hans'
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`"NotHans" notin [1,2,"Hans"]`, nil,
+		`
+notin
+  string: 'NotHans'
+  list
+    number: 1
+    number: 2
+    string: 'Hans'
+`[1:])
+
+	if fmt.Sprint(res) != "true" || err != nil {
+		t.Error(res, err)
+		return
+	}
+}

+ 79 - 0
interpreter/rt_const.go

@@ -0,0 +1,79 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import "devt.de/krotik/ecal/parser"
+
+/*
+trueRuntime is the runtime component for the true constant.
+*/
+type trueRuntime struct {
+	*baseRuntime
+}
+
+/*
+trueRuntimeInst returns a new runtime component instance.
+*/
+func trueRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &trueRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *trueRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+	return true, err
+}
+
+/*
+falseRuntime is the runtime component for the false constant.
+*/
+type falseRuntime struct {
+	*baseRuntime
+}
+
+/*
+falseRuntimeInst returns a new runtime component instance.
+*/
+func falseRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &falseRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *falseRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+	return false, err
+}
+
+/*
+nullRuntime is the runtime component for the null constant.
+*/
+type nullRuntime struct {
+	*baseRuntime
+}
+
+/*
+nullRuntimeInst returns a new runtime component instance.
+*/
+func nullRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &nullRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *nullRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+	return nil, err
+}

+ 211 - 0
interpreter/rt_func.go

@@ -0,0 +1,211 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"devt.de/krotik/ecal/parser"
+	"devt.de/krotik/ecal/scope"
+	"devt.de/krotik/ecal/util"
+)
+
+/*
+returnRuntime is a special runtime for return statements in functions.
+*/
+type returnRuntime struct {
+	*baseRuntime
+}
+
+/*
+voidRuntimeInst returns a new runtime component instance.
+*/
+func returnRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &returnRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Validate this node and all its child nodes.
+*/
+func (rt *returnRuntime) Validate() error {
+	return rt.baseRuntime.Validate()
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *returnRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		var res interface{}
+
+		if res, err = rt.node.Children[0].Runtime.Eval(vs, is); err == nil {
+			rerr := rt.erp.NewRuntimeError(util.ErrReturn, fmt.Sprintf("Return value: %v", res), rt.node)
+			err = &returnValue{
+				rerr.(*util.RuntimeError),
+				res,
+			}
+
+		}
+	}
+
+	return nil, err
+}
+
+type returnValue struct {
+	*util.RuntimeError
+	returnValue interface{}
+}
+
+/*
+funcRuntime is the runtime component for function declarations.
+*/
+type funcRuntime struct {
+	*baseRuntime
+}
+
+/*
+funcRuntimeInst returns a new runtime component instance.
+*/
+func funcRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &funcRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *funcRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var fc interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		name := ""
+
+		if rt.node.Children[0].Name == parser.NodeIDENTIFIER {
+			name = rt.node.Children[0].Token.Val
+		}
+
+		fc = &function{name, nil, nil, rt.node}
+
+		if name != "" {
+			vs.SetValue(name, fc)
+		}
+	}
+
+	return fc, err
+}
+
+/*
+function models a function in ECAL. It can have a context object attached - this.
+*/
+type function struct {
+	name        string
+	super       []interface{}   // Super function pointer
+	this        interface{}     // Function context
+	declaration *parser.ASTNode // Function declaration node
+}
+
+/*
+Run executes this function. The function is called with parameters and might also
+have a reference to a context state - this.
+*/
+func (f *function) Run(instanceID string, vs parser.Scope, is map[string]interface{}, args []interface{}) (interface{}, error) {
+	var res interface{}
+	var err error
+
+	nameOffset := 0
+	if f.declaration.Children[0].Name == parser.NodeIDENTIFIER {
+		nameOffset = 1
+	}
+	params := f.declaration.Children[0+nameOffset].Children
+	body := f.declaration.Children[1+nameOffset]
+
+	// Create varscope for the body - not a child scope but a new root
+
+	fvs := scope.NewScope(fmt.Sprintf("func: %v", f.name))
+
+	if f.this != nil {
+		fvs.SetValue("this", f.this)
+	}
+	if f.super != nil {
+		fvs.SetValue("super", f.super)
+	}
+
+	for i, p := range params {
+		var name string
+		var val interface{}
+
+		if err == nil {
+			name = ""
+
+			if p.Name == parser.NodeIDENTIFIER {
+				name = p.Token.Val
+
+				if i < len(args) {
+					val = args[i]
+				} else {
+					val = nil
+				}
+			} else if p.Name == parser.NodePRESET {
+				name = p.Children[0].Token.Val
+
+				if i < len(args) {
+					val = args[i]
+				} else {
+					val, err = p.Children[1].Runtime.Eval(vs, is)
+				}
+			}
+
+			if name != "" {
+				fvs.SetValue(name, val)
+			}
+		}
+	}
+
+	if err == nil {
+		res, err = body.Runtime.Eval(fvs, make(map[string]interface{}))
+
+		// Check for return value (delivered as error object)
+
+		if rval, ok := err.(*returnValue); ok {
+			res = rval.returnValue
+			err = nil
+		}
+	}
+
+	return res, err
+}
+
+/*
+DocString returns a descriptive string.
+*/
+func (f *function) DocString() (string, error) {
+	return fmt.Sprintf("Declared function: %v (%v)", f.name, f.declaration.Token.PosString()), nil
+}
+
+/*
+String returns a string representation of this function.
+*/
+func (f *function) String() string {
+	return fmt.Sprintf("ecal.function: %v (%v)", f.name, f.declaration.Token.PosString())
+}
+
+/*
+MarshalJSON returns a string representation of this function - a function cannot
+be JSON encoded.
+*/
+func (f *function) MarshalJSON() ([]byte, error) {
+	return json.Marshal(f.String())
+}

+ 290 - 0
interpreter/rt_func_test.go

@@ -0,0 +1,290 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"testing"
+
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/ecal/scope"
+)
+
+func TestFunctions(t *testing.T) {
+
+	vs := scope.NewScope(scope.GlobalScope)
+
+	res, err := UnitTestEvalAndAST(`
+foo := [ [ func (a, b, c=1) {
+    return a + b + c
+  }
+]]
+
+result1 := foo[0][0](3, 2)
+`, vs, `
+statements
+  :=
+    identifier: foo
+    list
+      list
+        function
+          params
+            identifier: a
+            identifier: b
+            preset
+              identifier: c
+              number: 1
+          statements
+            return
+              plus
+                plus
+                  identifier: a
+                  identifier: b
+                identifier: c
+  :=
+    identifier: result1
+    identifier: foo
+      compaccess
+        number: 0
+      compaccess
+        number: 0
+      funccall
+        number: 3
+        number: 2
+`[1:])
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    foo ([]interface {}) : [["ecal.function:  (Line 2, Pos 12)"]]
+    result1 (float64) : 6
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+
+	vs = scope.NewScope(scope.GlobalScope)
+
+	res, err = UnitTestEval(`
+b := "a"
+foo := [{
+  b : func (a, b, c=1) {
+    return [1,[a + b + c]]
+  }
+}]
+
+result1 := foo[0].a(3, 2)[1][0]
+`, vs)
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    b (string) : a
+    foo ([]interface {}) : [{"a":"ecal.function:  (Line 4, Pos 7)"}]
+    result1 (float64) : 6
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+
+	vs = scope.NewScope(scope.GlobalScope)
+
+	res, err = UnitTestEval(`
+b := "a"
+foo := {
+  b : [func (a, b, c=1) {
+    return { "x" : { "y" : [a + b + c] }}
+  }]
+}
+
+result1 := foo.a[0](3, 2).x.y[0]
+`, vs)
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    b (string) : a
+    foo (map[interface {}]interface {}) : {"a":["ecal.function:  (Line 4, Pos 8)"]}
+    result1 (float64) : 6
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+
+	vs = scope.NewScope(scope.GlobalScope)
+
+	res, err = UnitTestEvalAndAST(`
+foo := {
+  "a" : {
+	"b" : func myfunc(a, b, c=1) {
+	      d := a + b + c
+	      return d
+	    }
+	}
+}
+result1 := foo.a.b(3, 2)
+result2 := myfunc(3, 3)
+`, vs, `
+statements
+  :=
+    identifier: foo
+    map
+      kvp
+        string: 'a'
+        map
+          kvp
+            string: 'b'
+            function
+              identifier: myfunc
+              params
+                identifier: a
+                identifier: b
+                preset
+                  identifier: c
+                  number: 1
+              statements
+                :=
+                  identifier: d
+                  plus
+                    plus
+                      identifier: a
+                      identifier: b
+                    identifier: c
+                return
+                  identifier: d
+  :=
+    identifier: result1
+    identifier: foo
+      identifier: a
+        identifier: b
+          funccall
+            number: 3
+            number: 2
+  :=
+    identifier: result2
+    identifier: myfunc
+      funccall
+        number: 3
+        number: 3
+`[1:])
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    foo (map[interface {}]interface {}) : {"a":{"b":"ecal.function: myfunc (Line 4, Pos 8)"}}
+    myfunc (*interpreter.function) : ecal.function: myfunc (Line 4, Pos 8)
+    result1 (float64) : 6
+    result2 (float64) : 7
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+}
+
+func TestObjectInstantiation(t *testing.T) {
+
+	vs := scope.NewScope(scope.GlobalScope)
+
+	res, err := UnitTestEvalAndAST(`
+Super := {
+  "name" : "base"
+
+  "init" : func() {
+    this.name := "baseclass"
+  }
+}
+
+Bar := {
+  "super" : [ Super ]
+
+  "test" : ""
+
+  "init" : func(test) {
+    this.test := test
+    x := super[0]()
+  }
+}
+
+Bar2 := {
+  "getTest" : func() {
+      return this.test
+  }
+}
+
+Foo := {
+  "super" : [ Bar, Bar2 ]
+
+  # Object ID
+  #
+  "id" : 0
+
+  "idx" : 0
+
+  # Constructor
+  #
+  "init" : func(id, test) {
+    this.id := id
+    x := super[0](test)
+  }
+
+  # Return the object ID
+  #
+  "getId" : func() {
+      return this.idx
+  }
+
+  # Set the object ID
+  #
+  "setId" : func(id) {
+      this.idx := id
+  }
+}
+
+result1 := new(Foo, 123, "tester")
+result1.setId(500)
+result2 := result1.getId() + result1.id
+`, vs, "")
+
+	if err == nil {
+		v, _, _ := vs.GetValue("result1")
+		if res := stringutil.ConvertToPrettyString(v); res != `{
+  "getId": "ecal.function:  (Line 45, Pos 42)",
+  "getTest": "ecal.function:  (Line 22, Pos 15)",
+  "id": 123,
+  "idx": 500,
+  "init": "ecal.function:  (Line 38, Pos 32)",
+  "name": "baseclass",
+  "setId": "ecal.function:  (Line 51, Pos 39)",
+  "super": [
+    {
+      "init": "ecal.function:  (Line 15, Pos 12)",
+      "super": [
+        {
+          "init": "ecal.function:  (Line 5, Pos 12)",
+          "name": "base"
+        }
+      ],
+      "test": ""
+    },
+    {
+      "getTest": "ecal.function:  (Line 22, Pos 15)"
+    }
+  ],
+  "test": "tester"
+}` {
+			t.Error("Unexpected result: ", res)
+			return
+		}
+	}
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    Bar (map[interface {}]interface {}) : {"init":"ecal.function:  (Line 15, Pos 12)","super":[{"init":"ecal.function:  (Line 5, Pos 12)","name":"base"}],"test":""}
+    Bar2 (map[interface {}]interface {}) : {"getTest":"ecal.function:  (Line 22, Pos 15)"}
+    Foo (map[interface {}]interface {}) : {"getId":"ecal.function:  (Line 45, Pos 42)","id":0,"idx":0,"init":"ecal.function:  (Line 38, Pos 32)","setId":"ecal.function:  (Line 51, Pos 39)","super":[{"init":"ecal.function:  (Line 15, Pos 12)","super":[{"init":"ecal.function:  (Line 5, Pos 12)","name":"base"}],"test":""},{"getTest":"ecal.function:  (Line 22, Pos 15)"}]}
+    Super (map[interface {}]interface {}) : {"init":"ecal.function:  (Line 5, Pos 12)","name":"base"}
+    result1 (map[interface {}]interface {}) : {"getId":"ecal.function:  (Line 45, Pos 42)","getTest":"ecal.function:  (Line 22, Pos 15)","id":123,"idx":500,"init":"ecal.function:  (Line 38, Pos 32)","name":"baseclass","setId":"ecal.function:  (Line 51, Pos 39)","super":[{"init":"ecal.function:  (Line 15, Pos 12)","super":[{"init":"ecal.function:  (Line 5, Pos 12)","name":"base"}],"test":""},{"getTest":"ecal.function:  (Line 22, Pos 15)"}],"test":"tester"}
+    result2 (float64) : 623
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+}

+ 351 - 0
interpreter/rt_general.go

@@ -0,0 +1,351 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"fmt"
+
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/ecal/parser"
+	"devt.de/krotik/ecal/util"
+)
+
+// Base Runtime
+// ============
+
+/*
+baseRuntime models a base runtime component which provides the essential fields and functions.
+*/
+type baseRuntime struct {
+	instanceID string               // Unique identifier (should be used when instance state is stored)
+	erp        *ECALRuntimeProvider // Runtime provider
+	node       *parser.ASTNode      // AST node which this runtime component is servicing
+}
+
+var instanceCounter uint64 // Global instance counter to create unique identifiers for every runtime component instance
+
+/*
+Validate this node and all its child nodes.
+*/
+func (rt *baseRuntime) Validate() error {
+
+	// Validate all children
+
+	for _, child := range rt.node.Children {
+		if err := child.Runtime.Validate(); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *baseRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	return nil, nil
+}
+
+/*
+newBaseRuntime returns a new instance of baseRuntime.
+*/
+func newBaseRuntime(erp *ECALRuntimeProvider, node *parser.ASTNode) *baseRuntime {
+	instanceCounter++
+	return &baseRuntime{fmt.Sprint(instanceCounter), erp, node}
+}
+
+// Void Runtime
+// ============
+
+/*
+voidRuntime is a special runtime for constructs which are only evaluated as part
+of other components.
+*/
+type voidRuntime struct {
+	*baseRuntime
+}
+
+/*
+voidRuntimeInst returns a new runtime component instance.
+*/
+func voidRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &voidRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Validate this node and all its child nodes.
+*/
+func (rt *voidRuntime) Validate() error {
+	return rt.baseRuntime.Validate()
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *voidRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	return rt.baseRuntime.Eval(vs, is)
+}
+
+// Not Implemented Runtime
+// =======================
+
+/*
+invalidRuntime is a special runtime for not implemented constructs.
+*/
+type invalidRuntime struct {
+	*baseRuntime
+}
+
+/*
+invalidRuntimeInst returns a new runtime component instance.
+*/
+func invalidRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &invalidRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Validate this node and all its child nodes.
+*/
+func (rt *invalidRuntime) Validate() error {
+	err := rt.baseRuntime.Validate()
+	if err == nil {
+		err = rt.erp.NewRuntimeError(util.ErrInvalidConstruct,
+			fmt.Sprintf("Unknown node: %s", rt.node.Name), rt.node)
+	}
+	return err
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *invalidRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+	if err == nil {
+		err = rt.erp.NewRuntimeError(util.ErrInvalidConstruct, fmt.Sprintf("Unknown node: %s", rt.node.Name), rt.node)
+	}
+	return nil, err
+}
+
+// General Operator Runtime
+// ========================
+
+/*
+operatorRuntime is a general operator operation. Used for embedding.
+*/
+type operatorRuntime struct {
+	*baseRuntime
+}
+
+/*
+errorDetailString produces a detail string for errors.
+*/
+func (rt *operatorRuntime) errorDetailString(token *parser.LexToken, opVal interface{}) string {
+	if token.Identifier {
+		return token.Val
+	}
+
+	return fmt.Sprintf("%v=%v", token.Val, opVal)
+}
+
+/*
+numVal returns a transformed number value.
+*/
+func (rt *operatorRuntime) numVal(op func(float64) interface{}, vs parser.Scope,
+	is map[string]interface{}) (interface{}, error) {
+
+	errorutil.AssertTrue(len(rt.node.Children) == 1,
+		fmt.Sprint("Operation requires 1 operand", rt.node))
+
+	res, err := rt.node.Children[0].Runtime.Eval(vs, is)
+	if err != nil {
+		return nil, err
+	}
+
+	// Check if the value is a number
+
+	resNum, ok := res.(float64)
+
+	if !ok {
+
+		// Produce a runtime error if the value is not a number
+
+		return nil, rt.erp.NewRuntimeError(util.ErrNotANumber,
+			rt.errorDetailString(rt.node.Children[0].Token, res), rt.node.Children[0])
+	}
+
+	return op(resNum), nil
+}
+
+/*
+boolVal returns a transformed boolean value.
+*/
+func (rt *operatorRuntime) boolVal(op func(bool) interface{},
+	vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var err error
+
+	errorutil.AssertTrue(len(rt.node.Children) == 1,
+		fmt.Sprint("Operation requires 1 operand", rt.node))
+
+	res, err := rt.node.Children[0].Runtime.Eval(vs, is)
+	if err != nil {
+		return nil, err
+	}
+
+	resBool, ok := res.(bool)
+
+	if !ok {
+		return nil, rt.erp.NewRuntimeError(util.ErrNotABoolean,
+			rt.errorDetailString(rt.node.Children[0].Token, res), rt.node.Children[0])
+	}
+
+	return op(resBool), nil
+}
+
+/*
+numOp executes an operation on two number values.
+*/
+func (rt *operatorRuntime) numOp(op func(float64, float64) interface{},
+	vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var ok bool
+	var res1, res2 interface{}
+	var err error
+
+	errorutil.AssertTrue(len(rt.node.Children) == 2,
+		fmt.Sprint("Operation requires 2 operands", rt.node))
+
+	if res1, err = rt.node.Children[0].Runtime.Eval(vs, is); err == nil {
+		if res2, err = rt.node.Children[1].Runtime.Eval(vs, is); err == nil {
+			var res1Num, res2Num float64
+
+			if res1Num, ok = res1.(float64); !ok {
+				err = rt.erp.NewRuntimeError(util.ErrNotANumber,
+					rt.errorDetailString(rt.node.Children[0].Token, res1), rt.node.Children[0])
+			} else {
+				if res2Num, ok = res2.(float64); !ok {
+					err = rt.erp.NewRuntimeError(util.ErrNotANumber,
+						rt.errorDetailString(rt.node.Children[1].Token, res2), rt.node.Children[1])
+				} else {
+
+					return op(res1Num, res2Num), nil
+				}
+			}
+		}
+	}
+
+	return nil, err
+}
+
+/*
+genOp executes an operation on two general values.
+*/
+func (rt *operatorRuntime) genOp(op func(interface{}, interface{}) interface{},
+	vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+
+	errorutil.AssertTrue(len(rt.node.Children) == 2,
+		fmt.Sprint("Operation requires 2 operands", rt.node))
+
+	res1, err := rt.node.Children[0].Runtime.Eval(vs, is)
+	if err != nil {
+		return nil, err
+	}
+
+	res2, err := rt.node.Children[1].Runtime.Eval(vs, is)
+	if err != nil {
+		return nil, err
+	}
+
+	return op(res1, res2), nil
+}
+
+/*
+strOp executes an operation on two string values.
+*/
+func (rt *operatorRuntime) strOp(op func(string, string) interface{},
+	vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+
+	errorutil.AssertTrue(len(rt.node.Children) == 2,
+		fmt.Sprint("Operation requires 2 operands", rt.node))
+
+	res1, err := rt.node.Children[0].Runtime.Eval(vs, is)
+	if err != nil {
+		return nil, err
+	}
+
+	res2, err := rt.node.Children[1].Runtime.Eval(vs, is)
+	if err != nil {
+		return nil, err
+	}
+
+	return op(fmt.Sprint(res1), fmt.Sprint(res2)), nil
+}
+
+/*
+boolOp executes an operation on two boolean values.
+*/
+func (rt *operatorRuntime) boolOp(op func(bool, bool) interface{},
+	vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+
+	errorutil.AssertTrue(len(rt.node.Children) == 2,
+		fmt.Sprint("Operation requires 2 operands", rt.node))
+
+	res1, err := rt.node.Children[0].Runtime.Eval(vs, is)
+	if err != nil {
+		return nil, err
+	}
+
+	res2, err := rt.node.Children[1].Runtime.Eval(vs, is)
+	if err != nil {
+		return nil, err
+	}
+
+	res1bool, ok := res1.(bool)
+	if !ok {
+		return nil, rt.erp.NewRuntimeError(util.ErrNotABoolean,
+			rt.errorDetailString(rt.node.Children[0].Token, res1), rt.node.Children[0])
+	}
+
+	res2bool, ok := res2.(bool)
+	if !ok {
+		return nil, rt.erp.NewRuntimeError(util.ErrNotABoolean,
+			rt.errorDetailString(rt.node.Children[1].Token, res2), rt.node.Children[0])
+	}
+
+	return op(res1bool, res2bool), nil
+}
+
+/*
+listOp executes an operation on a value and a list.
+*/
+func (rt *operatorRuntime) listOp(op func(interface{}, []interface{}) interface{},
+	vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+
+	errorutil.AssertTrue(len(rt.node.Children) == 2,
+		fmt.Sprint("Operation requires 2 operands", rt.node))
+
+	res1, err := rt.node.Children[0].Runtime.Eval(vs, is)
+	if err != nil {
+		return nil, err
+	}
+
+	res2, err := rt.node.Children[1].Runtime.Eval(vs, is)
+	if err != nil {
+		return nil, err
+	}
+
+	res2list, ok := res2.([]interface{})
+	if !ok {
+		return nil, rt.erp.NewRuntimeError(util.ErrNotAList,
+			rt.errorDetailString(rt.node.Children[1].Token, res2), rt.node.Children[0])
+	}
+
+	return op(res1, res2list), nil
+}

+ 33 - 0
interpreter/rt_general_test.go

@@ -0,0 +1,33 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"testing"
+
+	"devt.de/krotik/ecal/parser"
+)
+
+func TestGeneralErrorCases(t *testing.T) {
+
+	n, _ := parser.Parse("a", "a")
+	inv := &invalidRuntime{newBaseRuntime(NewECALRuntimeProvider("a"), n)}
+
+	if err := inv.Validate().Error(); err != "ECAL error in a: Invalid construct (Unknown node: identifier) (Line:1 Pos:1)" {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if _, err := inv.Eval(nil, nil); err.Error() != "ECAL error in a: Invalid construct (Unknown node: identifier) (Line:1 Pos:1)" {
+		t.Error("Unexpected result:", err)
+		return
+	}
+}

+ 273 - 0
interpreter/rt_identifier.go

@@ -0,0 +1,273 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"fmt"
+	"strings"
+
+	"devt.de/krotik/ecal/parser"
+	"devt.de/krotik/ecal/scope"
+	"devt.de/krotik/ecal/stdlib"
+	"devt.de/krotik/ecal/util"
+)
+
+/*
+identifierRuntime is the runtime component for identifiers.
+*/
+type identifierRuntime struct {
+	*baseRuntime
+}
+
+/*
+identifierRuntimeInst returns a new runtime component instance.
+*/
+func identifierRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &identifierRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *identifierRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	return rt.resolveValue(vs, is, rt.node)
+}
+
+/*
+resolveValue resolves the value of this identifier.
+*/
+func (rt *identifierRuntime) resolveValue(vs parser.Scope, is map[string]interface{}, node *parser.ASTNode) (interface{}, error) {
+	var anode *parser.ASTNode
+	var astring string
+	var result interface{}
+	var err error
+
+	functionResolved := func(astring string, rnode *parser.ASTNode) *parser.ASTNode {
+
+		res := &parser.ASTNode{ // Create a dummy identifier which models the value evaluation so far
+			Name: parser.NodeIDENTIFIER,
+			Token: &parser.LexToken{
+				ID:         node.Token.ID,
+				Identifier: node.Token.Identifier,
+				Lline:      node.Token.Lline,
+				Lpos:       node.Token.Lpos,
+				Pos:        node.Token.Pos,
+				Val:        strings.Replace(astring, ".", ">", -1),
+			},
+			Children: nil,
+		}
+
+		for i, c := range rnode.Children {
+			if c.Name == parser.NodeFUNCCALL {
+				res.Children = rnode.Children[i+1:]
+			}
+		}
+
+		return res
+	}
+
+	anode, astring, err = buildAccessString(rt.erp, vs, is, node, node.Token.Val)
+
+	if len(node.Children) == 0 {
+
+		// Simple case we just have a variable
+
+		result, _, err = vs.GetValue(node.Token.Val)
+
+	} else if cval, ok := stdlib.GetStdlibConst(astring); ok {
+
+		result = cval
+
+	} else {
+
+		if rerr, ok := err.(*util.RuntimeError); err == nil || ok && rerr.Type == util.ErrInvalidConstruct {
+			funcCallInAccessStringExecuted := ok && rerr.Type == util.ErrInvalidConstruct
+
+			if result, _, err = vs.GetValue(astring); err == nil {
+
+				if funcCallInAccessStringExecuted {
+
+					result, err = rt.resolveFunction(astring, vs, is, rerr.Node, result, err)
+
+					node = functionResolved(astring, anode)
+
+					if len(node.Children) > 0 {
+
+						// We have more identifiers after the func call - there is more to do ...
+
+						vs = scope.NewScope("funcresult")
+						vs.SetValue(node.Token.Val, result)
+
+						result, err = rt.resolveValue(vs, is, node)
+					}
+				} else {
+
+					result, err = rt.resolveFunction(astring, vs, is, node, result, err)
+				}
+			}
+		}
+	}
+
+	return result, err
+}
+
+/*
+resolveFunction execute function calls and return the result.
+*/
+func (rt *identifierRuntime) resolveFunction(astring string, vs parser.Scope, is map[string]interface{},
+	node *parser.ASTNode, result interface{}, err error) (interface{}, error) {
+
+	for _, funccall := range node.Children {
+
+		if funccall.Name == parser.NodeFUNCCALL {
+
+			funcObj, ok := result.(util.ECALFunction)
+
+			if !ok {
+
+				// Check for inbuild function
+
+				funcObj, ok = inbuildFuncMap[astring]
+
+				if !ok {
+
+					// Check for stdlib function
+
+					funcObj, ok = stdlib.GetStdlibFunc(astring)
+				}
+			}
+
+			if ok {
+				var args []interface{}
+
+				// Collect the parameter values
+
+				for _, c := range funccall.Children {
+					var val interface{}
+
+					if err == nil {
+						val, err = c.Runtime.Eval(vs, is)
+						args = append(args, val)
+					}
+				}
+
+				if err == nil {
+
+					// Execute the function and
+
+					result, err = funcObj.Run(rt.instanceID, vs, is, args)
+
+					if _, ok := err.(*util.RuntimeError); err != nil && !ok {
+
+						// Convert into a proper runtime error if necessary
+
+						rerr := rt.erp.NewRuntimeError(util.ErrRuntimeError,
+							err.Error(), node).(*util.RuntimeError)
+
+						if err == util.ErrIsIterator || err == util.ErrEndOfIteration || err == util.ErrContinueIteration {
+							rerr.Type = err
+						}
+
+						err = rerr
+					}
+				}
+
+			} else {
+
+				err = rt.erp.NewRuntimeError(util.ErrUnknownConstruct,
+					fmt.Sprintf("Unknown function: %v", node.Token.Val), node)
+			}
+
+			break
+		}
+	}
+
+	return result, err
+}
+
+/*
+Set sets a value to this identifier.
+*/
+func (rt *identifierRuntime) Set(vs parser.Scope, is map[string]interface{}, value interface{}) error {
+	var err error
+
+	if len(rt.node.Children) == 0 {
+
+		// Simple case we just have a variable
+
+		err = vs.SetValue(rt.node.Token.Val, value)
+
+	} else {
+		var as string
+
+		_, as, err = buildAccessString(rt.erp, vs, is, rt.node, rt.node.Token.Val)
+
+		if err == nil {
+
+			// Collect all the children and find the right spot
+
+			err = vs.SetValue(as, value)
+		}
+	}
+
+	return err
+}
+
+/*
+buildAccessString builds an access string using a given node and a prefix.
+*/
+func buildAccessString(erp *ECALRuntimeProvider, vs parser.Scope, is map[string]interface{},
+	node *parser.ASTNode, prefix string) (*parser.ASTNode, string, error) {
+
+	var err error
+	res := prefix
+
+	for i, c := range node.Children {
+
+		if err == nil {
+
+			// The unexpected construct error is used in two ways:
+			// 1. Error message when a function call is used on the left hand of
+			// an assignment.
+			// 2. Signalling there is a function call involved on the right hand
+			// of an assignment.
+
+			if c.Name == parser.NodeCOMPACCESS {
+				var val interface{}
+				val, err = c.Children[0].Runtime.Eval(vs, is)
+				res = fmt.Sprintf("%v.%v", res, val)
+
+				if len(node.Children) > i+1 && node.Children[i+1].Name == parser.NodeFUNCCALL {
+
+					err = erp.NewRuntimeError(util.ErrInvalidConstruct,
+						"Unexpected construct", node)
+					break
+				}
+
+			} else if c.Name == parser.NodeIDENTIFIER {
+
+				res = fmt.Sprintf("%v.%v", res, c.Token.Val)
+
+				if len(c.Children) > 0 && c.Children[0].Name == parser.NodeFUNCCALL {
+					node = c
+
+					err = erp.NewRuntimeError(util.ErrInvalidConstruct,
+						"Unexpected construct", node)
+					break
+				}
+
+				node, res, err = buildAccessString(erp, vs, is, c, res)
+			}
+		}
+	}
+
+	return node, res, err
+}

+ 464 - 0
interpreter/rt_statements.go

@@ -0,0 +1,464 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"fmt"
+
+	"devt.de/krotik/common/sortutil"
+	"devt.de/krotik/ecal/parser"
+	"devt.de/krotik/ecal/scope"
+	"devt.de/krotik/ecal/util"
+)
+
+// Statements Runtime
+// ==================
+
+/*
+statementsRuntime is the runtime component for sequences of statements.
+*/
+type statementsRuntime struct {
+	*baseRuntime
+}
+
+/*
+statementsRuntimeInst returns a new runtime component instance.
+*/
+func statementsRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &statementsRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *statementsRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		for _, child := range rt.node.Children {
+			if _, err := child.Runtime.Eval(vs, is); err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	return nil, err
+}
+
+// Condition statement
+// ===================
+
+/*
+ifRuntime is the runtime for the if condition statement.
+*/
+type ifRuntime struct {
+	*baseRuntime
+}
+
+/*
+ifRuntimeInst returns a new runtime component instance.
+*/
+func ifRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &ifRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *ifRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+
+		// Create a new variable scope
+
+		vs = vs.NewChild(scope.NameFromASTNode(rt.node))
+
+		for offset := 0; offset < len(rt.node.Children); offset += 2 {
+			var guardres interface{}
+
+			// Evaluate guard
+
+			if err == nil {
+				guardres, err = rt.node.Children[offset].Runtime.Eval(vs, is)
+
+				if err == nil && guardres.(bool) {
+
+					// The guard holds true so we execture its statements
+
+					return rt.node.Children[offset+1].Runtime.Eval(vs, is)
+				}
+			}
+		}
+	}
+
+	return nil, err
+}
+
+// Guard Runtime
+// =============
+
+/*
+guardRuntime is the runtime for any guard condition (used in if, for, etc...).
+*/
+type guardRuntime struct {
+	*baseRuntime
+}
+
+/*
+guardRuntimeInst returns a new runtime component instance.
+*/
+func guardRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &guardRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *guardRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		var ret interface{}
+
+		// Evaluate the condition
+
+		ret, err = rt.node.Children[0].Runtime.Eval(vs, is)
+
+		// Guard returns always a boolean
+
+		res = ret != nil && ret != false && ret != 0
+	}
+
+	return res, err
+}
+
+// Loop statement
+// ==============
+
+/*
+loopRuntime is the runtime for the loop statement (for).
+*/
+type loopRuntime struct {
+	*baseRuntime
+	leftInVarName []string
+}
+
+/*
+loopRuntimeInst returns a new runtime component instance.
+*/
+func loopRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &loopRuntime{newBaseRuntime(erp, node), nil}
+}
+
+/*
+Validate this node and all its child nodes.
+*/
+func (rt *loopRuntime) Validate() error {
+
+	err := rt.baseRuntime.Validate()
+
+	if err == nil {
+
+		if rt.node.Children[0].Name == parser.NodeIN {
+
+			inVar := rt.node.Children[0].Children[0]
+
+			if inVar.Name == parser.NodeIDENTIFIER {
+
+				if len(inVar.Children) != 0 {
+					return rt.erp.NewRuntimeError(util.ErrInvalidConstruct,
+						"Must have a simple variable on the left side of the In expression", rt.node)
+				}
+
+				rt.leftInVarName = []string{inVar.Token.Val}
+
+			} else if inVar.Name == parser.NodeLIST {
+				rt.leftInVarName = make([]string, 0, len(inVar.Children))
+
+				for _, child := range inVar.Children {
+					if child.Name != parser.NodeIDENTIFIER || len(child.Children) != 0 {
+						return rt.erp.NewRuntimeError(util.ErrInvalidConstruct,
+							"Must have a list of simple variables on the left side of the In expression", rt.node)
+					}
+
+					rt.leftInVarName = append(rt.leftInVarName, child.Token.Val)
+				}
+			}
+		}
+	}
+
+	return err
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *loopRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		var guardres interface{}
+
+		// Create a new variable scope
+
+		vs = vs.NewChild(scope.NameFromASTNode(rt.node))
+
+		// Create a new instance scope - elements in each loop iteration start from scratch
+
+		is = make(map[string]interface{})
+
+		if rt.node.Children[0].Name == parser.NodeGUARD {
+
+			// Evaluate guard
+
+			guardres, err = rt.node.Children[0].Runtime.Eval(vs, is)
+
+			for err == nil && guardres.(bool) {
+
+				// Execute block
+
+				_, err = rt.node.Children[1].Runtime.Eval(vs, is)
+
+				// Check for continue
+
+				if err != nil {
+					if eoi, ok := err.(*util.RuntimeError); ok {
+						if eoi.Type == util.ErrContinueIteration {
+							err = nil
+						}
+					}
+				}
+
+				if err == nil {
+
+					// Evaluate guard
+
+					guardres, err = rt.node.Children[0].Runtime.Eval(vs, is)
+				}
+			}
+
+		} else if rt.node.Children[0].Name == parser.NodeIN {
+			var iterator func() (interface{}, error)
+			var val interface{}
+
+			it := rt.node.Children[0].Children[1]
+
+			val, err = it.Runtime.Eval(vs, is)
+
+			// Create an iterator object
+
+			if rterr, ok := err.(*util.RuntimeError); ok && rterr.Type == util.ErrIsIterator {
+
+				// We got an iterator - all subsequent calls will return values
+
+				iterator = func() (interface{}, error) {
+					return it.Runtime.Eval(vs, is)
+				}
+				err = nil
+
+			} else {
+
+				// We got a value over which we need to iterate
+
+				if valList, isList := val.([]interface{}); isList {
+
+					index := -1
+					end := len(valList)
+
+					iterator = func() (interface{}, error) {
+						index++
+						if index >= end {
+							return nil, rt.erp.NewRuntimeError(util.ErrEndOfIteration, "", rt.node)
+						}
+						return valList[index], nil
+					}
+
+				} else if valMap, isMap := val.(map[interface{}]interface{}); isMap {
+					var keys []interface{}
+
+					index := -1
+
+					for k := range valMap {
+						keys = append(keys, k)
+					}
+					end := len(keys)
+
+					// Try to sort according to string value
+
+					sortutil.InterfaceStrings(keys)
+
+					iterator = func() (interface{}, error) {
+						index++
+						if index >= end {
+							return nil, rt.erp.NewRuntimeError(util.ErrEndOfIteration, "", rt.node)
+						}
+						key := keys[index]
+						return []interface{}{key, valMap[key]}, nil
+					}
+
+				} else {
+
+					// A single value will do exactly one iteration
+
+					index := -1
+
+					iterator = func() (interface{}, error) {
+						index++
+						if index > 0 {
+							return nil, rt.erp.NewRuntimeError(util.ErrEndOfIteration, "", rt.node)
+						}
+						return val, nil
+					}
+				}
+			}
+
+			vars := rt.leftInVarName
+
+			for err == nil {
+				var res interface{}
+
+				res, err = iterator()
+
+				if err != nil {
+					if eoi, ok := err.(*util.RuntimeError); ok {
+						if eoi.Type == util.ErrIsIterator {
+							err = nil
+						}
+					}
+				}
+
+				if err == nil {
+
+					if len(vars) == 1 {
+						if err = vs.SetValue(vars[0], res); err != nil {
+							err = rt.erp.NewRuntimeError(util.ErrVarAccess,
+								err.Error(), rt.node)
+						}
+
+					} else if resList, ok := res.([]interface{}); ok {
+
+						if len(vars) != len(resList) {
+							return nil, rt.erp.NewRuntimeError(util.ErrInvalidState,
+								fmt.Sprintf("Assigned number of variables is different to "+
+									"number of values (%v variables vs %v values)",
+									len(vars), len(resList)), rt.node)
+						}
+
+						for i, v := range vars {
+							if err == nil {
+								if err = vs.SetValue(v, resList[i]); err != nil {
+									err = rt.erp.NewRuntimeError(util.ErrVarAccess,
+										err.Error(), rt.node)
+								}
+							}
+						}
+
+					} else {
+
+						return nil, rt.erp.NewRuntimeError(util.ErrInvalidState,
+							fmt.Sprintf("Result for loop variable is not a list (value is %v)", res),
+							rt.node)
+					}
+
+					// Execute block
+
+					if err == nil {
+						_, err = rt.node.Children[1].Runtime.Eval(vs, is)
+					}
+				}
+
+				// Check for continue
+
+				if err != nil {
+					if eoi, ok := err.(*util.RuntimeError); ok {
+						if eoi.Type == util.ErrContinueIteration {
+							err = nil
+						}
+					}
+				}
+			}
+
+			// Check for end of iteration error
+
+			if eoi, ok := err.(*util.RuntimeError); ok {
+				if eoi.Type == util.ErrEndOfIteration {
+					err = nil
+				}
+			}
+		}
+	}
+
+	return nil, err
+}
+
+// Break statement
+// ===============
+
+/*
+breakRuntime is the runtime for the break statement.
+*/
+type breakRuntime struct {
+	*baseRuntime
+}
+
+/*
+breakRuntimeInst returns a new runtime component instance.
+*/
+func breakRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &breakRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *breakRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		err = rt.erp.NewRuntimeError(util.ErrEndOfIteration, "", rt.node)
+	}
+
+	return nil, err
+}
+
+// Continue statement
+// ==================
+
+/*
+continueRuntime is the runtime for the continue statement.
+*/
+type continueRuntime struct {
+	*baseRuntime
+}
+
+/*
+continueRuntimeInst returns a new runtime component instance.
+*/
+func continueRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &continueRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *continueRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	if err == nil {
+		err = rt.erp.NewRuntimeError(util.ErrContinueIteration, "", rt.node)
+	}
+
+	return nil, err
+}

+ 860 - 0
interpreter/rt_statements_test.go

@@ -0,0 +1,860 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"testing"
+
+	"devt.de/krotik/ecal/scope"
+)
+
+func TestGuardStatements(t *testing.T) {
+
+	// Test normal if
+
+	vs := scope.NewScope(scope.GlobalScope)
+
+	_, err := UnitTestEvalAndAST(
+		`
+a := 1
+if a == 1 {
+	b := 1
+    a := a + 1	
+}
+`, vs,
+		`
+statements
+  :=
+    identifier: a
+    number: 1
+  if
+    guard
+      ==
+        identifier: a
+        number: 1
+    statements
+      :=
+        identifier: b
+        number: 1
+      :=
+        identifier: a
+        plus
+          identifier: a
+          number: 1
+`[1:])
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if vs.String() != `
+GlobalScope {
+    a (float64) : 2
+    block: if (Line:3 Pos:1) {
+        b (float64) : 1
+    }
+}`[1:] {
+		t.Error("Unexpected result: ", vs)
+		return
+	}
+
+	// Test elif
+
+	vs = scope.NewScope(scope.GlobalScope)
+
+	_, err = UnitTestEvalAndAST(
+		`
+	   a := 2
+	   if a == 1 {
+	       a := a + 1
+	   } elif a == 2 {
+	   	a := a + 2
+	   }
+	   `, vs, `
+statements
+  :=
+    identifier: a
+    number: 2
+  if
+    guard
+      ==
+        identifier: a
+        number: 1
+    statements
+      :=
+        identifier: a
+        plus
+          identifier: a
+          number: 1
+    guard
+      ==
+        identifier: a
+        number: 2
+    statements
+      :=
+        identifier: a
+        plus
+          identifier: a
+          number: 2
+`[1:])
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if vs.String() != `
+GlobalScope {
+    a (float64) : 4
+    block: if (Line:3 Pos:5) {
+    }
+}`[1:] {
+		t.Error("Unexpected result: ", vs)
+		return
+	}
+
+	// Test else
+
+	vs = scope.NewScope(scope.GlobalScope)
+
+	_, err = UnitTestEvalAndAST(
+		`
+	   a := 3
+	   if a == 1 {
+	       a := a + 1
+	   } elif a == 2 {
+	   	a := a + 2
+	   } else {
+	       a := 99
+	   }
+	   `, vs, `
+statements
+  :=
+    identifier: a
+    number: 3
+  if
+    guard
+      ==
+        identifier: a
+        number: 1
+    statements
+      :=
+        identifier: a
+        plus
+          identifier: a
+          number: 1
+    guard
+      ==
+        identifier: a
+        number: 2
+    statements
+      :=
+        identifier: a
+        plus
+          identifier: a
+          number: 2
+    guard
+      true
+    statements
+      :=
+        identifier: a
+        number: 99
+`[1:])
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if vs.String() != `
+GlobalScope {
+    a (float64) : 99
+    block: if (Line:3 Pos:5) {
+    }
+}`[1:] {
+		t.Error("Unexpected result: ", vs)
+		return
+	}
+}
+
+func TestLoopStatements(t *testing.T) {
+
+	vs := scope.NewScope(scope.GlobalScope)
+	buf := addLogFunction(vs)
+
+	_, err := UnitTestEvalAndAST(
+		`
+a := 10
+
+for a > 0 {
+
+	log("Info: ", "-> ", a)
+	a := a - 1
+}
+`, vs,
+		`
+statements
+  :=
+    identifier: a
+    number: 10
+  loop
+    guard
+      >
+        identifier: a
+        number: 0
+    statements
+      identifier: log
+        funccall
+          string: 'Info: '
+          string: '-> '
+          identifier: a
+      :=
+        identifier: a
+        minus
+          identifier: a
+          number: 1
+`[1:])
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if vs.String() != `
+GlobalScope {
+    a (float64) : 0
+    log (*interpreter.TestLogger) : TestLogger
+    block: loop (Line:4 Pos:1) {
+    }
+}`[1:] {
+		t.Error("Unexpected result: ", vs)
+		return
+	}
+
+	if res := buf.String(); res != `
+Info: -> 10
+Info: -> 9
+Info: -> 8
+Info: -> 7
+Info: -> 6
+Info: -> 5
+Info: -> 4
+Info: -> 3
+Info: -> 2
+Info: -> 1`[1:] {
+		t.Error("Unexpected result: ", res)
+		return
+	}
+
+	vs = scope.NewScope(scope.GlobalScope)
+	buf = addLogFunction(vs)
+
+	_, err = UnitTestEvalAndAST(
+		`
+	   for a in range(2, 10, 1) {
+           log("Info", "->", a)
+	   }
+	   `, vs,
+		`
+loop
+  in
+    identifier: a
+    identifier: range
+      funccall
+        number: 2
+        number: 10
+        number: 1
+  statements
+    identifier: log
+      funccall
+        string: 'Info'
+        string: '->'
+        identifier: a
+`[1:])
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if vs.String() != `
+GlobalScope {
+    log (*interpreter.TestLogger) : TestLogger
+    block: loop (Line:2 Pos:5) {
+        a (float64) : 10
+    }
+}`[1:] {
+		t.Error("Unexpected result: ", vs)
+		return
+	}
+
+	if res := buf.String(); res != `
+Info->2
+Info->3
+Info->4
+Info->5
+Info->6
+Info->7
+Info->8
+Info->9
+Info->10`[1:] {
+		t.Error("Unexpected result: ", res)
+		return
+	}
+
+	vs = scope.NewScope(scope.GlobalScope)
+	buf = addLogFunction(vs)
+
+	_, err = UnitTestEvalAndAST(
+		`
+for a in range(10, 3, -3) {
+  log("Info", "->", a)
+}
+	   `, vs,
+		`
+loop
+  in
+    identifier: a
+    identifier: range
+      funccall
+        number: 10
+        number: 3
+        minus
+          number: 3
+  statements
+    identifier: log
+      funccall
+        string: 'Info'
+        string: '->'
+        identifier: a
+`[1:])
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if vs.String() != `
+GlobalScope {
+    log (*interpreter.TestLogger) : TestLogger
+    block: loop (Line:2 Pos:1) {
+        a (float64) : 4
+    }
+}`[1:] {
+		t.Error("Unexpected result: ", vs)
+		return
+	}
+
+	if res := buf.String(); res != `
+Info->10
+Info->7
+Info->4`[1:] {
+		t.Error("Unexpected result: ", res)
+		return
+	}
+
+	// Test nested loops
+
+	vs = scope.NewScope(scope.GlobalScope)
+	buf = addLogFunction(vs)
+
+	_, err = UnitTestEvalAndAST(
+		`
+for a in range(10, 3, -3) {
+  for b in range(1, 3, 1) {
+    log("Info", "->", a, b)
+  }
+}
+	   `, vs,
+		`
+loop
+  in
+    identifier: a
+    identifier: range
+      funccall
+        number: 10
+        number: 3
+        minus
+          number: 3
+  statements
+    loop
+      in
+        identifier: b
+        identifier: range
+          funccall
+            number: 1
+            number: 3
+            number: 1
+      statements
+        identifier: log
+          funccall
+            string: 'Info'
+            string: '->'
+            identifier: a
+            identifier: b
+`[1:])
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if vs.String() != `
+GlobalScope {
+    log (*interpreter.TestLogger) : TestLogger
+    block: loop (Line:2 Pos:1) {
+        a (float64) : 4
+        block: loop (Line:3 Pos:3) {
+            b (float64) : 3
+        }
+    }
+}`[1:] {
+		t.Error("Unexpected result: ", vs)
+		return
+	}
+
+	if res := buf.String(); res != `
+Info->10 1
+Info->10 2
+Info->10 3
+Info->7 1
+Info->7 2
+Info->7 3
+Info->4 1
+Info->4 2
+Info->4 3`[1:] {
+		t.Error("Unexpected result: ", res)
+		return
+	}
+
+	// Break statement
+
+	vs = scope.NewScope(scope.GlobalScope)
+	buf = addLogFunction(vs)
+
+	_, err = UnitTestEvalAndAST(
+		`
+for a in range(1, 10, 1) {
+  log("Info", "->", a)
+  if a == 3 {
+    break
+  }
+}`, vs,
+		`
+loop
+  in
+    identifier: a
+    identifier: range
+      funccall
+        number: 1
+        number: 10
+        number: 1
+  statements
+    identifier: log
+      funccall
+        string: 'Info'
+        string: '->'
+        identifier: a
+    if
+      guard
+        ==
+          identifier: a
+          number: 3
+      statements
+        break
+`[1:])
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if vs.String() != `
+GlobalScope {
+    log (*interpreter.TestLogger) : TestLogger
+    block: loop (Line:2 Pos:1) {
+        a (float64) : 3
+        block: if (Line:4 Pos:3) {
+        }
+    }
+}`[1:] {
+		t.Error("Unexpected result: ", vs)
+		return
+	}
+
+	if res := buf.String(); res != `
+Info->1
+Info->2
+Info->3`[1:] {
+		t.Error("Unexpected result: ", res)
+		return
+	}
+
+	// Continue statement
+
+	vs = scope.NewScope(scope.GlobalScope)
+	buf = addLogFunction(vs)
+
+	_, err = UnitTestEvalAndAST(
+		`
+for a in range(1, 10, 1) {
+  if a > 3 and a < 6  {
+    continue
+  }
+  log("Info", "->", a)
+}`, vs,
+		`
+loop
+  in
+    identifier: a
+    identifier: range
+      funccall
+        number: 1
+        number: 10
+        number: 1
+  statements
+    if
+      guard
+        and
+          >
+            identifier: a
+            number: 3
+          <
+            identifier: a
+            number: 6
+      statements
+        continue
+    identifier: log
+      funccall
+        string: 'Info'
+        string: '->'
+        identifier: a
+`[1:])
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if vs.String() != `
+GlobalScope {
+    log (*interpreter.TestLogger) : TestLogger
+    block: loop (Line:2 Pos:1) {
+        a (float64) : 10
+        block: if (Line:3 Pos:3) {
+        }
+    }
+}`[1:] {
+		t.Error("Unexpected result: ", vs)
+		return
+	}
+
+	if res := buf.String(); res != `
+Info->1
+Info->2
+Info->3
+Info->6
+Info->7
+Info->8
+Info->9
+Info->10`[1:] {
+		t.Error("Unexpected result: ", res)
+		return
+	}
+
+	// Loop over lists
+
+	vs = scope.NewScope(scope.GlobalScope)
+	buf = addLogFunction(vs)
+
+	_, err = UnitTestEvalAndAST(
+		`
+for a in [1,2] {
+  for b in [1,2,3,"Hans", 4] {
+    log("Info", "->", a, "-", b)
+  }
+}
+	   `, vs,
+		`
+loop
+  in
+    identifier: a
+    list
+      number: 1
+      number: 2
+  statements
+    loop
+      in
+        identifier: b
+        list
+          number: 1
+          number: 2
+          number: 3
+          string: 'Hans'
+          number: 4
+      statements
+        identifier: log
+          funccall
+            string: 'Info'
+            string: '->'
+            identifier: a
+            string: '-'
+            identifier: b
+`[1:])
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if vs.String() != `
+GlobalScope {
+    log (*interpreter.TestLogger) : TestLogger
+    block: loop (Line:2 Pos:1) {
+        a (float64) : 2
+        block: loop (Line:3 Pos:3) {
+            b (float64) : 4
+        }
+    }
+}`[1:] {
+		t.Error("Unexpected result: ", vs)
+		return
+	}
+
+	if res := buf.String(); res != `
+Info->1-1
+Info->1-2
+Info->1-3
+Info->1-Hans
+Info->1-4
+Info->2-1
+Info->2-2
+Info->2-3
+Info->2-Hans
+Info->2-4`[1:] {
+		t.Error("Unexpected result: ", res)
+		return
+	}
+
+	vs = scope.NewScope(scope.GlobalScope)
+	buf = addLogFunction(vs)
+
+	_, err = UnitTestEvalAndAST(
+		`
+l := [1,2,3,4]
+for a in range(0, 3, 1) {
+  log("Info", "-a>", a, "-", l[a])
+}
+for a in range(0, 3, 1) {
+  log("Info", "-b>", a, "-", l[-a])
+}
+log("Info", "xxx>", l[-1])
+	   `, vs,
+		`
+statements
+  :=
+    identifier: l
+    list
+      number: 1
+      number: 2
+      number: 3
+      number: 4
+  loop
+    in
+      identifier: a
+      identifier: range
+        funccall
+          number: 0
+          number: 3
+          number: 1
+    statements
+      identifier: log
+        funccall
+          string: 'Info'
+          string: '-a>'
+          identifier: a
+          string: '-'
+          identifier: l
+            compaccess
+              identifier: a
+  loop
+    in
+      identifier: a
+      identifier: range
+        funccall
+          number: 0
+          number: 3
+          number: 1
+    statements
+      identifier: log
+        funccall
+          string: 'Info'
+          string: '-b>'
+          identifier: a
+          string: '-'
+          identifier: l
+            compaccess
+              minus
+                identifier: a
+  identifier: log
+    funccall
+      string: 'Info'
+      string: 'xxx>'
+      identifier: l
+        compaccess
+          minus
+            number: 1
+`[1:])
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if vs.String() != `
+GlobalScope {
+    l ([]interface {}) : [1,2,3,4]
+    log (*interpreter.TestLogger) : TestLogger
+    block: loop (Line:3 Pos:1) {
+        a (float64) : 3
+    }
+    block: loop (Line:6 Pos:1) {
+        a (float64) : 3
+    }
+}`[1:] {
+		t.Error("Unexpected result: ", vs)
+		return
+	}
+
+	if res := buf.String(); res != `
+Info-a>0-1
+Info-a>1-2
+Info-a>2-3
+Info-a>3-4
+Info-b>0-1
+Info-b>1-4
+Info-b>2-3
+Info-b>3-2
+Infoxxx>4`[1:] {
+		t.Error("Unexpected result: ", res)
+		return
+	}
+
+	// Loop over a map
+
+	vs = scope.NewScope(scope.GlobalScope)
+	buf = addLogFunction(vs)
+
+	_, err = UnitTestEvalAndAST(
+		`
+x := { "c": 0, "a":2, "b":4}
+for [a, b] in x {
+  log("Info", "->", a, "-", b)
+}
+	   `, vs,
+		`
+statements
+  :=
+    identifier: x
+    map
+      kvp
+        string: 'c'
+        number: 0
+      kvp
+        string: 'a'
+        number: 2
+      kvp
+        string: 'b'
+        number: 4
+  loop
+    in
+      list
+        identifier: a
+        identifier: b
+      identifier: x
+    statements
+      identifier: log
+        funccall
+          string: 'Info'
+          string: '->'
+          identifier: a
+          string: '-'
+          identifier: b
+`[1:])
+
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if res := buf.String(); res != `
+Info->a-2
+Info->b-4
+Info->c-0`[1:] {
+		t.Error("Unexpected result: ", res)
+		return
+	}
+
+	// Test continue
+
+	_, err = UnitTestEval(`
+for [a] in [1,2,3] {
+  continue
+  [a, b] := "Hans"
+}
+	   `[1:], vs)
+
+	if err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	_, err = UnitTestEval(`
+a := 1
+for a < 10 {
+  a := a + 1
+  continue
+  [a,b] := "Hans"
+}
+	   `[1:], vs)
+
+	if err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	// Test single value
+
+	_, err = UnitTestEval(`
+for a in 1 {
+  continue
+  [a,b] := "Hans"
+}
+	   `[1:], vs)
+
+	if err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	_, err = UnitTestEval(`
+for a[t] in 1 {
+  continue
+  [a,b] := "Hans"
+}
+	   `[1:], vs)
+
+	if err == nil || err.Error() != "ECAL error in ECALTestRuntime: Invalid construct (Must have a simple variable on the left side of the In expression) (Line:1 Pos:1)" {
+		t.Error("Unexpected result:", err)
+		return
+	}
+}

+ 159 - 0
interpreter/rt_value.go

@@ -0,0 +1,159 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"strconv"
+
+	"devt.de/krotik/ecal/parser"
+)
+
+/*
+numberValueRuntime is the runtime component for constant numeric values.
+*/
+type numberValueRuntime struct {
+	*baseRuntime
+	numValue float64 // Numeric value
+}
+
+/*
+numberValueRuntimeInst returns a new runtime component instance.
+*/
+func numberValueRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &numberValueRuntime{newBaseRuntime(erp, node), 0}
+}
+
+/*
+Validate this node and all its child nodes.
+*/
+func (rt *numberValueRuntime) Validate() error {
+	err := rt.baseRuntime.Validate()
+
+	if err == nil {
+		rt.numValue, err = strconv.ParseFloat(rt.node.Token.Val, 64)
+	}
+
+	return err
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *numberValueRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	return rt.numValue, err
+}
+
+/*
+stringValueRuntime is the runtime component for constant string values.
+*/
+type stringValueRuntime struct {
+	*baseRuntime
+}
+
+/*
+stringValueRuntimeInst returns a new runtime component instance.
+*/
+func stringValueRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &stringValueRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *stringValueRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	// Do some string interpolation
+
+	return rt.node.Token.Val, err
+}
+
+/*
+mapValueRuntime is the runtime component for map values.
+*/
+type mapValueRuntime struct {
+	*baseRuntime
+}
+
+/*
+mapValueRuntimeInst returns a new runtime component instance.
+*/
+func mapValueRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &mapValueRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *mapValueRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	m := make(map[interface{}]interface{})
+
+	if err == nil {
+		for _, kvp := range rt.node.Children {
+
+			key, err := kvp.Children[0].Runtime.Eval(vs, is)
+			if err != nil {
+				return nil, err
+			}
+
+			val, err := kvp.Children[1].Runtime.Eval(vs, is)
+			if err != nil {
+				return nil, err
+			}
+
+			m[key] = val
+		}
+	}
+
+	return m, err
+}
+
+/*
+listValueRuntime is the runtime component for list values.
+*/
+type listValueRuntime struct {
+	*baseRuntime
+}
+
+/*
+listValueRuntimeInst returns a new runtime component instance.
+*/
+func listValueRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
+	return &listValueRuntime{newBaseRuntime(erp, node)}
+}
+
+/*
+Eval evaluate this runtime component.
+*/
+func (rt *listValueRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	var l []interface{}
+
+	if err == nil {
+
+		for _, item := range rt.node.Children {
+
+			val, err := item.Runtime.Eval(vs, is)
+			if err != nil {
+				return nil, err
+			}
+
+			l = append(l, val)
+		}
+	}
+
+	return l, nil
+}

+ 120 - 0
interpreter/rt_value_test.go

@@ -0,0 +1,120 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package interpreter
+
+import (
+	"testing"
+
+	"devt.de/krotik/ecal/scope"
+)
+
+func TestSimpleValues(t *testing.T) {
+
+	res, err := UnitTestEvalAndAST(
+		`4`, nil,
+		`
+number: 4
+`[1:])
+
+	if err != nil || res != 4. {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`"test"`, nil,
+		`
+string: 'test'
+`[1:])
+
+	if err != nil || res != "test" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+}
+
+func TestCompositionValues(t *testing.T) {
+
+	res, err := UnitTestEvalAndAST(
+		`{"a":1, "b": 2, "c" : 3}`, nil,
+		`
+map
+  kvp
+    string: 'a'
+    number: 1
+  kvp
+    string: 'b'
+    number: 2
+  kvp
+    string: 'c'
+    number: 3
+`[1:])
+
+	if resStr := scope.EvalToString(res); err != nil || resStr != `{"a":1,"b":2,"c":3}` {
+		t.Error("Unexpected result: ", resStr, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`{"a":1, "b": {"a":1, "b": 2, "c" : 3}, "c" : 3}`, nil,
+		`
+map
+  kvp
+    string: 'a'
+    number: 1
+  kvp
+    string: 'b'
+    map
+      kvp
+        string: 'a'
+        number: 1
+      kvp
+        string: 'b'
+        number: 2
+      kvp
+        string: 'c'
+        number: 3
+  kvp
+    string: 'c'
+    number: 3
+`[1:])
+
+	if resStr := scope.EvalToString(res); err != nil || resStr != `{"a":1,"b":{"a":1,"b":2,"c":3},"c":3}` {
+		t.Error("Unexpected result: ", resStr, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`{"a":1, "b": [1, [2, 3], 4], "c" : 3}`, nil,
+		`
+map
+  kvp
+    string: 'a'
+    number: 1
+  kvp
+    string: 'b'
+    list
+      number: 1
+      list
+        number: 2
+        number: 3
+      number: 4
+  kvp
+    string: 'c'
+    number: 3
+`[1:])
+
+	if resStr := scope.EvalToString(res); err != nil || resStr != `{"a":1,"b":[1,[2,3],4],"c":3}` {
+		t.Error("Unexpected result: ", resStr, err)
+		return
+	}
+
+}

+ 64 - 0
scope/helper.go

@@ -0,0 +1,64 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+/*
+Package scope contains the block scope implementation for the event condition language ECAL.
+*/
+package scope
+
+import (
+	"fmt"
+
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/ecal/parser"
+)
+
+/*
+Default scope names
+*/
+const (
+	GlobalScope = "GlobalScope"
+)
+
+/*
+NameFromASTNode returns a scope name from a given ASTNode.
+*/
+func NameFromASTNode(node *parser.ASTNode) string {
+	return fmt.Sprintf("block: %v (Line:%d Pos:%d)", node.Name, node.Token.Lline, node.Token.Lpos)
+}
+
+/*
+EvalToString should be used if a value should be converted into a string.
+*/
+func EvalToString(v interface{}) string {
+	return stringutil.ConvertToString(v)
+}
+
+/*
+ToObject converts a Scope into an object.
+*/
+func ToObject(vs parser.Scope) map[string]interface{} {
+	res := make(map[string]interface{})
+	for k, v := range vs.(*varsScope).storage {
+		res[k] = v
+	}
+	return res
+}
+
+/*
+ToScope converts a given object into a Scope.
+*/
+func ToScope(name string, o map[string]interface{}) parser.Scope {
+	vs := NewScope(name)
+	for k, v := range o {
+		vs.SetValue(k, v)
+	}
+	return vs
+}

+ 30 - 0
scope/helper_test.go

@@ -0,0 +1,30 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package scope
+
+import (
+	"testing"
+)
+
+func TestScopeConversion(t *testing.T) {
+	vs := NewScope("foo")
+
+	vs.SetValue("a", 1)
+	vs.SetValue("b", 2)
+	vs.SetValue("c", 3)
+
+	vs2 := ToScope("foo", ToObject(vs))
+
+	if vs.String() != vs2.String() {
+		t.Error("Unexpected result:", vs.String(), vs2.String())
+		return
+	}
+}

+ 399 - 0
scope/varsscope.go

@@ -0,0 +1,399 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package scope
+
+import (
+	"bytes"
+	"fmt"
+	"sort"
+	"strconv"
+	"strings"
+	"sync"
+
+	"devt.de/krotik/ecal/parser"
+)
+
+/*
+varsScope models a scope for variables in ECAL.
+*/
+type varsScope struct {
+	name     string                 // Name of the scope
+	parent   parser.Scope           // Parent scope
+	children []*varsScope           // Children of this scope (only if tracking is enabled)
+	storage  map[string]interface{} // Storage for variables
+	lock     sync.RWMutex           // Lock for this scope
+}
+
+/*
+NewScope creates a new variable scope.
+*/
+func NewScope(name string) parser.Scope {
+	return NewScopeWithParent(name, nil)
+}
+
+/*
+NewScopeWithParent creates a new variable scope with a parent. This can be
+used to create scope structures without children links.
+*/
+func NewScopeWithParent(name string, parent parser.Scope) parser.Scope {
+	return &varsScope{name, parent, nil, make(map[string]interface{}), sync.RWMutex{}}
+}
+
+/*
+NewChild creates a new child scope for variables. The new child scope is tracked
+by the parent scope. This means it should not be used for global scopes with
+many children.
+*/
+func (s *varsScope) NewChild(name string) parser.Scope {
+	for _, c := range s.children {
+		if c.name == name {
+			return c
+		}
+	}
+
+	child := NewScope(name).(*varsScope)
+	child.parent = s
+	s.children = append(s.children, child)
+
+	return child
+}
+
+/*
+Parent returns the parent scope or nil.
+*/
+func (s *varsScope) Parent() parser.Scope {
+	return s.parent
+}
+
+/*
+SetValue sets a new value for a variable.
+*/
+func (s *varsScope) SetValue(varName string, varValue interface{}) error {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	return s.setValue(varName, varValue)
+}
+
+/*
+setValue sets a new value for a variable.
+*/
+func (s *varsScope) setValue(varName string, varValue interface{}) error {
+	var err error
+
+	// Check for dotted names which access a container structure
+
+	if cFields := strings.Split(varName, "."); len(cFields) > 1 {
+
+		// Get the container
+
+		if container, ok, _ := s.getValue(cFields[0]); ok {
+
+			if len(cFields) > 2 {
+
+				var containerAccess func(fields []string)
+
+				containerAccess = func(fields []string) {
+
+					// Get inner container
+
+					if mapContainer, ok := container.(map[interface{}]interface{}); ok {
+
+						if container, ok = mapContainer[fields[0]]; !ok {
+							err = fmt.Errorf("Container field %v does not exist",
+								strings.Join(cFields[:len(cFields)-len(fields)+1], "."))
+						}
+
+					} else if listContainer, ok := container.([]interface{}); ok {
+						var index int
+
+						if index, err = strconv.Atoi(fmt.Sprint(fields[0])); err == nil {
+
+							if index < 0 {
+
+								// Handle negative numbers
+
+								index = len(listContainer) + index
+							}
+
+							if index < len(listContainer) {
+								container = listContainer[index]
+							} else {
+								err = fmt.Errorf("Out of bounds access to list %v with index: %v",
+									strings.Join(cFields[:len(cFields)-len(fields)], "."), index)
+							}
+
+						} else {
+							container = nil
+							err = fmt.Errorf("List %v needs a number index not: %v",
+								strings.Join(cFields[:len(cFields)-len(fields)], "."), fields[0])
+						}
+
+					} else {
+						container = nil
+						err = fmt.Errorf("Variable %v is not a container",
+							strings.Join(cFields[:len(cFields)-len(fields)], "."))
+					}
+
+					if err == nil && len(fields) > 2 {
+						containerAccess(fields[1:])
+					}
+				}
+
+				containerAccess(cFields[1:])
+			}
+
+			if err == nil && container != nil {
+
+				fieldIndex := cFields[len(cFields)-1]
+
+				if mapContainer, ok := container.(map[interface{}]interface{}); ok {
+
+					mapContainer[fieldIndex] = varValue
+
+				} else if listContainer, ok := container.([]interface{}); ok {
+					var index int
+
+					if index, err = strconv.Atoi(fieldIndex); err == nil {
+
+						if index < 0 {
+
+							// Handle negative numbers
+
+							index = len(listContainer) + index
+						}
+
+						if index < len(listContainer) {
+							listContainer[index] = varValue
+						} else {
+							err = fmt.Errorf("Out of bounds access to list %v with index: %v",
+								strings.Join(cFields[:len(cFields)-1], "."), index)
+						}
+					} else {
+						err = fmt.Errorf("List %v needs a number index not: %v",
+							strings.Join(cFields[:len(cFields)-1], "."), fieldIndex)
+					}
+				} else {
+					err = fmt.Errorf("Variable %v is not a container",
+						strings.Join(cFields[:len(cFields)-1], "."))
+				}
+			}
+
+		} else {
+			err = fmt.Errorf("Variable %v is not a container", cFields[0])
+		}
+
+		return err
+	}
+
+	// Check if the variable is already defined in a parent scope
+
+	if vs := s.getScopeForVariable(varName); vs != nil {
+		s = vs
+	}
+
+	// Set value newly in scope
+
+	s.storage[varName] = varValue
+
+	return err
+}
+
+/*
+getScopeForVariable returns the scope (this or a parent scope) which holds a
+given variable.
+*/
+func (s *varsScope) getScopeForVariable(varName string) *varsScope {
+
+	_, ok := s.storage[varName]
+
+	if ok {
+		return s
+	} else if s.parent != nil {
+		return s.parent.(*varsScope).getScopeForVariable(varName)
+	}
+
+	return nil
+}
+
+/*
+GetValue gets the current value of a variable.
+*/
+func (s *varsScope) GetValue(varName string) (interface{}, bool, error) {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	return s.getValue(varName)
+}
+
+/*
+getValue gets the current value of a variable.
+*/
+func (s *varsScope) getValue(varName string) (interface{}, bool, error) {
+
+	// Check for dotted names which access a container structure
+
+	if cFields := strings.Split(varName, "."); len(cFields) > 1 {
+		var err error
+		var containerAccess func(fields []string, container interface{}) (interface{}, bool, error)
+
+		// Get the container
+
+		container, ok, _ := s.getValue(cFields[0])
+
+		if !ok {
+			return nil, ok, err
+		}
+
+		// Now look into the container and get the value
+
+		containerAccess = func(fields []string, container interface{}) (interface{}, bool, error) {
+			var retContainer interface{}
+
+			if mapContainer, ok := container.(map[interface{}]interface{}); ok {
+				var ok bool
+
+				if index, err := strconv.Atoi(fmt.Sprint(fields[0])); err == nil {
+
+					// Numbers are usually converted to float64
+
+					retContainer, ok = mapContainer[float64(index)]
+				}
+
+				if !ok {
+					retContainer = mapContainer[fields[0]]
+				}
+
+			} else if listContainer, ok := container.([]interface{}); ok {
+				var index int
+
+				if index, err = strconv.Atoi(fmt.Sprint(fields[0])); err == nil {
+
+					if index < 0 {
+
+						// Handle negative numbers
+
+						index = len(listContainer) + index
+					}
+
+					if index < len(listContainer) {
+						retContainer = listContainer[index]
+					} else {
+						err = fmt.Errorf("Out of bounds access to list %v with index: %v",
+							strings.Join(cFields[:len(cFields)-len(fields)], "."), index)
+					}
+
+				} else {
+					err = fmt.Errorf("List %v needs a number index not: %v",
+						strings.Join(cFields[:len(cFields)-len(fields)], "."), fields[0])
+				}
+
+			} else {
+				err = fmt.Errorf("Variable %v is not a container",
+					strings.Join(cFields[:len(cFields)-len(fields)], "."))
+			}
+
+			if err == nil && len(fields) > 1 {
+				return containerAccess(fields[1:], retContainer)
+			}
+
+			return retContainer, retContainer != nil, err
+		}
+
+		return containerAccess(cFields[1:], container)
+	}
+
+	if vs := s.getScopeForVariable(varName); vs != nil {
+
+		ret := vs.storage[varName]
+
+		return ret, true, nil
+	}
+
+	return nil, false, nil
+}
+
+/*
+String returns a string representation of this varsScope and all its
+parents.
+*/
+func (s *varsScope) String() string {
+	s.lock.RLock()
+	defer s.lock.RUnlock()
+
+	return s.scopeStringParents(s.scopeStringChildren())
+}
+
+/*
+scopeStringChildren returns a string representation of all children scopes.
+*/
+func (s *varsScope) scopeStringChildren() string {
+	var buf bytes.Buffer
+
+	// Write the known child scopes
+
+	for i, c := range s.children {
+		buf.WriteString(c.scopeString(c.scopeStringChildren()))
+		if i < len(s.children)-1 {
+			buf.WriteString("\n")
+		}
+	}
+
+	return buf.String()
+}
+
+/*
+scopeStringParents returns a string representation of this varsScope
+with initial children and all its parents.
+*/
+func (s *varsScope) scopeStringParents(childrenString string) string {
+	ss := s.scopeString(childrenString)
+
+	if s.parent != nil {
+		return s.parent.(*varsScope).scopeStringParents(ss)
+	}
+
+	return fmt.Sprint(ss)
+}
+
+/*
+scopeString returns a string representation of this varsScope.
+*/
+func (s *varsScope) scopeString(childrenString string) string {
+	buf := bytes.Buffer{}
+	varList := []string{}
+
+	buf.WriteString(fmt.Sprintf("%v {\n", s.name))
+
+	for k := range s.storage {
+		varList = append(varList, k)
+	}
+
+	sort.Strings(varList)
+
+	for _, v := range varList {
+		buf.WriteString(fmt.Sprintf("    %s (%T) : %v\n", v, s.storage[v],
+			EvalToString(s.storage[v])))
+	}
+
+	if childrenString != "" {
+
+		// Indent all
+
+		buf.WriteString("    ")
+		buf.WriteString(strings.Replace(childrenString, "\n", "\n    ", -1))
+		buf.WriteString("\n")
+	}
+
+	buf.WriteString("}")
+
+	return buf.String()
+}

+ 344 - 0
scope/varsscope_test.go

@@ -0,0 +1,344 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package scope
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestVarScopeSetMap(t *testing.T) {
+
+	parentVS := NewScope("global")
+	childVs := NewScopeWithParent("c1", parentVS)
+
+	// Test map
+
+	parentVS.SetValue("xx", map[interface{}]interface{}{
+		"foo": map[interface{}]interface{}{},
+	})
+
+	childVs.SetValue("xx.foo.bar", map[interface{}]interface{}{})
+
+	childVs.SetValue("xx.foo.bar.99", "tester")
+
+	if childVs.Parent().String() != `
+global {
+    xx (map[interface {}]interface {}) : {"foo":{"bar":{"99":"tester"}}}
+}`[1:] {
+		t.Error("Unexpected result:", parentVS.String())
+		return
+	}
+
+	childVs.SetValue("xx.foo.bar.99", []interface{}{1, 2})
+
+	if parentVS.String() != `
+global {
+    xx (map[interface {}]interface {}) : {"foo":{"bar":{"99":[1,2]}}}
+}`[1:] {
+		t.Error("Unexpected result:", parentVS.String())
+		return
+	}
+
+	childVs.SetValue("xx.foo.bar", map[interface{}]interface{}{
+		float64(22): "foo",
+		"33":        "bar",
+	})
+
+	if res, _, _ := childVs.GetValue("xx.foo.bar.33"); res != "bar" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res, _, _ := childVs.GetValue("xx.foo.bar.22"); res != "foo" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	// Test errors
+
+	err := parentVS.SetValue("xx.foo.a.b", 5)
+
+	if err == nil || err.Error() != "Container field xx.foo.a does not exist" {
+		t.Error("Unexpected result:", parentVS.String(), err)
+		return
+	}
+
+	// Test list (test also overwriting of variables)
+
+	parentVS.SetValue("xx", []interface{}{1, 2, []interface{}{3, 4}})
+
+	if parentVS.String() != `
+global {
+    xx ([]interface {}) : [1,2,[3,4]]
+}`[1:] {
+		t.Error("Unexpected result:", parentVS.String())
+		return
+	}
+
+	parentVS.SetValue("xx.2", []interface{}{3, 4, 5})
+
+	if parentVS.String() != `
+global {
+    xx ([]interface {}) : [1,2,[3,4,5]]
+}`[1:] {
+		t.Error("Unexpected result:", parentVS.String())
+		return
+	}
+
+	parentVS.SetValue("xx.-1", []interface{}{3, 4, 6})
+
+	if parentVS.String() != `
+global {
+    xx ([]interface {}) : [1,2,[3,4,6]]
+}`[1:] {
+		t.Error("Unexpected result:", parentVS.String())
+		return
+	}
+
+	parentVS.SetValue("xx.-1.-1", 7)
+
+	if parentVS.String() != `
+global {
+    xx ([]interface {}) : [1,2,[3,4,7]]
+}`[1:] {
+		t.Error("Unexpected result:", parentVS.String())
+		return
+	}
+
+	err = parentVS.SetValue("xx.a", []interface{}{3, 4, 5})
+
+	if err == nil || err.Error() != "List xx needs a number index not: a" {
+		t.Error("Unexpected result:", parentVS.String(), err)
+		return
+	}
+
+	err = parentVS.SetValue("xx.2.b", []interface{}{3, 4, 5})
+
+	if err == nil || err.Error() != "List xx.2 needs a number index not: b" {
+		t.Error("Unexpected result:", parentVS.String(), err)
+		return
+	}
+
+	err = parentVS.SetValue("xx.2.b.1", []interface{}{3, 4, 5})
+
+	if err == nil || err.Error() != "List xx.2 needs a number index not: b" {
+		t.Error("Unexpected result:", parentVS.String(), err)
+		return
+	}
+
+	err = parentVS.SetValue("xx.2.1.b.1", []interface{}{3, 4, 5})
+
+	if err == nil || err.Error() != "Variable xx.2.1 is not a container" {
+		t.Error("Unexpected result:", parentVS.String(), err)
+		return
+	}
+
+	err = parentVS.SetValue("xx.2.1.2", []interface{}{3, 4, 5})
+
+	if err == nil || err.Error() != "Variable xx.2.1 is not a container" {
+		t.Error("Unexpected result:", parentVS.String(), err)
+		return
+	}
+
+	err = parentVS.SetValue("xx.5", []interface{}{3, 4, 5})
+
+	if err == nil || err.Error() != "Out of bounds access to list xx with index: 5" {
+		t.Error("Unexpected result:", parentVS.String(), err)
+		return
+	}
+
+	err = parentVS.SetValue("xx.5.1", []interface{}{3, 4, 5})
+
+	if err == nil || err.Error() != "Out of bounds access to list xx with index: 5" {
+		t.Error("Unexpected result:", parentVS.String(), err)
+		return
+	}
+
+	err = parentVS.SetValue("xx.2.5.1", []interface{}{3, 4, 5})
+
+	if err == nil || err.Error() != "Out of bounds access to list xx.2 with index: 5" {
+		t.Error("Unexpected result:", parentVS.String(), err)
+		return
+	}
+
+	err = parentVS.SetValue("yy.2.5.1", []interface{}{3, 4, 5})
+
+	if err == nil || err.Error() != "Variable yy is not a container" {
+		t.Error("Unexpected result:", parentVS.String(), err)
+		return
+	}
+}
+
+func TestVarScopeGet(t *testing.T) {
+
+	parentVs := NewScope("")
+	childVs := parentVs.NewChild("")
+
+	parentVs.SetValue("xx", map[interface{}]interface{}{
+		"foo": map[interface{}]interface{}{
+			"bar": 99,
+		},
+	})
+
+	parentVs.SetValue("test", []interface{}{1, 2, []interface{}{3, 4}})
+
+	if res := fmt.Sprint(childVs.GetValue("xx")); res != "map[foo:map[bar:99]] true <nil>" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := fmt.Sprint(childVs.GetValue("xx.foo")); res != "map[bar:99] true <nil>" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := fmt.Sprint(childVs.GetValue("xx.foo.bar")); res != "99 true <nil>" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := fmt.Sprint(childVs.GetValue("test")); res != "[1 2 [3 4]] true <nil>" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := fmt.Sprint(childVs.GetValue("test.2")); res != "[3 4] true <nil>" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := fmt.Sprint(childVs.GetValue("test.2.1")); res != "4 true <nil>" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := fmt.Sprint(childVs.GetValue("test.-1.1")); res != "4 true <nil>" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := fmt.Sprint(childVs.GetValue("test.-1.-1")); res != "4 true <nil>" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := fmt.Sprint(childVs.GetValue("test.-1.-2")); res != "3 true <nil>" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	// Test error cases
+
+	if res := fmt.Sprint(childVs.GetValue("test.a")); res != "<nil> false List test needs a number index not: a" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := fmt.Sprint(childVs.GetValue("test.5")); res != "<nil> false Out of bounds access to list test with index: 5" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := fmt.Sprint(childVs.GetValue("test.2.1.1")); res != "<nil> false Variable test.2.1 is not a container" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := fmt.Sprint(childVs.GetValue("test.1.1.1")); res != "<nil> false Variable test.1 is not a container" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := fmt.Sprint(childVs.GetValue("test2.1.1.1")); res != "<nil> false <nil>" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+}
+
+func TestVarScopeDump(t *testing.T) {
+
+	// Build a small tree of VS
+
+	globalVS1 := NewScope("global")
+	globalVS2 := NewScopeWithParent("global2", globalVS1)
+	globalVS3 := NewScopeWithParent("global3", globalVS1)
+	sinkVs1 := globalVS2.NewChild("sink: 1")
+	globalVS2.NewChild("sink: 1") // This should have no effect
+	globalVS2.NewChild("sink: 2") // Reference is provided in the next call
+	sinkVs2 := globalVS1.NewChild("sink: 2")
+	for1Vs := sinkVs1.NewChild("block: for1")
+	for2Vs := sinkVs1.NewChild("block: for2")
+	for21Vs := for2Vs.NewChild("block: for2-1")
+	for211Vs := for21Vs.NewChild("block: for2-1-1")
+
+	// Populate tree
+
+	globalVS1.SetValue("0", 0)
+	globalVS2.SetValue("a", 1)
+	globalVS3.SetValue("a", 2)
+	sinkVs1.SetValue("b", 2)
+	for1Vs.SetValue("c", 3)
+	for2Vs.SetValue("d", 4)
+	for21Vs.SetValue("e", 5)
+	for211Vs.SetValue("f", 6)
+	sinkVs2.SetValue("g", 2)
+
+	// Dump the sinkVs1 scope
+
+	if res := sinkVs1.String(); res != `global {
+    0 (int) : 0
+    global2 {
+        a (int) : 1
+        sink: 1 {
+            b (int) : 2
+            block: for1 {
+                c (int) : 3
+            }
+            block: for2 {
+                d (int) : 4
+                block: for2-1 {
+                    e (int) : 5
+                    block: for2-1-1 {
+                        f (int) : 6
+                    }
+                }
+            }
+        }
+    }
+}` {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := sinkVs2.String(); res != `global {
+    0 (int) : 0
+    sink: 2 {
+        g (int) : 2
+    }
+}` {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	// Dumping the global scope results in the same output as it does not
+	// track child scopes
+
+	if res := globalVS1.String(); res != `global {
+    0 (int) : 0
+    sink: 2 {
+        g (int) : 2
+    }
+}` {
+		t.Error("Unexpected result:", res)
+		return
+	}
+}

+ 192 - 0
stdlib/adapter.go

@@ -0,0 +1,192 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package stdlib
+
+import (
+	"fmt"
+	"go/ast"
+	"go/doc"
+	goparser "go/parser"
+	"go/token"
+	"path/filepath"
+	"reflect"
+	"runtime"
+	"strings"
+
+	"devt.de/krotik/ecal/parser"
+)
+
+/*
+ECALFunctionAdapter models a bridge adapter between an ECAL function to a Go function.
+*/
+type ECALFunctionAdapter struct {
+	funcval reflect.Value
+}
+
+/*
+Run executes this function.
+*/
+func (ea *ECALFunctionAdapter) Run(instanceID string, vs parser.Scope,
+	is map[string]interface{}, args []interface{}) (ret interface{}, err error) {
+
+	defer func() {
+		if r := recover(); r != nil {
+			err = fmt.Errorf("Error: %v", r)
+		}
+	}()
+
+	funcType := ea.funcval.Type()
+
+	// Build arguments
+
+	fargs := make([]reflect.Value, 0, len(args))
+	for i, arg := range args {
+
+		if i == funcType.NumIn() {
+			return nil, fmt.Errorf("Too many parameters - got %v expected %v",
+				len(args), funcType.NumIn())
+		}
+
+		expectedType := funcType.In(i)
+
+		// Try to convert into correct number types
+
+		if float64Arg, ok := arg.(float64); ok {
+			switch expectedType.Kind() {
+			case reflect.Int:
+				arg = int(float64Arg)
+			case reflect.Int8:
+				arg = int8(float64Arg)
+			case reflect.Int16:
+				arg = int16(float64Arg)
+			case reflect.Int32:
+				arg = int32(float64Arg)
+			case reflect.Int64:
+				arg = int64(float64Arg)
+			case reflect.Uint:
+				arg = uint(float64Arg)
+			case reflect.Uint8:
+				arg = uint8(float64Arg)
+			case reflect.Uint16:
+				arg = uint16(float64Arg)
+			case reflect.Uint32:
+				arg = uint32(float64Arg)
+			case reflect.Uint64:
+				arg = uint64(float64Arg)
+			case reflect.Uintptr:
+				arg = uintptr(float64Arg)
+			case reflect.Float32:
+				arg = float32(float64Arg)
+			}
+		}
+
+		givenType := reflect.TypeOf(arg)
+
+		// Check that the right types were given
+
+		if givenType != expectedType &&
+			!(expectedType.Kind() == reflect.Interface &&
+				givenType.Kind() == reflect.Interface &&
+				givenType.Implements(expectedType)) &&
+			expectedType != reflect.TypeOf([]interface{}{}) {
+
+			return nil, fmt.Errorf("Parameter %v should be of type %v but is of type %v",
+				i+1, expectedType, givenType)
+		}
+
+		fargs = append(fargs, reflect.ValueOf(arg))
+	}
+
+	// Call the function
+
+	vals := ea.funcval.Call(fargs)
+
+	// Convert result value
+
+	results := make([]interface{}, 0, len(vals))
+
+	for i, v := range vals {
+		res := v.Interface()
+
+		if i == len(vals)-1 {
+
+			// If the last item is an error then it is not part of the resutls
+			// (it will be wrapped into a proper runtime error later)
+
+			if funcType.Out(i) == reflect.TypeOf((*error)(nil)).Elem() {
+
+				if res != nil {
+					err = res.(error)
+				}
+
+				break
+			}
+		}
+
+		// Convert result if it is a primitive type
+
+		switch v.Kind() {
+		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+			res = float64(v.Int())
+		case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+			res = float64(v.Uint())
+		case reflect.Float32, reflect.Float64:
+			res = v.Float()
+		}
+
+		results = append(results, res)
+	}
+
+	ret = results
+
+	// Return a single value if results contains only a single item
+
+	if len(results) == 1 {
+		ret = results[0]
+	}
+
+	return ret, err
+}
+
+/*
+DocString returns the docstring of the wrapped function.
+*/
+func (ea *ECALFunctionAdapter) DocString() (string, error) {
+	ffunc := runtime.FuncForPC(ea.funcval.Pointer())
+	fileName, _ := ffunc.FileLine(0)
+	funcNameSplit := strings.Split(ffunc.Name(), ".")
+	funcName := funcNameSplit[len(funcNameSplit)-1]
+	fset := token.NewFileSet()
+	res := ""
+
+	parsedAst, err := goparser.ParseFile(fset, fileName, nil, goparser.ParseComments)
+
+	if err == nil {
+
+		pkg := &ast.Package{
+			Name:  "Any",
+			Files: make(map[string]*ast.File),
+		}
+		pkg.Files[fileName] = parsedAst
+
+		importPath, _ := filepath.Abs("/")
+		myDoc := doc.New(pkg, importPath, doc.AllDecls)
+
+		for _, theFunc := range myDoc.Funcs {
+			if theFunc.Name == funcName {
+				res = theFunc.Doc
+				break
+			}
+		}
+	}
+
+	return res, err
+}

+ 286 - 0
stdlib/adapter_test.go

@@ -0,0 +1,286 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package stdlib
+
+import (
+	"fmt"
+	"math"
+	"reflect"
+	"strconv"
+	"testing"
+
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/ecal/scope"
+)
+
+func TestECALFunctionAdapter(t *testing.T) {
+
+	res, err := runAdapterTest(
+		reflect.ValueOf(strconv.Atoi),
+		[]interface{}{"1"},
+	)
+
+	if err != nil || res != float64(1) {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(strconv.ParseUint),
+		[]interface{}{"123", float64(0), float64(0)},
+	)
+
+	if err != nil || res != float64(123) {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(strconv.ParseFloat),
+		[]interface{}{"123.123", float64(0)},
+	)
+
+	if err != nil || res != float64(123.123) {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(fmt.Sprintf),
+		[]interface{}{"foo %v", "bar"},
+	)
+
+	if err != nil || res != "foo bar" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(math.Float32bits),
+		[]interface{}{float64(11)},
+	)
+
+	if r := fmt.Sprintf("%X", uint32(res.(float64))); err != nil || r != fmt.Sprintf("%X", math.Float32bits(11)) {
+		t.Error("Unexpected result: ", r, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(math.Float32frombits),
+		[]interface{}{float64(math.Float32bits(11))},
+	)
+
+	if r := fmt.Sprintf("%v", res.(float64)); err != nil || r != "11" {
+		t.Error("Unexpected result: ", r, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(math.Float32frombits),
+		[]interface{}{math.Float32bits(11)}, // Giving the correct type also works
+	)
+
+	if r := fmt.Sprintf("%v", res.(float64)); err != nil || r != "11" {
+		t.Error("Unexpected result: ", r, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(math.Float64bits),
+		[]interface{}{float64(11)},
+	)
+
+	if r := fmt.Sprintf("%X", uint64(res.(float64))); err != nil || r != fmt.Sprintf("%X", math.Float64bits(11)) {
+		t.Error("Unexpected result: ", r, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(math.Float64frombits),
+		[]interface{}{float64(math.Float64bits(11))},
+	)
+
+	if r := fmt.Sprintf("%v", res.(float64)); err != nil || r != "11" {
+		t.Error("Unexpected result: ", r, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(dummyUint),
+		[]interface{}{float64(1)},
+	)
+
+	if err != nil || res != "1" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(dummyUint8),
+		[]interface{}{float64(1)},
+	)
+
+	if err != nil || res != "1" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(dummyUint16),
+		[]interface{}{float64(1)},
+	)
+
+	if err != nil || res != "1" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(dummyUintptr),
+		[]interface{}{float64(1)},
+	)
+
+	if err != nil || res != "1" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(dummyInt8),
+		[]interface{}{float64(1)},
+	)
+
+	if err != nil || res != "1" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(dummyInt16),
+		[]interface{}{float64(1)},
+	)
+
+	if err != nil || res != "1" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(dummyInt32),
+		[]interface{}{float64(1)},
+	)
+
+	if err != nil || res != "1" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(dummyInt64),
+		[]interface{}{float64(1)},
+	)
+
+	if err != nil || res != "1" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	// Test Error cases
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(strconv.ParseFloat),
+		[]interface{}{"123.123", 0, 0},
+	)
+
+	if err == nil || err.Error() != "Too many parameters - got 3 expected 2" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(strconv.ParseFloat),
+		[]interface{}{"Hans", 0},
+	)
+
+	if err == nil || err.Error() != `strconv.ParseFloat: parsing "Hans": invalid syntax` {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(strconv.ParseFloat),
+		[]interface{}{123, 0},
+	)
+
+	if err == nil || err.Error() != `Parameter 1 should be of type string but is of type int` {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	// Make sure we are never panicing but just returning an error
+
+	res, err = runAdapterTest(
+		reflect.ValueOf(errorutil.AssertTrue),
+		[]interface{}{false, "Some Panic Description"},
+	)
+
+	if err == nil || err.Error() != `Error: Some Panic Description` {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	// Get documentation
+
+	afuncEcal := &ECALFunctionAdapter{reflect.ValueOf(fmt.Sprint)}
+
+	if s, err := afuncEcal.DocString(); s == "" || err != nil {
+		t.Error("Docstring should return something")
+		return
+	}
+}
+
+func runAdapterTest(afunc reflect.Value, args []interface{}) (interface{}, error) {
+	afuncEcal := &ECALFunctionAdapter{afunc}
+	return afuncEcal.Run("test", scope.NewScope(""), make(map[string]interface{}), args)
+
+}
+
+func dummyUint(v uint) string {
+	return fmt.Sprint(v)
+}
+
+func dummyUint8(v uint8) string {
+	return fmt.Sprint(v)
+}
+
+func dummyUint16(v uint16) string {
+	return fmt.Sprint(v)
+}
+
+func dummyUintptr(v uintptr) string {
+	return fmt.Sprint(v)
+}
+
+func dummyInt8(v int8) string {
+	return fmt.Sprint(v)
+}
+
+func dummyInt16(v int16) string {
+	return fmt.Sprint(v)
+}
+
+func dummyInt32(v int32) string {
+	return fmt.Sprint(v)
+}
+
+func dummyInt64(v int64) string {
+	return fmt.Sprint(v)
+}

+ 198 - 0
stdlib/generate/generate.go

@@ -0,0 +1,198 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package main
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"go/importer"
+	"go/token"
+	"go/types"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"sort"
+	"unicode"
+)
+
+//go:generate echo Generating ECAL stdlib from Go functions ...
+//go:generate go run devt.de/krotik/ecal/stdlib/generate $PWD/stdlib
+
+/*
+Stdlib candidates modules:
+
+go list std | grep -v internal | grep -v '\.' | grep -v unsafe | grep -v syscall
+*/
+
+var pkgNames = map[string][]string{
+	"math": {"Pi"},
+	"fmt":  {"Println", "Sprint"},
+}
+
+var filename = filepath.Join(os.Args[1], "stdlib_gen.go")
+
+var stderrPrint = fmt.Println
+var stdoutPrint = fmt.Println
+
+func main() {
+	var err error
+	var outbuf bytes.Buffer
+
+	flag.Parse()
+
+	// Make sure pkgNames is sorted
+
+	var importList []string
+	for pkgName, names := range pkgNames {
+		sort.Strings(names)
+		importList = append(importList, pkgName)
+	}
+	sort.Strings(importList)
+
+	outbuf.WriteString(`
+// Code generated by ecal/stdlib/generate; DO NOT EDIT.
+
+package stdlib
+
+`)
+
+	outbuf.WriteString("import (\n")
+	for _, pkgName := range importList {
+		outbuf.WriteString(fmt.Sprintf("\t\"%v\"\n", pkgName))
+	}
+
+	outbuf.WriteString(`	"reflect"
+)
+
+`)
+
+	outbuf.WriteString(`/*
+genStdlib contains all generated stdlib constructs.
+*/
+`)
+	outbuf.WriteString("var genStdlib = map[interface{}]interface{}{\n")
+	for _, pkgName := range importList {
+		outbuf.WriteString(fmt.Sprintf("\t\"%v-const\" : %vConstMap,\n", pkgName, pkgName))
+		outbuf.WriteString(fmt.Sprintf("\t\"%v-func\" : %vFuncMap,\n", pkgName, pkgName))
+	}
+	outbuf.WriteString("}\n\n")
+
+	for _, pkgName := range importList {
+		var pkg *types.Package
+
+		pkgSymbols := pkgNames[pkgName]
+
+		if err == nil {
+			pkg, err = importer.ForCompiler(token.NewFileSet(), "source", nil).Import(pkgName)
+
+			if err == nil {
+				stdoutPrint("Generating adapter functions for", pkg)
+
+				scope := pkg.Scope()
+
+				// Write constants
+
+				outbuf.WriteString(fmt.Sprintf(`/*
+%vConstMap contains the mapping of stdlib %v constants.
+*/
+var %vConstMap = map[interface{}]interface{}{
+`, pkgName, pkgName, pkgName))
+
+				for _, name := range scope.Names() {
+
+					if !containsSymbol(pkgSymbols, name) {
+						continue
+					}
+
+					switch obj := scope.Lookup(name).(type) {
+					case *types.Const:
+
+						if unicode.IsUpper([]rune(name)[0]) {
+
+							line := fmt.Sprintf(`	"%v": %v.%v,
+`, name, pkgName, obj.Name())
+
+							if basicType, ok := obj.Type().(*types.Basic); ok {
+
+								// Convert number constants so they can be used in calculations
+
+								switch basicType.Kind() {
+								case types.Int,
+									types.Int8,
+									types.Int16,
+									types.Int32,
+									types.Int64,
+									types.Uint,
+									types.Uint8,
+									types.Uint16,
+									types.Uint32,
+									types.Uint64,
+									types.Uintptr,
+									types.Float32,
+									types.UntypedInt,
+									types.UntypedFloat:
+
+									line = fmt.Sprintf(`	"%v": float64(%v.%v),
+`, name, pkgName, obj.Name())
+								}
+							}
+
+							outbuf.WriteString(line)
+						}
+					}
+				}
+
+				outbuf.WriteString("}\n\n")
+
+				// Write functions
+
+				outbuf.WriteString(fmt.Sprintf(`/*
+%vFuncMap contains the mapping of stdlib %v functions.
+*/
+var %vFuncMap = map[interface{}]interface{}{
+`, pkgName, pkgName, pkgName))
+
+				for _, name := range scope.Names() {
+
+					if !containsSymbol(pkgSymbols, name) {
+						continue
+					}
+
+					switch obj := scope.Lookup(name).(type) {
+					case *types.Func:
+						if unicode.IsUpper([]rune(name)[0]) {
+							outbuf.WriteString(
+								fmt.Sprintf(`	"%v": &ECALFunctionAdapter{reflect.ValueOf(%v)},
+`, name, obj.FullName()))
+						}
+					}
+				}
+
+				outbuf.WriteString("}\n\n")
+			}
+
+		}
+	}
+
+	if err == nil {
+		err = ioutil.WriteFile(filename, outbuf.Bytes(), 0644)
+	}
+
+	if err != nil {
+		stderrPrint("Error:", err)
+	}
+}
+
+func containsSymbol(symbols []string, item string) bool {
+	i := sort.SearchStrings(symbols, item)
+	return i < len(symbols) && symbols[i] == item
+}

+ 113 - 0
stdlib/generate/generate_test.go

@@ -0,0 +1,113 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package main
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"testing"
+)
+
+const InvalidFileName = "**" + string(0x0)
+
+func TestGenerate(t *testing.T) {
+	filename = InvalidFileName
+
+	var buf bytes.Buffer
+	stderrPrint = func(v ...interface{}) (int, error) {
+		buf.WriteString(fmt.Sprint(v...))
+		return 0, nil
+	}
+	stdoutPrint = func(v ...interface{}) (int, error) {
+		return 0, nil
+	}
+
+	main()
+
+	if buf.String() != "Error:open **"+string(0)+": invalid argument" {
+		t.Error("Unexpected output:", buf.String())
+		return
+	}
+
+	filename = "test_out.txt"
+
+	pkgNames = map[string][]string{
+		"math": {"Pi"},
+		"fmt":  {"Println"},
+	}
+
+	defer func() {
+		os.Remove(filename)
+	}()
+
+	main()
+
+	out, err := ioutil.ReadFile(filename)
+
+	if err != nil {
+		t.Error("Could not read file:", filename, " ", err)
+		return
+	}
+
+	if string(out) != `
+// Code generated by ecal/stdlib/generate; DO NOT EDIT.
+
+package stdlib
+
+import (
+	"fmt"
+	"math"
+	"reflect"
+)
+
+/*
+genStdlib contains all generated stdlib constructs.
+*/
+var genStdlib = map[interface{}]interface{}{
+	"fmt-const" : fmtConstMap,
+	"fmt-func" : fmtFuncMap,
+	"math-const" : mathConstMap,
+	"math-func" : mathFuncMap,
+}
+
+/*
+fmtConstMap contains the mapping of stdlib fmt constants.
+*/
+var fmtConstMap = map[interface{}]interface{}{
+}
+
+/*
+fmtFuncMap contains the mapping of stdlib fmt functions.
+*/
+var fmtFuncMap = map[interface{}]interface{}{
+	"Println": &ECALFunctionAdapter{reflect.ValueOf(fmt.Println)},
+}
+
+/*
+mathConstMap contains the mapping of stdlib math constants.
+*/
+var mathConstMap = map[interface{}]interface{}{
+	"Pi": float64(math.Pi),
+}
+
+/*
+mathFuncMap contains the mapping of stdlib math functions.
+*/
+var mathFuncMap = map[interface{}]interface{}{
+}
+
+` {
+		t.Errorf("Unexpected result: Go string: %#v\nNormal output: %v", string(out), string(out))
+		return
+	}
+}

+ 61 - 0
stdlib/stdlib.go

@@ -0,0 +1,61 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package stdlib
+
+import (
+	"fmt"
+	"strings"
+
+	"devt.de/krotik/ecal/util"
+)
+
+/*
+GetStdlibConst looks up a constant from stdlib.
+*/
+func GetStdlibConst(name string) (interface{}, bool) {
+	m, n := splitModuleAndName(name)
+
+	if n != "" {
+		if cmap, ok := genStdlib[fmt.Sprintf("%v-const", m)]; ok {
+			if cv, ok := cmap.(map[interface{}]interface{})[n]; ok {
+				return cv.(interface{}), true
+			}
+		}
+	}
+
+	return nil, false
+}
+
+/*
+GetStdlibFunc looks up a function from stdlib.
+*/
+func GetStdlibFunc(name string) (util.ECALFunction, bool) {
+	m, n := splitModuleAndName(name)
+
+	if n != "" {
+		if fmap, ok := genStdlib[fmt.Sprintf("%v-func", m)]; ok {
+			if fn, ok := fmap.(map[interface{}]interface{})[n]; ok {
+				return fn.(util.ECALFunction), true
+			}
+		}
+	}
+
+	return nil, false
+}
+
+func splitModuleAndName(name string) (string, string) {
+	ccSplit := strings.SplitN(name, ".", 2)
+
+	if len(ccSplit) == 0 {
+		return "", ""
+	}
+	return ccSplit[0], strings.Join(ccSplit[1:], "")
+}

+ 62 - 0
stdlib/stdlib_test.go

@@ -0,0 +1,62 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package stdlib
+
+import (
+	"math"
+	"testing"
+)
+
+func TestSplitModuleAndName(t *testing.T) {
+
+	if m, n := splitModuleAndName("fmt.Println"); m != "fmt" || n != "Println" {
+		t.Error("Unexpected result:", m, n)
+		return
+	}
+
+	if m, n := splitModuleAndName(""); m != "" || n != "" {
+		t.Error("Unexpected result:", m, n)
+		return
+	}
+
+	if m, n := splitModuleAndName("a"); m != "a" || n != "" {
+		t.Error("Unexpected result:", m, n)
+		return
+	}
+
+	if m, n := splitModuleAndName("my.FuncCall"); m != "my" || n != "FuncCall" {
+		t.Error("Unexpected result:", m, n)
+		return
+	}
+}
+
+func TestGetStdLibItems(t *testing.T) {
+
+	if f, _ := GetStdlibFunc("fmt.Println"); f != fmtFuncMap["Println"] {
+		t.Error("Unexpected resutl: functions should lookup correctly")
+		return
+	}
+
+	if c, ok := GetStdlibFunc("foo"); c != nil || ok {
+		t.Error("Unexpected resutl: constants should lookup correctly")
+		return
+	}
+
+	if c, _ := GetStdlibConst("math.Pi"); c != math.Pi {
+		t.Error("Unexpected resutl: constants should lookup correctly")
+		return
+	}
+
+	if c, ok := GetStdlibConst("foo"); c != nil || ok {
+		t.Error("Unexpected resutl: constants should lookup correctly")
+		return
+	}
+}

+ 83 - 0
util/error.go

@@ -0,0 +1,83 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+/*
+Package util contains utility definitions and functions for the event condition language ECAL.
+*/
+package util
+
+import (
+	"errors"
+	"fmt"
+
+	"devt.de/krotik/ecal/parser"
+)
+
+/*
+RuntimeError is a runtime related error.
+*/
+type RuntimeError struct {
+	Source string          // Name of the source which was given to the parser
+	Type   error           // Error type (to be used for equal checks)
+	Detail string          // Details of this error
+	Node   *parser.ASTNode // AST Node where the error occurred
+	Line   int             // Line of the error
+	Pos    int             // Position of the error
+}
+
+/*
+Runtime related error types.
+*/
+var (
+	ErrRuntimeError     = errors.New("Runtime error")
+	ErrUnknownConstruct = errors.New("Unknown construct")
+	ErrInvalidConstruct = errors.New("Invalid construct")
+	ErrInvalidState     = errors.New("Invalid state")
+	ErrVarAccess        = errors.New("Cannot access variable")
+	ErrNotANumber       = errors.New("Operand is not a number")
+	ErrNotABoolean      = errors.New("Operand is not a boolean")
+	ErrNotAList         = errors.New("Operand is not a list")
+	ErrNotAMap          = errors.New("Operand is not a map")
+	ErrNotAListOrMap    = errors.New("Operand is not a list nor a map")
+
+	// ErrReturn is not an error. It is used to return when executing a function
+	ErrReturn = errors.New("*** return ***")
+
+	// Error codes for loop operations
+	ErrIsIterator        = errors.New("Function is an iterator")
+	ErrEndOfIteration    = errors.New("End of iteration was reached")
+	ErrContinueIteration = errors.New("End of iteration step - Continue iteration")
+)
+
+/*
+NewRuntimeError creates a new RuntimeError object.
+*/
+func NewRuntimeError(source string, t error, d string, node *parser.ASTNode) error {
+	if node.Token != nil {
+		return &RuntimeError{source, t, d, node, node.Token.Lline, node.Token.Lpos}
+	}
+	return &RuntimeError{source, t, d, node, 0, 0}
+}
+
+/*
+Error returns a human-readable string representation of this error.
+*/
+func (re *RuntimeError) Error() string {
+	ret := fmt.Sprintf("ECAL error in %s: %v (%v)", re.Source, re.Type, re.Detail)
+
+	if re.Line != 0 {
+
+		// Add line if available
+
+		ret = fmt.Sprintf("%s (Line:%d Pos:%d)", ret, re.Line, re.Pos)
+	}
+
+	return ret
+}

+ 155 - 0
util/logging.go

@@ -0,0 +1,155 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package util
+
+import (
+	"fmt"
+	"log"
+
+	"devt.de/krotik/common/datautil"
+)
+
+// Logging implementations
+// =======================
+
+/*
+MemoryLogger collects log messages in a RingBuffer in memory.
+*/
+type MemoryLogger struct {
+	*datautil.RingBuffer
+}
+
+/*
+NewMemoryLogger returns a new memory logger instance.
+*/
+func NewMemoryLogger(size int) *MemoryLogger {
+	return &MemoryLogger{datautil.NewRingBuffer(size)}
+}
+
+/*
+LogError adds a new error log message.
+*/
+func (ml *MemoryLogger) LogError(p *Processor, m ...interface{}) {
+	ml.RingBuffer.Add(fmt.Sprintf("error: %v", fmt.Sprint(m...)))
+}
+
+/*
+LogInfo adds a new info log message.
+*/
+func (ml *MemoryLogger) LogInfo(p *Processor, m ...interface{}) {
+	ml.RingBuffer.Add(fmt.Sprintf("info: %v", fmt.Sprint(m...)))
+}
+
+/*
+LogDebug adds a new debug log message.
+*/
+func (ml *MemoryLogger) LogDebug(p *Processor, m ...interface{}) {
+	ml.RingBuffer.Add(fmt.Sprintf("debug: %v", fmt.Sprint(m...)))
+}
+
+/*
+Slice returns the contents of the current log as a slice.
+*/
+func (ml *MemoryLogger) Slice() []string {
+	sl := ml.RingBuffer.Slice()
+	ret := make([]string, len(sl))
+	for i, lm := range sl {
+		ret[i] = lm.(string)
+	}
+	return ret
+}
+
+/*
+Reset resets the current log.
+*/
+func (ml *MemoryLogger) Reset() {
+	ml.RingBuffer.Reset()
+}
+
+/*
+Size returns the current log size.
+*/
+func (ml *MemoryLogger) Size() int {
+	return ml.RingBuffer.Size()
+}
+
+/*
+String returns the current log as a string.
+*/
+func (ml *MemoryLogger) String() string {
+	return ml.RingBuffer.String()
+}
+
+/*
+StdOutLogger writes log messages to stdout.
+*/
+type StdOutLogger struct {
+	stdlog func(v ...interface{})
+}
+
+/*
+NewStdOutLogger returns a stdout logger instance.
+*/
+func NewStdOutLogger() *StdOutLogger {
+	return &StdOutLogger{log.Print}
+}
+
+/*
+LogError adds a new error log message.
+*/
+func (sl *StdOutLogger) LogError(p *Processor, m ...interface{}) {
+	sl.stdlog(fmt.Sprintf("error: %v", fmt.Sprint(m...)))
+}
+
+/*
+LogInfo adds a new info log message.
+*/
+func (sl *StdOutLogger) LogInfo(p *Processor, m ...interface{}) {
+	sl.stdlog(fmt.Sprintf("error: %v", fmt.Sprint(m...)))
+}
+
+/*
+LogDebug adds a new debug log message.
+*/
+func (sl *StdOutLogger) LogDebug(p *Processor, m ...interface{}) {
+	sl.stdlog(fmt.Sprintf("error: %v", fmt.Sprint(m...)))
+}
+
+/*
+NullLogger discards log messages.
+*/
+type NullLogger struct {
+}
+
+/*
+NewNullLogger returns a null logger instance.
+*/
+func NewNullLogger() *NullLogger {
+	return &NullLogger{}
+}
+
+/*
+LogError adds a new error log message.
+*/
+func (nl *NullLogger) LogError(p *Processor, m ...interface{}) {
+}
+
+/*
+LogInfo adds a new info log message.
+*/
+func (nl *NullLogger) LogInfo(p *Processor, m ...interface{}) {
+}
+
+/*
+LogDebug adds a new debug log message.
+*/
+func (nl *NullLogger) LogDebug(p *Processor, m ...interface{}) {
+}

+ 62 - 0
util/logging_test.go

@@ -0,0 +1,62 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package util
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestLogging(t *testing.T) {
+
+	ml := NewMemoryLogger(5)
+
+	ml.LogDebug(nil, "test")
+	ml.LogInfo(nil, "test")
+
+	if ml.String() != `debug: test
+info: test` {
+		t.Error("Unexpected result:", ml.String())
+		return
+	}
+
+	if res := fmt.Sprint(ml.Slice()); res != "[debug: test info: test]" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	ml.Reset()
+
+	ml.LogError(nil, "test1")
+
+	if res := fmt.Sprint(ml.Slice()); res != "[error: test1]" {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	if res := ml.Size(); res != 1 {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	// Test that the functions can be called
+
+	nl := NewNullLogger()
+	nl.LogDebug(nil, "test")
+	nl.LogInfo(nil, "test")
+	nl.LogError(nil, "test")
+
+	sol := NewStdOutLogger()
+	sol.stdlog = func(v ...interface{}) {}
+	sol.LogDebug(nil, "test")
+	sol.LogInfo(nil, "test")
+	sol.LogError(nil, "test")
+}

+ 60 - 0
util/types.go

@@ -0,0 +1,60 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package util
+
+import "devt.de/krotik/ecal/parser"
+
+/*
+Processor models a top level execution instance for ECAL.
+*/
+type Processor interface {
+}
+
+/*
+ECALFunction models a callable function in ECAL.
+*/
+type ECALFunction interface {
+
+	/*
+		Run executes this function. The envirnment provides a unique instanceID for
+		every code location in the running code, the variable scope of the function,
+		an instance state which can be used in combinartion with the instanceID
+		to store instance specific state (e.g. for iterator functions) and a list
+		of argument values which were passed to the function by the calling code.
+	*/
+	Run(instanceID string, vs parser.Scope, is map[string]interface{}, args []interface{}) (interface{}, error)
+
+	/*
+	   DocString returns a descriptive text about this function.
+	*/
+	DocString() (string, error)
+}
+
+/*
+Logger is required external object to which the interpreter releases its log messages.
+*/
+type Logger interface {
+
+	/*
+	   LogError adds a new error log message.
+	*/
+	LogError(v ...interface{})
+
+	/*
+	   LogInfo adds a new info log message.
+	*/
+	LogInfo(v ...interface{})
+
+	/*
+	   LogDebug adds a new debug log message.
+	*/
+	LogDebug(v ...interface{})
+}