Browse Source

chore: Cleanup code

Matthias Ladkau 3 years ago
parent
commit
0207b5c065
44 changed files with 2739 additions and 1207 deletions
  1. 14 5
      cli/tool/debug.go
  2. 400 0
      cli/tool/debug_test.go
  3. 23 17
      cli/tool/format.go
  4. 122 0
      cli/tool/format_test.go
  5. 17 0
      cli/tool/helper.go
  6. 123 0
      cli/tool/helper_test.go
  7. 61 40
      cli/tool/interpret.go
  8. 355 0
      cli/tool/interpret_test.go
  9. 47 43
      cli/tool/pack.go
  10. 278 0
      cli/tool/pack_test.go
  11. 1 1
      ecal-support/README.md
  12. 1 1
      ecal-support/src/ecalDebugAdapter.ts
  13. 1 1
      ecal-support/src/extension.ts
  14. 18 13
      engine/processor_test.go
  15. 71 62
      engine/pubsub/eventpump_test.go
  16. 21 22
      engine/taskqueue_test.go
  17. 8 2
      examples/embedding/main.go
  18. 18 18
      interpreter/debug.go
  19. 10 10
      interpreter/debug_cmd.go
  20. 86 122
      interpreter/debug_test.go
  21. 2 2
      interpreter/func_provider.go
  22. 29 14
      interpreter/func_provider_test.go
  23. 4 1
      interpreter/rt_arithmetic_test.go
  24. 9 9
      interpreter/rt_assign_test.go
  25. 9 2
      interpreter/rt_boolean_test.go
  26. 90 65
      interpreter/rt_identifier.go
  27. 124 111
      interpreter/rt_sink.go
  28. 184 156
      interpreter/rt_statements.go
  29. 18 7
      interpreter/rt_statements_test.go
  30. 14 36
      parser/helper.go
  31. 47 14
      parser/helper_test.go
  32. 9 2
      parser/lexer.go
  33. 46 56
      parser/parser.go
  34. 184 148
      parser/prettyprinter.go
  35. 54 51
      scope/varsscope.go
  36. 17 10
      scope/varsscope_test.go
  37. 52 36
      stdlib/adapter.go
  38. 31 20
      stdlib/adapter_test.go
  39. 117 93
      stdlib/generate/generate.go
  40. 1 1
      stdlib/stdlib.go
  41. 1 1
      util/error.go
  42. 12 8
      util/import_test.go
  43. 4 1
      util/logging.go
  44. 6 6
      util/types.go

+ 14 - 5
cli/tool/debug.go

