Browse Source

feat: Adding initial parts for debug support

Matthias Ladkau 3 years ago
parent
commit
ecfaf8048d

+ 4 - 0
.gitignore

@@ -8,3 +8,7 @@ ecal
 /dist
 /build
 /stdlib/stdlib_gen.go
+/ecal-support/node_modules
+/ecal-support/out
+/ecal-support/*.vsix
+/ecal-support/package-lock.json

+ 25 - 16
cli/ecal.go

@@ -23,6 +23,7 @@ import (
 TODO:
 - create executable binary (pack into single binary)
 - debug server support (vscode)
+- pretty printer
 */
 
 func main() {
@@ -45,7 +46,7 @@ func main() {
 		fmt.Println()
 		fmt.Println("    console   Interactive console (default)")
 		fmt.Println("    run       Execute ECAL code")
-		fmt.Println("    debug     Run a debug server")
+		fmt.Println("    debug     Run in debug mode")
 		fmt.Println("    pack      Create a single executable from ECAL code")
 		fmt.Println()
 		fmt.Println(fmt.Sprintf("Use %s <command> -help for more information about a given command.", os.Args[0]))
@@ -54,26 +55,34 @@ func main() {
 
 	// Parse the command bit
 
-	err := flag.CommandLine.Parse(os.Args[1:])
+	if err := flag.CommandLine.Parse(os.Args[1:]); err == nil {
+		interpreter := tool.NewCLIInterpreter()
 
-	if len(flag.Args()) > 0 {
+		if len(flag.Args()) > 0 {
 
-		arg := flag.Args()[0]
+			arg := flag.Args()[0]
 
-		if arg == "console" {
-			err = tool.Interpret(true)
-		} else if arg == "run" {
-			err = tool.Interpret(false)
-		} else {
-			flag.Usage()
-		}
+			if arg == "console" {
+				err = interpreter.Interpret(true)
+			} else if arg == "run" {
+				err = interpreter.Interpret(false)
+			} else if arg == "debug" {
+				debugInterpreter := tool.NewCLIDebugInterpreter(interpreter)
+				err = debugInterpreter.Interpret()
+			} else {
+				flag.Usage()
+			}
 
-	} else if err == nil {
+		} else if err == nil {
 
-		err = tool.Interpret(true)
-	}
+			err = interpreter.Interpret(true)
+		}
+
+		if err != nil {
+			fmt.Println(fmt.Sprintf("Error: %v", err))
+		}
 
-	if err != nil {
-		fmt.Println(fmt.Sprintf("Error: %v", err))
+	} else {
+		flag.Usage()
 	}
 }

+ 191 - 0
cli/tool/debug.go

@@ -0,0 +1,191 @@
+/*
+ * 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"
+	"flag"
+	"fmt"
+	"net"
+	"strings"
+	"time"
+
+	"devt.de/krotik/ecal/interpreter"
+	"devt.de/krotik/ecal/util"
+)
+
+/*
+CLIDebugInterpreter is a commandline interpreter with debug capabilities for ECAL.
+*/
+type CLIDebugInterpreter struct {
+	*CLIInterpreter
+
+	// Parameter these can either be set programmatically or via CLI args
+
+	DebugServerAddr *string // Debug server address
+	RunDebugServer  *bool   // Run a debug server
+	Interactive     *bool   // Flag if the interpreter should open a console in the current tty.
+}
+
+/*
+NewCLIDebugInterpreter wraps an existing CLIInterpreter object and adds capabilities.
+*/
+func NewCLIDebugInterpreter(i *CLIInterpreter) *CLIDebugInterpreter {
+	return &CLIDebugInterpreter{i, nil, nil, nil}
+}
+
+/*
+ParseArgs parses the command line arguments.
+*/
+func (i *CLIDebugInterpreter) ParseArgs() bool {
+
+	if i.Interactive != nil {
+		return false
+	}
+
+	i.DebugServerAddr = flag.String("serveraddr", "localhost:33274", "Debug server address") // Think BERTA
+	i.RunDebugServer = flag.Bool("server", false, "Run a debug server")
+	i.Interactive = flag.Bool("interactive", true, "Run interactive console")
+
+	return i.CLIInterpreter.ParseArgs()
+}
+
+/*
+Interpret starts the ECAL code interpreter with debug capabilities.
+*/
+func (i *CLIDebugInterpreter) Interpret() error {
+
+	if i.ParseArgs() {
+		return nil
+	}
+
+	i.CLIInterpreter.CustomWelcomeMessage = "Running in debug mode - "
+	if *i.RunDebugServer {
+		i.CLIInterpreter.CustomWelcomeMessage += fmt.Sprintf("with debug server on %v - ", *i.DebugServerAddr)
+	}
+	i.CLIInterpreter.CustomWelcomeMessage += "prefix debug commands with ##"
+	i.CustomHelpString = "    @dbg [glob] - List all available debug commands.\n"
+
+	err := i.CreateRuntimeProvider("debug console")
+
+	if err == nil {
+
+		if *i.RunDebugServer {
+			debugServer := &debugTelnetServer{*i.DebugServerAddr, "ECALDebugServer: ",
+				nil, true, i, i.RuntimeProvider.Logger}
+			go debugServer.Run()
+			time.Sleep(500 * time.Millisecond) // Too lazy to do proper signalling
+			defer func() {
+				if debugServer.listener != nil {
+					debugServer.listen = false
+					debugServer.listener.Close() // Attempt to cleanup
+				}
+			}()
+		}
+
+		err = i.CLIInterpreter.Interpret(*i.Interactive)
+	}
+
+	return err
+}
+
+/*
+debugTelnetServer is a simple telnet server to send and receive debug data.
+*/
+type debugTelnetServer struct {
+	address     string
+	logPrefix   string
+	listener    *net.TCPListener
+	listen      bool
+	interpreter *CLIDebugInterpreter
+	logger      util.Logger
+}
+
+/*
+Run runs the debug server.
+*/
+func (s *debugTelnetServer) Run() {
+	tcpaddr, err := net.ResolveTCPAddr("tcp", s.address)
+
+	if err == nil {
+
+		if s.listener, err = net.ListenTCP("tcp", tcpaddr); err == nil {
+
+			s.logger.LogInfo(s.logPrefix,
+				"Running Debug Server on ", tcpaddr.String())
+
+			for s.listen {
+				var conn net.Conn
+
+				if conn, err = s.listener.Accept(); err == nil {
+
+					go s.HandleConnection(conn)
+
+				} else if s.listen {
+					s.logger.LogError(s.logPrefix, err)
+					err = nil
+				}
+			}
+		}
+	}
+
+	if s.listen && err != nil {
+		s.logger.LogError(s.logPrefix, "Could not start debug server - ", err)
+	}
+}
+
+/*
+HandleConnection handles an incoming connection.
+*/
+func (s *debugTelnetServer) HandleConnection(conn net.Conn) {
+	inputReader := bufio.NewReader(conn)
+	outputTerminal := interpreter.OutputTerminal(&bufioWriterShim{bufio.NewWriter(conn)})
+
+	line := ""
+
+	s.logger.LogDebug(s.logPrefix, "Connect ", conn.RemoteAddr())
+
+	for {
+		var err error
+
+		if line, err = inputReader.ReadString('\n'); err == nil {
+			line = strings.TrimSpace(line)
+
+			if line == "exit" || line == "q" || line == "quit" || line == "bye" || line == "\x04" {
+				break
+			}
+
+			s.interpreter.HandleInput(outputTerminal, line)
+		}
+
+		if err != nil {
+			s.logger.LogDebug(s.logPrefix, "Disconnect ", conn.RemoteAddr(), " - ", err)
+			break
+		}
+	}
+
+	conn.Close()
+}
+
+/*
+bufioWriterShim is a shim to allow a bufio.Writer to be used as an OutputTerminal.
+*/
+type bufioWriterShim struct {
+	writer *bufio.Writer
+}
+
+/*
+WriteString write a string to the writer.
+*/
+func (shim *bufioWriterShim) WriteString(s string) {
+	shim.writer.WriteString(s)
+	shim.writer.Flush()
+}

+ 19 - 3
cli/tool/helper.go

@@ -16,14 +16,30 @@ import (
 	"strings"
 
 	"devt.de/krotik/common/stringutil"
-	"devt.de/krotik/common/termutil"
+	"devt.de/krotik/ecal/interpreter"
 )
 
+/*
+CLIInputHandler is a handler object for CLI input.
+*/
+type CLIInputHandler interface {
+
+	/*
+	   CanHandle checks if a given string can be handled by this handler.
+	*/
+	CanHandle(s string) bool
+
+	/*
+	   Handle handles a given input string.
+	*/
+	Handle(ot interpreter.OutputTerminal, args []string)
+}
+
 /*
 matchesFulltextSearch checks if a given text matches a given glob expression. Returns
 true if an error occurs.
 */
-func matchesFulltextSearch(clt termutil.ConsoleLineTerminal, text string, glob string) bool {
+func matchesFulltextSearch(ot interpreter.OutputTerminal, text string, glob string) bool {
 	var res bool
 
 	re, err := stringutil.GlobToRegex(glob)
@@ -33,7 +49,7 @@ func matchesFulltextSearch(clt termutil.ConsoleLineTerminal, text string, glob s
 	}
 
 	if err != nil {
-		clt.WriteString(fmt.Sprintln("Invalid search expression:", err.Error()))
+		ot.WriteString(fmt.Sprintln("Invalid search expression:", err.Error()))
 		res = true
 	}
 

+ 189 - 123
cli/tool/interpret.go

@@ -30,211 +30,277 @@ import (
 )
 
 /*
-Interpret starts the ECAL code interpreter from a CLI application which
-calls the interpret function as a sub executable. Starts an interactive console
-if the interactive flag is set.
+CLIInterpreter is a commandline interpreter for ECAL.
 */
-func Interpret(interactive bool) error {
-	var err error
+type CLIInterpreter struct {
+	GlobalVS        parser.Scope                     // Global variable scope
+	RuntimeProvider *interpreter.ECALRuntimeProvider // Runtime provider of the interpreter
+
+	// Customizations of output and input handling
+
+	CustomHandler        CLIInputHandler
+	CustomWelcomeMessage string
+	CustomHelpString     string
+
+	// Parameter these can either be set programmatically or via CLI args
+
+	Dir      *string // Root dir for interpreter
+	LogFile  *string // Logfile (blank for stdout)
+	LogLevel *string // Log level string (Debug, Info, Error)
+}
+
+/*
+NewCLIInterpreter creates a new commandline interpreter for ECAL.
+*/
+func NewCLIInterpreter() *CLIInterpreter {
+	return &CLIInterpreter{scope.NewScope(scope.GlobalScope), nil, nil, "", "", nil, nil, nil}
+}
+
+/*
+ParseArgs parses the command line arguments. Call this after adding custon flags.
+Returns true if the program should exit.
+*/
+func (i *CLIInterpreter) ParseArgs() bool {
+
+	if i.Dir != nil && i.LogFile != nil && i.LogLevel != nil {
+		return false
+	}
 
 	wd, _ := os.Getwd()
 
-	idir := flag.String("dir", wd, "Root directory for ECAL interpreter")
-	ilogFile := flag.String("logfile", "", "Log to a file")
-	ilogLevel := flag.String("loglevel", "Info", "Logging level (Debug, Info, Error)")
+	i.Dir = flag.String("dir", wd, "Root directory for ECAL interpreter")
+	i.LogFile = flag.String("logfile", "", "Log to a file")
+	i.LogLevel = flag.String("loglevel", "Info", "Logging level (Debug, Info, Error)")
 	showHelp := flag.Bool("help", false, "Show this help message")
 
 	flag.Usage = func() {
 		fmt.Println()
-		if !interactive {
-			fmt.Println(fmt.Sprintf("Usage of %s run [options] <file>", os.Args[0]))
-		} else {
-			fmt.Println(fmt.Sprintf("Usage of %s [options]", os.Args[0]))
-		}
+		fmt.Println(fmt.Sprintf("Usage of %s run [options] [file]", os.Args[0]))
 		fmt.Println()
 		flag.PrintDefaults()
 		fmt.Println()
 	}
 
-	if len(os.Args) > 2 {
+	if len(os.Args) >= 2 {
 		flag.CommandLine.Parse(os.Args[2:])
 
 		if *showHelp {
 			flag.Usage()
-			return nil
 		}
 	}
 
-	var clt termutil.ConsoleLineTerminal
-	var logger util.Logger
+	return *showHelp
+}
 
-	clt, err = termutil.NewConsoleLineTerminal(os.Stdout)
+/*
+Create the runtime provider of this interpreter. This function expects Dir,
+LogFile and LogLevel to be set.
+*/
+func (i *CLIInterpreter) CreateRuntimeProvider(name string) error {
+	var logger util.Logger
+	var err error
 
-	if interactive {
-		fmt.Println(fmt.Sprintf("ECAL %v", config.ProductVersion))
+	if i.RuntimeProvider != nil {
+		return nil
 	}
 
-	// Create the logger
+	// Check if we should log to a file
 
-	if err == nil {
+	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)
 
-		// Check if we should log to a file
+	} else {
 
-		if ilogFile != nil && *ilogFile != "" {
-			var logWriter io.Writer
-			logFileRollover := fileutil.SizeBasedRolloverCondition(1000000) // Each file can be up to a megabyte
-			logWriter, err = fileutil.NewMultiFileBuffer(*ilogFile, fileutil.ConsecutiveNumberIterator(10), logFileRollover)
-			logger = util.NewBufferLogger(logWriter)
+		// Log to the console by default
 
-		} else {
+		logger = util.NewStdOutLogger()
+	}
 
-			// Log to the console by default
+	// Set the log level
 
-			logger = util.NewStdOutLogger()
+	if err == nil {
+		if i.LogLevel != nil && *i.LogLevel != "" {
+			logger, err = util.NewLogLevelLogger(logger, *i.LogLevel)
 		}
 
-		// Set the log level
-
 		if err == nil {
-			if ilogLevel != nil && *ilogLevel != "" {
-				if logger, err = util.NewLogLevelLogger(logger, *ilogLevel); err == nil && interactive {
-					fmt.Print(fmt.Sprintf("Log level: %v - ", logger.(*util.LogLevelLogger).Level()))
-				}
-			}
-		}
-	}
+			// Get the import locator
 
-	if err == nil {
+			importLocator := &util.FileImportLocator{Root: *i.Dir}
 
-		// Get the import locator
+			// Create interpreter
 
-		if interactive {
-			fmt.Println(fmt.Sprintf("Root directory: %v", *idir))
+			i.RuntimeProvider = interpreter.NewECALRuntimeProvider(name, importLocator, logger)
 		}
+	}
+
+	return err
+}
 
-		importLocator := &util.FileImportLocator{Root: *idir}
+/*
+Interpret starts the ECAL code interpreter. Starts an interactive console in
+the current tty if the interactive flag is set.
+*/
+func (i *CLIInterpreter) Interpret(interactive bool) error {
 
-		name := "console"
+	if i.ParseArgs() {
+		return nil
+	}
 
-		// Create interpreter
+	clt, err := termutil.NewConsoleLineTerminal(os.Stdout)
 
-		erp := interpreter.NewECALRuntimeProvider(name, importLocator, logger)
+	if interactive {
+		fmt.Println(fmt.Sprintf("ECAL %v", config.ProductVersion))
+	}
 
-		// Create global variable scope
+	// Create Runtime Provider
 
-		vs := scope.NewScope(scope.GlobalScope)
+	if err == nil {
 
-		// Execute file if given
+		if err = i.CreateRuntimeProvider("console"); err == nil {
 
-		if cargs := flag.Args(); len(cargs) > 0 {
-			var ast *parser.ASTNode
-			var initFile []byte
+			if interactive {
+				if lll, ok := i.RuntimeProvider.Logger.(*util.LogLevelLogger); ok {
+					fmt.Print(fmt.Sprintf("Log level: %v - ", lll.Level()))
+				}
 
-			initFileName := flag.Arg(0)
-			initFile, err = ioutil.ReadFile(initFileName)
+				fmt.Println(fmt.Sprintf("Root directory: %v", *i.Dir))
 
-			if ast, err = parser.ParseWithRuntime(initFileName, string(initFile), erp); err == nil {
-				if err = ast.Runtime.Validate(); err == nil {
-					_, err = ast.Runtime.Eval(vs, make(map[string]interface{}))
+				if i.CustomWelcomeMessage != "" {
+					fmt.Println(fmt.Sprintf(i.CustomWelcomeMessage))
 				}
 			}
-		}
 
-		if err == nil {
+			// Execute file if given
 
-			if interactive {
+			if cargs := flag.Args(); len(cargs) > 0 {
+				var ast *parser.ASTNode
+				var initFile []byte
 
-				// Drop into interactive shell
+				initFileName := flag.Arg(0)
+				initFile, err = ioutil.ReadFile(initFileName)
 
-				if err == nil {
-					isExitLine := func(s string) bool {
-						return s == "exit" || s == "q" || s == "quit" || s == "bye" || s == "\x04"
+				if ast, err = parser.ParseWithRuntime(initFileName, string(initFile), i.RuntimeProvider); err == nil {
+					if err = ast.Runtime.Validate(); err == nil {
+						_, err = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{}))
 					}
+				}
+			}
+
+			if err == nil {
 
-					// Add history functionality without file persistence
+				if interactive {
 
-					clt, err = termutil.AddHistoryMixin(clt, "",
-						func(s string) bool {
-							return isExitLine(s)
-						})
+					// Drop into interactive shell
 
 					if err == nil {
+						isExitLine := func(s string) bool {
+							return s == "exit" || s == "q" || s == "quit" || s == "bye" || s == "\x04"
+						}
 
-						if err = clt.StartTerm(); err == nil {
-							var line string
+						// Add history functionality without file persistence
 
-							defer clt.StopTerm()
+						clt, err = termutil.AddHistoryMixin(clt, "",
+							func(s string) bool {
+								return isExitLine(s)
+							})
 
-							fmt.Println("Type 'q' or 'quit' to exit the shell and '?' to get help")
+						if err == nil {
 
-							line, err = clt.NextLine()
-							for err == nil && !isExitLine(line) {
-								trimmedLine := strings.TrimSpace(line)
+							if err = clt.StartTerm(); err == nil {
+								var line string
 
-								// Process the entered line
+								defer clt.StopTerm()
 
-								if line == "?" {
+								fmt.Println("Type 'q' or 'quit' to exit the shell and '?' to get help")
 
-									// Show help
+								line, err = clt.NextLine()
+								for err == nil && !isExitLine(line) {
+									trimmedLine := strings.TrimSpace(line)
 
-									clt.WriteString(fmt.Sprintf("ECAL %v\n", config.ProductVersion))
-									clt.WriteString(fmt.Sprintf("\n"))
-									clt.WriteString(fmt.Sprintf("Console supports all normal ECAL statements and the following special commands:\n"))
-									clt.WriteString(fmt.Sprintf("\n"))
-									clt.WriteString(fmt.Sprintf("    @sym [glob] - List all available inbuild functions and available stdlib packages of ECAL.\n"))
-									clt.WriteString(fmt.Sprintf("    @std <package> [glob] - List all available constants and functions of a stdlib package.\n"))
-									clt.WriteString(fmt.Sprintf("\n"))
-									clt.WriteString(fmt.Sprintf("Add an argument after a list command to do a full text search. The search string should be in glob format.\n"))
+									i.HandleInput(clt, trimmedLine)
 
-								} else if strings.HasPrefix(trimmedLine, "@sym") {
-									displaySymbols(clt, strings.Split(trimmedLine, " ")[1:])
+									line, err = clt.NextLine()
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
 
-								} else if strings.HasPrefix(trimmedLine, "@std") {
-									displayPackage(clt, strings.Split(trimmedLine, " ")[1:])
+	return err
+}
 
-								} else {
-									var ierr error
-									var ast *parser.ASTNode
-									var res interface{}
+/*
+HandleInput handles input to this interpreter. It parses a given input line
+and outputs on the given output terminal.
+*/
+func (i *CLIInterpreter) HandleInput(ot interpreter.OutputTerminal, line string) {
 
-									if ast, ierr = parser.ParseWithRuntime("console input", line, erp); ierr == nil {
+	// Process the entered line
 
-										if ierr = ast.Runtime.Validate(); ierr == nil {
+	if line == "?" {
 
-											if res, ierr = ast.Runtime.Eval(vs, make(map[string]interface{})); ierr == nil && res != nil {
-												clt.WriteString(fmt.Sprintln(res))
-											}
-										}
-									}
+		// Show help
 
-									if ierr != nil {
-										clt.WriteString(fmt.Sprintln(ierr.Error()))
-									}
-								}
+		ot.WriteString(fmt.Sprintf("ECAL %v\n", config.ProductVersion))
+		ot.WriteString(fmt.Sprint("\n"))
+		ot.WriteString(fmt.Sprint("Console supports all normal ECAL statements and the following special commands:\n"))
+		ot.WriteString(fmt.Sprint("\n"))
+		ot.WriteString(fmt.Sprint("    @sym [glob] - List all available inbuild functions and available stdlib packages of ECAL.\n"))
+		ot.WriteString(fmt.Sprint("    @std <package> [glob] - List all available constants and functions of a stdlib package.\n"))
+		if i.CustomHelpString != "" {
+			ot.WriteString(i.CustomHelpString)
+		}
+		ot.WriteString(fmt.Sprint("\n"))
+		ot.WriteString(fmt.Sprint("Add an argument after a list command to do a full text search. The search string should be in glob format.\n"))
 
-								line, err = clt.NextLine()
-							}
-						}
-					}
+	} else if strings.HasPrefix(line, "@sym") {
+		i.displaySymbols(ot, strings.Split(line, " ")[1:])
+
+	} else if strings.HasPrefix(line, "@std") {
+		i.displayPackage(ot, strings.Split(line, " ")[1:])
+
+	} else if i.CustomHandler != nil && i.CustomHandler.CanHandle(line) {
+		i.CustomHandler.Handle(ot, strings.Split(line, " ")[1:])
+
+	} else {
+		var ierr error
+		var ast *parser.ASTNode
+		var res interface{}
+
+		if ast, ierr = parser.ParseWithRuntime("console input", line, i.RuntimeProvider); ierr == nil {
+
+			if ierr = ast.Runtime.Validate(); ierr == nil {
+
+				if res, ierr = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{})); ierr == nil && res != nil {
+					ot.WriteString(fmt.Sprintln(res))
 				}
 			}
 		}
-	}
 
-	return err
+		if ierr != nil {
+			ot.WriteString(fmt.Sprintln(ierr.Error()))
+		}
+	}
 }
 
 /*
 displaySymbols lists all available inbuild functions and available stdlib packages of ECAL.
 */
-func displaySymbols(clt termutil.ConsoleLineTerminal, args []string) {
+func (i *CLIInterpreter) displaySymbols(ot interpreter.OutputTerminal, args []string) {
 
 	tabData := []string{"Inbuild function", "Description"}
 
 	for name, f := range interpreter.InbuildFuncMap {
 		ds, _ := f.DocString()
 
-		if len(args) > 0 && !matchesFulltextSearch(clt, fmt.Sprintf("%v %v", name, ds), args[0]) {
+		if len(args) > 0 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", name, ds), args[0]) {
 			continue
 		}
 
@@ -242,7 +308,7 @@ func displaySymbols(clt termutil.ConsoleLineTerminal, args []string) {
 	}
 
 	if len(tabData) > 2 {
-		clt.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
+		ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
 			stringutil.SingleDoubleLineTable))
 	}
 
@@ -253,7 +319,7 @@ func displaySymbols(clt termutil.ConsoleLineTerminal, args []string) {
 	for _, p := range packageNames {
 		ps, _ := stdlib.GetPkgDocString(p)
 
-		if len(args) > 0 && !matchesFulltextSearch(clt, fmt.Sprintf("%v %v", p, ps), args[0]) {
+		if len(args) > 0 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", p, ps), args[0]) {
 			continue
 		}
 
@@ -261,7 +327,7 @@ func displaySymbols(clt termutil.ConsoleLineTerminal, args []string) {
 	}
 
 	if len(tabData) > 2 {
-		clt.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
+		ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
 			stringutil.SingleDoubleLineTable))
 	}
 }
@@ -269,7 +335,7 @@ func displaySymbols(clt termutil.ConsoleLineTerminal, args []string) {
 /*
 displayPackage list all available constants and functions of a stdlib package.
 */
-func displayPackage(clt termutil.ConsoleLineTerminal, args []string) {
+func (i *CLIInterpreter) displayPackage(ot interpreter.OutputTerminal, args []string) {
 
 	_, constSymbols, funcSymbols := stdlib.GetStdlibSymbols()
 
@@ -287,7 +353,7 @@ func displayPackage(clt termutil.ConsoleLineTerminal, args []string) {
 	}
 
 	if len(tabData) > 2 {
-		clt.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
+		ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
 			stringutil.SingleDoubleLineTable))
 	}
 
@@ -304,7 +370,7 @@ func displayPackage(clt termutil.ConsoleLineTerminal, args []string) {
 		fDoc = strings.Replace(fDoc, "\n", " ", -1)
 		fDoc = strings.Replace(fDoc, "\t", " ", -1)
 
-		if len(args) > 1 && !matchesFulltextSearch(clt, fmt.Sprintf("%v %v", f, fDoc), args[1]) {
+		if len(args) > 1 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", f, fDoc), args[1]) {
 			continue
 		}
 
@@ -312,7 +378,7 @@ func displayPackage(clt termutil.ConsoleLineTerminal, args []string) {
 	}
 
 	if len(tabData) > 2 {
-		clt.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
+		ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
 			stringutil.SingleDoubleLineTable))
 	}
 }

+ 75 - 0
debug.md

@@ -0,0 +1,75 @@
+ECAL Debugger
+--
+ECAL comes with extensive debugging support featuring:
+
+- Breakpoints
+
+
+Getting started
+--
+The simplest way to debug a given program is to run the interpreter in debug mode.
+
+```
+ecal debug
+```
+
+The interpreter can also start a telnet like debug server.
+```
+ecal debug -server
+```
+Note: The debug server is not secured and will run any code which is passed to it.
+
+
+Debug commands
+--
+#### `info`
+Get environment information.
+
+Example:
+```
+## info
+```
+
+#### `break`
+Set a break point to a specific line or identifier.
+
+Parameter | Description
+-|-
+file and line number as `file:line` / identifier | Line or identifier which should trigger the breakpoint.
+
+Example:
+```
+## break 5
+```
+
+#### `status`
+Check all running threads if a breakpoint has been reached and the execution has been halted.
+
+Example:
+```
+## status
+```
+
+#### `inspect`
+Show the context of a breakpoint if the execution has been halted.
+
+Parameter | Description
+-|-
+thread ID | Thread ID of a halted thread.
+
+Example:
+```
+## inspect 123
+```
+
+#### `cont`
+Continue the execution of a halted thread.
+
+Parameter | Description
+-|-
+thread ID | Thread ID of a halted thread.
+
+Example:
+```
+## cont
+```

+ 19 - 0
ecal-support/.eslintrc.js

@@ -0,0 +1,19 @@
+module.exports = {
+  env: {
+    browser: true,
+    es2021: true
+  },
+  extends: [
+    'standard'
+  ],
+  parser: '@typescript-eslint/parser',
+  parserOptions: {
+    ecmaVersion: 12,
+    sourceType: 'module'
+  },
+  plugins: [
+    '@typescript-eslint'
+  ],
+  rules: {
+  }
+}

+ 1 - 1
ecal-support/.vscode/launch.json

@@ -1,6 +1,6 @@
 // A launch configuration that launches the extension inside a new window
 {
-	"version": "0.2.0",
+    "version": "0.2.0",
     "configurations": [
         {
             "name": "Extension",

+ 38 - 1
ecal-support/README.md

@@ -6,6 +6,43 @@
 * `syntaxes/ecal.tmLanguage.json` - Text mate grammar file
 * `language-configuration.json` - language configuration for VSCode
 
+## Build the extention
+
+To build the extention you need `npm` installed.
+
+VSIX file can be build with `npm run package`
+
 ## Install the extension
 
-To start using your extension with Visual Studio Code copy it into the `<user home>/.vscode/extensions` folder and restart Code.
+The extention can be installed using a precompiled VSIX file which can be downloaded from here:
+
+https://devt.de/krotik/ecal/releases
+
+## Launch config for ecal projects
+
+```
+{
+	"version": "0.2.0",
+	"configurations": [
+		{
+			"type": "ecaldebug",
+			"request": "launch",
+			"name": "Debug ECAL script with ECAL Debug Server",
+
+			"serverURL": "localhost:43806",
+            "dir": "${workspaceFolder}",
+			"executeOnEntry": true,
+			"trace": false,
+		}
+	]
+}
+```
+
+- serverURL: URL of the ECAL debug server.
+- dir: Root directory for ECAL debug server.
+- executeOnEntry: (optional) Execute the ECAL script on entry. If this is set to false then code needs to be manually started from the ECAL debug server console.
+- trace: (optional) Enable tracing messages for debug adapter (useful when debugging the debugger).
+
+## Developing the extension
+
+In VSCode the extention can be launched and debugged using the included launch configuration. Press F5 to start a VS Code instance with ECAL support extention form the development code.

+ 24 - 0
ecal-support/notes.txt

@@ -0,0 +1,24 @@
+debug extension - how to declare it - must have a bin file
+https://code.visualstudio.com/api/extension-guides/debugger-extension
+
+https://github.com/Microsoft/vscode-mock-debug/blob/d33d6057c2d7d7f0495e26d6fa8844c8336b2408/src/extension.ts#L79
+
+
+https://microsoft.github.io/debug-adapter-protocol/overview
+
+https://github.com/google/go-dap
+
+https://github.com/golang/vscode-go/blob/master/package.json
+
+
+Firefox vscode extension
+https://github.com/firefox-devtools/vscode-firefox-debug
+
+
+Blog:
+https://code.visualstudio.com/blogs/2018/07/12/introducing-logpoints-and-auto-attach
+
+
+BUG:
+https://github.com/Microsoft/vscode/issues/63896
+

+ 115 - 13
ecal-support/package.json

@@ -1,25 +1,127 @@
 {
     "name": "ecal-support",
     "displayName": "ECAL Support",
-    "description": "Support for the Event Condition Action Language (ECAL).",
-    "version": "0.0.1",
+    "version": "0.9.1",
+    "publisher": "krotik",
+    "description": "Extension to support the development of ECAL scripts in VS Code.",
+    "author": {
+        "name": "Matthias Ladkau",
+        "email": "github@ladkau.de"
+    },
+    "license": "MIT",
     "engines": {
         "vscode": "^1.50.0"
     },
+    "icon": "images/logo.png",
     "categories": [
         "Programming Languages"
     ],
+    "repository": {
+        "type": "git",
+        "url": "https://devt.de/krotik/ecal.git"
+    },
+    "scripts": {
+        "compile": "tsc",
+        "watch": "tsc -w",
+        "package": "vsce package",
+        "pretty": "eslint 'src/**/*.{js,ts,tsx}' --quiet --fix"
+    },
+    "dependencies": {
+        "@jpwilliams/waitgroup": "1.0.1",
+        "vscode-debugadapter": "^1.42.1"
+    },
+    "devDependencies": {
+        "@types/node": "^14.14.2",
+        "@types/vscode": "^1.50.0",
+        "@typescript-eslint/eslint-plugin": "^4.5.0",
+        "@typescript-eslint/parser": "^4.5.0",
+        "eslint": "^7.12.0",
+        "eslint-config-standard": "^15.0.0",
+        "eslint-plugin-import": "^2.22.1",
+        "eslint-plugin-node": "^11.1.0",
+        "eslint-plugin-promise": "^4.2.1",
+        "eslint-plugin-standard": "^4.0.2",
+        "typescript": "^4.0.3",
+        "vsce": "^1.81.1"
+    },
+    "main": "./out/extension.js",
+    "activationEvents": [
+        "onDebug"
+    ],
     "contributes": {
-        "languages": [{
-            "id": "ecal",
-            "aliases": ["Event Condition Action Language", "ecal"],
-            "extensions": [".ecal"],
-            "configuration": "./language-configuration.json"
-        }],
-        "grammars": [{
-            "language": "ecal",
-            "scopeName": "source.ecal",
-            "path": "./syntaxes/ecal.tmLanguage.json"
-        }]
+        "languages": [
+            {
+                "id": "ecal",
+                "aliases": [
+                    "Event Condition Action Language",
+                    "ecal"
+                ],
+                "extensions": [
+                    ".ecal"
+                ],
+                "configuration": "./language-configuration.json"
+            }
+        ],
+        "grammars": [
+            {
+                "language": "ecal",
+                "scopeName": "source.ecal",
+                "path": "./syntaxes/ecal.tmLanguage.json"
+            }
+        ],
+        "breakpoints": [
+            {
+                "language": "ecal"
+            }
+        ],
+        "debuggers": [
+            {
+                "type": "ecaldebug",
+                "label": "ECAL Debug",
+                "program": "./out/ecalDebugAdapter.js",
+                "runtime": "node",
+                "configurationAttributes": {
+                    "launch": {
+                        "required": [
+                            "serverURL",
+                            "dir"
+                        ],
+                        "properties": {
+                            "serverURL": {
+                                "type": "string",
+                                "description": "URL of the ECAL debug server.",
+                                "default": "localhost:43806"
+                            },
+                            "dir": {
+                                "type": "string",
+                                "description": "Root directory for ECAL debug server.",
+                                "default": "${workspaceFolder}"
+                            },
+                            "executeOnEntry": {
+                                "type": "boolean",
+                                "description": "Execute the ECAL script on entry. If this is set to false then code needs to be manually started from the ECAL debug server console.",
+                                "default": true
+                            },
+                            "trace": {
+                                "type": "boolean",
+                                "description": "Enable logging of the Debug Adapter Protocol.",
+                                "default": false
+                            }
+                        }
+                    }
+                },
+                "initialConfigurations": [
+                    {
+                        "type": "ecaldebug",
+                        "request": "launch",
+                        "name": "Debug ECAL script with ECAL Debug Server",
+                        "serverURL": "localhost:43806",
+                        "dir": "${workspaceFolder}",
+                        "executeOnEntry": true,
+                        "trace": false
+                    }
+                ]
+            }
+        ]
     }
 }

+ 262 - 0
ecal-support/src/ecalDebugAdapter.ts

@@ -0,0 +1,262 @@
+/**
+ * Debug Adapter for VS Code to support the ECAL debugger.
+ *
+ * See the debugger extension guide:
+ * https://code.visualstudio.com/api/extension-guides/debugger-extension
+ */
+
+import { logger, Logger, LoggingDebugSession, InitializedEvent, Thread } from 'vscode-debugadapter'
+import { DebugProtocol } from 'vscode-debugprotocol'
+import { WaitGroup } from '@jpwilliams/waitgroup'
+
+/**
+ * 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
+ * debug adapter from a lauch configuration ('.vscode/launch.json') in a project folder.
+ */
+interface ECALDebugArguments extends DebugProtocol.LaunchRequestArguments {
+
+    dir: string; // Root directory for ECAL interpreter
+    serverURL: string // URL of the ECAL debug server
+    executeOnEntry?: boolean; // Flag if the debugged script should be executed when the debug session is started
+    trace?: boolean; // Flag to enable verbose logging of the adapter protocol
+}
+
+/**
+ * Debug adapter implementation.
+ *
+ * Uses: https://github.com/microsoft/vscode-debugadapter-node
+ *
+ * See the Debug Adapter Protocol (DAP) documentation:
+ * https://microsoft.github.io/debug-adapter-protocol/overview#How_it_works
+ */
+export class ECALDebugSession extends LoggingDebugSession {
+    /**
+     * WaitGroup to wait the finish of the configuration sequence
+     */
+    private wgConfig = new WaitGroup();
+
+    /**
+     * Create a new debug adapter which is used for one debug session.
+     */
+    public constructor () {
+      super('mock-debug.txt')
+      console.error('##### constructor')
+      // Lines and columns start at 1
+      this.setDebuggerLinesStartAt1(true)
+      this.setDebuggerColumnsStartAt1(true)
+
+      // Increment the config WaitGroup counter for configurationDoneRequest()
+      this.wgConfig.add(1)
+    }
+
+    /**
+     * Called as the first step in the DAP. The client (e.g. VSCode)
+     * interrogates the debug adapter on the features which it provides.
+     */
+    protected initializeRequest (response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
+      console.log('##### initializeRequest:', args)
+
+      response.body = response.body || {}
+
+      // The adapter implements the configurationDoneRequest.
+      response.body.supportsConfigurationDoneRequest = true
+
+      this.sendResponse(response)
+
+      this.sendEvent(new InitializedEvent())
+    }
+
+    /**
+     * Called as part of the "configuration Done" step in the DAP. The client (e.g. VSCode) has
+     * finished the initialization of the debug adapter.
+     */
+    protected configurationDoneRequest (response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void {
+      console.error('##### configurationDoneRequest')
+
+      super.configurationDoneRequest(response, args)
+      this.wgConfig.done()
+    }
+
+    /**
+     * The client (e.g. VSCode) asks the debug adapter to start the debuggee communication.
+     */
+    protected async launchRequest (response: DebugProtocol.LaunchResponse, args: ECALDebugArguments) {
+      console.error('##### launchRequest:', args)
+
+      // Setup logging either verbose or just on errors
+
+      logger.setup(args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Error, false)
+
+      await this.wgConfig.wait() // Wait for configuration sequence to finish
+
+      this.sendResponse(response)
+    }
+
+    protected async setBreakPointsRequest (response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): Promise<void> {
+      console.error('##### setBreakPointsRequest:', args)
+
+      response.body = {
+        breakpoints: []
+      }
+      this.sendResponse(response)
+    }
+
+    protected threadsRequest (response: DebugProtocol.ThreadsResponse): void {
+      console.error('##### threadsRequest')
+
+      // runtime supports no threads so just return a default thread.
+      response.body = {
+        threads: [
+          new Thread(1, 'thread 1')
+        ]
+      }
+      this.sendResponse(response)
+    }
+
+    protected stackTraceRequest (response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void {
+      console.error('##### stackTraceRequest:', args)
+
+      response.body = {
+        stackFrames: []
+      }
+      this.sendResponse(response)
+    }
+
+    protected scopesRequest (response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void {
+      console.error('##### scopesRequest:', args)
+
+      response.body = {
+        scopes: []
+      }
+      this.sendResponse(response)
+    }
+
+    protected async variablesRequest (response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments, request?: DebugProtocol.Request) {
+      console.error('##### variablesRequest', args, request)
+
+      response.body = {
+        variables: []
+      }
+      this.sendResponse(response)
+    }
+
+    protected continueRequest (response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void {
+      console.error('##### continueRequest', args)
+      this.sendResponse(response)
+    }
+
+    protected reverseContinueRequest (response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments): void {
+      console.error('##### reverseContinueRequest', args)
+      this.sendResponse(response)
+    }
+
+    protected nextRequest (response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void {
+      console.error('##### nextRequest', args)
+      this.sendResponse(response)
+    }
+
+    protected stepBackRequest (response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void {
+      console.error('##### stepBackRequest', args)
+      this.sendResponse(response)
+    }
+
+    protected stepInTargetsRequest (response: DebugProtocol.StepInTargetsResponse, args: DebugProtocol.StepInTargetsArguments) {
+      console.error('##### stepInTargetsRequest', args)
+      response.body = {
+        targets: []
+      }
+      this.sendResponse(response)
+    }
+
+    protected stepInRequest (response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments): void {
+      console.error('##### stepInRequest', args)
+      this.sendResponse(response)
+    }
+
+    protected stepOutRequest (response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments): void {
+      console.error('##### stepOutRequest', args)
+      this.sendResponse(response)
+    }
+
+    protected async evaluateRequest (response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): Promise<void> {
+      console.error('##### evaluateRequest', args)
+
+      response.body = {
+        result: 'evaluate',
+        variablesReference: 0
+      }
+      this.sendResponse(response)
+    }
+
+    protected dataBreakpointInfoRequest (response: DebugProtocol.DataBreakpointInfoResponse, args: DebugProtocol.DataBreakpointInfoArguments): void {
+      console.error('##### dataBreakpointInfoRequest', args)
+
+      response.body = {
+        dataId: null,
+        description: 'cannot break on data access',
+        accessTypes: undefined,
+        canPersist: false
+      }
+
+      this.sendResponse(response)
+    }
+
+    protected setDataBreakpointsRequest (response: DebugProtocol.SetDataBreakpointsResponse, args: DebugProtocol.SetDataBreakpointsArguments): void {
+      console.error('##### setDataBreakpointsRequest', args)
+
+      response.body = {
+        breakpoints: []
+      }
+
+      this.sendResponse(response)
+    }
+
+    protected completionsRequest (response: DebugProtocol.CompletionsResponse, args: DebugProtocol.CompletionsArguments): void {
+      console.error('##### completionsRequest', args)
+
+      response.body = {
+        targets: [
+          {
+            label: 'item 10',
+            sortText: '10'
+          },
+          {
+            label: 'item 1',
+            sortText: '01'
+          },
+          {
+            label: 'item 2',
+            sortText: '02'
+          },
+          {
+            label: 'array[]',
+            selectionStart: 6,
+            sortText: '03'
+          },
+          {
+            label: 'func(arg)',
+            selectionStart: 5,
+            selectionLength: 3,
+            sortText: '04'
+          }
+        ]
+      }
+      this.sendResponse(response)
+    }
+
+    protected cancelRequest (response: DebugProtocol.CancelResponse, args: DebugProtocol.CancelArguments) {
+      console.error('##### cancelRequest', args)
+      this.sendResponse(response)
+    }
+
+    protected customRequest (command: string, response: DebugProtocol.Response, args: any) {
+      console.error('##### customRequest', args)
+
+      if (command === 'toggleFormatting') {
+        this.sendResponse(response)
+      } else {
+        super.customRequest(command, response, args)
+      }
+    }
+}

+ 19 - 0
ecal-support/src/extension.ts

@@ -0,0 +1,19 @@
+import * as vscode from 'vscode'
+import { ProviderResult } from 'vscode'
+import { ECALDebugSession } from './ecalDebugAdapter'
+
+export function activate (context: vscode.ExtensionContext) {
+  context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory(
+    'ecaldebug', new InlineDebugAdapterFactory()))
+}
+
+export function deactivate () {
+}
+
+class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
+  createDebugAdapterDescriptor (_session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> {
+    // Declare the ECALDebugSession as an DebugAdapterInlineImplementation so extention and adapter can
+    // run in-process (making it possible to easily debug the adapter)
+    return new vscode.DebugAdapterInlineImplementation(new ECALDebugSession())
+  }
+}

+ 26 - 0
ecal-support/tsconfig.json

@@ -0,0 +1,26 @@
+{
+	"compilerOptions": {
+		"module": "commonjs",
+		"target": "es6",
+		"outDir": "out",
+		"lib": [
+			"es6"
+		],
+		"sourceMap": true,
+		"rootDir": "src",
+		"strict": true,
+		"noImplicitReturns": true,
+		"noFallthroughCasesInSwitch": true,
+		"noUnusedParameters": true,
+		"noImplicitAny": true,
+		"removeComments": true,
+		"noUnusedLocals": true,
+		"noImplicitThis": true,
+		"inlineSourceMap": false,
+		"preserveConstEnums": true,
+		"strictNullChecks": true
+	},
+	"exclude": [
+		"node_modules", "tmp"
+	]
+}

+ 12 - 0
ecal.md

@@ -461,6 +461,18 @@ Example:
 doc(len)
 ```
 
+#### `sleep(micros)`
+Sleep pauses the current thread for a number of micro seconds.
+
+Parameter | Description
+-|-
+micros | Number of micro seconds to sleep
+
+Example:
+```
+sleep(1000000) // Sleep a millisecond
+```
+
 #### `setCronTrigger(cronspec, eventname, eventkind) : string`
 Adds a periodic cron job which fires events. Use this function for long running
 periodic tasks.

+ 13 - 0
examples/game_of_life/.vscode/launch.json

@@ -0,0 +1,13 @@
+{
+    "version": "0.2.0",
+    "configurations": [		{
+        "type": "ecaldebug",
+        "request": "launch",
+        "name": "Debug ECAL script with ECAL Debug Server",
+
+        "serverURL": "localhost:43806",
+        "dir": "${workspaceFolder}",
+        "executeOnEntry": true,
+        "trace": true,
+    }]
+}

+ 17 - 1
interpreter/debug.go

@@ -13,4 +13,20 @@ Package interpreter contains the ECAL interpreter.
 */
 package interpreter
 
-// TODO: Stacktrace
+/*
+OutputTerminal is a generic output terminal which can write strings.
+*/
+type OutputTerminal interface {
+
+	/*
+	   WriteString write a string on this terminal.
+	*/
+	WriteString(s string)
+}
+
+/*
+Debugger is a debugging object which can be used to inspect and modify a running
+ECAL environment.
+*/
+type Debugger interface {
+}

+ 38 - 0
interpreter/func_provider.go

@@ -37,6 +37,7 @@ var InbuildFuncMap = map[string]util.ECALFunction{
 	"concat":          &concatFunc{&inbuildBaseFunc{}},
 	"dumpenv":         &dumpenvFunc{&inbuildBaseFunc{}},
 	"doc":             &docFunc{&inbuildBaseFunc{}},
+	"sleep":           &sleepFunc{&inbuildBaseFunc{}},
 	"raise":           &raise{&inbuildBaseFunc{}},
 	"addEvent":        &addevent{&inbuildBaseFunc{}},
 	"addEventAndWait": &addeventandwait{&addevent{&inbuildBaseFunc{}}},
@@ -541,6 +542,43 @@ func (rf *docFunc) DocString() (string, error) {
 	return "Doc returns the docstring of a function.", nil
 }
 
+// sleep
+// =====
+
+/*
+sleepFunc pauses the current thread for a number of micro seconds.
+*/
+type sleepFunc struct {
+	*inbuildBaseFunc
+}
+
+/*
+Run executes this function.
+*/
+func (rf *sleepFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, args []interface{}) (interface{}, error) {
+	var res interface{}
+	err := fmt.Errorf("Need number of micro seconds as parameter")
+
+	if len(args) > 0 {
+		var micros float64
+
+		micros, err = rf.AssertNumParam(1, args[0])
+
+		if err == nil {
+			time.Sleep(time.Duration(micros) * time.Microsecond)
+		}
+	}
+
+	return res, err
+}
+
+/*
+DocString returns a descriptive string.
+*/
+func (rf *sleepFunc) DocString() (string, error) {
+	return "Sleep pauses the current thread for a number of micro seconds.", nil
+}
+
 // raise
 // =====
 

+ 6 - 0
interpreter/func_provider_test.go

@@ -316,6 +316,12 @@ identifier: a
 		return
 	}
 
+	_, err = UnitTestEval(`sleep(10)`, nil)
+
+	if err != nil {
+		t.Error("Unexpected result: ", err)
+		return
+	}
 }
 
 func TestCronTrigger(t *testing.T) {

+ 2 - 1
interpreter/provider.go

@@ -140,6 +140,7 @@ type ECALRuntimeProvider struct {
 	Logger        util.Logger            // Logger object for log messages
 	Processor     engine.Processor       // Processor of the ECA engine
 	Cron          *timeutil.Cron         // Cron object for scheduled execution
+	Debugger      Debugger               // Optional: Debugger object
 }
 
 /*
@@ -171,7 +172,7 @@ func NewECALRuntimeProvider(name string, importLocator util.ECALImportLocator, l
 	cron := timeutil.NewCron()
 	cron.Start()
 
-	return &ECALRuntimeProvider{name, importLocator, logger, proc, cron}
+	return &ECALRuntimeProvider{name, importLocator, logger, proc, cron, nil}
 }
 
 /*