@@ -21,6 +21,7 @@ import (
 	"net"
 	"os"
 	"strings"
+	"sync"
 	"time"
 
 	"devt.de/krotik/common/errorutil"
@@ -113,8 +114,12 @@ func (i *CLIDebugInterpreter) Interpret() error {
 
 			debugServer := &debugTelnetServer{*i.DebugServerAddr, "ECALDebugServer: ",
 				nil, true, *i.EchoDebugServer, i, i.RuntimeProvider.Logger}
-			go debugServer.Run()
-			time.Sleep(500 * time.Millisecond) // Too lazy to do proper signalling
+
+			wg := &sync.WaitGroup{}
+			wg.Add(1)
+			go debugServer.Run(wg)
+			wg.Wait()
+
 			defer func() {
 				if debugServer.listener != nil {
 					debugServer.listen = false
@@ -211,12 +216,16 @@ type debugTelnetServer struct {
 /*
 Run runs the debug server.
 */
-func (s *debugTelnetServer) Run() {
+func (s *debugTelnetServer) Run(wg *sync.WaitGroup) {
 	tcpaddr, err := net.ResolveTCPAddr("tcp", s.address)
 
 	if err == nil {
 
-		if s.listener, err = net.ListenTCP("tcp", tcpaddr); err == nil {
+		s.listener, err = net.ListenTCP("tcp", tcpaddr)
+
+		if err == nil {
+
+			wg.Done()
 
 			s.logger.LogInfo(s.logPrefix,
 				"Running Debug Server on ", tcpaddr.String())
@@ -225,7 +234,6 @@ func (s *debugTelnetServer) Run() {
 				var conn net.Conn
 
 				if conn, err = s.listener.Accept(); err == nil {
-
 					go s.HandleConnection(conn)
 
 				} else if s.listen {
@@ -238,6 +246,7 @@ func (s *debugTelnetServer) Run() {
 
 	if s.listen && err != nil {
 		s.logger.LogError(s.logPrefix, "Could not start debug server - ", err)
+		wg.Done()
 	}
 }
 

+ 400 - 0
cli/tool/debug_test.go

@@ -0,0 +1,400 @@
+/*
+ * 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 tool
+
+import (
+	"bufio"
+	"bytes"
+	"flag"
+	"fmt"
+	"net"
+	"os"
+	"reflect"
+	"strconv"
+	"strings"
+	"sync"
+	"testing"
+	"time"
+
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/ecal/config"
+	"devt.de/krotik/ecal/interpreter"
+	"devt.de/krotik/ecal/stdlib"
+	"devt.de/krotik/ecal/util"
+)
+
+var testDebugLogOut *bytes.Buffer
+
+func newTestDebugWithConfig() *CLIDebugInterpreter {
+	tdin := NewCLIDebugInterpreter(newTestInterpreterWithConfig())
+
+	testDebugLogOut = &bytes.Buffer{}
+	tdin.LogOut = testDebugLogOut
+
+	return tdin
+}
+
+func TestDebugBasicFunctions(t *testing.T) {
+	tdin := newTestDebugWithConfig()
+	defer tearDown()
+
+	// Test help output
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) // Reset CLI parsing
+
+	osArgs = []string{"foo", "bar", "-help"}
+	defer func() { osArgs = []string{} }()
+
+	flag.CommandLine.SetOutput(&testTerm.out)
+
+	if stop := tdin.ParseArgs(); !stop {
+		t.Error("Asking for help should request to stop the program")
+		return
+	}
+
+	if !strings.Contains(testTerm.out.String(), "Root directory for ECAL interpreter") {
+		t.Error("Helptext does not contain expected string - output:", testTerm.out.String())
+		return
+	}
+
+	if stop := tdin.ParseArgs(); stop {
+		t.Error("Asking again should be caught by the short circuit")
+		return
+	}
+
+	tdin = newTestDebugWithConfig()
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) // Reset CLI parsing
+
+	flag.CommandLine.SetOutput(&testTerm.out)
+
+	if err := tdin.Interpret(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+}
+
+func TestDebugInterpret(t *testing.T) {
+	tdin := newTestDebugWithConfig()
+	defer tearDown()
+
+	if stop := tdin.ParseArgs(); stop {
+		t.Error("Setting default args should be fine")
+		return
+	}
+
+	if err := tdin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	tdin.RuntimeProvider.Logger, _ = util.NewLogLevelLogger(util.NewMemoryLogger(10), "info")
+	tdin.RuntimeProvider.ImportLocator = &util.MemoryImportLocator{
+		Files: map[string]string{
+			"foo": "a := 1",
+		},
+	}
+
+	l1 := ""
+	tdin.LogFile = &l1
+	l2 := ""
+	tdin.LogLevel = &l2
+	l3 := true
+	tdin.Interactive = &l3
+	tdin.RunDebugServer = &l3
+
+	testTerm.in = []string{"xxx := 1", "q"}
+
+	// The interpret call takes quite long because the debug server is
+	// closed by the defer call when the call returns
+
+	if err := tdin.Interpret(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if testLogOut.String() != `ECAL `+config.ProductVersion+`
+Log level: info - Root directory: /home/ml/data/krotik/ecal/cli/tool
+Running in debug mode - with debug server on localhost:33274 - prefix debug commands with ##
+Type 'q' or 'quit' to exit the shell and '?' to get help
+` {
+		t.Error("Unexpected result:", testLogOut.String())
+		return
+	}
+
+	if tdin.GlobalVS.String() != `GlobalScope {
+    xxx (float64) : 1
+}` {
+		t.Error("Unexpected scope:", tdin.GlobalVS)
+		return
+	}
+}
+
+func TestDebugHandleInput(t *testing.T) {
+	tdin := newTestDebugWithConfig()
+	defer tearDown()
+
+	if stop := tdin.ParseArgs(); stop {
+		t.Error("Setting default args should be fine")
+		return
+	}
+
+	if err := tdin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	stdlib.AddStdlibPkg("foo", "bar")
+	stdlib.AddStdlibFunc("foo", "Println",
+		stdlib.NewECALFunctionAdapter(reflect.ValueOf(fmt.Println), "xxx"))
+	stdlib.AddStdlibFunc("foo", "Atoi",
+		stdlib.NewECALFunctionAdapter(reflect.ValueOf(strconv.Atoi), "xxx"))
+
+	tdin.RuntimeProvider.Logger, _ = util.NewLogLevelLogger(util.NewMemoryLogger(10), "info")
+	tdin.RuntimeProvider.ImportLocator = &util.MemoryImportLocator{}
+	tdin.CustomHelpString = "123"
+
+	l1 := ""
+	tdin.LogFile = &l1
+	l2 := ""
+	tdin.LogLevel = &l2
+	l3 := true
+	tdin.Interactive = &l3
+	l4 := false
+	tdin.RunDebugServer = &l4
+
+	testTerm.in = []string{"?", "@dbg", "##status", "##foo", "q"}
+
+	if err := tdin.Interpret(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	// Just check for a simple string no need for the whole thing
+	time.Sleep(1 * time.Second)
+
+	if !strings.Contains(testTerm.out.String(), "Set a breakpoint specifying <source>:<line>") {
+		t.Error("Unexpected result:", testTerm.out.String())
+		return
+	}
+
+	if !strings.Contains(testTerm.out.String(), "Unknown command: foo") {
+		t.Error("Unexpected result:", testTerm.out.String())
+		return
+	}
+
+	testTerm.out.Reset()
+
+	testTerm.in = []string{"@dbg status", "q"}
+
+	if err := tdin.Interpret(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if testTerm.out.String() != `╒══════════════╤═════════════════════════════════════════╕
+│Debug command │Description                              │
+╞══════════════╪═════════════════════════════════════════╡
+│status        │Shows breakpoints and suspended threads. │
+│              │                                         │
+╘══════════════╧═════════════════════════════════════════╛
+
+
+` {
+		t.Error("Unexpected result:", "#"+testTerm.out.String()+"#")
+		return
+	}
+
+	testTerm.out.Reset()
+
+	testTerm.in = []string{"1", "raise(123)", "q"}
+
+	if err := tdin.Interpret(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if testTerm.out.String() != `1
+ECAL error in foo: 123 () (Line:1 Pos:1)
+` {
+		t.Error("Unexpected result:", testTerm.out.String())
+		return
+	}
+}
+
+func TestDebugTelnetServer(t *testing.T) {
+	tdin := newTestDebugWithConfig()
+	defer tearDown()
+
+	if err := tdin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	tdin.RuntimeProvider.Logger = util.NewMemoryLogger(10)
+	tdin.RuntimeProvider.ImportLocator = &util.MemoryImportLocator{}
+	tdin.RuntimeProvider.Debugger = interpreter.NewECALDebugger(tdin.GlobalVS)
+	tdin.RuntimeProvider.Debugger.BreakOnError(false)
+	tdin.CustomHandler = tdin
+
+	addr := "localhost:33274"
+	mlog := util.NewMemoryLogger(10)
+
+	srv := &debugTelnetServer{
+		address:     addr,
+		logPrefix:   "testdebugserver",
+		listener:    nil,
+		listen:      true,
+		echo:        true,
+		interpreter: tdin,
+		logger:      mlog,
+	}
+	defer func() {
+		srv.listen = false
+		srv.listener.Close() // Attempt to cleanup
+	}()
+
+	wg := &sync.WaitGroup{}
+	wg.Add(1)
+	go srv.Run(wg)
+	wg.Wait()
+
+	conn, err := net.Dial("tcp", addr)
+	errorutil.AssertOk(err)
+	reader := bufio.NewReader(conn)
+
+	fmt.Fprintf(conn, "a:= 1; a\n")
+
+	line, err := reader.ReadString('}')
+	errorutil.AssertOk(err)
+
+	if line != `{
+  "EncodedOutput": "MQo="
+}` {
+		t.Error("Unexpected output:", line)
+		return
+	}
+
+	if tdin.GlobalVS.String() != `GlobalScope {
+    a (float64) : 1
+}` {
+		t.Error("Unexpected result:", tdin.GlobalVS)
+		return
+	}
+
+	fmt.Fprintf(conn, "##status\n")
+
+	line, err = reader.ReadString('}')
+	errorutil.AssertOk(err)
+	l, err := reader.ReadString('}')
+	errorutil.AssertOk(err)
+	line += l
+	l, err = reader.ReadString('}')
+	errorutil.AssertOk(err)
+	line += l
+	line = strings.TrimSpace(line)
+
+	if line != `{
+  "breakonstart": false,
+  "breakpoints": {},
+  "sources": [
+    "console input"
+  ],
+  "threads": {}
+}` {
+		t.Error("Unexpected output:", line)
+		return
+	}
+
+	fmt.Fprintf(conn, "@sym\n")
+
+	line, err = reader.ReadString('}')
+	errorutil.AssertOk(err)
+
+	if !strings.Contains(line, "KioqKioqKioqKioqKioqKioq") {
+		t.Error("Unexpected output:", line)
+		return
+	}
+
+	fmt.Fprintf(conn, "raise(123);1\n")
+
+	line, err = reader.ReadString('}')
+	errorutil.AssertOk(err)
+	line = strings.TrimSpace(line)
+
+	if line != `{
+  "EncodedOutput": "RUNBTCBlcnJvciBpbiBmb286IDEyMyAoKSAoTGluZToxIFBvczoxKQo="
+}` {
+		t.Error("Unexpected output:", line)
+		return
+	}
+
+	testDebugLogOut.Reset()
+
+	errorutil.AssertOk(conn.Close())
+
+	time.Sleep(10 * time.Millisecond)
+
+	if !strings.Contains(testDebugLogOut.String(), "Disconnected") {
+		t.Error("Unexpected output:", testDebugLogOut)
+		return
+	}
+
+	testDebugLogOut.Reset()
+
+	conn, err = net.Dial("tcp", addr)
+	errorutil.AssertOk(err)
+
+	if _, err := fmt.Fprintf(conn, "q\n"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	// Make sure we can't start a second server on the same port
+
+	mlog2 := util.NewMemoryLogger(10)
+
+	srv2 := &debugTelnetServer{
+		address:     addr,
+		logPrefix:   "testdebugserver",
+		listener:    nil,
+		listen:      true,
+		echo:        true,
+		interpreter: tdin,
+		logger:      mlog2,
+	}
+	defer func() {
+		srv2.listen = false
+		srv2.listener.Close() // Attempt to cleanup
+	}()
+
+	mlog2.Reset()
+
+	wg = &sync.WaitGroup{}
+	wg.Add(1)
+	go srv2.Run(wg)
+	wg.Wait()
+
+	if !strings.Contains(mlog2.String(), "address already in use") {
+		t.Error("Unexpected output:", mlog2.String())
+		return
+	}
+
+	mlog.Reset()
+
+	srv.listener.Close()
+
+	time.Sleep(5 * time.Millisecond)
+
+	if !strings.Contains(mlog.String(), "use of closed network connection") {
+		t.Error("Unexpected output:", mlog.String())
+		return
+	}
+}

+ 23 - 17
cli/tool/format.go

@@ -16,10 +16,14 @@ import (
 	"io/ioutil"
 	"os"
 	"path/filepath"
+	"strings"
 
 	"devt.de/krotik/ecal/parser"
 )
 
+/*
+Format formats a given set of ECAL files.
+*/
 func Format() error {
 	var err error
 
@@ -30,17 +34,17 @@ func Format() error {
 	showHelp := flag.Bool("help", false, "Show this help message")
 
 	flag.Usage = func() {
-		fmt.Println()
-		fmt.Println(fmt.Sprintf("Usage of %s format [options]", os.Args[0]))
-		fmt.Println()
+		fmt.Fprintln(flag.CommandLine.Output())
+		fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Usage of %s format [options]", os.Args[0]))
+		fmt.Fprintln(flag.CommandLine.Output())
 		flag.PrintDefaults()
-		fmt.Println()
-		fmt.Println("This tool will format all ECAL files in a directory structure.")
-		fmt.Println()
+		fmt.Fprintln(flag.CommandLine.Output())
+		fmt.Fprintln(flag.CommandLine.Output(), "This tool will format all ECAL files in a directory structure.")
+		fmt.Fprintln(flag.CommandLine.Output())
 	}
 
 	if len(os.Args) >= 2 {
-		flag.CommandLine.Parse(os.Args[2:])
+		flag.CommandLine.Parse(osArgs[2:])
 
 		if *showHelp {
 			flag.Usage()
@@ -48,26 +52,28 @@ func Format() error {
 		}
 	}
 
-	fmt.Println(fmt.Sprintf("Formatting all %v files in %v", *ext, *dir))
+	fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Formatting all %v files in %v", *ext, *dir))
 
-	err = filepath.Walk(".",
+	err = filepath.Walk(*dir,
 		func(path string, i os.FileInfo, err error) error {
 			if err == nil && !i.IsDir() {
 				var data []byte
 				var ast *parser.ASTNode
 				var srcFormatted string
 
-				if data, err = ioutil.ReadFile(path); err == nil {
-					var ferr error
+				if strings.HasSuffix(path, *ext) {
+					if data, err = ioutil.ReadFile(path); err == nil {
+						var ferr error
 
-					if ast, ferr = parser.Parse(path, string(data)); ferr == nil {
-						if srcFormatted, ferr = parser.PrettyPrint(ast); ferr == nil {
-							ioutil.WriteFile(path, []byte(srcFormatted), i.Mode())
+						if ast, ferr = parser.Parse(path, string(data)); ferr == nil {
+							if srcFormatted, ferr = parser.PrettyPrint(ast); ferr == nil {
+								ioutil.WriteFile(path, []byte(srcFormatted), i.Mode())
+							}
 						}
-					}
 
-					if ferr != nil {
-						fmt.Fprintln(os.Stderr, fmt.Sprintf("Could not format %v: %v", path, ferr))
+						if ferr != nil {
+							fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Could not format %v: %v", path, ferr))
+						}
 					}
 				}
 			}

+ 122 - 0
cli/tool/format_test.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 tool
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/fileutil"
+)
+
+const formatTestDir = "formattest"
+
+func setupFormatTestDir() {
+
+	if res, _ := fileutil.PathExists(formatTestDir); res {
+		os.RemoveAll(formatTestDir)
+	}
+
+	err := os.Mkdir(formatTestDir, 0770)
+	if err != nil {
+		fmt.Print("Could not create test directory:", err.Error())
+		os.Exit(1)
+	}
+}
+
+func tearDownFormatTestDir() {
+	err := os.RemoveAll(formatTestDir)
+	if err != nil {
+		fmt.Print("Could not remove test directory:", err.Error())
+	}
+}
+
+func TestFormat(t *testing.T) {
+	setupFormatTestDir()
+	defer tearDownFormatTestDir()
+
+	out := bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{"foo", "bar", "-help"}
+
+	if err := Format(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if !strings.Contains(out.String(), "Root directory for ECAL files") {
+		t.Error("Unexpected output:", out.String())
+		return
+	}
+
+	myfile := filepath.Join(formatTestDir, "myfile.ecal")
+	myfile2 := filepath.Join(formatTestDir, "myfile.eca")
+	myfile3 := filepath.Join(formatTestDir, "myinvalidfile.ecal")
+
+	originalContent := "if a == 1 { b := 1 }"
+
+	err := ioutil.WriteFile(myfile, []byte(originalContent), 0777)
+	errorutil.AssertOk(err)
+
+	err = ioutil.WriteFile(myfile2, []byte(originalContent), 0777)
+	errorutil.AssertOk(err)
+
+	err = ioutil.WriteFile(myfile3, []byte(originalContent[5:]), 0777)
+	errorutil.AssertOk(err)
+
+	out = bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{"foo", "bar", "-dir", formatTestDir}
+
+	if err := Format(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if out.String() != `Formatting all .ecal files in formattest
+Could not format formattest/myinvalidfile.ecal: Parse error in formattest/myinvalidfile.ecal: Term cannot start an expression (==) (Line:1 Pos:1)
+` {
+		t.Error("Unexpected output:", out.String())
+		return
+	}
+
+	myfileContent, err := ioutil.ReadFile(myfile)
+	errorutil.AssertOk(err)
+
+	if string(myfileContent) != `if a == 1 {
+    b := 1
+}
+` {
+		t.Error("Unexpected result:", string(myfileContent))
+		return
+	}
+
+	myfileContent, err = ioutil.ReadFile(myfile2)
+	errorutil.AssertOk(err)
+
+	if string(myfileContent) != originalContent {
+		t.Error("Unexpected result:", string(myfileContent))
+		return
+	}
+}

+ 17 - 0
cli/tool/helper.go

@@ -12,12 +12,29 @@ package tool
 
 import (
 	"fmt"
+	"io"
+	"os"
 	"regexp"
 	"strings"
 
 	"devt.de/krotik/common/stringutil"
 )
 
+/*
+osArgs is a local copy of os.Args (used for unit tests)
+*/
+var osArgs = os.Args
+
+/*
+osStderr is a local copy of os.Stderr (used for unit tests)
+*/
+var osStderr io.Writer = os.Stderr
+
+/*
+osExit is a local variable pointing to os.Exit (used for unit tests)
+*/
+var osExit func(int) = os.Exit
+
 /*
 CLIInputHandler is a handler object for CLI input.
 */

+ 123 - 0
cli/tool/helper_test.go

@@ -0,0 +1,123 @@
+/*
+ * 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 tool
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"strings"
+	"testing"
+
+	"devt.de/krotik/common/stringutil"
+	"devt.de/krotik/common/termutil"
+)
+
+type testConsoleLineTerminal struct {
+	in  []string
+	out bytes.Buffer
+}
+
+func (t *testConsoleLineTerminal) StartTerm() error {
+	return nil
+}
+
+func (t *testConsoleLineTerminal) AddKeyHandler(handler termutil.KeyHandler) {
+}
+
+func (t *testConsoleLineTerminal) NextLine() (string, error) {
+	var err error
+	var ret string
+
+	if len(t.in) > 0 {
+		ret = t.in[0]
+		t.in = t.in[1:]
+	} else {
+		err = fmt.Errorf("Input is empty in testConsoleLineTerminal")
+	}
+	return ret, err
+}
+
+func (t *testConsoleLineTerminal) NextLinePrompt(prompt string, echo rune) (string, error) {
+	return t.NextLine()
+}
+
+func (t *testConsoleLineTerminal) WriteString(s string) {
+	t.out.WriteString(s)
+}
+
+func (t *testConsoleLineTerminal) Write(p []byte) (n int, err error) {
+	return t.out.Write(p)
+}
+
+func (t *testConsoleLineTerminal) StopTerm() {
+}
+
+type testCustomHandler struct {
+}
+
+func (t *testCustomHandler) CanHandle(s string) bool {
+	return s == "@cus"
+}
+
+func (t *testCustomHandler) Handle(ot OutputTerminal, input string) {}
+
+func (t *testCustomHandler) LoadInitialFile(tid uint64) error {
+	return nil
+}
+
+type testOutputTerminal struct {
+	b bytes.Buffer
+}
+
+func (t *testOutputTerminal) WriteString(s string) {
+	t.b.WriteString(s)
+}
+
+func TestMatchesFulltextSearch(t *testing.T) {
+	ot := &testOutputTerminal{}
+
+	ok := matchesFulltextSearch(ot, "abc", "s[")
+
+	if !ok && strings.HasPrefix(ot.b.String(), "Invalid search expression") {
+		t.Error("Unexpected result:", ot.b.String(), ok)
+		return
+	}
+
+	ot.b = bytes.Buffer{}
+
+	ok = matchesFulltextSearch(ot, "abc", "a*")
+
+	if !ok || ot.b.String() != "" {
+		t.Error("Unexpected result:", ot.b.String(), ok)
+		return
+	}
+
+	ok = matchesFulltextSearch(ot, "abc", "ac*")
+
+	if ok || ot.b.String() != "" {
+		t.Error("Unexpected result:", ot.b.String(), ok)
+		return
+	}
+}
+
+func TestFillTableRow(t *testing.T) {
+
+	res := fillTableRow([]string{}, "test", stringutil.GenerateRollingString("123 ", 100))
+
+	b, _ := json.Marshal(&res)
+
+	if string(b) != `["test","123 123 123 123 123 123 123 123 123 123 123 123 `+
+		`123 123 123 123 123 123 123 123","","123 123 123 123 123","",""]` {
+		t.Error("Unexpected result:", string(b))
+		return
+	}
+}

+ 61 - 40
cli/tool/interpret.go

@@ -62,6 +62,10 @@ type CLIInterpreter struct {
 	LogFile  *string // Logfile (blank for stdout)
 	LogLevel *string // Log level string (Debug, Info, Error)
 
+	// User terminal
+
+	Term termutil.ConsoleLineTerminal
+
 	// Log output
 
 	LogOut io.Writer
@@ -71,7 +75,7 @@ type CLIInterpreter struct {
 NewCLIInterpreter creates a new commandline interpreter for ECAL.
 */
 func NewCLIInterpreter() *CLIInterpreter {
-	return &CLIInterpreter{scope.NewScope(scope.GlobalScope), nil, nil, "", "", "", nil, nil, nil, os.Stdout}
+	return &CLIInterpreter{scope.NewScope(scope.GlobalScope), nil, nil, "", "", "", nil, nil, nil, nil, os.Stdout}
 }
 
 /*
@@ -92,15 +96,15 @@ func (i *CLIInterpreter) ParseArgs() bool {
 	showHelp := flag.Bool("help", false, "Show this help message")
 
 	flag.Usage = func() {
-		fmt.Println()
-		fmt.Println(fmt.Sprintf("Usage of %s run [options] [file]", os.Args[0]))
-		fmt.Println()
+		fmt.Fprintln(flag.CommandLine.Output())
+		fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Usage of %s run [options] [file]", osArgs[0]))
+		fmt.Fprintln(flag.CommandLine.Output())
 		flag.PrintDefaults()
-		fmt.Println()
+		fmt.Fprintln(flag.CommandLine.Output())
 	}
 
-	if len(os.Args) >= 2 {
-		flag.CommandLine.Parse(os.Args[2:])
+	if len(osArgs) >= 2 {
+		flag.CommandLine.Parse(osArgs[2:])
 
 		if cargs := flag.Args(); len(cargs) > 0 {
 			i.EntryFile = flag.Arg(0)
@@ -115,7 +119,7 @@ func (i *CLIInterpreter) ParseArgs() bool {
 }
 
 /*
-Create the runtime provider of this interpreter. This function expects Dir,
+CreateRuntimeProvider creates the runtime provider of this interpreter. This function expects Dir,
 LogFile and LogLevel to be set.
 */
 func (i *CLIInterpreter) CreateRuntimeProvider(name string) error {
@@ -130,6 +134,7 @@ func (i *CLIInterpreter) CreateRuntimeProvider(name string) error {
 
 	if i.LogFile != nil && *i.LogFile != "" {
 		var logWriter io.Writer
+
 		logFileRollover := fileutil.SizeBasedRolloverCondition(1000000) // Each file can be up to a megabyte
 		logWriter, err = fileutil.NewMultiFileBuffer(*i.LogFile, fileutil.ConsecutiveNumberIterator(10), logFileRollover)
 		logger = util.NewBufferLogger(logWriter)
@@ -180,21 +185,36 @@ func (i *CLIInterpreter) LoadInitialFile(tid uint64) error {
 
 		initFile, err = ioutil.ReadFile(i.EntryFile)
 
-		if ast, err = parser.ParseWithRuntime(i.EntryFile, string(initFile), i.RuntimeProvider); err == nil {
-			if err = ast.Runtime.Validate(); err == nil {
-				_, err = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{}), tid)
-			}
-			defer func() {
-				if i.RuntimeProvider.Debugger != nil {
-					i.RuntimeProvider.Debugger.RecordThreadFinished(tid)
+		if err == nil {
+			if ast, err = parser.ParseWithRuntime(i.EntryFile, string(initFile), i.RuntimeProvider); err == nil {
+				if err = ast.Runtime.Validate(); err == nil {
+					_, err = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{}), tid)
 				}
-			}()
+				defer func() {
+					if i.RuntimeProvider.Debugger != nil {
+						i.RuntimeProvider.Debugger.RecordThreadFinished(tid)
+					}
+				}()
+			}
 		}
 	}
 
 	return err
 }
 
+/*
+CreateTerm creates a new console terminal for stdout.
+*/
+func (i *CLIInterpreter) CreateTerm() error {
+	var err error
+
+	if i.Term == nil {
+		i.Term, err = termutil.NewConsoleLineTerminal(os.Stdout)
+	}
+
+	return err
+}
+
 /*
 Interpret starts the ECAL code interpreter. Starts an interactive console in
 the current tty if the interactive flag is set.
@@ -205,7 +225,7 @@ func (i *CLIInterpreter) Interpret(interactive bool) error {
 		return nil
 	}
 
-	clt, err := termutil.NewConsoleLineTerminal(os.Stdout)
+	err := i.CreateTerm()
 
 	if interactive {
 		fmt.Fprintln(i.LogOut, fmt.Sprintf("ECAL %v", config.ProductVersion))
@@ -235,39 +255,33 @@ func (i *CLIInterpreter) Interpret(interactive bool) error {
 
 			if err = i.LoadInitialFile(tid); err == nil {
 
-				if interactive {
+				// Drop into interactive shell
 
-					// Drop into interactive shell
-
-					if err == nil {
-						isExitLine := func(s string) bool {
-							return s == "exit" || s == "q" || s == "quit" || s == "bye" || s == "\x04"
-						}
+				if interactive {
 
-						// Add history functionality without file persistence
+					// Add history functionality without file persistence
 
-						clt, err = termutil.AddHistoryMixin(clt, "",
-							func(s string) bool {
-								return isExitLine(s)
-							})
+					i.Term, err = termutil.AddHistoryMixin(i.Term, "",
+						func(s string) bool {
+							return i.isExitLine(s)
+						})
 
-						if err == nil {
+					if err == nil {
 
-							if err = clt.StartTerm(); err == nil {
-								var line string
+						if err = i.Term.StartTerm(); err == nil {
+							var line string
 
-								defer clt.StopTerm()
+							defer i.Term.StopTerm()
 
-								fmt.Fprintln(i.LogOut, "Type 'q' or 'quit' to exit the shell and '?' to get help")
+							fmt.Fprintln(i.LogOut, "Type 'q' or 'quit' to exit the shell and '?' to get help")
 
-								line, err = clt.NextLine()
-								for err == nil && !isExitLine(line) {
-									trimmedLine := strings.TrimSpace(line)
+							line, err = i.Term.NextLine()
+							for err == nil && !i.isExitLine(line) {
+								trimmedLine := strings.TrimSpace(line)
 
-									i.HandleInput(clt, trimmedLine, tid)
+								i.HandleInput(i.Term, trimmedLine, tid)
 
-									line, err = clt.NextLine()
-								}
+								line, err = i.Term.NextLine()
 							}
 						}
 					}
@@ -279,6 +293,13 @@ func (i *CLIInterpreter) Interpret(interactive bool) error {
 	return err
 }
 
+/*
+isExitLine returns if a given input line should exit the interpreter.
+*/
+func (i *CLIInterpreter) isExitLine(s string) bool {
+	return s == "exit" || s == "q" || s == "quit" || s == "bye" || s == "\x04"
+}
+
 /*
 HandleInput handles input to this interpreter. It parses a given input line
 and outputs on the given output terminal. Requires a thread ID of the executing

+ 355 - 0
cli/tool/interpret_test.go

@@ -0,0 +1,355 @@
+/*
+ * 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 tool
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"reflect"
+	"strconv"
+	"strings"
+	"testing"
+
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/fileutil"
+	"devt.de/krotik/ecal/config"
+	"devt.de/krotik/ecal/interpreter"
+	"devt.de/krotik/ecal/stdlib"
+	"devt.de/krotik/ecal/util"
+)
+
+const testDir = "tooltest"
+
+var testLogOut *bytes.Buffer
+var testTerm *testConsoleLineTerminal
+
+func newTestInterpreter() *CLIInterpreter {
+	tin := NewCLIInterpreter()
+
+	// Redirect I/O bits into internal buffers
+
+	testTerm = &testConsoleLineTerminal{nil, bytes.Buffer{}}
+	tin.Term = testTerm
+
+	testLogOut = &bytes.Buffer{}
+	tin.LogOut = testLogOut
+
+	return tin
+}
+
+func newTestInterpreterWithConfig() *CLIInterpreter {
+	tin := newTestInterpreter()
+
+	// Setup
+	if res, _ := fileutil.PathExists(testDir); res {
+		os.RemoveAll(testDir)
+	}
+
+	err := os.Mkdir(testDir, 0770)
+	if err != nil {
+		fmt.Print("Could not create test directory:", err.Error())
+		os.Exit(1)
+	}
+
+	l := testDir
+	tin.Dir = &l
+
+	tin.CustomWelcomeMessage = "123"
+
+	// Teardown
+
+	return tin
+}
+
+func tearDown() {
+	err := os.RemoveAll(testDir)
+	if err != nil {
+		fmt.Print("Could not remove test directory:", err.Error())
+	}
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) // Reset CLI parsing
+}
+
+func TestInterpretBasicFunctions(t *testing.T) {
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) // Reset CLI parsing
+
+	// Test normal initialisation
+
+	tin := NewCLIInterpreter()
+	if err := tin.CreateTerm(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	tin = newTestInterpreter()
+
+	// Test help output
+
+	osArgs = []string{"foo", "bar", "-help"}
+
+	flag.CommandLine.SetOutput(&testTerm.out)
+
+	if stop := tin.ParseArgs(); !stop {
+		t.Error("Asking for help should request to stop the program")
+		return
+	}
+
+	if !strings.Contains(testTerm.out.String(), "Root directory for ECAL interpreter") {
+		t.Error("Helptext does not contain expected string - output:", testTerm.out.String())
+		return
+	}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) // Reset CLI parsing
+
+	// Test interpret
+
+	tin = newTestInterpreter()
+
+	osArgs = []string{"foo", "bar", "-help"}
+
+	flag.CommandLine.SetOutput(&testTerm.out)
+
+	errorutil.AssertOk(tin.Interpret(true))
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) // Reset CLI parsing
+
+	// Test entry file parsing
+
+	tin = NewCLIInterpreter()
+
+	osArgs = []string{"foo", "bar", "myfile"}
+
+	if stop := tin.ParseArgs(); stop {
+		t.Error("Giving an entry file should not stop the program")
+		return
+	}
+
+	if stop := tin.ParseArgs(); stop {
+		t.Error("Giving an entry file should not stop the program")
+		return
+	}
+
+	if tin.EntryFile != "myfile" {
+		t.Error("Unexpected entryfile:", tin.EntryFile)
+		return
+	}
+}
+
+func TestCreateRuntimeProvider(t *testing.T) {
+	tin := newTestInterpreterWithConfig()
+	defer tearDown()
+
+	l := filepath.Join(testDir, "test.log")
+	tin.LogFile = &l
+
+	if err := tin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if _, ok := tin.RuntimeProvider.Logger.(*util.BufferLogger); !ok {
+		t.Errorf("Unexpected logger: %#v", tin.RuntimeProvider.Logger)
+		return
+	}
+
+	tin = newTestInterpreterWithConfig()
+	defer tearDown()
+
+	l = "error"
+	tin.LogLevel = &l
+
+	if err := tin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if _, ok := tin.RuntimeProvider.Logger.(*util.LogLevelLogger); !ok {
+		t.Errorf("Unexpected logger: %#v", tin.RuntimeProvider.Logger)
+		return
+	}
+
+	if err := tin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if _, ok := tin.RuntimeProvider.Logger.(*util.LogLevelLogger); !ok {
+		t.Errorf("Unexpected logger: %#v", tin.RuntimeProvider.Logger)
+		return
+	}
+}
+
+func TestLoadInitialFile(t *testing.T) {
+	tin := NewCLIDebugInterpreter(newTestInterpreterWithConfig())
+	defer tearDown()
+
+	if err := tin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	tin.RuntimeProvider.Debugger = interpreter.NewECALDebugger(tin.GlobalVS)
+	tin.RuntimeProvider.Logger = util.NewMemoryLogger(10)
+	tin.RuntimeProvider.ImportLocator = &util.MemoryImportLocator{}
+
+	tin.EntryFile = filepath.Join(testDir, "foo.ecal")
+
+	ioutil.WriteFile(tin.EntryFile, []byte("a := 1"), 0777)
+
+	if err := tin.CLIInterpreter.LoadInitialFile(1); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if tin.GlobalVS.String() != `GlobalScope {
+    a (float64) : 1
+}` {
+		t.Error("Unexpected scope:", tin.GlobalVS)
+		return
+	}
+}
+
+func TestInterpret(t *testing.T) {
+	tin := newTestInterpreterWithConfig()
+	defer tearDown()
+
+	if err := tin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	tin.RuntimeProvider.Logger, _ = util.NewLogLevelLogger(util.NewMemoryLogger(10), "info")
+	tin.RuntimeProvider.ImportLocator = &util.MemoryImportLocator{
+		Files: map[string]string{
+			"foo": "a := 1",
+		},
+	}
+
+	l1 := ""
+	tin.LogFile = &l1
+	l2 := ""
+	tin.LogLevel = &l2
+
+	testTerm.in = []string{"xxx := 1", "q"}
+
+	if err := tin.Interpret(true); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if testLogOut.String() != `ECAL `+config.ProductVersion+`
+Log level: info - Root directory: tooltest
+123
+Type 'q' or 'quit' to exit the shell and '?' to get help
+` {
+		t.Error("Unexpected result:", testLogOut.String())
+		return
+	}
+
+	if tin.GlobalVS.String() != `GlobalScope {
+    xxx (float64) : 1
+}` {
+		t.Error("Unexpected scope:", tin.GlobalVS)
+		return
+	}
+}
+
+func TestHandleInput(t *testing.T) {
+	tin := newTestInterpreterWithConfig()
+	defer tearDown()
+
+	tin.CustomHandler = &testCustomHandler{}
+
+	if err := tin.CreateRuntimeProvider("foo"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	stdlib.AddStdlibPkg("foo", "bar")
+	stdlib.AddStdlibFunc("foo", "Println",
+		stdlib.NewECALFunctionAdapter(reflect.ValueOf(fmt.Println), "xxx"))
+	stdlib.AddStdlibFunc("foo", "Atoi",
+		stdlib.NewECALFunctionAdapter(reflect.ValueOf(strconv.Atoi), "xxx"))
+
+	tin.RuntimeProvider.Logger, _ = util.NewLogLevelLogger(util.NewMemoryLogger(10), "info")
+	tin.RuntimeProvider.ImportLocator = &util.MemoryImportLocator{}
+	tin.CustomHelpString = "123"
+
+	l1 := ""
+	tin.LogFile = &l1
+	l2 := ""
+	tin.LogLevel = &l2
+
+	testTerm.in = []string{"?", "@reload", "@sym", "@std", "@cus", "q"}
+
+	if err := tin.Interpret(true); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	// Just check for a simple string no need for the whole thing
+
+	if !strings.Contains(testTerm.out.String(), "New creates a new object instance.") {
+		t.Error("Unexpected result:", testTerm.out.String())
+		return
+	}
+
+	testTerm.out.Reset()
+
+	testTerm.in = []string{"@sym raise", "@std math.Phi", "@std foo Print", "q"}
+
+	if err := tin.Interpret(true); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if testTerm.out.String() != `╒═════════════════╤═══════════════════════════════╕
+│Inbuild function │Description                    │
+╞═════════════════╪═══════════════════════════════╡
+│raise            │Raise returns an error object. │
+│                 │                               │
+╘═════════════════╧═══════════════════════════════╛
+╒═════════╤══════════════════╕
+│Constant │Value             │
+╞═════════╪══════════════════╡
+│math.Phi │1.618033988749895 │
+│         │                  │
+╘═════════╧══════════════════╛
+╒════════════╤════════════╕
+│Function    │Description │
+╞════════════╪════════════╡
+│foo.Println │xxx         │
+│            │            │
+╘════════════╧════════════╛
+` {
+		t.Error("Unexpected result:", testTerm.out.String())
+		return
+	}
+
+	testTerm.out.Reset()
+
+	testTerm.in = []string{"1", "raise(123)", "q"}
+
+	if err := tin.Interpret(true); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if testTerm.out.String() != `1
+ECAL error in foo: 123 () (Line:1 Pos:1)
+` {
+		t.Error("Unexpected result:", testTerm.out.String())
+		return
+	}
+}

+ 47 - 43
cli/tool/pack.go

@@ -47,6 +47,9 @@ type CLIPacker struct {
 	LogOut io.Writer
 }
 
+var packmarkerend = "####"
+var packmarker = fmt.Sprintf("\n%v%v%v\n", packmarkerend, "ECALSRC", packmarkerend)
+
 /*
 NewCLIPacker creates a new commandline packer.
 */
@@ -63,7 +66,7 @@ func (p *CLIPacker) ParseArgs() bool {
 		return false
 	}
 
-	binname, err := filepath.Abs(os.Args[0])
+	binname, err := filepath.Abs(osArgs[0])
 	errorutil.AssertOk(err)
 
 	wd, _ := os.Getwd()
@@ -74,18 +77,18 @@ func (p *CLIPacker) ParseArgs() bool {
 	showHelp := flag.Bool("help", false, "Show this help message")
 
 	flag.Usage = func() {
-		fmt.Println()
-		fmt.Println(fmt.Sprintf("Usage of %s pack [options] [entry file]", os.Args[0]))
-		fmt.Println()
+		fmt.Fprintln(flag.CommandLine.Output())
+		fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Usage of %s pack [options] [entry file]", os.Args[0]))
+		fmt.Fprintln(flag.CommandLine.Output())
 		flag.PrintDefaults()
-		fmt.Println()
-		fmt.Println("This tool will collect all files in the root directory and " +
+		fmt.Fprintln(flag.CommandLine.Output())
+		fmt.Fprintln(flag.CommandLine.Output(), "This tool will collect all files in the root directory and "+
 			"build a standalone executable from the given source binary and the collected files.")
-		fmt.Println()
+		fmt.Fprintln(flag.CommandLine.Output())
 	}
 
 	if len(os.Args) >= 2 {
-		flag.CommandLine.Parse(os.Args[2:])
+		flag.CommandLine.Parse(osArgs[2:])
 
 		if cargs := flag.Args(); len(cargs) > 0 {
 			p.EntryFile = flag.Arg(0)
@@ -128,10 +131,7 @@ func (p *CLIPacker) Pack() error {
 				fmt.Fprintln(p.LogOut, fmt.Sprintf("Copied %v bytes for interpreter.", bytes))
 				var bytes int
 
-				end := "####"
-				marker := fmt.Sprintf("\n%v%v%v\n", end, "ECALSRC", end)
-
-				if bytes, err = dest.WriteString(marker); err == nil {
+				if bytes, err = dest.WriteString(packmarker); err == nil {
 					var data []byte
 					fmt.Fprintln(p.LogOut, fmt.Sprintf("Writing marker %v bytes for source archive.", bytes))
 
@@ -147,11 +147,12 @@ func (p *CLIPacker) Pack() error {
 
 								// Add files to the archive
 
-								if err = p.packFiles(w, *p.Dir, ""); err == nil {
-									err = w.Close()
-
+								defer func() {
+									w.Close()
 									os.Chmod(*p.TargetBinary, 0775) // Try a chmod but don't care about any errors
-								}
+								}()
+
+								err = p.packFiles(w, *p.Dir, "")
 							}
 						}
 					}
@@ -193,6 +194,16 @@ func (p *CLIPacker) packFiles(w *zip.Writer, filePath string, zipPath string) er
 	return err
 }
 
+var ( // Internal reading buffers
+	b1 = 4096
+	b2 = len(packmarker) + 11
+)
+
+/*
+handleError is the error handling function for runtime errors in packed binaries.
+*/
+var handleError func(error) = errorutil.AssertOk
+
 /*
 RunPackedBinary runs ECAL code is it has been attached to the currently running binary.
 Exits if attached ECAL code has been executed.
@@ -201,12 +212,9 @@ func RunPackedBinary() {
 	var retCode = 0
 	var result bool
 
-	exename, err := filepath.Abs(os.Args[0])
+	exename, err := filepath.Abs(osArgs[0])
 	errorutil.AssertOk(err)
 
-	end := "####"
-	marker := fmt.Sprintf("\n%v%v%v\n", end, "ECALSRC", end)
-
 	if ok, _ := fileutil.PathExists(exename); !ok {
 
 		// Try an optional .exe suffix which might work on Windows
@@ -224,8 +232,8 @@ func RunPackedBinary() {
 			defer f.Close()
 
 			found := false
-			buf := make([]byte, 4096)
-			buf2 := make([]byte, len(marker)+11)
+			buf := make([]byte, b1)
+			buf2 := make([]byte, b2)
 
 			// Look for the marker which marks the beginning of the attached zip file
 
@@ -240,12 +248,12 @@ func RunPackedBinary() {
 					if i2, err := f.Read(buf2); err == nil || err == io.EOF {
 						candidateString := string(append(buf, buf2...))
 
-						// Now determine the position if the zip file
+						// Now determine the position of the zip file
 
-						markerIndex := strings.Index(candidateString, marker)
+						markerIndex := strings.Index(candidateString, packmarker)
 
 						if found = markerIndex >= 0; found {
-							start := int64(markerIndex + len(marker))
+							start := int64(markerIndex + len(packmarker))
 							for unicode.IsSpace(rune(candidateString[start])) || unicode.IsControl(rune(candidateString[start])) {
 								start++ // Skip final control characters \n or \r\n
 							}
@@ -271,9 +279,8 @@ func RunPackedBinary() {
 
 					ret, err = runInterpreter(io.NewSectionReader(f, pos, zipLen), zipLen)
 
-					if retNum, ok := ret.(float64); ok {
-						retCode = int(retNum)
-					}
+					retNum, _ := ret.(float64)
+					retCode = int(retNum)
 
 					result = err == nil
 				}
@@ -281,10 +288,10 @@ func RunPackedBinary() {
 		}
 	}
 
-	errorutil.AssertOk(err)
+	handleError(err)
 
 	if result {
-		os.Exit(retCode)
+		osExit(retCode)
 	}
 }
 
@@ -299,27 +306,24 @@ func runInterpreter(reader io.ReaderAt, size int64) (interface{}, error) {
 	if err == nil {
 
 		for _, f := range r.File {
+			if err == nil {
+				if rc, err = f.Open(); err == nil {
+					var data []byte
 
-			if rc, err = f.Open(); err == nil {
-				var data []byte
-
-				defer rc.Close()
+					defer rc.Close()
 
-				if data, err = ioutil.ReadAll(rc); err == nil {
-					il.Files[f.Name] = string(data)
+					if data, err = ioutil.ReadAll(rc); err == nil {
+						il.Files[f.Name] = string(data)
+					}
 				}
 			}
-
-			if err != nil {
-				break
-			}
 		}
 	}
 
 	if err == nil {
 		var ast *parser.ASTNode
 
-		erp := interpreter.NewECALRuntimeProvider(os.Args[0], il, util.NewStdOutLogger())
+		erp := interpreter.NewECALRuntimeProvider(osArgs[0], il, util.NewStdOutLogger())
 
 		if ast, err = parser.ParseWithRuntime(os.Args[0], il.Files[".ecalsrc-entry"], erp); err == nil {
 			if err = ast.Runtime.Validate(); err == nil {
@@ -334,10 +338,10 @@ func runInterpreter(reader io.ReaderAt, size int64) (interface{}, error) {
 				res, err = ast.Runtime.Eval(vs, make(map[string]interface{}), erp.NewThreadID())
 
 				if err != nil {
-					fmt.Fprintln(os.Stderr, err.Error())
+					fmt.Fprintln(osStderr, err.Error())
 
 					if terr, ok := err.(util.TraceableRuntimeError); ok {
-						fmt.Fprintln(os.Stderr, fmt.Sprint("  ", strings.Join(terr.GetTraceString(), fmt.Sprint(fmt.Sprintln(), "  "))))
+						fmt.Fprintln(osStderr, fmt.Sprint("  ", strings.Join(terr.GetTraceString(), fmt.Sprint(fmt.Sprintln(), "  "))))
 					}
 
 					err = nil

+ 278 - 0
cli/tool/pack_test.go

@@ -0,0 +1,278 @@
+/*
+ * 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 tool
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/fileutil"
+	"devt.de/krotik/common/stringutil"
+)
+
+const packTestDir = "packtest"
+
+var testPackOut *bytes.Buffer
+
+var lastReturnCode = 0
+var lastRuntimeError error
+
+func setupPackTestDir() {
+	if res, _ := fileutil.PathExists(packTestDir); res {
+		os.RemoveAll(packTestDir)
+	}
+
+	err := os.Mkdir(packTestDir, 0770)
+	if err != nil {
+		fmt.Print("Could not create test directory:", err.Error())
+		os.Exit(1)
+	}
+
+	err = os.Mkdir(filepath.Join(packTestDir, "sub"), 0770)
+	if err != nil {
+		fmt.Print("Could not create test directory:", err.Error())
+		os.Exit(1)
+	}
+
+	osExit = func(code int) {
+		lastReturnCode = code
+	}
+
+	handleError = func(err error) {
+		lastRuntimeError = err
+	}
+}
+
+func tearDownPackTestDir() {
+	err := os.RemoveAll(packTestDir)
+	if err != nil {
+		fmt.Print("Could not remove test directory:", err.Error())
+	}
+}
+
+func newTestCLIPacker() *CLIPacker {
+	clip := NewCLIPacker()
+
+	testPackOut = &bytes.Buffer{}
+	clip.LogOut = testPackOut
+
+	return clip
+}
+
+func TestPackParseArgs(t *testing.T) {
+	setupPackTestDir()
+	defer tearDownPackTestDir()
+
+	clip := newTestCLIPacker()
+
+	packTestSrcBin := filepath.Join(packTestDir, "source.bin")
+	out := bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{packTestSrcBin, "foo", "-help"}
+
+	if ok := clip.ParseArgs(); !ok {
+		t.Error("Asking for help should ask to finish the program")
+		return
+	}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{packTestSrcBin, "foo", "-help"}
+
+	if err := clip.Pack(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if !strings.Contains(out.String(), "Root directory for ECAL interpreter") {
+		t.Error("Unexpected output:", out.String())
+		return
+	}
+
+	out = bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{packTestSrcBin, "foo", "myentryfile"}
+
+	if ok := clip.ParseArgs(); ok {
+		t.Error("Only asking for help should finish the program")
+		return
+	}
+
+	if ok := clip.ParseArgs(); ok {
+		t.Error("Only asking for help should finish the program")
+		return
+	}
+
+	if clip.EntryFile != "myentryfile" {
+		t.Error("Unexpected output:", clip.EntryFile)
+		return
+	}
+}
+
+func TestPackPacking(t *testing.T) {
+	setupPackTestDir()
+	defer tearDownPackTestDir()
+
+	clip := newTestCLIPacker()
+
+	packTestSrcBin := filepath.Join(packTestDir, "source.bin")
+	packTestEntry := filepath.Join(packTestDir, "myentry.ecal")
+	packAnotherFile := filepath.Join(packTestDir, "sub", "anotherfile.ecal")
+	packTestDestBin := filepath.Join(packTestDir, "dest.exe")
+
+	b1 = 5
+	b2 = len(packmarker) + 11
+
+	err := ioutil.WriteFile(packTestSrcBin, []byte("mybinaryfilecontent#somemorecontent"+
+		stringutil.GenerateRollingString("123", 30)), 0777)
+	errorutil.AssertOk(err)
+
+	err = ioutil.WriteFile(packTestEntry, []byte("myvar := 1; 5"), 0777)
+	errorutil.AssertOk(err)
+
+	err = ioutil.WriteFile(packAnotherFile, []byte("func f() { raise(123) };f()"), 0777)
+	errorutil.AssertOk(err)
+
+	out := bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	// Write a binary with return code
+
+	osArgs = []string{packTestSrcBin, "foo", "-dir", packTestDir, "-target",
+		packTestDestBin, packTestEntry}
+
+	// Simulate that whitespaces are added around the pack marker
+
+	oldpackmarker := packmarker
+	packmarker = fmt.Sprintf("\n\n\n%v\n\n\n", packmarker)
+
+	if err := clip.Pack(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	packmarker = oldpackmarker
+
+	if !strings.Contains(testPackOut.String(), "bytes for intro") {
+		t.Error("Unexpected output:", testPackOut.String())
+		return
+	}
+
+	// Write a binary with which errors
+
+	clip = newTestCLIPacker()
+
+	out = bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{packTestSrcBin, "foo", "-dir", packTestDir, "-target",
+		packTestDestBin + ".error", packAnotherFile}
+
+	if err := clip.Pack(); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if !strings.Contains(testPackOut.String(), "bytes for intro") {
+		t.Error("Unexpected output:", testPackOut.String())
+		return
+	}
+
+	// Write also a corrupted binary
+
+	err = ioutil.WriteFile(packTestDestBin+".corrupted", []byte(
+		"mybinaryfilecontent#somemorecontent"+
+			stringutil.GenerateRollingString("123", 30)+
+			"\n"+
+			packmarker+
+			"\n"+
+			stringutil.GenerateRollingString("123", 30)), 0777)
+
+	errorutil.AssertOk(err)
+
+	testRunningPackedBinary(t)
+}
+
+func testRunningPackedBinary(t *testing.T) {
+	packTestDestBin := filepath.Join(packTestDir, "dest") // Suffix .exe should be appended
+
+	out := bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{packTestDestBin + ".exe.corrupted"}
+
+	RunPackedBinary()
+
+	if lastRuntimeError == nil || lastRuntimeError.Error() != "zip: not a valid zip file" {
+		t.Error("Unexpected result:", lastRuntimeError)
+		return
+	}
+
+	out = bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+
+	osArgs = []string{packTestDestBin}
+
+	RunPackedBinary()
+
+	if lastRuntimeError != nil {
+		t.Error("Unexpected result:", lastRuntimeError)
+		return
+	}
+
+	if lastReturnCode != 5 {
+		t.Error("Unexpected result:", lastReturnCode)
+		return
+	}
+
+	out = bytes.Buffer{}
+
+	flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) // Reset CLI parsing
+	flag.CommandLine.SetOutput(&out)
+	osStderr = &out
+
+	osArgs = []string{packTestDestBin + ".exe.error"}
+
+	RunPackedBinary()
+
+	if lastRuntimeError != nil {
+		t.Error("Unexpected result:", lastRuntimeError)
+		return
+	}
+
+	if !strings.HasPrefix(out.String(), "ECAL error in packtest/dest.exe.error: 123 () (Line:1 Pos:12)") ||
+		!strings.Contains(out.String(), "raise(123)") {
+		t.Error("Unexpected result:", out.String())
+		return
+	}
+}

+ 1 - 1
ecal-support/README.md

@@ -16,7 +16,7 @@ The extension supports the following features:
 
 ## Install the extension
 
-The extention can be installed using a precompiled VSIX file which can be downloaded from here:
+The extension can be installed using a precompiled VSIX file which can be downloaded from here:
 
 https://devt.de/krotik/ecal/releases
 

+ 1 - 1
ecal-support/src/ecalDebugAdapter.ts

@@ -28,7 +28,7 @@ import * as path from "path";
 
 /**
  * ECALDebugArguments are the arguments which VSCode can pass to the debug adapter.
- * This defines the parameter which a VSCode instance using the ECAL extention can pass to the
+ * This defines the parameter which a VSCode instance using the ECAL extension can pass to the
  * debug adapter from a lauch configuration ('.vscode/launch.json') in a project folder.
  */
 interface ECALDebugArguments extends DebugProtocol.LaunchRequestArguments {

+ 1 - 1
ecal-support/src/extension.ts

@@ -18,7 +18,7 @@ class InlineDebugAdapterFactory
   createDebugAdapterDescriptor(
     _session: vscode.DebugSession
   ): ProviderResult<vscode.DebugAdapterDescriptor> {
-    // Declare the ECALDebugSession as an DebugAdapterInlineImplementation so extention and adapter can
+    // Declare the ECALDebugSession as an DebugAdapterInlineImplementation so extension and adapter can
     // run in-process (making it possible to easily debug the adapter)
     return new vscode.DebugAdapterInlineImplementation(new ECALDebugSession());
   }

+ 18 - 13
engine/processor_test.go

@@ -142,7 +142,7 @@ func TestProcessorSimpleCascade(t *testing.T) {
 	rootm.SetFinishHandler(func(p Processor) {
 		log.WriteString("finished!")
 	})
-	proc.AddEvent(e, rootm)
+	proc.AddEventAndWait(e, rootm)
 
 	if err := proc.AddRule(rule3); err.Error() != "Cannot add rule if the processor has not stopped" {
 		t.Error("Unexpected error:", err)
@@ -187,7 +187,7 @@ finished!` {
 
 	// Push a root event
 
-	proc.AddEvent(&Event{
+	proc.AddEventAndWait(&Event{
 		"InitialEvent",
 		[]string{"core", "main", "event1"},
 		nil,
@@ -700,7 +700,7 @@ func TestProcessorSimpleErrorHandling(t *testing.T) {
 
 	// Push a root event
 
-	mon, err := proc.AddEvent(&Event{
+	mon, err := proc.AddEventAndWait(&Event{
 		"InitialEvent",
 		[]string{"core", "main", "event1"},
 		map[interface{}]interface{}{"name": "foo", "test": "123"},
@@ -720,7 +720,7 @@ func TestProcessorSimpleErrorHandling(t *testing.T) {
 	}
 
 	_, err = proc.AddEvent(&Event{}, nil)
-	if err.Error() != "Cannot add event if the processor is not running" {
+	if err.Error() != "Cannot add event if the processor is stopping or not running" {
 		t.Error("Unexpected error", err)
 		return
 	}
@@ -749,25 +749,30 @@ InitialEvent -> event2 -> event3 -> TestRule3 : testerror2]` {
 		return
 	}
 
+	testProcessorAdvancedErrorHandling(t, proc, &recordedErrors)
+}
+
+func testProcessorAdvancedErrorHandling(t *testing.T, proc Processor, recordedErrorsPtr *int) {
+
 	// Second test will fail on the first failed rule in an event trigger sequence
 
 	proc.SetFailOnFirstErrorInTriggerSequence(true)
 
 	proc.Start()
 
-	mon, err = proc.AddEvent(&Event{
+	mon, err := proc.AddEventAndWait(&Event{
 		"InitialEvent",
 		[]string{"core", "main", "event1"},
 		map[interface{}]interface{}{"name": "foo", "test": "123"},
 	}, nil)
-	rmon, ok = mon.(*RootMonitor)
+	rmon, ok := mon.(*RootMonitor)
 	if !ok {
 		t.Error("Root monitor expected:", mon, err)
 		return
 	}
 	proc.Finish()
 
-	errs = rmon.AllErrors()
+	errs := rmon.AllErrors()
 
 	if len(errs) != 2 {
 		t.Error("Unexpected number of errors:", len(errs))
@@ -781,8 +786,8 @@ InitialEvent -> event2 -> event3 -> TestRule3 : testerror2]` {
 		return
 	}
 
-	if recordedErrors != 1 {
-		t.Error("Unexpected number of recorded errors:", recordedErrors)
+	if *recordedErrorsPtr != 1 {
+		t.Error("Unexpected number of recorded errors:", *recordedErrorsPtr)
 		return
 	}
 
@@ -797,8 +802,8 @@ InitialEvent -> event2 -> event3 -> TestRule3 : testerror2]` {
 		map[interface{}]interface{}{"name": "foo", "test": "123"},
 	}, nil)
 
-	if mon != nil {
-		t.Error("Nothing should have triggered")
+	if mon != nil || err != nil {
+		t.Error("Nothing should have triggered: ", err)
 		return
 	}
 
@@ -833,8 +838,8 @@ InitialEvent -> event2 -> event3 -> TestRule3 : testerror2]` {
 		return
 	}
 
-	if recordedErrors != 3 {
-		t.Error("Unexpected number of recorded errors:", recordedErrors)
+	if *recordedErrorsPtr != 3 {
+		t.Error("Unexpected number of recorded errors:", *recordedErrorsPtr)
 		return
 	}
 

+ 71 - 62
engine/pubsub/eventpump_test.go

@@ -18,49 +18,46 @@ import (
 	"testing"
 )
 
-func TestEventPump(t *testing.T) {
-	var res []string
-
-	source1 := &bytes.Buffer{}
-	source2 := errors.New("TEST")
+var res []string
+var source1 = &bytes.Buffer{}
+var errSource2 error = fmt.Errorf("TEST")
+var ep = NewEventPump()
 
-	ep := NewEventPump()
+func addObservers2(t *testing.T) {
 
-	// Add observer 1
+	// Add observer 4
 
-	ep.AddObserver("event1", source1, func(event string, eventSource interface{}) {
+	ep.AddObserver("", source1, func(event string, eventSource interface{}) {
 		if eventSource != source1 {
 			t.Error("Unexpected event source:", eventSource)
 			return
 		}
-		res = append(res, "1")
+		res = append(res, "4")
 		sort.Strings(res)
-
 	})
 
-	// Add observer 2
+	// Add observer 5
 
-	ep.AddObserver("event2", source2, func(event string, eventSource interface{}) {
-		if eventSource != source2 {
-			t.Error("Unexpected event source:", eventSource)
-			return
-		}
-		res = append(res, "2")
+	ep.AddObserver("", nil, func(event string, eventSource interface{}) {
+		res = append(res, "5")
 		sort.Strings(res)
-
 	})
 
-	// Add observer 3
+	// Add observer 6
 
-	ep.AddObserver("event2", source2, func(event string, eventSource interface{}) {
-		if eventSource != source2 {
+	ep.AddObserver("", errSource2, func(event string, eventSource interface{}) {
+		if eventSource != errSource2 {
 			t.Error("Unexpected event source:", eventSource)
 			return
 		}
-		res = append(res, "3")
+		res = append(res, "6")
 		sort.Strings(res)
-
 	})
+}
+
+func TestEventPump(t *testing.T) {
+
+	addObservers1(t)
 
 	// Run the tests
 
@@ -75,7 +72,7 @@ func TestEventPump(t *testing.T) {
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event2", source2)
+	ep.PostEvent("event2", errSource2)
 
 	if fmt.Sprint(res) != "[2 3]" {
 		t.Error("Unexpected result:", res)
@@ -84,45 +81,18 @@ func TestEventPump(t *testing.T) {
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event1", source2)
+	ep.PostEvent("event1", errSource2)
 
 	if fmt.Sprint(res) != "[]" {
 		t.Error("Unexpected result:", res)
 		return
 	}
 
-	// Add observer 4
-
-	ep.AddObserver("", source1, func(event string, eventSource interface{}) {
-		if eventSource != source1 {
-			t.Error("Unexpected event source:", eventSource)
-			return
-		}
-		res = append(res, "4")
-		sort.Strings(res)
-	})
-
-	// Add observer 5
-
-	ep.AddObserver("", nil, func(event string, eventSource interface{}) {
-		res = append(res, "5")
-		sort.Strings(res)
-	})
-
-	// Add observer 6
-
-	ep.AddObserver("", source2, func(event string, eventSource interface{}) {
-		if eventSource != source2 {
-			t.Error("Unexpected event source:", eventSource)
-			return
-		}
-		res = append(res, "6")
-		sort.Strings(res)
-	})
+	addObservers2(t)
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event1", source2)
+	ep.PostEvent("event1", errSource2)
 
 	if fmt.Sprint(res) != "[5 6]" {
 		t.Error("Unexpected result:", res)
@@ -131,7 +101,7 @@ func TestEventPump(t *testing.T) {
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event3", source2)
+	ep.PostEvent("event3", errSource2)
 
 	if fmt.Sprint(res) != "[5 6]" {
 		t.Error("Unexpected result:", res)
@@ -160,28 +130,28 @@ func TestEventPump(t *testing.T) {
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event2", source2)
+	ep.PostEvent("event2", errSource2)
 
 	if fmt.Sprint(res) != "[2 3 5 6]" {
 		t.Error("Unexpected result:", res)
 		return
 	}
-	ep.RemoveObservers("event2", source2)
+	ep.RemoveObservers("event2", errSource2)
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event2", source2)
+	ep.PostEvent("event2", errSource2)
 
 	if fmt.Sprint(res) != "[5 6]" {
 		t.Error("Unexpected result:", res)
 		return
 	}
 
-	ep.RemoveObservers("", source2) // Remove all handlers specific to source 2
+	ep.RemoveObservers("", errSource2) // Remove all handlers specific to source 2
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event2", source2)
+	ep.PostEvent("event2", errSource2)
 
 	if fmt.Sprint(res) != "[5]" {
 		t.Error("Unexpected result:", res)
@@ -199,7 +169,7 @@ func TestEventPump(t *testing.T) {
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event2", source2)
+	ep.PostEvent("event2", errSource2)
 
 	if fmt.Sprint(res) != "[5]" {
 		t.Error("Unexpected result:", res)
@@ -210,7 +180,7 @@ func TestEventPump(t *testing.T) {
 
 	res = make([]string, 0) // Reset res
 
-	ep.PostEvent("event2", source2)
+	ep.PostEvent("event2", errSource2)
 
 	if fmt.Sprint(res) != "[]" {
 		t.Error("Unexpected result:", res)
@@ -227,6 +197,45 @@ func TestEventPump(t *testing.T) {
 	}
 }
 
+func addObservers1(t *testing.T) {
+
+	// Add observer 1
+
+	ep.AddObserver("event1", source1, func(event string, eventSource interface{}) {
+		if eventSource != source1 {
+			t.Error("Unexpected event source:", eventSource)
+			return
+		}
+		res = append(res, "1")
+		sort.Strings(res)
+
+	})
+
+	// Add observer 2
+
+	ep.AddObserver("event2", errSource2, func(event string, eventSource interface{}) {
+		if eventSource != errSource2 {
+			t.Error("Unexpected event source:", eventSource)
+			return
+		}
+		res = append(res, "2")
+		sort.Strings(res)
+
+	})
+
+	// Add observer 3
+
+	ep.AddObserver("event2", errSource2, func(event string, eventSource interface{}) {
+		if eventSource != errSource2 {
+			t.Error("Unexpected event source:", eventSource)
+			return
+		}
+		res = append(res, "3")
+		sort.Strings(res)
+
+	})
+}
+
 func TestWrongPostEvent(t *testing.T) {
 	defer func() {
 		if r := recover(); r == nil {

+ 21 - 22
engine/taskqueue_test.go

@@ -33,16 +33,10 @@ func TestTaskQueue(t *testing.T) {
 	// Create different root monitors with different IDs
 
 	m1 := newRootMonitor(nil, NewRuleScope(map[string]bool{"": true}), proc.(*eventProcessor).messageQueue)
-	m2 := newRootMonitor(nil, NewRuleScope(map[string]bool{"": true}), proc.(*eventProcessor).messageQueue)
-	m3 := newRootMonitor(nil, NewRuleScope(map[string]bool{"": true}), proc.(*eventProcessor).messageQueue)
 
 	// Create now different tasks which come from the different monitors
 
 	t1 := &Task{proc, m1, event}
-	t2 := &Task{proc, m2, event}
-	t3 := &Task{proc, m3, event}
-	t4 := &Task{proc, m2.NewChildMonitor(5), event}
-	t5 := &Task{proc, m2.NewChildMonitor(10), event}
 
 	tq := NewTaskQueue(proc.(*eventProcessor).messageQueue)
 
@@ -70,6 +64,19 @@ func TestTaskQueue(t *testing.T) {
 		return
 	}
 
+	testTaskQueuePushPop(t, tq, proc, event, t1)
+}
+
+func testTaskQueuePushPop(t *testing.T, tq *TaskQueue, proc Processor, event *Event, t1 *Task) {
+
+	m2 := newRootMonitor(nil, NewRuleScope(map[string]bool{"": true}), proc.(*eventProcessor).messageQueue)
+	m3 := newRootMonitor(nil, NewRuleScope(map[string]bool{"": true}), proc.(*eventProcessor).messageQueue)
+
+	t2 := &Task{proc, m2, event}
+	t3 := &Task{proc, m3, event}
+	t4 := &Task{proc, m2.NewChildMonitor(5), event}
+	t5 := &Task{proc, m2.NewChildMonitor(10), event}
+
 	tq.Push(t1)
 	tq.Push(t2)
 	tq.Push(t3)
@@ -101,35 +108,23 @@ func TestTaskQueue(t *testing.T) {
 		return
 	}
 
-	if e := tq.Pop(); e != t1 && e != t2 && e != t3 && e != t4 && e != t5 {
-		t.Error("Unexpected event:", e)
-		return
-	}
+	tq.Pop()
 
 	if res := len(tq.queues); res != 3 && res != 2 {
 		t.Error("Unexpected size:", res)
 		return
 	}
 
-	if e := tq.Pop(); e != t1 && e != t2 && e != t3 && e != t4 && e != t5 {
-		t.Error("Unexpected event:", e)
-		return
-	}
+	tq.Pop()
 
 	if s := tq.Size(); s != 2 {
 		t.Error("Unexpected result:", s)
 		return
 	}
 
-	if e := tq.Pop(); e != t1 && e != t2 && e != t3 && e != t4 && e != t5 {
-		t.Error("Unexpected event:", e)
-		return
-	}
+	tq.Pop()
 
-	if e := tq.Pop(); e != t1 && e != t2 && e != t3 && e != t4 && e != t5 {
-		t.Error("Unexpected event:", e)
-		return
-	}
+	tq.Pop()
 
 	if s := tq.Size(); s != 0 {
 		t.Error("Unexpected result:", s)
@@ -141,6 +136,10 @@ func TestTaskQueue(t *testing.T) {
 		return
 	}
 
+	testTaskQueueMisc(t, tq, t5)
+}
+
+func testTaskQueueMisc(t *testing.T, tq *TaskQueue, t5 *Task) {
 	tq.Push(t5)
 
 	if fmt.Sprint(tq.queues) != "map[2:[ Task: RumbleProcessor 1 (workers:1) Monitor 5 (parent: Monitor 2 (parent: <nil> priority: 0 activated: false finished: false) priority: 10 activated: false finished: false) Event: DummyEvent main {} (10) ]]" {

+ 8 - 2
examples/embedding/main.go

@@ -83,11 +83,11 @@ mystuff.add(compute(5), 1)
 
 	// Each thread which evaluates the Runtime of an AST should get a unique thread ID
 
-	var threadId uint64 = 1
+	var threadID uint64 = 1
 
 	// Evaluate the Runtime of an AST with a variable scope
 
-	res, err := ast.Runtime.Eval(vs, make(map[string]interface{}), threadId)
+	res, err := ast.Runtime.Eval(vs, make(map[string]interface{}), threadID)
 	if err != nil {
 		log.Fatal(err)
 	}
@@ -123,6 +123,9 @@ AddFunc is a simple add function which calculates the sum of two numbers.
 type AddFunc struct {
 }
 
+/*
+Run executes the add function
+*/
 func (f *AddFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
 
 	// This should have some proper error checking
@@ -133,6 +136,9 @@ func (f *AddFunc) Run(instanceID string, vs parser.Scope, is map[string]interfac
 	return args[0].(float64) + args[1].(float64), nil
 }
 
+/*
+DocString returns the doc string for the add function.
+*/
 func (f *AddFunc) DocString() (string, error) {
 	return "Sum up two numbers", nil
 }

+ 18 - 18
interpreter/debug.go

@@ -90,7 +90,7 @@ func newInterrogationState(node *parser.ASTNode, vs parser.Scope) *interrogation
 }
 
 /*
-NewDebugger returns a new debugger object.
+NewECALDebugger returns a new debugger object.
 */
 func NewECALDebugger(globalVS parser.Scope) util.ECALDebugger {
 	return &ecalDebugger{
@@ -135,7 +135,7 @@ func (ed *ecalDebugger) HandleInput(input string) (interface{}, error) {
 /*
 StopThreads will continue all suspended threads and set them to be killed.
 Returns true if a waiting thread was resumed. Can wait for threads to end
-by ensuring that for at least d time no state change occured.
+by ensuring that for at least d time no state change occurred.
 */
 func (ed *ecalDebugger) StopThreads(d time.Duration) bool {
 	var ret = false
@@ -445,17 +445,17 @@ func (ed *ecalDebugger) RemoveBreakPoint(source string, line int) {
 ExtractValue copies a value from a suspended thread into the
 global variable scope.
 */
-func (ed *ecalDebugger) ExtractValue(threadId uint64, varName string, destVarName string) error {
+func (ed *ecalDebugger) ExtractValue(threadID uint64, varName string, destVarName string) error {
 	if ed.globalScope == nil {
 		return fmt.Errorf("Cannot access global scope")
 	}
 
-	err := fmt.Errorf("Cannot find suspended thread %v", threadId)
+	err := fmt.Errorf("Cannot find suspended thread %v", threadID)
 
 	ed.lock.Lock()
 	defer ed.lock.Unlock()
 
-	is, ok := ed.interrogationStates[threadId]
+	is, ok := ed.interrogationStates[threadID]
 
 	if ok && !is.running {
 		var val interface{}
@@ -475,17 +475,17 @@ func (ed *ecalDebugger) ExtractValue(threadId uint64, varName string, destVarNam
 InjectValue copies a value from an expression (using the global variable scope) into
 a suspended thread.
 */
-func (ed *ecalDebugger) InjectValue(threadId uint64, varName string, expression string) error {
+func (ed *ecalDebugger) InjectValue(threadID uint64, varName string, expression string) error {
 	if ed.globalScope == nil {
 		return fmt.Errorf("Cannot access global scope")
 	}
 
-	err := fmt.Errorf("Cannot find suspended thread %v", threadId)
+	err := fmt.Errorf("Cannot find suspended thread %v", threadID)
 
 	ed.lock.Lock()
 	defer ed.lock.Unlock()
 
-	is, ok := ed.interrogationStates[threadId]
+	is, ok := ed.interrogationStates[threadID]
 
 	if ok && !is.running {
 		var ast *parser.ASTNode
@@ -515,11 +515,11 @@ func (ed *ecalDebugger) InjectValue(threadId uint64, varName string, expression
 /*
 Continue will continue a suspended thread.
 */
-func (ed *ecalDebugger) Continue(threadId uint64, contType util.ContType) {
+func (ed *ecalDebugger) Continue(threadID uint64, contType util.ContType) {
 	ed.lock.RLock()
 	defer ed.lock.RUnlock()
 
-	if is, ok := ed.interrogationStates[threadId]; ok && !is.running {
+	if is, ok := ed.interrogationStates[threadID]; ok && !is.running {
 
 		switch contType {
 		case util.Resume:
@@ -530,7 +530,7 @@ func (ed *ecalDebugger) Continue(threadId uint64, contType util.ContType) {
 			is.cmd = StepOver
 		case util.StepOut:
 			is.cmd = StepOut
-			stack := ed.callStacks[threadId]
+			stack := ed.callStacks[threadID]
 			is.stepOutStack = stack[:len(stack)-1]
 		}
 
@@ -581,17 +581,17 @@ func (ed *ecalDebugger) Status() interface{} {
 }
 
 /*
-Describe decribes a thread currently observed by the debugger.
+Describe describes a thread currently observed by the debugger.
 */
-func (ed *ecalDebugger) Describe(threadId uint64) interface{} {
+func (ed *ecalDebugger) Describe(threadID uint64) interface{} {
 	ed.lock.RLock()
 	defer ed.lock.RUnlock()
 
 	var res map[string]interface{}
 
-	threadCallStack, ok1 := ed.callStacks[threadId]
+	threadCallStack, ok1 := ed.callStacks[threadID]
 
-	if is, ok2 := ed.interrogationStates[threadId]; ok1 && ok2 {
+	if is, ok2 := ed.interrogationStates[threadID]; ok1 && ok2 {
 		callStackNode := make([]map[string]interface{}, 0)
 
 		for _, sn := range threadCallStack {
@@ -603,8 +603,8 @@ func (ed *ecalDebugger) Describe(threadId uint64) interface{} {
 			"error":                     is.err,
 			"callStack":                 ed.prettyPrintCallStack(threadCallStack),
 			"callStackNode":             callStackNode,
-			"callStackVsSnapshot":       ed.callStackVsSnapshots[threadId],
-			"callStackVsSnapshotGlobal": ed.callStackGlobalVsSnapshots[threadId],
+			"callStackVsSnapshot":       ed.callStackVsSnapshots[threadID],
+			"callStackVsSnapshotGlobal": ed.callStackGlobalVsSnapshots[threadID],
 		}
 
 		if !is.running {
@@ -672,7 +672,7 @@ func (ed *ecalDebugger) MergeMaps(maps ...map[string]interface{}) map[string]int
 }
 
 /*
-Describe decribes a thread currently observed by the debugger.
+Describe describes a thread currently observed by the debugger.
 */
 func (ed *ecalDebugger) prettyPrintCallStack(threadCallStack []*parser.ASTNode) []string {
 	cs := []string{}

+ 10 - 10
interpreter/debug_cmd.go

@@ -23,7 +23,7 @@ import (
 )
 
 /*
-InbuildDebugCommandsMap contains the mapping of inbuild debug commands.
+DebugCommandsMap contains the mapping of inbuild debug commands.
 */
 var DebugCommandsMap = map[string]util.DebugCommand{
 	"breakonstart": &breakOnStartCommand{&inbuildDebugCommand{}},
@@ -231,7 +231,7 @@ func (c *contCommand) Run(debugger util.ECALDebugger, args []string) (interface{
 		return nil, fmt.Errorf("Need a thread ID and a command Resume, StepIn, StepOver or StepOut")
 	}
 
-	threadId, err := c.AssertNumParam(1, args[0])
+	threadID, err := c.AssertNumParam(1, args[0])
 
 	if err == nil {
 		cmdString := strings.ToLower(args[1])
@@ -248,7 +248,7 @@ func (c *contCommand) Run(debugger util.ECALDebugger, args []string) (interface{
 			return nil, fmt.Errorf("Invalid command %v - must be resume, stepin, stepover or stepout", cmdString)
 		}
 
-		debugger.Continue(threadId, cmd)
+		debugger.Continue(threadID, cmd)
 	}
 
 	return nil, err
@@ -258,7 +258,7 @@ func (c *contCommand) Run(debugger util.ECALDebugger, args []string) (interface{
 DocString returns a descriptive text about this command.
 */
 func (c *contCommand) DocString() string {
-	return "Continues a suspended thread. Specify <threadId> <Resume | StepIn | StepOver | StepOut>"
+	return "Continues a suspended thread. Specify <threadID> <Resume | StepIn | StepOver | StepOut>"
 }
 
 // describe
@@ -282,11 +282,11 @@ func (c *describeCommand) Run(debugger util.ECALDebugger, args []string) (interf
 		return nil, fmt.Errorf("Need a thread ID")
 	}
 
-	threadId, err := c.AssertNumParam(1, args[0])
+	threadID, err := c.AssertNumParam(1, args[0])
 
 	if err == nil {
 
-		res = debugger.Describe(threadId)
+		res = debugger.Describe(threadID)
 	}
 
 	return res, err
@@ -344,7 +344,7 @@ func (c *extractCommand) Run(debugger util.ECALDebugger, args []string) (interfa
 		return nil, fmt.Errorf("Need a thread ID, a variable name and a destination variable name")
 	}
 
-	threadId, err := c.AssertNumParam(1, args[0])
+	threadID, err := c.AssertNumParam(1, args[0])
 
 	if err == nil {
 		if !parser.NamePattern.MatchString(args[1]) || !parser.NamePattern.MatchString(args[2]) {
@@ -352,7 +352,7 @@ func (c *extractCommand) Run(debugger util.ECALDebugger, args []string) (interfa
 		}
 
 		if err == nil {
-			err = debugger.ExtractValue(threadId, args[1], args[2])
+			err = debugger.ExtractValue(threadID, args[1], args[2])
 		}
 	}
 
@@ -386,13 +386,13 @@ func (c *injectCommand) Run(debugger util.ECALDebugger, args []string) (interfac
 		return nil, fmt.Errorf("Need a thread ID, a variable name and an expression")
 	}
 
-	threadId, err := c.AssertNumParam(1, args[0])
+	threadID, err := c.AssertNumParam(1, args[0])
 
 	if err == nil {
 		varName := args[1]
 		expression := strings.Join(args[2:], " ")
 
-		err = debugger.InjectValue(threadId, varName, expression)
+		err = debugger.InjectValue(threadID, varName, expression)
 	}
 
 	return nil, err

+ 86 - 122
interpreter/debug_test.go

@@ -33,18 +33,12 @@ func TestSimpleDebugging(t *testing.T) {
 
 	testDebugger = NewECALDebugger(nil)
 
-	if _, err = testDebugger.HandleInput("break ECALEvalTest:3"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
-	if _, err = testDebugger.HandleInput("break ECALEvalTest:4"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
-	if _, err = testDebugger.HandleInput("disablebreak ECALEvalTest:4"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput("break ECALEvalTest:3")
+	errorutil.AssertOk(err)
+	_, err = testDebugger.HandleInput("break ECALEvalTest:4")
+	errorutil.AssertOk(err)
+	_, err = testDebugger.HandleInput("disablebreak ECALEvalTest:4")
+	errorutil.AssertOk(err)
 
 	wg := &sync.WaitGroup{}
 	wg.Add(1)
@@ -145,10 +139,8 @@ log("test3")
 
 	// Continue until the end
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v Resume", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v Resume", tid))
+	errorutil.AssertOk(err)
 
 	wg.Wait()
 
@@ -160,10 +152,8 @@ test3`[1:] {
 		return
 	}
 
-	if _, err = testDebugger.HandleInput("rmbreak ECALEvalTest:4"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput("rmbreak ECALEvalTest:4")
+	errorutil.AssertOk(err)
 
 	out, err = testDebugger.HandleInput(fmt.Sprintf("status"))
 
@@ -184,15 +174,11 @@ test3`[1:] {
 		return
 	}
 
-	if _, err = testDebugger.HandleInput("break ECALEvalTest:4"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput("break ECALEvalTest:4")
+	errorutil.AssertOk(err)
 
-	if _, err = testDebugger.HandleInput("rmbreak ECALEvalTest"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput("rmbreak ECALEvalTest")
+	errorutil.AssertOk(err)
 
 	out, err = testDebugger.HandleInput(fmt.Sprintf("status"))
 
@@ -552,17 +538,17 @@ log("test4")
 func waitForThreadSuspension(t *testing.T) uint64 {
 	var tid uint64
 
-	for i := 0; i < 100; i += 1 {
+	for i := 0; i < 100; i++ {
 		state, err := testDebugger.HandleInput("status")
 		errorutil.AssertOk(err)
 
 		threads := state.(map[string]interface{})["threads"].(map[string]map[string]interface{})
 		if len(threads) > 0 {
-			for threadId, status := range threads {
+			for threadID, status := range threads {
 
 				if r, ok := status["threadRunning"]; ok && !r.(bool) {
-					threadIdNum, _ := strconv.ParseInt(threadId, 10, 0)
-					tid = uint64(threadIdNum)
+					threadIDNum, _ := strconv.ParseInt(threadID, 10, 0)
+					tid = uint64(threadIDNum)
 					return tid
 				}
 			}
@@ -577,7 +563,7 @@ func waitForThreadSuspension(t *testing.T) uint64 {
 func waitForAllThreadSuspension(t *testing.T) uint64 {
 	var tid uint64
 
-	for i := 0; i < 100; i += 1 {
+	for i := 0; i < 100; i++ {
 		state, err := testDebugger.HandleInput("status")
 		errorutil.AssertOk(err)
 
@@ -640,15 +626,11 @@ d(d())
 log("finish")
 `
 
-	if _, err = testDebugger.HandleInput("break ECALEvalTest:10"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput("break ECALEvalTest:10")
+	errorutil.AssertOk(err)
 
-	if _, err = testDebugger.HandleInput("breakonstart true"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput("breakonstart true")
+	errorutil.AssertOk(err)
 
 	wg := &sync.WaitGroup{}
 	wg.Add(1)
@@ -681,10 +663,8 @@ log("finish")
 		return
 	}
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v resume", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v resume", tid))
+	errorutil.AssertOk(err)
 
 	tid = waitForThreadSuspension(t)
 
@@ -713,10 +693,8 @@ log("finish")
 
 	// Step in without a function
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepin", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v stepin", tid))
+	errorutil.AssertOk(err)
 
 	tid = waitForThreadSuspension(t)
 
@@ -746,10 +724,8 @@ log("finish")
 
 	// Normal step over
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepover", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v stepover", tid))
+	errorutil.AssertOk(err)
 
 	tid = waitForThreadSuspension(t)
 
@@ -779,10 +755,8 @@ log("finish")
 
 	// Normal step in
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepin", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v stepin", tid))
+	errorutil.AssertOk(err)
 
 	tid = waitForThreadSuspension(t)
 
@@ -810,10 +784,8 @@ log("finish")
 
 	// Normal step out
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepout", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v stepout", tid))
+	errorutil.AssertOk(err)
 
 	tid = waitForThreadSuspension(t)
 
@@ -843,12 +815,15 @@ log("finish")
 
 	// Step in and step out - we should end up on the same line as before
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepin", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v stepin", tid))
+	errorutil.AssertOk(err)
 
-	tid = waitForThreadSuspension(t)
+	testStepDebugging2(t, testDebugger, wg)
+}
+
+func testStepDebugging2(t *testing.T, testDebugger util.ECALDebugger, wg *sync.WaitGroup) {
+
+	tid := waitForThreadSuspension(t)
 
 	if state := getDebuggerState(tid, t); state != `{
   "breakpoints": {
@@ -872,10 +847,9 @@ log("finish")
 		return
 	}
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepout", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepout", tid))
+	errorutil.AssertOk(err)
+
 	tid = waitForThreadSuspension(t)
 
 	if state := getDebuggerState(tid, t); state != `{
@@ -904,10 +878,9 @@ log("finish")
 
 	// Normal step out
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepout", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v stepout", tid))
+	errorutil.AssertOk(err)
+
 	tid = waitForThreadSuspension(t)
 
 	if state := getDebuggerState(tid, t); state != `{
@@ -935,15 +908,11 @@ log("finish")
 
 	// Set a new breakpoint
 
-	if _, err = testDebugger.HandleInput("break ECALEvalTest:28"); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput("break ECALEvalTest:28")
+	errorutil.AssertOk(err)
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v Resume", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v Resume", tid))
+	errorutil.AssertOk(err)
 
 	tid = waitForThreadSuspension(t)
 
@@ -974,10 +943,9 @@ log("finish")
 
 	// Normal step over
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v stepover", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v stepover", tid))
+	errorutil.AssertOk(err)
+
 	tid = waitForThreadSuspension(t)
 
 	if state := getDebuggerState(tid, t); state != `{
@@ -1038,10 +1006,8 @@ log("finish")
 
 	// Continue until the end
 
-	if _, err := testDebugger.HandleInput(fmt.Sprintf("cont %v Resume", tid)); err != nil {
-		t.Error("Unexpected result:", err)
-		return
-	}
+	_, err = testDebugger.HandleInput(fmt.Sprintf("cont %v Resume", tid))
+	errorutil.AssertOk(err)
 
 	wg.Wait()
 
@@ -1463,38 +1429,32 @@ func TestDebuggingErrorInput(t *testing.T) {
 
 	testDebugger = NewECALDebugger(vs)
 
-	if _, err = testDebugger.HandleInput("uuu"); err == nil ||
-		err.Error() != `Unknown command: uuu` {
+	if _, err = testDebugger.HandleInput("uuu"); err.Error() != `Unknown command: uuu` {
 		t.Error("Unexpected result:", err)
 		return
 	}
 
-	if _, err = testDebugger.HandleInput("break"); err == nil ||
-		err.Error() != `Need a break target (<source>:<line>) as first parameter` {
+	if _, err = testDebugger.HandleInput("break"); err.Error() != `Need a break target (<source>:<line>) as first parameter` {
 		t.Error("Unexpected result:", err)
 		return
 	}
 
-	if _, err = testDebugger.HandleInput("break foo"); err == nil ||
-		err.Error() != `Invalid break target - should be <source>:<line>` {
+	if _, err = testDebugger.HandleInput("break foo"); err.Error() != `Invalid break target - should be <source>:<line>` {
 		t.Error("Unexpected result:", err)
 		return
 	}
 
-	if _, err = testDebugger.HandleInput("rmbreak"); err == nil ||
-		err.Error() != `Need a break target (<source>[:<line>]) as first parameter` {
+	if _, err = testDebugger.HandleInput("rmbreak"); err.Error() != `Need a break target (<source>[:<line>]) as first parameter` {
 		t.Error("Unexpected result:", err)
 		return
 	}
 
-	if _, err = testDebugger.HandleInput("disablebreak"); err == nil ||
-		err.Error() != `Need a break target (<source>:<line>) as first parameter` {
+	if _, err = testDebugger.HandleInput("disablebreak"); err.Error() != `Need a break target (<source>:<line>) as first parameter` {
 		t.Error("Unexpected result:", err)
 		return
 	}
 
-	if _, err = testDebugger.HandleInput("disablebreak foo"); err == nil ||
-		err.Error() != `Invalid break target - should be <source>:<line>` {
+	if _, err = testDebugger.HandleInput("disablebreak foo"); err.Error() != `Invalid break target - should be <source>:<line>` {
 		t.Error("Unexpected result:", err)
 		return
 	}
@@ -1547,64 +1507,68 @@ log("test3")
 		return
 	}
 
-	if _, err = testDebugger.HandleInput("cont foo"); err == nil ||
-		err.Error() != `Need a thread ID and a command Resume, StepIn, StepOver or StepOut` {
+	if _, err = testDebugger.HandleInput(fmt.Sprintf("extract %v foo foo", tid)); err.Error() != `No such value foo` {
 		t.Error("Unexpected result:", err)
 		return
 	}
+}
+
+func TestDebuggingErrorInput2(t *testing.T) {
+	var err error
+
+	tid := 1
+
+	defer func() {
+		testDebugger = nil
+	}()
+
+	vs := scope.NewScope(scope.GlobalScope)
+
+	testDebugger = NewECALDebugger(vs)
 
-	if _, err = testDebugger.HandleInput("cont foo bar"); err == nil ||
-		err.Error() != `Parameter 1 should be a number` {
+	if _, err = testDebugger.HandleInput("cont foo"); err.Error() != `Need a thread ID and a command Resume, StepIn, StepOver or StepOut` {
 		t.Error("Unexpected result:", err)
 		return
 	}
 
-	if _, err = testDebugger.HandleInput("cont 99 bar"); err == nil ||
-		err.Error() != `Invalid command bar - must be resume, stepin, stepover or stepout` {
+	if _, err = testDebugger.HandleInput("cont foo bar"); err.Error() != `Parameter 1 should be a number` {
 		t.Error("Unexpected result:", err)
 		return
 	}
 
-	if _, err = testDebugger.HandleInput("describe"); err == nil ||
-		err.Error() != `Need a thread ID` {
+	if _, err = testDebugger.HandleInput("cont 99 bar"); err.Error() != `Invalid command bar - must be resume, stepin, stepover or stepout` {
 		t.Error("Unexpected result:", err)
 		return
 	}
 
-	if _, err = testDebugger.HandleInput(fmt.Sprintf("extract %v foo", tid)); err == nil ||
-		err.Error() != `Need a thread ID, a variable name and a destination variable name` {
+	if _, err = testDebugger.HandleInput("describe"); err.Error() != `Need a thread ID` {
 		t.Error("Unexpected result:", err)
 		return
 	}
 
-	if _, err = testDebugger.HandleInput(fmt.Sprintf("extract %v _foo foo", tid)); err == nil ||
-		err.Error() != `Variable names may only contain [a-zA-Z] and [a-zA-Z0-9] from the second character` {
+	if _, err = testDebugger.HandleInput(fmt.Sprintf("extract %v foo", tid)); err.Error() != `Need a thread ID, a variable name and a destination variable name` {
 		t.Error("Unexpected result:", err)
 		return
 	}
 
-	if _, err = testDebugger.HandleInput(fmt.Sprintf("extract %v foo foo", tid)); err == nil ||
-		err.Error() != `No such value foo` {
+	if _, err = testDebugger.HandleInput(fmt.Sprintf("extract %v _foo foo", tid)); err.Error() != `Variable names may only contain [a-zA-Z] and [a-zA-Z0-9] from the second character` {
 		t.Error("Unexpected result:", err)
 		return
 	}
 
-	if _, err = testDebugger.HandleInput(fmt.Sprintf("inject %v", tid)); err == nil ||
-		err.Error() != `Need a thread ID, a variable name and an expression` {
+	if _, err = testDebugger.HandleInput(fmt.Sprintf("inject %v", tid)); err.Error() != `Need a thread ID, a variable name and an expression` {
 		t.Error("Unexpected result:", err)
 		return
 	}
 
 	testDebugger.(*ecalDebugger).globalScope = nil
 
-	if _, err = testDebugger.HandleInput(fmt.Sprintf("extract %v foo foo", tid)); err == nil ||
-		err.Error() != `Cannot access global scope` {
+	if _, err = testDebugger.HandleInput(fmt.Sprintf("extract %v foo foo", tid)); err.Error() != `Cannot access global scope` {
 		t.Error("Unexpected result:", err)
 		return
 	}
 
-	if _, err = testDebugger.HandleInput(fmt.Sprintf("inject %v foo foo", tid)); err == nil ||
-		err.Error() != `Cannot access global scope` {
+	if _, err = testDebugger.HandleInput(fmt.Sprintf("inject %v foo foo", tid)); err.Error() != `Cannot access global scope` {
 		t.Error("Unexpected result:", err)
 		return
 	}

+ 2 - 2
interpreter/func_provider.go

@@ -824,7 +824,7 @@ func (ct *setCronTrigger) Run(instanceID string, vs parser.Scope, is map[string]
 			tick := 0
 
 			erp.Cron.RegisterSpec(cs, func() {
-				tick += 1
+				tick++
 				now := erp.Cron.NowFunc()
 				event := engine.NewEvent(eventname, eventkind, map[interface{}]interface{}{
 					"time":      now,
@@ -894,7 +894,7 @@ func (pt *setPulseTrigger) Run(instanceID string, vs parser.Scope, is map[string
 				for {
 					time.Sleep(time.Duration(micros) * time.Microsecond)
 
-					tick += 1
+					tick++
 					now := time.Now()
 					micros := now.UnixNano() / int64(time.Microsecond)
 					event := engine.NewEvent(eventname, eventkind, map[interface{}]interface{}{

+ 29 - 14
interpreter/func_provider_test.go

@@ -17,6 +17,7 @@ import (
 	"testing"
 	"time"
 
+	"devt.de/krotik/common/errorutil"
 	"devt.de/krotik/ecal/stdlib"
 )
 
@@ -90,8 +91,9 @@ identifier: len
       number: 2
       number: 3
 `[1:])
+	errorutil.AssertOk(err)
 
-	if err != nil || res != 3. {
+	if res != 3. {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -109,8 +111,9 @@ identifier: len
         number: 2
         string: 'b'
 `[1:])
+	errorutil.AssertOk(err)
 
-	if err != nil || res != 2. {
+	if res != 2. {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -126,8 +129,9 @@ identifier: del
       number: 3
     number: 1
 `[1:])
+	errorutil.AssertOk(err)
 
-	if err != nil || fmt.Sprint(res) != "[1 3]" {
+	if fmt.Sprint(res) != "[1 3]" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -153,8 +157,9 @@ identifier: del
         number: 3
     string: 'b'
 `[1:])
+	errorutil.AssertOk(err)
 
-	if err != nil || fmt.Sprint(res) != "map[a:1 c:3]" {
+	if fmt.Sprint(res) != "map[a:1 c:3]" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -188,8 +193,9 @@ identifier: add
     number: 4
     number: 0
 `[1:])
+	errorutil.AssertOk(err)
 
-	if err != nil || fmt.Sprint(res) != "[4 1 2 3]" {
+	if fmt.Sprint(res) != "[4 1 2 3]" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -206,13 +212,17 @@ identifier: add
     number: 4
     number: 1
 `[1:])
+	errorutil.AssertOk(err)
 
-	if err != nil || fmt.Sprint(res) != "[1 4 2 3]" {
+	if fmt.Sprint(res) != "[1 4 2 3]" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
+}
 
-	res, err = UnitTestEvalAndAST(
+func TestSimpleFunctions2(t *testing.T) {
+
+	res, err := UnitTestEvalAndAST(
 		`concat([1,2,3], [4,5,6], [7,8,9])`, nil,
 		`
 identifier: concat
@@ -230,8 +240,9 @@ identifier: concat
       number: 8
       number: 9
 `[1:])
+	errorutil.AssertOk(err)
 
-	if err != nil || fmt.Sprint(res) != "[1 2 3 4 5 6 7 8 9]" {
+	if fmt.Sprint(res) != "[1 2 3 4 5 6 7 8 9]" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -242,8 +253,9 @@ identifier: concat
 identifier: dumpenv
   funccall
 `[1:])
+	errorutil.AssertOk(err)
 
-	if err != nil || fmt.Sprint(res) != `GlobalScope {
+	if fmt.Sprint(res) != `GlobalScope {
 }` {
 		t.Error("Unexpected result: ", res, err)
 		return
@@ -255,16 +267,18 @@ func foo() {
 	log("hello")
 }
 doc(foo)`, nil)
+	errorutil.AssertOk(err)
 
-	if err != nil || fmt.Sprint(res) != `Declared function: foo (Line 2, Pos 1)` {
+	if fmt.Sprint(res) != `Declared function: foo (Line 2, Pos 1)` {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
 
 	res, err = UnitTestEval(
 		`doc(len)`, nil)
+	errorutil.AssertOk(err)
 
-	if err != nil || fmt.Sprint(res) != `Len returns the size of a list or map.` {
+	if fmt.Sprint(res) != `Len returns the size of a list or map.` {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -275,8 +289,9 @@ doc(foo)`, nil)
 
 	res, err = UnitTestEval(
 		`doc(fmt.Println)`, nil)
+	errorutil.AssertOk(err)
 
-	if err != nil || res != "foo" {
+	if res != "foo" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -335,7 +350,7 @@ func TestCronTrigger(t *testing.T) {
 		return
 	}
 
-	res, err = UnitTestEval(
+	_, err = UnitTestEval(
 		`
 sink test
   kindmatch [ "foo.*" ],
@@ -399,7 +414,7 @@ func TestPulseTrigger(t *testing.T) {
 		return
 	}
 
-	res, err = UnitTestEval(
+	_, err = UnitTestEval(
 		`
 sink test
   kindmatch [ "foo.*" ],

+ 4 - 1
interpreter/rt_arithmetic_test.go

@@ -71,8 +71,11 @@ minus
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
+}
 
-	res, err = UnitTestEvalAndAST(
+func TestSimpleArithmetics2(t *testing.T) {
+
+	res, err := UnitTestEvalAndAST(
 		`-5.2 - 2.2`, nil,
 		`
 minus

+ 9 - 9
interpreter/rt_assign_test.go

@@ -29,7 +29,7 @@ func TestSimpleAssignments(t *testing.T) {
   number: 42
 `[1:])
 
-	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+	if vsRes := vs.String(); vsRes != `GlobalScope {
     a (float64) : 42
 }` {
 		t.Error("Unexpected result: ", vsRes, res, err)
@@ -44,7 +44,7 @@ func TestSimpleAssignments(t *testing.T) {
   string: 'test'
 `[1:])
 
-	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+	if vsRes := vs.String(); vsRes != `GlobalScope {
     a (string) : test
 }` {
 		t.Error("Unexpected result: ", vsRes, res, err)
@@ -62,7 +62,7 @@ func TestSimpleAssignments(t *testing.T) {
     number: 3
 `[1:])
 
-	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+	if vsRes := vs.String(); vsRes != `GlobalScope {
     a ([]interface {}) : [1,2,3]
 }` {
 		t.Error("Unexpected result: ", vsRes, res, err)
@@ -94,7 +94,7 @@ func TestSimpleAssignments(t *testing.T) {
       number: 4
 `[1:])
 
-	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+	if vsRes := vs.String(); vsRes != `GlobalScope {
     a (map[interface {}]interface {}) : {"1":"foo","2":"bar","foobar":3,"null":4}
 }` {
 		t.Error("Unexpected result: ", vsRes, res, err)
@@ -104,7 +104,7 @@ func TestSimpleAssignments(t *testing.T) {
 	_, err = UnitTestEval(
 		`1 := [1, 2]`, vs)
 
-	if err == nil || err.Error() != "ECAL error in ECALTestRuntime: Cannot access variable (Must have a variable or list of variables on the left side of the assignment) (Line:1 Pos:3)" {
+	if err.Error() != "ECAL error in ECALTestRuntime: Cannot access variable (Must have a variable or list of variables on the left side of the assignment) (Line:1 Pos:3)" {
 		t.Error("Unexpected result:", err)
 		return
 	}
@@ -112,7 +112,7 @@ func TestSimpleAssignments(t *testing.T) {
 	_, err = UnitTestEval(
 		`[1] := [1, 2]`, vs)
 
-	if err == nil || err.Error() != "ECAL error in ECALTestRuntime: Cannot access variable (Must have a list of variables on the left side of the assignment) (Line:1 Pos:5)" {
+	if err.Error() != "ECAL error in ECALTestRuntime: Cannot access variable (Must have a list of variables on the left side of the assignment) (Line:1 Pos:5)" {
 		t.Error("Unexpected result:", err)
 		return
 	}
@@ -120,7 +120,7 @@ func TestSimpleAssignments(t *testing.T) {
 	_, err = UnitTestEval(
 		`[a, b] := [1, 2, 3]`, vs)
 
-	if err == nil || err.Error() != "ECAL error in ECALTestRuntime: Invalid state (Assigned number of variables is different to number of values (2 variables vs 3 values)) (Line:1 Pos:8)" {
+	if err.Error() != "ECAL error in ECALTestRuntime: Invalid state (Assigned number of variables is different to number of values (2 variables vs 3 values)) (Line:1 Pos:8)" {
 		t.Error("Unexpected result:", err)
 		return
 	}
@@ -128,7 +128,7 @@ func TestSimpleAssignments(t *testing.T) {
 	_, err = UnitTestEval(
 		`[a, b] := 1`, vs)
 
-	if err == nil || err.Error() != "ECAL error in ECALTestRuntime: Invalid state (Result is not a list (value is 1)) (Line:1 Pos:8)" {
+	if err.Error() != "ECAL error in ECALTestRuntime: Invalid state (Result is not a list (value is 1)) (Line:1 Pos:8)" {
 		t.Error("Unexpected result:", err)
 		return
 	}
@@ -136,7 +136,7 @@ func TestSimpleAssignments(t *testing.T) {
 	_, err = UnitTestEval(
 		`[a, b.c, c] := [1, 2, 3]`, vs)
 
-	if err == nil || err.Error() != "ECAL error in ECALTestRuntime: Cannot access variable (Variable b is not a container) (Line:1 Pos:13)" {
+	if err.Error() != "ECAL error in ECALTestRuntime: Cannot access variable (Variable b is not a container) (Line:1 Pos:13)" {
 		t.Error("Unexpected result:", err)
 		return
 	}

+ 9 - 2
interpreter/rt_boolean_test.go

@@ -94,8 +94,11 @@ func TestSimpleBoolean(t *testing.T) {
 		t.Error(res, err)
 		return
 	}
+}
 
-	res, err = UnitTestEvalAndAST(
+func TestSimpleBoolean2(t *testing.T) {
+
+	res, err := UnitTestEvalAndAST(
 		`2 < 3`, nil,
 		`
 <
@@ -177,7 +180,11 @@ func TestSimpleBoolean(t *testing.T) {
 		return
 	}
 
-	res, err = UnitTestEvalAndAST(
+}
+
+func TestSimpleBoolean3(t *testing.T) {
+
+	res, err := UnitTestEvalAndAST(
 		`null == null`, nil,
 		`
 ==

+ 90 - 65
interpreter/rt_identifier.go

@@ -137,28 +137,8 @@ func (rt *identifierRuntime) resolveFunction(astring string, vs parser.Scope, is
 	for _, funccall := range node.Children {
 
 		if funccall.Name == parser.NodeFUNCCALL {
-			var funcObj util.ECALFunction
 
-			ok := astring == "log" || astring == "error" || astring == "debug"
-
-			if !ok {
-
-				funcObj, ok = result.(util.ECALFunction)
-
-				if !ok {
-
-					// Check for stdlib function
-
-					funcObj, ok = stdlib.GetStdlibFunc(astring)
-
-					if !ok {
-
-						// Check for inbuild function
-
-						funcObj, ok = InbuildFuncMap[astring]
-					}
-				}
-			}
+			funcObj, ok := rt.resolveFunctionObject(astring, result)
 
 			if ok {
 				var args []interface{}
@@ -175,72 +155,117 @@ func (rt *identifierRuntime) resolveFunction(astring string, vs parser.Scope, is
 				}
 
 				if err == nil {
+					result, err = rt.executeFunction(astring, funcObj, args, vs, is, tid, node)
+				}
 
-					if astring == "log" || astring == "error" || astring == "debug" {
+			} else {
 
-						// Convert non-string structures
+				err = rt.erp.NewRuntimeError(util.ErrUnknownConstruct,
+					fmt.Sprintf("Unknown function: %v", node.Token.Val), node)
+			}
 
-						for i, a := range args {
-							if _, ok := a.(string); !ok {
-								args[i] = stringutil.ConvertToPrettyString(a)
-							}
-						}
+			break
+		}
+	}
 
-						if astring == "log" {
-							rt.erp.Logger.LogInfo(args...)
-						} else if astring == "error" {
-							rt.erp.Logger.LogError(args...)
-						} else if astring == "debug" {
-							rt.erp.Logger.LogDebug(args...)
-						}
+	return result, err
+}
 
-					} else {
+/*
+resolveFunctionObject will resolve a given string or object into a concrete ECAL function.
+*/
+func (rt *identifierRuntime) resolveFunctionObject(astring string, result interface{}) (util.ECALFunction, bool) {
+	var funcObj util.ECALFunction
 
-						if rt.erp.Debugger != nil {
-							rt.erp.Debugger.VisitStepInState(node, vs, tid)
-						}
+	ok := astring == "log" || astring == "error" || astring == "debug"
 
-						// Execute the function
+	if !ok {
 
-						result, err = funcObj.Run(rt.instanceID, vs, is, tid, args)
+		funcObj, ok = result.(util.ECALFunction)
 
-						if rt.erp.Debugger != nil {
-							rt.erp.Debugger.VisitStepOutState(node, vs, tid, err)
-						}
+		if !ok {
 
-						_, ok1 := err.(*util.RuntimeError)
-						_, ok2 := err.(*util.RuntimeErrorWithDetail)
+			// Check for stdlib function
 
-						if err != nil && !ok1 && !ok2 {
+			funcObj, ok = stdlib.GetStdlibFunc(astring)
 
-							// Convert into a proper runtime error if necessary
+			if !ok {
 
-							rerr := rt.erp.NewRuntimeError(util.ErrRuntimeError,
-								err.Error(), node).(*util.RuntimeError)
+				// Check for inbuild function
 
-							if err == util.ErrIsIterator || err == util.ErrEndOfIteration || err == util.ErrContinueIteration {
-								rerr.Type = err
-							}
+				funcObj, ok = InbuildFuncMap[astring]
+			}
+		}
+	}
 
-							err = rerr
-						}
+	return funcObj, ok
+}
 
-						if tr, ok := err.(util.TraceableRuntimeError); ok {
+/*
+executeFunction executes a function call with a given list of arguments and return the result.
+*/
+func (rt *identifierRuntime) executeFunction(astring string, funcObj util.ECALFunction, args []interface{},
+	vs parser.Scope, is map[string]interface{}, tid uint64, node *parser.ASTNode) (interface{}, error) {
 
-							// Add tracing information to the error
+	var result interface{}
+	var err error
 
-							tr.AddTrace(rt.node)
-						}
-					}
-				}
+	if stringutil.IndexOf(astring, []string{"log", "error", "debug"}) != -1 {
 
-			} else {
+		// Convert non-string structures
 
-				err = rt.erp.NewRuntimeError(util.ErrUnknownConstruct,
-					fmt.Sprintf("Unknown function: %v", node.Token.Val), node)
+		for i, a := range args {
+			if _, ok := a.(string); !ok {
+				args[i] = stringutil.ConvertToPrettyString(a)
 			}
+		}
 
-			break
+		if astring == "log" {
+			rt.erp.Logger.LogInfo(args...)
+		} else if astring == "error" {
+			rt.erp.Logger.LogError(args...)
+		} else if astring == "debug" {
+			rt.erp.Logger.LogDebug(args...)
+		}
+
+	} else {
+
+		if rt.erp.Debugger != nil {
+			rt.erp.Debugger.VisitStepInState(node, vs, tid)
+		}
+
+		// Execute the function
+
+		result, err = funcObj.Run(rt.instanceID, vs, is, tid, args)
+
+		if rt.erp.Debugger != nil {
+			rt.erp.Debugger.VisitStepOutState(node, vs, tid, err)
+		}
+
+		_, ok1 := err.(*util.RuntimeError)
+		_, ok2 := err.(*util.RuntimeErrorWithDetail)
+
+		if err != nil && !ok1 && !ok2 {
+
+			// Convert into a proper runtime error if necessary
+
+			rerr := rt.erp.NewRuntimeError(util.ErrRuntimeError,
+				err.Error(), node).(*util.RuntimeError)
+
+			if stringutil.IndexOf(err.Error(), []string{util.ErrIsIterator.Error(),
+				util.ErrEndOfIteration.Error(), util.ErrContinueIteration.Error()}) != -1 {
+
+				rerr.Type = err
+			}
+
+			err = rerr
+		}
+
+		if tr, ok := err.(util.TraceableRuntimeError); ok {
+
+			// Add tracing information to the error
+
+			tr.AddTrace(rt.node)
 		}
 	}
 

+ 124 - 111
interpreter/rt_sink.go

@@ -73,164 +73,177 @@ func (rt *sinkRuntime) Validate() error {
 Eval evaluate this runtime component.
 */
 func (rt *sinkRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
-	var kindMatch, scopeMatch, suppresses []string
-	var stateMatch map[string]interface{}
-	var priority int
-	var statements *parser.ASTNode
-
 	_, err := rt.baseRuntime.Eval(vs, is, tid)
 
 	if err == nil {
-		// Create default scope
+		var rule *engine.Rule
+		var statements *parser.ASTNode
 
-		scopeMatch = []string{}
+		rule, statements, err = rt.createRule(vs, is, tid)
 
-		// Get the name of the sink
+		if err == nil && statements != nil {
+
+			if len(rt.node.Meta) > 0 &&
+				(rt.node.Meta[0].Type() == parser.MetaDataPreComment ||
+					rt.node.Meta[0].Type() == parser.MetaDataPostComment) {
+				rule.Desc = strings.TrimSpace(rt.node.Meta[0].Value())
+			}
 
-		name := rt.node.Children[0].Token.Val
+			rule.Action = func(p engine.Processor, m engine.Monitor, e *engine.Event, tid uint64) error { // Action of the rule
 
-		// Create helper function
+				// Create a new root variable scope
 
-		makeStringList := func(child *parser.ASTNode) ([]string, error) {
-			var ret []string
+				sinkVS := scope.NewScope(fmt.Sprintf("sink: %v", rule.Name))
 
-			val, err := child.Runtime.Eval(vs, is, tid)
+				// Create a new instance state with the monitor - everything called
+				// by the rule will have access to the current monitor.
 
-			if err == nil {
-				for _, v := range val.([]interface{}) {
-					ret = append(ret, fmt.Sprint(v))
+				sinkIs := map[string]interface{}{
+					"monitor": m,
 				}
-			}
 
-			return ret, err
-		}
+				err = sinkVS.SetValue("event", map[interface{}]interface{}{
+					"name":  e.Name(),
+					"kind":  strings.Join(e.Kind(), engine.RuleKindSeparator),
+					"state": e.State(),
+				})
 
-		// Collect values from children
+				if err == nil {
+					scope.SetParentOfScope(sinkVS, vs)
 
-		for _, child := range rt.node.Children[1:] {
+					if _, err = statements.Runtime.Eval(sinkVS, sinkIs, tid); err != nil {
 
-			switch child.Name {
+						if sre, ok := err.(*util.RuntimeErrorWithDetail); ok {
+							sre.Environment = sinkVS
 
-			case parser.NodeKINDMATCH:
-				kindMatch, err = makeStringList(child)
-				break
+						} else {
+							var data interface{}
+							rerr := rt.erp.NewRuntimeError(util.ErrSink, err.Error(), rt.node).(*util.RuntimeError)
 
-			case parser.NodeSCOPEMATCH:
-				scopeMatch, err = makeStringList(child)
-				break
+							if e, ok := err.(*util.RuntimeError); ok {
+								rerr = e
+							} else if r, ok := err.(*returnValue); ok {
+								rerr = r.RuntimeError
+								data = r.returnValue
+							}
 
-			case parser.NodeSTATEMATCH:
-				var val interface{}
-				stateMatch = make(map[string]interface{})
+							// Provide additional information for unexpected errors
 
-				if val, err = child.Runtime.Eval(vs, is, tid); err == nil {
-					for k, v := range val.(map[interface{}]interface{}) {
-						stateMatch[fmt.Sprint(k)] = v
+							err = &util.RuntimeErrorWithDetail{
+								RuntimeError: rerr,
+								Environment:  sinkVS,
+								Data:         data,
+							}
+						}
 					}
 				}
-				break
-
-			case parser.NodePRIORITY:
-				var val interface{}
-
-				if val, err = child.Runtime.Eval(vs, is, tid); err == nil {
-					priority = int(math.Floor(val.(float64)))
-				}
-				break
 
-			case parser.NodeSUPPRESSES:
-				suppresses, err = makeStringList(child)
-				break
-
-			case parser.NodeSTATEMENTS:
-				statements = child
-				break
+				return err
 			}
 
-			if err != nil {
-				break
+			if err = rt.erp.Processor.AddRule(rule); err != nil {
+				err = rt.erp.NewRuntimeError(util.ErrInvalidState, err.Error(), rt.node)
 			}
 		}
+	}
 
-		if err == nil && statements != nil {
-			var desc string
-
-			sinkName := fmt.Sprint(name)
+	return nil, err
+}
 
-			if len(rt.node.Meta) > 0 &&
-				(rt.node.Meta[0].Type() == parser.MetaDataPreComment ||
-					rt.node.Meta[0].Type() == parser.MetaDataPostComment) {
-				desc = strings.TrimSpace(rt.node.Meta[0].Value())
-			}
+/*
+createRule creates a rule for the ECA engine.
+*/
+func (rt *sinkRuntime) createRule(vs parser.Scope, is map[string]interface{},
+	tid uint64) (*engine.Rule, *parser.ASTNode, error) {
 
-			rule := &engine.Rule{
-				Name:            sinkName,   // Name
-				Desc:            desc,       // Description
-				KindMatch:       kindMatch,  // Kind match
-				ScopeMatch:      scopeMatch, // Match on event cascade scope
-				StateMatch:      stateMatch, // No state match
-				Priority:        priority,   // Priority of the rule
-				SuppressionList: suppresses, // List of suppressed rules by this rule
-				Action: func(p engine.Processor, m engine.Monitor, e *engine.Event, tid uint64) error { // Action of the rule
+	var kindMatch, scopeMatch, suppresses []string
+	var stateMatch map[string]interface{}
+	var priority int
+	var statements *parser.ASTNode
+	var err error
 
-					// Create a new root variable scope
+	// Create default scope
 
-					sinkVS := scope.NewScope(fmt.Sprintf("sink: %v", sinkName))
+	scopeMatch = []string{}
 
-					// Create a new instance state with the monitor - everything called
-					// by the rule will have access to the current monitor.
+	// Get sink name
 
-					sinkIs := map[string]interface{}{
-						"monitor": m,
-					}
+	sinkName := fmt.Sprint(rt.node.Children[0].Token.Val)
 
-					err = sinkVS.SetValue("event", map[interface{}]interface{}{
-						"name":  e.Name(),
-						"kind":  strings.Join(e.Kind(), engine.RuleKindSeparator),
-						"state": e.State(),
-					})
+	// Collect values from children
 
-					if err == nil {
-						scope.SetParentOfScope(sinkVS, vs)
+	for _, child := range rt.node.Children[1:] {
 
-						if _, err = statements.Runtime.Eval(sinkVS, sinkIs, tid); err != nil {
+		switch child.Name {
 
-							if sre, ok := err.(*util.RuntimeErrorWithDetail); ok {
-								sre.Environment = sinkVS
+		case parser.NodeKINDMATCH:
+			kindMatch, err = rt.makeStringList(child, vs, is, tid)
+			break
 
-							} else {
-								var data interface{}
-								rerr := rt.erp.NewRuntimeError(util.ErrSink, err.Error(), rt.node).(*util.RuntimeError)
+		case parser.NodeSCOPEMATCH:
+			scopeMatch, err = rt.makeStringList(child, vs, is, tid)
+			break
 
-								if e, ok := err.(*util.RuntimeError); ok {
-									rerr = e
-								} else if r, ok := err.(*returnValue); ok {
-									rerr = r.RuntimeError
-									data = r.returnValue
-								}
+		case parser.NodeSTATEMATCH:
+			var val interface{}
+			stateMatch = make(map[string]interface{})
 
-								// Provide additional information for unexpected errors
+			if val, err = child.Runtime.Eval(vs, is, tid); err == nil {
+				for k, v := range val.(map[interface{}]interface{}) {
+					stateMatch[fmt.Sprint(k)] = v
+				}
+			}
+			break
 
-								err = &util.RuntimeErrorWithDetail{
-									RuntimeError: rerr,
-									Environment:  sinkVS,
-									Data:         data,
-								}
-							}
-						}
-					}
+		case parser.NodePRIORITY:
+			var val interface{}
 
-					return err
-				},
+			if val, err = child.Runtime.Eval(vs, is, tid); err == nil {
+				priority = int(math.Floor(val.(float64)))
 			}
+			break
 
-			if err = rt.erp.Processor.AddRule(rule); err != nil {
-				err = rt.erp.NewRuntimeError(util.ErrInvalidState, err.Error(), rt.node)
-			}
+		case parser.NodeSUPPRESSES:
+			suppresses, err = rt.makeStringList(child, vs, is, tid)
+			break
+
+		case parser.NodeSTATEMENTS:
+			statements = child
+			break
+		}
+
+		if err != nil {
+			break
 		}
 	}
 
-	return nil, err
+	return &engine.Rule{
+		Name:            sinkName,   // Name
+		KindMatch:       kindMatch,  // Kind match
+		ScopeMatch:      scopeMatch, // Match on event cascade scope
+		StateMatch:      stateMatch, // No state match
+		Priority:        priority,   // Priority of the rule
+		SuppressionList: suppresses, // List of suppressed rules by this rule
+	}, statements, err
+}
+
+/*
+makeStringList evaluates a given child node into a list of strings.
+*/
+func (rt *sinkRuntime) makeStringList(child *parser.ASTNode, vs parser.Scope,
+	is map[string]interface{}, tid uint64) ([]string, error) {
+
+	var ret []string
+
+	val, err := child.Runtime.Eval(vs, is, tid)
+
+	if err == nil {
+		for _, v := range val.([]interface{}) {
+			ret = append(ret, fmt.Sprint(v))
+		}
+	}
+
+	return ret, err
 }
 
 // Sink child nodes

+ 184 - 156
interpreter/rt_statements.go

@@ -253,153 +253,180 @@ func (rt *loopRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint
 			}
 
 		} else if rt.node.Children[0].Name == parser.NodeIN {
-			var iterator func() (interface{}, error)
-			var val interface{}
 
-			it := rt.node.Children[0].Children[1]
+			err = rt.handleIterator(vs, is, tid)
+		}
+	}
 
-			val, err = it.Runtime.Eval(vs, is, tid)
+	return nil, err
+}
 
-			// Create an iterator object
+/*
+handleIterator handles iterator functions for loops.
+*/
+func (rt *loopRuntime) handleIterator(vs parser.Scope, is map[string]interface{}, tid uint64) error {
+	var res interface{}
 
-			if rterr, ok := err.(*util.RuntimeError); ok && rterr.Type == util.ErrIsIterator {
+	iterator, err := rt.getIterator(vs, is, tid)
 
-				// We got an iterator - all subsequent calls will return values
+	vars := rt.leftInVarName
 
-				iterator = func() (interface{}, error) {
-					return it.Runtime.Eval(vs, is, tid)
-				}
-				err = nil
+	for err == nil {
 
-			} else {
+		if res, err = rt.getIteratorValue(iterator); err == nil {
 
-				// We got a value over which we need to iterate
+			if len(vars) == 1 {
+				err = vs.SetValue(vars[0], res)
 
-				if valList, isList := val.([]interface{}); isList {
+			} else if resList, ok := res.([]interface{}); ok {
 
-					index := -1
-					end := len(valList)
+				if len(vars) != len(resList) {
+					err = fmt.Errorf("Assigned number of variables is different to "+
+						"number of values (%v variables vs %v values)",
+						len(vars), len(resList))
+				}
 
-					iterator = func() (interface{}, error) {
-						index++
-						if index >= end {
-							return nil, rt.erp.NewRuntimeError(util.ErrEndOfIteration, "", rt.node)
+				if err == nil {
+					for i, v := range vars {
+						if err == nil {
+							err = vs.SetValue(v, resList[i])
 						}
-						return valList[index], nil
 					}
+				}
 
-				} else if valMap, isMap := val.(map[interface{}]interface{}); isMap {
-					var keys []interface{}
+			} else {
 
-					index := -1
+				err = fmt.Errorf("Result for loop variable is not a list (value is %v)", res)
+			}
 
-					for k := range valMap {
-						keys = append(keys, k)
-					}
-					end := len(keys)
+			if err != nil {
+				return rt.erp.NewRuntimeError(util.ErrRuntimeError,
+					err.Error(), rt.node)
+			}
 
-					// Try to sort according to string value
+			// Execute block
 
-					sortutil.InterfaceStrings(keys)
+			_, err = rt.node.Children[1].Runtime.Eval(vs, is, tid)
+		}
 
-					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
-					}
+		// Check for continue
 
-				} else {
+		if err != nil {
+			if eoi, ok := err.(*util.RuntimeError); ok {
+				if eoi.Type == util.ErrContinueIteration {
+					err = nil
+				}
+			}
+		}
+	}
 
-					// A single value will do exactly one iteration
+	// Check for end of iteration error
 
-					index := -1
+	if eoi, ok := err.(*util.RuntimeError); ok {
+		if eoi.Type == util.ErrEndOfIteration {
+			err = nil
+		}
+	}
 
-					iterator = func() (interface{}, error) {
-						index++
-						if index > 0 {
-							return nil, rt.erp.NewRuntimeError(util.ErrEndOfIteration, "", rt.node)
-						}
-						return val, nil
-					}
-				}
-			}
+	return err
+}
 
-			vars := rt.leftInVarName
+/*
+getIteratorValue gets the next iterator value.
+*/
+func (rt *loopRuntime) getIteratorValue(iterator func() (interface{}, error)) (interface{}, error) {
+	var err error
+	var res interface{}
 
-			for err == nil {
-				var res interface{}
+	if res, err = iterator(); err != nil {
+		if eoi, ok := err.(*util.RuntimeError); ok {
+			if eoi.Type == util.ErrIsIterator {
+				err = nil
+			}
+		}
+	}
 
-				res, err = iterator()
+	return res, err
+}
 
-				if err != nil {
-					if eoi, ok := err.(*util.RuntimeError); ok {
-						if eoi.Type == util.ErrIsIterator {
-							err = nil
-						}
-					}
-				}
+/*
+getIterator create an iterator object.
+*/
+func (rt *loopRuntime) getIterator(vs parser.Scope, is map[string]interface{}, tid uint64) (func() (interface{}, error), error) {
+	var iterator func() (interface{}, error)
 
-				if err == nil {
+	it := rt.node.Children[0].Children[1]
 
-					if len(vars) == 1 {
-						err = vs.SetValue(vars[0], res)
+	val, err := it.Runtime.Eval(vs, is, tid)
 
-					} else if resList, ok := res.([]interface{}); ok {
+	// Create an iterator object
 
-						if len(vars) != len(resList) {
-							err = fmt.Errorf("Assigned number of variables is different to "+
-								"number of values (%v variables vs %v values)",
-								len(vars), len(resList))
-						}
+	if rterr, ok := err.(*util.RuntimeError); ok && rterr.Type == util.ErrIsIterator {
 
-						if err == nil {
-							for i, v := range vars {
-								if err == nil {
-									err = vs.SetValue(v, resList[i])
-								}
-							}
-						}
+		// We got an iterator - all subsequent calls will return values
 
-					} else {
+		iterator = func() (interface{}, error) {
+			return it.Runtime.Eval(vs, is, tid)
+		}
+		err = nil
 
-						err = fmt.Errorf("Result for loop variable is not a list (value is %v)", res)
-					}
+	} else {
 
-					if err != nil {
-						return nil, rt.erp.NewRuntimeError(util.ErrRuntimeError,
-							err.Error(), rt.node)
-					}
+		// We got a value over which we need to iterate
+
+		if valList, isList := val.([]interface{}); isList {
 
-					// Execute block
+			index := -1
+			end := len(valList)
 
-					_, err = rt.node.Children[1].Runtime.Eval(vs, is, tid)
+			iterator = func() (interface{}, error) {
+				index++
+				if index >= end {
+					return nil, rt.erp.NewRuntimeError(util.ErrEndOfIteration, "", rt.node)
 				}
+				return valList[index], nil
+			}
 
-				// Check for continue
+		} else if valMap, isMap := val.(map[interface{}]interface{}); isMap {
+			var keys []interface{}
 
-				if err != nil {
-					if eoi, ok := err.(*util.RuntimeError); ok {
-						if eoi.Type == util.ErrContinueIteration {
-							err = nil
-						}
-					}
+			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
 			}
 
-			// Check for end of iteration error
+		} else {
 
-			if eoi, ok := err.(*util.RuntimeError); ok {
-				if eoi.Type == util.ErrEndOfIteration {
-					err = nil
+			// 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
 			}
 		}
 	}
 
-	return nil, err
+	return iterator, err
 }
 
 // Break statement
@@ -485,63 +512,6 @@ Eval evaluate this runtime component.
 func (rt *tryRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
 	var res interface{}
 
-	evalExcept := func(errObj map[interface{}]interface{}, except *parser.ASTNode) bool {
-		ret := false
-
-		if len(except.Children) == 1 {
-
-			// We only have statements - any exception is handled here
-
-			evs := vs.NewChild(scope.NameFromASTNode(except))
-
-			except.Children[0].Runtime.Eval(evs, is, tid)
-
-			ret = true
-
-		} else if len(except.Children) == 2 {
-
-			// We have statements and the error object is available - any exception is handled here
-
-			evs := vs.NewChild(scope.NameFromASTNode(except))
-			evs.SetValue(except.Children[0].Token.Val, errObj)
-
-			except.Children[1].Runtime.Eval(evs, is, tid)
-
-			ret = true
-
-		} else {
-			errorVar := ""
-
-			for i := 0; i < len(except.Children); i++ {
-				child := except.Children[i]
-
-				if !ret && child.Name == parser.NodeSTRING {
-					exceptError, evalErr := child.Runtime.Eval(vs, is, tid)
-
-					// If we fail evaluating the string we panic as otherwise
-					// we would need to generate a new error while trying to handle another error
-					errorutil.AssertOk(evalErr)
-
-					ret = exceptError == fmt.Sprint(errObj["type"])
-
-				} else if ret && child.Name == parser.NodeAS {
-					errorVar = child.Children[0].Token.Val
-
-				} else if ret && child.Name == parser.NodeSTATEMENTS {
-					evs := vs.NewChild(scope.NameFromASTNode(except))
-
-					if errorVar != "" {
-						evs.SetValue(errorVar, errObj)
-					}
-
-					child.Runtime.Eval(evs, is, tid)
-				}
-			}
-		}
-
-		return ret
-	}
-
 	// Make sure the finally block is executed in any case
 
 	if finally := rt.node.Children[len(rt.node.Children)-1]; finally.Name == parser.NodeFINALLY {
@@ -591,7 +561,7 @@ func (rt *tryRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint6
 
 			for i := 1; i < len(rt.node.Children); i++ {
 				if child := rt.node.Children[i]; child.Name == parser.NodeEXCEPT {
-					if evalExcept(errObj, child) {
+					if rt.evalExcept(vs, is, tid, errObj, child) {
 						err = nil
 						break
 					}
@@ -603,6 +573,64 @@ func (rt *tryRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint6
 	return res, err
 }
 
+func (rt *tryRuntime) evalExcept(vs parser.Scope, is map[string]interface{},
+	tid uint64, errObj map[interface{}]interface{}, except *parser.ASTNode) bool {
+	ret := false
+
+	if len(except.Children) == 1 {
+
+		// We only have statements - any exception is handled here
+
+		evs := vs.NewChild(scope.NameFromASTNode(except))
+
+		except.Children[0].Runtime.Eval(evs, is, tid)
+
+		ret = true
+
+	} else if len(except.Children) == 2 {
+
+		// We have statements and the error object is available - any exception is handled here
+
+		evs := vs.NewChild(scope.NameFromASTNode(except))
+		evs.SetValue(except.Children[0].Token.Val, errObj)
+
+		except.Children[1].Runtime.Eval(evs, is, tid)
+
+		ret = true
+
+	} else {
+		errorVar := ""
+
+		for i := 0; i < len(except.Children); i++ {
+			child := except.Children[i]
+
+			if !ret && child.Name == parser.NodeSTRING {
+				exceptError, evalErr := child.Runtime.Eval(vs, is, tid)
+
+				// If we fail evaluating the string we panic as otherwise
+				// we would need to generate a new error while trying to handle another error
+				errorutil.AssertOk(evalErr)
+
+				ret = exceptError == fmt.Sprint(errObj["type"])
+
+			} else if ret && child.Name == parser.NodeAS {
+				errorVar = child.Children[0].Token.Val
+
+			} else if ret && child.Name == parser.NodeSTATEMENTS {
+				evs := vs.NewChild(scope.NameFromASTNode(except))
+
+				if errorVar != "" {
+					evs.SetValue(errorVar, errObj)
+				}
+
+				child.Runtime.Eval(evs, is, tid)
+			}
+		}
+	}
+
+	return ret
+}
+
 // Mutex Runtime
 // =============
 

+ 18 - 7
interpreter/rt_statements_test.go

@@ -361,13 +361,16 @@ Info->4`[1:] {
 		t.Error("Unexpected result: ", res)
 		return
 	}
+}
+
+func TestLoopStatements2(t *testing.T) {
 
 	// Test nested loops
 
-	vs = scope.NewScope(scope.GlobalScope)
-	buf = addLogFunction(vs)
+	vs := scope.NewScope(scope.GlobalScope)
+	buf := addLogFunction(vs)
 
-	_, err = UnitTestEvalAndAST(
+	_, err := UnitTestEvalAndAST(
 		`
 for a in range(10, 3, -3) {
   for b in range(1, 3, 1) {
@@ -571,12 +574,16 @@ Info->10`[1:] {
 		return
 	}
 
+}
+
+func TestLoopStatements3(t *testing.T) {
+
 	// Loop over lists
 
-	vs = scope.NewScope(scope.GlobalScope)
-	buf = addLogFunction(vs)
+	vs := scope.NewScope(scope.GlobalScope)
+	buf := addLogFunction(vs)
 
-	_, err = UnitTestEvalAndAST(
+	_, err := UnitTestEvalAndAST(
 		`
 for a in [1,2] {
   for b in [1,2,3,"Hans", 4] {
@@ -816,10 +823,14 @@ for [1, b] in x {
 		t.Error("Unexpected result:", err)
 		return
 	}
+}
+
+func TestLoopStatements4(t *testing.T) {
+	vs := scope.NewScope(scope.GlobalScope)
 
 	// Test continue
 
-	_, err = UnitTestEval(`
+	_, err := UnitTestEval(`
 for [a] in [1,2,3] {
   continue
   [a, b] := "Hans"

+ 14 - 36
parser/helper.go

@@ -287,7 +287,6 @@ The following nested map structure is expected:
 func ASTFromJSONObject(jsonAST map[string]interface{}) (*ASTNode, error) {
 	var astMeta []MetaData
 	var astChildren []*ASTNode
-	var pos, line, linepos int
 
 	nodeID := TokenANY
 
@@ -302,43 +301,22 @@ func ASTFromJSONObject(jsonAST map[string]interface{}) (*ASTNode, error) {
 		}
 	}
 
-	value, ok := jsonAST["value"]
-	if !ok {
-		value = ""
-	}
-
-	identifier, ok := jsonAST["identifier"]
-	if !ok {
-		identifier = false
-	}
-
-	allowescapes, ok := jsonAST["allowescapes"]
-	if !ok {
-		allowescapes = false
-	}
-
-	if posString, ok := jsonAST["pos"]; ok {
-		pos, _ = strconv.Atoi(fmt.Sprint(posString))
-	} else {
-		pos = 0
-	}
-
-	if lineString, ok := jsonAST["line"]; ok {
-		line, _ = strconv.Atoi(fmt.Sprint(lineString))
-	} else {
-		line = 0
-	}
-
-	if lineposString, ok := jsonAST["linepos"]; ok {
-		linepos, _ = strconv.Atoi(fmt.Sprint(lineposString))
-	} else {
-		linepos = 0
+	getVal := func(k string, d interface{}) (interface{}, int) {
+		value, ok := jsonAST[k]
+		if !ok {
+			value = d
+		}
+		numVal, _ := strconv.Atoi(fmt.Sprint(value))
+		return value, numVal
 	}
 
-	source, ok := jsonAST["source"]
-	if !ok {
-		source = ""
-	}
+	value, _ := getVal("value", "")
+	identifier, _ := getVal("identifier", false)
+	allowescapes, _ := getVal("allowescapes", false)
+	_, pos := getVal("pos", "")
+	_, line := getVal("line", "")
+	_, linepos := getVal("linepos", "")
+	source, _ := getVal("source", "")
 
 	// Create meta data
 

+ 47 - 14
parser/helper_test.go

@@ -114,14 +114,17 @@ identifier: a
 		t.Error("Unexpected result: ", msg)
 		return
 	}
+}
+
+func TestASTNode2(t *testing.T) {
 
-	n, err = ParseWithRuntime("", "- 1", &DummyRuntimeProvider{})
+	n, err := ParseWithRuntime("", "- 1", &DummyRuntimeProvider{})
 	if err != nil {
 		t.Error("Cannot parse test AST:", err)
 		return
 	}
 
-	n2, err = ParseWithRuntime("", "a - b", &DummyRuntimeProvider{})
+	n2, err := ParseWithRuntime("", "a - b", &DummyRuntimeProvider{})
 	if err != nil {
 		t.Error("Cannot parse test AST:", err)
 		return
@@ -192,14 +195,17 @@ number: 1 # wurst
 		t.Error("Unexpected result: ", msg)
 		return
 	}
+}
+
+func TestASTNode3(t *testing.T) {
 
-	n, err = ParseWithRuntime("", "1 #test", &DummyRuntimeProvider{})
+	n, err := ParseWithRuntime("", "1 #test", &DummyRuntimeProvider{})
 	if err != nil {
 		t.Error("Cannot parse test AST:", err)
 		return
 	}
 
-	n2, err = ParseWithRuntime("", "/*test*/ 1", &DummyRuntimeProvider{})
+	n2, err := ParseWithRuntime("", "/*test*/ 1", &DummyRuntimeProvider{})
 	if err != nil {
 		t.Error("Cannot parse test AST:", err)
 		return
@@ -241,6 +247,9 @@ number: 1 # test
 		t.Error("Unexpected result: ", msg)
 		return
 	}
+}
+
+func TestASTNode4(t *testing.T) {
 
 	// Test building an AST from an invalid
 
@@ -299,6 +308,11 @@ func TestLABuffer(t *testing.T) {
 		return
 	}
 
+	testLABufferPeek(t, buf)
+}
+
+func testLABufferPeek(t *testing.T, buf *LABuffer) {
+
 	// Check Peek
 
 	if token, ok := buf.Peek(0); token.Val != "3" || !ok {
@@ -320,6 +334,10 @@ func TestLABuffer(t *testing.T) {
 		t.Error("Unexpected result: ", token, ok)
 		return
 	}
+	testLABufferContinue(t, buf)
+}
+
+func testLABufferContinue(t *testing.T, buf *LABuffer) {
 
 	// Continue
 
@@ -352,6 +370,10 @@ func TestLABuffer(t *testing.T) {
 		t.Error("Unexpected result: ", token, ok)
 		return
 	}
+	testLABufferPeek2(t, buf)
+}
+
+func testLABufferPeek2(t *testing.T, buf *LABuffer) {
 
 	// Check Peek
 
@@ -370,6 +392,11 @@ func TestLABuffer(t *testing.T) {
 		return
 	}
 
+	testLABufferContinue2(t, buf)
+}
+
+func testLABufferContinue2(t *testing.T, buf *LABuffer) {
+
 	// Continue
 
 	if token, ok := buf.Next(); token.Val != "9" || !ok {
@@ -395,25 +422,28 @@ func TestLABuffer(t *testing.T) {
 		t.Error("Unexpected result: ", token, ok)
 		return
 	}
+}
+
+func TestLABuffer2(t *testing.T) {
 
 	// New Buffer
 
-	buf = NewLABuffer(Lex("test", "1 2 3"), 3)
+	buf := NewLABuffer(Lex("test", "1 2 3"), 3)
 
-	if token, ok := buf.Next(); token.Val != "1" || !ok {
-		t.Error("Unexpected result: ", token, ok)
+	if token, _ := buf.Next(); token.Val != "1" {
+		t.Error("Unexpected result: ", token)
 		return
 	}
 
-	if token, ok := buf.Next(); token.Val != "2" || !ok {
-		t.Error("Unexpected result: ", token, ok)
+	if token, _ := buf.Next(); token.Val != "2" {
+		t.Error("Unexpected result: ", token)
 		return
 	}
 
 	// Check Peek
 
-	if token, ok := buf.Peek(0); token.Val != "3" || !ok {
-		t.Error("Unexpected result: ", token, ok)
+	if token, _ := buf.Peek(0); token.Val != "3" {
+		t.Error("Unexpected result: ", token)
 		return
 	}
 
@@ -427,8 +457,8 @@ func TestLABuffer(t *testing.T) {
 		return
 	}
 
-	if token, ok := buf.Next(); token.Val != "3" || !ok {
-		t.Error("Unexpected result: ", token, ok)
+	if token, _ := buf.Next(); token.Val != "3" {
+		t.Error("Unexpected result: ", token)
 		return
 	}
 
@@ -436,10 +466,13 @@ func TestLABuffer(t *testing.T) {
 		t.Error("Unexpected result: ", token, ok)
 		return
 	}
+}
+
+func TestLABuffer3(t *testing.T) {
 
 	// New Buffer - test edge case
 
-	buf = NewLABuffer(Lex("test", ""), 0)
+	buf := NewLABuffer(Lex("test", ""), 0)
 
 	if token, ok := buf.Peek(0); token.ID != TokenEOF || !ok {
 		t.Error("Unexpected result: ", token, ok)

+ 9 - 2
parser/lexer.go

@@ -21,8 +21,15 @@ import (
 	"unicode/utf8"
 )
 
+/*
+NamePattern is the pattern for valid names.
+*/
 var NamePattern = regexp.MustCompile("^[A-Za-z][A-Za-z0-9]*$")
-var NumberPattern = regexp.MustCompile("^[0-9].*$")
+
+/*
+numberPattern is a hint pattern for numbers.
+*/
+var numberPattern = regexp.MustCompile("^[0-9].*$")
 
 /*
 LexToken represents a token which is returned by the lexer.
@@ -588,7 +595,7 @@ func lexToken(l *lexer) lexFunc {
 
 	// Check for number
 
-	if NumberPattern.MatchString(keywordCandidate) {
+	if numberPattern.MatchString(keywordCandidate) {
 		_, err := strconv.ParseFloat(keywordCandidate, 64)
 
 		if err == nil {

+ 46 - 56
parser/parser.go

@@ -427,7 +427,7 @@ func ndSkink(p *parser, self *ASTNode) (*ASTNode, error) {
 
 		// Parse the rest of the parameters as children until we reach the body
 
-		for err == nil && IsNotEndAndNotToken(p, TokenLBRACE) {
+		for err == nil && IsNotEndAndNotTokens(p, []LexTokenID{TokenLBRACE}) {
 			if exp, err = p.run(150); err == nil {
 				self.Children = append(self.Children, exp)
 
@@ -471,7 +471,7 @@ func ndFunc(p *parser, self *ASTNode) (*ASTNode, error) {
 		params := astNodeMap[TokenPARAMS].instance(p, nil)
 		self.Children = append(self.Children, params)
 
-		for err == nil && IsNotEndAndNotToken(p, TokenRPAREN) {
+		for err == nil && IsNotEndAndNotTokens(p, []LexTokenID{TokenRPAREN}) {
 
 			// Parse all the expressions inside
 
@@ -535,6 +535,8 @@ func ndIdentifier(p *parser, self *ASTNode) (*ASTNode, error) {
 			err = parseFuncCall(current)
 		} else if p.node.Token.ID == TokenLBRACK && p.node.Token.Lline == self.Token.Lline {
 
+			skipToken(p, TokenLBRACK)
+
 			// Composition access needs to be on the same line as the identifier
 			// as we might otherwise have a list
 
@@ -568,7 +570,7 @@ func ndIdentifier(p *parser, self *ASTNode) (*ASTNode, error) {
 
 		// Read in parameters
 
-		for err == nil && IsNotEndAndNotToken(p, TokenRPAREN) {
+		for err == nil && IsNotEndAndNotTokens(p, []LexTokenID{TokenRPAREN}) {
 
 			// Parse all the expressions inside the directives
 
@@ -582,8 +584,7 @@ func ndIdentifier(p *parser, self *ASTNode) (*ASTNode, error) {
 		}
 
 		if err == nil {
-			err = skipToken(p, TokenRPAREN)
-			if err == nil {
+			if err = skipToken(p, TokenRPAREN); err == nil {
 				err = parseMore(current)
 			}
 		}
@@ -593,22 +594,18 @@ func ndIdentifier(p *parser, self *ASTNode) (*ASTNode, error) {
 
 	parseCompositionAccess = func(current *ASTNode) error {
 		var exp *ASTNode
+		var err error
 
-		err := skipToken(p, TokenLBRACK)
-
-		if err == nil {
-
-			ca := astNodeMap[TokenCOMPACCESS].instance(p, nil)
-			current.Children = append(current.Children, ca)
+		ca := astNodeMap[TokenCOMPACCESS].instance(p, nil)
+		current.Children = append(current.Children, ca)
 
-			// Parse all the expressions inside the directives
+		// Parse all the expressions inside the directives
 
-			if exp, err = p.run(0); err == nil {
-				ca.Children = append(ca.Children, exp)
+		if exp, err = p.run(0); err == nil {
+			ca.Children = append(ca.Children, exp)
 
-				if err = skipToken(p, TokenRBRACK); err == nil {
-					err = parseMore(current)
-				}
+			if err = skipToken(p, TokenRBRACK); err == nil {
+				err = parseMore(current)
 			}
 		}
 
@@ -631,7 +628,7 @@ func ndList(p *parser, self *ASTNode) (*ASTNode, error) {
 
 	// Get the inner expression
 
-	for err == nil && IsNotEndAndNotToken(p, TokenRBRACK) {
+	for err == nil && IsNotEndAndNotTokens(p, []LexTokenID{TokenRBRACK}) {
 
 		// Parse all the expressions inside
 
@@ -666,7 +663,7 @@ func ndMap(p *parser, self *ASTNode) (*ASTNode, error) {
 
 	// Get the inner expression
 
-	for err == nil && IsNotEndAndNotToken(p, TokenRBRACE) {
+	for err == nil && IsNotEndAndNotTokens(p, []LexTokenID{TokenRBRACE}) {
 
 		// Parse all the expressions inside
 
@@ -778,55 +775,44 @@ func ndLoop(p *parser, self *ASTNode) (*ASTNode, error) {
 ndTry is used to parse a try block.
 */
 func ndTry(p *parser, self *ASTNode) (*ASTNode, error) {
-
 	try, err := parseInnerStatements(p, self)
 
-	if p.node.Token.ID != TokenFINALLY {
+	for err == nil && IsNotEndAndToken(p, TokenEXCEPT) {
+		except := p.node
 
-		for err == nil && IsNotEndAndToken(p, TokenEXCEPT) {
+		err = acceptChild(p, try, TokenEXCEPT)
 
-			except := p.node
+		for err == nil &&
+			IsNotEndAndNotTokens(p, []LexTokenID{TokenAS, TokenIDENTIFIER, TokenLBRACE}) {
 
-			err = acceptChild(p, try, TokenEXCEPT)
+			if err = acceptChild(p, except, TokenSTRING); err == nil {
 
-			if err == nil {
-				for err == nil &&
-					IsNotEndAndNotToken(p, TokenAS) &&
-					IsNotEndAndNotToken(p, TokenIDENTIFIER) &&
-					IsNotEndAndNotToken(p, TokenLBRACE) {
-
-					if err = acceptChild(p, except, TokenSTRING); err == nil {
-
-						// Skip commas
+				// Skip commas
 
-						if p.node.Token.ID == TokenCOMMA {
-							err = skipToken(p, TokenCOMMA)
-						}
-					}
+				if p.node.Token.ID == TokenCOMMA {
+					err = skipToken(p, TokenCOMMA)
 				}
+			}
+		}
 
-				if err == nil {
-
-					if p.node.Token.ID == TokenAS {
-						as := p.node
-
-						err = acceptChild(p, except, TokenAS)
-
-						if err == nil {
-							err = acceptChild(p, as, TokenIDENTIFIER)
-						}
+		if err == nil {
 
-					} else if p.node.Token.ID == TokenIDENTIFIER {
+			if p.node.Token.ID == TokenAS {
+				as := p.node
 
-						err = acceptChild(p, except, TokenIDENTIFIER)
-					}
+				if err = acceptChild(p, except, TokenAS); err == nil {
+					err = acceptChild(p, as, TokenIDENTIFIER)
 				}
 
-				if err == nil {
-					_, err = parseInnerStatements(p, except)
-				}
+			} else if p.node.Token.ID == TokenIDENTIFIER {
+
+				err = acceptChild(p, except, TokenIDENTIFIER)
 			}
 		}
+
+		if err == nil {
+			_, err = parseInnerStatements(p, except)
+		}
 	}
 
 	if err == nil && p.node.Token.ID == TokenFINALLY {
@@ -885,10 +871,14 @@ func IsNotEndAndToken(p *parser, i LexTokenID) bool {
 }
 
 /*
-IsNotEndAndNotToken checks if the next token is not of a specific type or the end has been reached.
+IsNotEndAndNotTokens checks if the next token is not of a specific type or the end has been reached.
 */
-func IsNotEndAndNotToken(p *parser, i LexTokenID) bool {
-	return p.node != nil && p.node.Name != NodeEOF && p.node.Token.ID != i
+func IsNotEndAndNotTokens(p *parser, tokens []LexTokenID) bool {
+	ret := p.node != nil && p.node.Name != NodeEOF
+	for _, t := range tokens {
+		ret = ret && p.node.Token.ID != t
+	}
+	return ret
 }
 
 /*

+ 184 - 148
parser/prettyprinter.go

@@ -155,33 +155,14 @@ PrettyPrint produces pretty printed code from a given AST.
 func PrettyPrint(ast *ASTNode) (string, error) {
 	var visit func(ast *ASTNode, level int) (string, error)
 
-	ppMetaData := func(ast *ASTNode, ppString string) string {
-		ret := ppString
-
-		// Add meta data
-
-		if len(ast.Meta) > 0 {
-			for _, meta := range ast.Meta {
-				if meta.Type() == MetaDataPreComment {
-					ret = fmt.Sprintf("/*%v*/ %v", meta.Value(), ret)
-				} else if meta.Type() == MetaDataPostComment {
-					ret = fmt.Sprintf("%v #%v", ret, meta.Value())
-				}
-			}
-		}
-
-		return ret
-	}
-
 	visit = func(ast *ASTNode, level int) (string, error) {
 		var buf bytes.Buffer
-		var numChildren int
 
 		if ast == nil {
 			return "", fmt.Errorf("Nil pointer in AST at level: %v", level)
 		}
 
-		numChildren = len(ast.Children)
+		numChildren := len(ast.Children)
 
 		tempKey := ast.Name
 		tempParam := make(map[string]string)
@@ -209,194 +190,249 @@ func PrettyPrint(ast *ASTNode) (string, error) {
 			tempKey += fmt.Sprint("_", len(tempParam))
 		}
 
-		// Handle special cases - children in tempParam have been resolved
+		if res, ok := ppSpecialDefs(ast, level, tempParam, &buf); ok {
+			return res, nil
+		} else if res, ok := ppSpecialBlocks(ast, level, tempParam, &buf); ok {
+			return res, nil
+		} else if res, ok := ppSpecialStatements(ast, level, tempParam, &buf); ok {
+			return res, nil
+		}
 
-		if ast.Name == NodeSTATEMENTS {
+		if ast.Token != nil {
 
-			// For statements just concat all children
+			// Adding node value to template parameters
 
-			for i := 0; i < numChildren; i++ {
-				buf.WriteString(stringutil.GenerateRollingString(" ", level*4))
-				buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
-				buf.WriteString("\n")
-			}
+			tempParam["val"] = ast.Token.Val
+			tempParam["qval"] = strconv.Quote(ast.Token.Val)
+		}
 
-			return ppMetaData(ast, buf.String()), nil
+		// Retrieve the template
 
-		} else if ast.Name == NodeSINK {
+		temp, ok := prettyPrinterMap[tempKey]
+		errorutil.AssertTrue(ok,
+			fmt.Sprintf("Could not find template for %v (tempkey: %v)",
+				ast.Name, tempKey))
 
-			buf.WriteString("sink ")
-			buf.WriteString(tempParam["c1"])
-			buf.WriteString("\n")
+		// Use the children as parameters for template
 
-			for i := 1; i < len(ast.Children)-1; i++ {
-				buf.WriteString("  ")
-				buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
-				buf.WriteString("\n")
-			}
+		errorutil.AssertOk(temp.Execute(&buf, tempParam))
 
-			buf.WriteString("{\n")
-			buf.WriteString(tempParam[fmt.Sprint("c", len(ast.Children))])
-			buf.WriteString("}\n")
+		return ppMetaData(ast, buf.String()), nil
+	}
 
-			return ppMetaData(ast, buf.String()), nil
+	return visit(ast, 0)
+}
 
-		} else if ast.Name == NodeFUNCCALL {
+/*
+ppMetaData pretty prints meta data.
+*/
+func ppMetaData(ast *ASTNode, ppString string) string {
+	ret := ppString
 
-			// For statements just concat all children
+	// Add meta data
 
-			for i := 0; i < numChildren; i++ {
-				buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
-				if i < numChildren-1 {
-					buf.WriteString(", ")
-				}
+	if len(ast.Meta) > 0 {
+		for _, meta := range ast.Meta {
+			if meta.Type() == MetaDataPreComment {
+				ret = fmt.Sprintf("/*%v*/ %v", meta.Value(), ret)
+			} else if meta.Type() == MetaDataPostComment {
+				ret = fmt.Sprintf("%v #%v", ret, meta.Value())
 			}
+		}
+	}
 
-			return ppMetaData(ast, buf.String()), nil
+	return ret
+}
 
-		} else if ast.Name == NodeIDENTIFIER {
+/*
+ppSpecialDefs pretty prints special cases.
+*/
+func ppSpecialDefs(ast *ASTNode, level int, tempParam map[string]string, buf *bytes.Buffer) (string, bool) {
+	numChildren := len(ast.Children)
 
-			buf.WriteString(ast.Token.Val)
+	if ast.Name == NodeFUNCCALL {
 
-			for i := 0; i < numChildren; i++ {
-				if ast.Children[i].Name == NodeIDENTIFIER {
-					buf.WriteString(".")
-					buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
-				} else if ast.Children[i].Name == NodeFUNCCALL {
-					buf.WriteString("(")
-					buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
-					buf.WriteString(")")
-				} else if ast.Children[i].Name == NodeCOMPACCESS {
-					buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
-				}
+		for i := 0; i < numChildren; i++ {
+			buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
+			if i < numChildren-1 {
+				buf.WriteString(", ")
 			}
+		}
 
-			return ppMetaData(ast, buf.String()), nil
+		return ppMetaData(ast, buf.String()), true
 
-		} else if ast.Name == NodeLIST {
+	} else if ast.Name == NodeSINK {
 
-			buf.WriteString("[")
-			i := 1
-			for ; i < numChildren; i++ {
-				buf.WriteString(tempParam[fmt.Sprint("c", i)])
-				buf.WriteString(", ")
-			}
-			buf.WriteString(tempParam[fmt.Sprint("c", i)])
-			buf.WriteString("]")
+		buf.WriteString("sink ")
+		buf.WriteString(tempParam["c1"])
+		buf.WriteString("\n")
 
-			return ppMetaData(ast, buf.String()), nil
+		for i := 1; i < len(ast.Children)-1; i++ {
+			buf.WriteString("  ")
+			buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
+			buf.WriteString("\n")
+		}
 
-		} else if ast.Name == NodeMAP {
+		buf.WriteString("{\n")
+		buf.WriteString(tempParam[fmt.Sprint("c", len(ast.Children))])
+		buf.WriteString("}\n")
 
-			buf.WriteString("{")
-			i := 1
-			for ; i < numChildren; i++ {
-				buf.WriteString(tempParam[fmt.Sprint("c", i)])
-				buf.WriteString(", ")
-			}
-			buf.WriteString(tempParam[fmt.Sprint("c", i)])
-			buf.WriteString("}")
+		return ppMetaData(ast, buf.String()), true
+	}
 
-			return ppMetaData(ast, buf.String()), nil
+	return "", false
+}
 
-		} else if ast.Name == NodePARAMS {
+/*
+ppSpecialBlocks pretty prints special cases.
+*/
+func ppSpecialBlocks(ast *ASTNode, level int, tempParam map[string]string, buf *bytes.Buffer) (string, bool) {
+	numChildren := len(ast.Children)
 
-			buf.WriteString("(")
-			i := 1
-			for ; i < numChildren; i++ {
-				buf.WriteString(tempParam[fmt.Sprint("c", i)])
-				buf.WriteString(", ")
-			}
+	// Handle special cases - children in tempParam have been resolved
+
+	if stringutil.IndexOf(ast.Name, []string{NodeSTATEMENTS}) != -1 {
+
+		// For statements just concat all children
+
+		for i := 0; i < numChildren; i++ {
+			buf.WriteString(stringutil.GenerateRollingString(" ", level*4))
+			buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
+			buf.WriteString("\n")
+		}
+
+		return ppMetaData(ast, buf.String()), true
+
+	} else if ast.Name == NodeLIST {
+
+		buf.WriteString("[")
+		i := 1
+		for ; i < numChildren; i++ {
 			buf.WriteString(tempParam[fmt.Sprint("c", i)])
-			buf.WriteString(")")
+			buf.WriteString(", ")
+		}
+		buf.WriteString(tempParam[fmt.Sprint("c", i)])
+		buf.WriteString("]")
 
-			return ppMetaData(ast, buf.String()), nil
+		return ppMetaData(ast, buf.String()), true
 
-		} else if ast.Name == NodeIF {
+	} else if ast.Name == NodeMAP {
 
-			writeGUARD := func(child int) {
-				buf.WriteString(tempParam[fmt.Sprint("c", child)])
-				buf.WriteString(" {\n")
-				buf.WriteString(tempParam[fmt.Sprint("c", child+1)])
-				buf.WriteString("}")
-			}
+		buf.WriteString("{")
+		i := 1
+		for ; i < numChildren; i++ {
+			buf.WriteString(tempParam[fmt.Sprint("c", i)])
+			buf.WriteString(", ")
+		}
+		buf.WriteString(tempParam[fmt.Sprint("c", i)])
+		buf.WriteString("}")
 
-			buf.WriteString("if ")
+		return ppMetaData(ast, buf.String()), true
 
-			writeGUARD(1)
+	} else if ast.Name == NodeTRY {
 
-			for i := 0; i < len(ast.Children); i += 2 {
-				if i+2 == len(ast.Children) && ast.Children[i].Children[0].Name == NodeTRUE {
-					buf.WriteString(" else {\n")
-					buf.WriteString(tempParam[fmt.Sprint("c", i+2)])
-					buf.WriteString("}")
-				} else if i > 0 {
-					buf.WriteString(" elif ")
-					writeGUARD(i + 1)
-				}
-			}
+		buf.WriteString("try {\n")
+		buf.WriteString(tempParam[fmt.Sprint("c1")])
 
-			buf.WriteString("\n")
+		buf.WriteString("}")
 
-			return ppMetaData(ast, buf.String()), nil
+		for i := 1; i < len(ast.Children); i++ {
+			buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
+		}
 
-		} else if ast.Name == NodeTRY {
+		buf.WriteString("\n")
 
-			buf.WriteString("try {\n")
-			buf.WriteString(tempParam[fmt.Sprint("c1")])
+		return ppMetaData(ast, buf.String()), true
 
-			buf.WriteString("}")
+	} else if ast.Name == NodeEXCEPT {
 
-			for i := 1; i < len(ast.Children); i++ {
-				buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
+		buf.WriteString(" except ")
+
+		for i := 0; i < len(ast.Children)-1; i++ {
+			buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
+
+			if ast.Children[i+1].Name != NodeAS && i < len(ast.Children)-2 {
+				buf.WriteString(",")
 			}
+			buf.WriteString(" ")
+		}
 
-			buf.WriteString("\n")
+		buf.WriteString("{\n")
+		buf.WriteString(tempParam[fmt.Sprint("c", len(ast.Children))])
+		buf.WriteString("}")
 
-			return ppMetaData(ast, buf.String()), nil
+		return ppMetaData(ast, buf.String()), true
+	}
 
-		} else if ast.Name == NodeEXCEPT {
-			buf.WriteString(" except ")
+	return "", false
+}
 
-			for i := 0; i < len(ast.Children)-1; i++ {
-				buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
+/*
+ppSpecialStatements pretty prints special cases.
+*/
+func ppSpecialStatements(ast *ASTNode, level int, tempParam map[string]string, buf *bytes.Buffer) (string, bool) {
+	numChildren := len(ast.Children)
 
-				if ast.Children[i+1].Name != NodeAS && i < len(ast.Children)-2 {
-					buf.WriteString(",")
-				}
-				buf.WriteString(" ")
-			}
+	if ast.Name == NodeIDENTIFIER {
+
+		buf.WriteString(ast.Token.Val)
 
-			buf.WriteString("{\n")
+		for i := 0; i < numChildren; i++ {
+			if ast.Children[i].Name == NodeIDENTIFIER {
+				buf.WriteString(".")
+				buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
+			} else if ast.Children[i].Name == NodeFUNCCALL {
+				buf.WriteString("(")
+				buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
+				buf.WriteString(")")
+			} else if ast.Children[i].Name == NodeCOMPACCESS {
+				buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
+			}
+		}
 
-			buf.WriteString(tempParam[fmt.Sprint("c", len(ast.Children))])
+		return ppMetaData(ast, buf.String()), true
 
-			buf.WriteString("}")
+	} else if ast.Name == NodePARAMS {
 
-			return ppMetaData(ast, buf.String()), nil
+		buf.WriteString("(")
+		i := 1
+		for ; i < numChildren; i++ {
+			buf.WriteString(tempParam[fmt.Sprint("c", i)])
+			buf.WriteString(", ")
 		}
+		buf.WriteString(tempParam[fmt.Sprint("c", i)])
+		buf.WriteString(")")
 
-		if ast.Token != nil {
+		return ppMetaData(ast, buf.String()), true
 
-			// Adding node value to template parameters
+	} else if ast.Name == NodeIF {
 
-			tempParam["val"] = ast.Token.Val
-			tempParam["qval"] = strconv.Quote(ast.Token.Val)
+		writeGUARD := func(child int) {
+			buf.WriteString(tempParam[fmt.Sprint("c", child)])
+			buf.WriteString(" {\n")
+			buf.WriteString(tempParam[fmt.Sprint("c", child+1)])
+			buf.WriteString("}")
 		}
 
-		// Retrieve the template
+		buf.WriteString("if ")
 
-		temp, ok := prettyPrinterMap[tempKey]
-		errorutil.AssertTrue(ok,
-			fmt.Sprintf("Could not find template for %v (tempkey: %v)",
-				ast.Name, tempKey))
+		writeGUARD(1)
 
-		// Use the children as parameters for template
+		for i := 0; i < len(ast.Children); i += 2 {
+			if i+2 == len(ast.Children) && ast.Children[i].Children[0].Name == NodeTRUE {
+				buf.WriteString(" else {\n")
+				buf.WriteString(tempParam[fmt.Sprint("c", i+2)])
+				buf.WriteString("}")
+			} else if i > 0 {
+				buf.WriteString(" elif ")
+				writeGUARD(i + 1)
+			}
+		}
 
-		errorutil.AssertOk(temp.Execute(&buf, tempParam))
+		buf.WriteString("\n")
 
-		return ppMetaData(ast, buf.String()), nil
+		return ppMetaData(ast, buf.String()), true
 	}
 
-	return visit(ast, 0)
+	return "", false
 }

+ 54 - 51
scope/varsscope.go

@@ -156,57 +156,7 @@ func (s *varsScope) setValue(varName string, varValue interface{}) error {
 		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:])
+				container, err = s.containerAccess(container, cFields, cFields[1:])
 			}
 
 			if err == nil && container != nil {
@@ -265,6 +215,59 @@ func (s *varsScope) setValue(varName string, varValue interface{}) error {
 	return err
 }
 
+/*
+containerAccess recursively accesses a field in a container structure.
+*/
+func (s *varsScope) containerAccess(container interface{}, cFields []string, fields []string) (interface{}, error) {
+	var err error
+
+	// 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 {
+		container, err = s.containerAccess(container, cFields, fields[1:])
+	}
+
+	return container, err
+}
+
 /*
 getScopeForVariable returns the scope (this or a parent scope) which holds a
 given variable.

+ 17 - 10
scope/varsscope_test.go

@@ -14,6 +14,8 @@ import (
 	"encoding/json"
 	"fmt"
 	"testing"
+
+	"devt.de/krotik/ecal/parser"
 )
 
 func TestVarScopeSetMap(t *testing.T) {
@@ -115,65 +117,70 @@ global {
 		return
 	}
 
-	err = parentVS.SetValue("xx.a", []interface{}{3, 4, 5})
+	testVarScopeSetMapErrors(t, parentVS)
+}
+
+func testVarScopeSetMapErrors(t *testing.T, parentVS parser.Scope) {
+
+	err := parentVS.SetValue("xx.a", []interface{}{3, 4, 5})
 
-	if err == nil || err.Error() != "List xx needs a number index not: a" {
+	if 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" {
+	if 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" {
+	if 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" {
+	if 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" {
+	if 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" {
+	if 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" {
+	if 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" {
+	if 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" {
+	if err.Error() != "Variable yy is not a container" {
 		t.Error("Unexpected result:", parentVS.String(), err)
 		return
 	}

+ 52 - 36
stdlib/adapter.go

@@ -61,32 +61,7 @@ func (ea *ECALFunctionAdapter) Run(instanceID string, vs parser.Scope,
 		// 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)
-			}
+			arg = ea.convertNumber(arg, float64Arg, expectedType)
 		}
 
 		givenType := reflect.TypeOf(arg)
@@ -134,16 +109,7 @@ func (ea *ECALFunctionAdapter) Run(instanceID string, vs parser.Scope,
 
 		// 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)
+		results = append(results, ea.convertResultNumber(res, v))
 	}
 
 	ret = results
@@ -157,6 +123,56 @@ func (ea *ECALFunctionAdapter) Run(instanceID string, vs parser.Scope,
 	return ret, err
 }
 
+/*
+convertNumber converts number arguments into the right type.
+*/
+func (ea *ECALFunctionAdapter) convertNumber(arg interface{}, float64Arg float64, expectedType reflect.Type) interface{} {
+	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)
+	}
+
+	return arg
+}
+
+/*
+convertResultNumber converts result numbers into the right type.
+*/
+func (ea *ECALFunctionAdapter) convertResultNumber(res interface{}, v reflect.Value) interface{} {
+	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()
+	}
+
+	return res
+}
+
 /*
 DocString returns the docstring of the wrapped function.
 */

+ 31 - 20
stdlib/adapter_test.go

@@ -21,14 +21,14 @@ import (
 	"devt.de/krotik/ecal/scope"
 )
 
-func TestECALFunctionAdapter(t *testing.T) {
+func TestECALFunctionAdapterSimple(t *testing.T) {
 
 	res, err := runAdapterTest(
 		reflect.ValueOf(strconv.Atoi),
 		[]interface{}{"1"},
 	)
 
-	if err != nil || res != float64(1) {
+	if errorutil.AssertOk(err); res != float64(1) {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -38,7 +38,7 @@ func TestECALFunctionAdapter(t *testing.T) {
 		[]interface{}{"123", float64(0), float64(0)},
 	)
 
-	if err != nil || res != float64(123) {
+	if errorutil.AssertOk(err); res != float64(123) {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -48,7 +48,7 @@ func TestECALFunctionAdapter(t *testing.T) {
 		[]interface{}{"123.123", float64(0)},
 	)
 
-	if err != nil || res != float64(123.123) {
+	if errorutil.AssertOk(err); res != float64(123.123) {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -58,7 +58,7 @@ func TestECALFunctionAdapter(t *testing.T) {
 		[]interface{}{"foo %v", "bar"},
 	)
 
-	if err != nil || res != "foo bar" {
+	if errorutil.AssertOk(err); res != "foo bar" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -67,8 +67,9 @@ func TestECALFunctionAdapter(t *testing.T) {
 		reflect.ValueOf(math.Float32bits),
 		[]interface{}{float64(11)},
 	)
+	errorutil.AssertOk(err)
 
-	if r := fmt.Sprintf("%X", uint32(res.(float64))); err != nil || r != fmt.Sprintf("%X", math.Float32bits(11)) {
+	if r := fmt.Sprintf("%X", uint32(res.(float64))); r != fmt.Sprintf("%X", math.Float32bits(11)) {
 		t.Error("Unexpected result: ", r, err)
 		return
 	}
@@ -77,8 +78,9 @@ func TestECALFunctionAdapter(t *testing.T) {
 		reflect.ValueOf(math.Float32frombits),
 		[]interface{}{float64(math.Float32bits(11))},
 	)
+	errorutil.AssertOk(err)
 
-	if r := fmt.Sprintf("%v", res.(float64)); err != nil || r != "11" {
+	if r := fmt.Sprintf("%v", res.(float64)); r != "11" {
 		t.Error("Unexpected result: ", r, err)
 		return
 	}
@@ -87,18 +89,23 @@ func TestECALFunctionAdapter(t *testing.T) {
 		reflect.ValueOf(math.Float32frombits),
 		[]interface{}{math.Float32bits(11)}, // Giving the correct type also works
 	)
+	errorutil.AssertOk(err)
 
-	if r := fmt.Sprintf("%v", res.(float64)); err != nil || r != "11" {
+	if r := fmt.Sprintf("%v", res.(float64)); r != "11" {
 		t.Error("Unexpected result: ", r, err)
 		return
 	}
+}
 
-	res, err = runAdapterTest(
+func TestECALFunctionAdapterSimple2(t *testing.T) {
+
+	res, err := runAdapterTest(
 		reflect.ValueOf(math.Float64bits),
 		[]interface{}{float64(11)},
 	)
+	errorutil.AssertOk(err)
 
-	if r := fmt.Sprintf("%X", uint64(res.(float64))); err != nil || r != fmt.Sprintf("%X", math.Float64bits(11)) {
+	if r := fmt.Sprintf("%X", uint64(res.(float64))); r != fmt.Sprintf("%X", math.Float64bits(11)) {
 		t.Error("Unexpected result: ", r, err)
 		return
 	}
@@ -107,8 +114,9 @@ func TestECALFunctionAdapter(t *testing.T) {
 		reflect.ValueOf(math.Float64frombits),
 		[]interface{}{float64(math.Float64bits(11))},
 	)
+	errorutil.AssertOk(err)
 
-	if r := fmt.Sprintf("%v", res.(float64)); err != nil || r != "11" {
+	if r := fmt.Sprintf("%v", res.(float64)); r != "11" {
 		t.Error("Unexpected result: ", r, err)
 		return
 	}
@@ -118,7 +126,7 @@ func TestECALFunctionAdapter(t *testing.T) {
 		[]interface{}{float64(1)},
 	)
 
-	if err != nil || res != "1" {
+	if errorutil.AssertOk(err); res != "1" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -128,7 +136,7 @@ func TestECALFunctionAdapter(t *testing.T) {
 		[]interface{}{float64(1)},
 	)
 
-	if err != nil || res != "1" {
+	if errorutil.AssertOk(err); res != "1" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -138,7 +146,7 @@ func TestECALFunctionAdapter(t *testing.T) {
 		[]interface{}{float64(1)},
 	)
 
-	if err != nil || res != "1" {
+	if errorutil.AssertOk(err); res != "1" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -148,7 +156,7 @@ func TestECALFunctionAdapter(t *testing.T) {
 		[]interface{}{float64(1)},
 	)
 
-	if err != nil || res != "1" {
+	if errorutil.AssertOk(err); res != "1" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -158,7 +166,7 @@ func TestECALFunctionAdapter(t *testing.T) {
 		[]interface{}{float64(1)},
 	)
 
-	if err != nil || res != "1" {
+	if errorutil.AssertOk(err); res != "1" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -168,7 +176,7 @@ func TestECALFunctionAdapter(t *testing.T) {
 		[]interface{}{float64(1)},
 	)
 
-	if err != nil || res != "1" {
+	if errorutil.AssertOk(err); res != "1" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -178,7 +186,7 @@ func TestECALFunctionAdapter(t *testing.T) {
 		[]interface{}{float64(1)},
 	)
 
-	if err != nil || res != "1" {
+	if errorutil.AssertOk(err); res != "1" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
@@ -188,14 +196,17 @@ func TestECALFunctionAdapter(t *testing.T) {
 		[]interface{}{float64(1)},
 	)
 
-	if err != nil || res != "1" {
+	if errorutil.AssertOk(err); res != "1" {
 		t.Error("Unexpected result: ", res, err)
 		return
 	}
+}
+
+func TestECALFunctionAdapterErrors(t *testing.T) {
 
 	// Test Error cases
 
-	res, err = runAdapterTest(
+	res, err := runAdapterTest(
 		reflect.ValueOf(strconv.ParseFloat),
 		[]interface{}{"123.123", 0, 0},
 	)

+ 117 - 93
stdlib/generate/generate.go

@@ -152,144 +152,168 @@ genStdlib contains all generated stdlib constructs.
 
 				// Write constants
 
-				outbuf.WriteString(fmt.Sprintf(`/*
+				writeConstants(&outbuf, pkgName, pkgSymbols, scope)
+
+				// Write function documentation
+
+				writeFuncDoc(&outbuf, pkgName, pkgDocs, pkgSymbols, scope)
+
+				// Write functions
+
+				writeFuncs(&outbuf, pkgName, pkgSymbols, scope)
+			}
+		}
+	}
+
+	// Write dummy statement
+	outbuf.WriteString("// Dummy statement to prevent declared and not used errors\n")
+	outbuf.WriteString("var Dummy = fmt.Sprint(reflect.ValueOf(fmt.Sprint))\n\n")
+
+	if err == nil {
+		err = ioutil.WriteFile(filename, outbuf.Bytes(), 0644)
+	}
+
+	if err != nil {
+		stderrPrint("Error:", err)
+	}
+}
+
+var (
+	fset = token.NewFileSet()
+	ctx  = &build.Default
+)
+
+/*
+writeConstants writes out all stdlib constant definitions.
+*/
+func writeConstants(outbuf *bytes.Buffer, pkgName string, pkgSymbols []string, scope *types.Scope) {
+
+	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() {
+	for _, name := range scope.Names() {
 
-					if !containsSymbol(pkgSymbols, name) {
-						continue
-					}
+		if !containsSymbol(pkgSymbols, name) {
+			continue
+		}
 
-					switch obj := scope.Lookup(name).(type) {
-					case *types.Const:
+		switch obj := scope.Lookup(name).(type) {
+		case *types.Const:
 
-						if unicode.IsUpper([]rune(name)[0]) {
+			if unicode.IsUpper([]rune(name)[0]) {
 
-							line := fmt.Sprintf(`	"%v": %v.%v,
+				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),
+				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")
+				outbuf.WriteString(line)
+			}
+		}
+	}
 
-				// Write function documentation
+	outbuf.WriteString("}\n\n")
+}
 
-				outbuf.WriteString(fmt.Sprintf(`/*
+/*
+writeFuncDoc writes out all stdlib function documentation.
+*/
+func writeFuncDoc(outbuf *bytes.Buffer, pkgName string, pkgDocs map[string]*doc.Package,
+	pkgSymbols []string, scope *types.Scope) {
+
+	outbuf.WriteString(fmt.Sprintf(`/*
 %vFuncDocMap contains the documentation of stdlib %v functions.
 */
 var %vFuncDocMap = map[interface{}]interface{}{
 `, pkgName, pkgName, pkgName))
 
-				if pkgDoc, ok := pkgDocs[pkgName]; ok {
+	if pkgDoc, ok := pkgDocs[pkgName]; ok {
 
-					for _, name := range scope.Names() {
+		for _, name := range scope.Names() {
 
-						if !containsSymbol(pkgSymbols, name) {
-							continue
-						}
+			if !containsSymbol(pkgSymbols, name) {
+				continue
+			}
 
-						for _, f := range pkgDoc.Funcs {
-							if f.Name == name {
-								outbuf.WriteString(
-									fmt.Sprintf(`	"%v": %#v,
+			for _, f := range pkgDoc.Funcs {
+				if f.Name == name {
+					outbuf.WriteString(
+						fmt.Sprintf(`	"%v": %#v,
 `, name, f.Doc))
-							}
-						}
-					}
+				}
+			}
+		}
 
-				} else {
+	} else {
 
-					for _, name := range pkgSymbols {
-						switch scope.Lookup(name).(type) {
-						case *types.Func:
-							outbuf.WriteString(
-								fmt.Sprintf(`	"%v": "Function: %v",
+		for _, name := range pkgSymbols {
+			switch scope.Lookup(name).(type) {
+			case *types.Func:
+				outbuf.WriteString(
+					fmt.Sprintf(`	"%v": "Function: %v",
 `, name, name))
-						}
-					}
-				}
-
-				outbuf.WriteString("}\n\n")
+			}
+		}
+	}
 
-				// Write functions
+	outbuf.WriteString("}\n\n")
+}
 
-				outbuf.WriteString(fmt.Sprintf(`/*
+/*
+writeFuncs writes out all stdlib function definitions.
+*/
+func writeFuncs(outbuf *bytes.Buffer, pkgName string, pkgSymbols []string, scope *types.Scope) {
+	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() {
+	for _, name := range scope.Names() {
 
-					if !containsSymbol(pkgSymbols, name) {
-						continue
-					}
+		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), fmt.Sprint(%vFuncDocMap[%#v])},
+		switch obj := scope.Lookup(name).(type) {
+		case *types.Func:
+			if unicode.IsUpper([]rune(name)[0]) {
+				outbuf.WriteString(
+					fmt.Sprintf(`	%#v: &ECALFunctionAdapter{reflect.ValueOf(%v), fmt.Sprint(%vFuncDocMap[%#v])},
 `, name, obj.FullName(), pkgName, name))
-						}
-					}
-				}
-
-				outbuf.WriteString("}\n\n")
 			}
 		}
 	}
 
-	// Write dummy statement
-	outbuf.WriteString("// Dummy statement to prevent declared and not used errors\n")
-	outbuf.WriteString("var Dummy = fmt.Sprint(reflect.ValueOf(fmt.Sprint))\n\n")
-
-	if err == nil {
-		err = ioutil.WriteFile(filename, outbuf.Bytes(), 0644)
-	}
-
-	if err != nil {
-		stderrPrint("Error:", err)
-	}
+	outbuf.WriteString("}\n\n")
 }
 
-var (
-	fset = token.NewFileSet()
-	ctx  = &build.Default
-)
-
 /*
 getPackageDocs returns the source code documentation of as given Go package.
 Returns a short synopsis and a documentation object.

+ 1 - 1
stdlib/stdlib.go

@@ -145,7 +145,7 @@ func GetStdlibFunc(name string) (util.ECALFunction, bool) {
 GetPkgDocString returns the docstring of a stdlib package.
 */
 func GetPkgDocString(name string) (string, bool) {
-	var res = ""
+	var res string
 	s, ok := genStdlib[fmt.Sprintf("%v-synopsis", name)]
 	if ok {
 		res = fmt.Sprint(s)

+ 1 - 1
util/error.go

@@ -122,7 +122,7 @@ func (re *RuntimeError) GetTrace() []*parser.ASTNode {
 }
 
 /*
-GetTrace returns the current stacktrace as a string.
+GetTraceString returns the current stacktrace as a string.
 */
 func (re *RuntimeError) GetTraceString() []string {
 	res := []string{}

+ 12 - 8
util/import_test.go

@@ -18,6 +18,7 @@ import (
 	"strings"
 	"testing"
 
+	"devt.de/krotik/common/errorutil"
 	"devt.de/krotik/common/fileutil"
 )
 
@@ -62,28 +63,29 @@ func TestImportLocater(t *testing.T) {
 	expectedError := fmt.Sprintf("Import path is outside of code root: ..%vt",
 		string(os.PathSeparator))
 
-	if res != "" || err == nil || err.Error() != expectedError {
+	if res != "" || err.Error() != expectedError {
 		t.Error("Unexpected result:", res, err)
 		return
 	}
 
 	res, err = fil.Resolve(filepath.Join("..", importTestDir, "x"))
 
-	if res != "" || err == nil || !strings.HasPrefix(err.Error(), "Could not import path") {
+	if res != "" || !strings.HasPrefix(err.Error(), "Could not import path") {
 		t.Error("Unexpected result:", res, err)
 		return
 	}
 
 	res, err = fil.Resolve(filepath.Join("..", importTestDir, "x"))
 
-	if res != "" || err == nil || !strings.HasPrefix(err.Error(), "Could not import path") {
+	if res != "" || !strings.HasPrefix(err.Error(), "Could not import path") {
 		t.Error("Unexpected result:", res, err)
 		return
 	}
 
 	res, err = fil.Resolve(filepath.Join("test1", "myfile.ecal"))
+	errorutil.AssertOk(err)
 
-	if res != codecontent || err != nil {
+	if res != codecontent {
 		t.Error("Unexpected result:", res, err)
 		return
 	}
@@ -93,23 +95,25 @@ func TestImportLocater(t *testing.T) {
 	mil.Files["foo"] = "bar"
 	mil.Files["test"] = "test1"
 
-	res, err = mil.Resolve("xxx")
+	_, err = mil.Resolve("xxx")
 
-	if res != "" || err == nil || err.Error() != "Could not find import path: xxx" {
+	if err.Error() != "Could not find import path: xxx" {
 		t.Error("Unexpected result:", res, err)
 		return
 	}
 
 	res, err = mil.Resolve("foo")
+	errorutil.AssertOk(err)
 
-	if res != "bar" || err != nil {
+	if res != "bar" {
 		t.Error("Unexpected result:", res, err)
 		return
 	}
 
 	res, err = mil.Resolve("test")
+	errorutil.AssertOk(err)
 
-	if res != "test1" || err != nil {
+	if res != "test1" {
 		t.Error("Unexpected result:", res, err)
 		return
 	}

+ 4 - 1
util/logging.go

@@ -44,6 +44,9 @@ type LogLevelLogger struct {
 	level  LogLevel
 }
 
+/*
+NewLogLevelLogger wraps a given logger and adds level based filtering functionality.
+*/
 func NewLogLevelLogger(logger Logger, level string) (*LogLevelLogger, error) {
 	llevel := LogLevel(strings.ToLower(level))
 
@@ -234,7 +237,7 @@ type BufferLogger struct {
 }
 
 /*
-NewNullLogger returns a buffer logger instance.
+NewBufferLogger returns a buffer logger instance.
 */
 func NewBufferLogger(buf io.Writer) *BufferLogger {
 	return &BufferLogger{buf}

+ 6 - 6
util/types.go

@@ -104,7 +104,7 @@ type ECALDebugger interface {
 	/*
 	   StopThreads will continue all suspended threads and set them to be killed.
 	   Returns true if a waiting thread was resumed. Can wait for threads to end
-	   by ensuring that for at least d time no state change occured.
+	   by ensuring that for at least d time no state change occurred.
 	*/
 	StopThreads(d time.Duration) bool
 
@@ -157,18 +157,18 @@ type ECALDebugger interface {
 		ExtractValue copies a value from a suspended thread into the
 		global variable scope.
 	*/
-	ExtractValue(threadId uint64, varName string, destVarName string) error
+	ExtractValue(threadID uint64, varName string, destVarName string) error
 
 	/*
 		InjectValue copies a value from an expression (using the global
 		variable scope) into a suspended thread.
 	*/
-	InjectValue(threadId uint64, varName string, expression string) error
+	InjectValue(threadID uint64, varName string, expression string) error
 
 	/*
 	   Continue will continue a suspended thread.
 	*/
-	Continue(threadId uint64, contType ContType)
+	Continue(threadID uint64, contType ContType)
 
 	/*
 		Status returns the current status of the debugger.
@@ -176,9 +176,9 @@ type ECALDebugger interface {
 	Status() interface{}
 
 	/*
-	   Describe decribes a thread currently observed by the debugger.
+	   Describe describes a thread currently observed by the debugger.
 	*/
-	Describe(threadId uint64) interface{}
+	Describe(threadID uint64) interface{}
 }
 
 /*