/*
* 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"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
"net"
"os"
"strings"
"sync"
"time"
"devt.de/krotik/common/errorutil"
"devt.de/krotik/common/stringutil"
"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
EchoDebugServer *bool // Echo all input and output of the debug server
Interactive *bool // Flag if the interpreter should open a console in the current tty.
BreakOnStart *bool // Flag if the debugger should stop the execution on start
BreakOnError *bool // Flag if the debugger should stop when encountering an error
LogOut io.Writer // Log output
debugServer *debugTelnetServer // Debug server if started
}
/*
NewCLIDebugInterpreter wraps an existing CLIInterpreter object and adds capabilities.
*/
func NewCLIDebugInterpreter(i *CLIInterpreter) *CLIDebugInterpreter {
return &CLIDebugInterpreter{i, nil, nil, nil, nil, nil, nil, os.Stdout, 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.EchoDebugServer = flag.Bool("echo", false, "Echo all i/o of the debug server")
i.Interactive = flag.Bool("interactive", true, "Run interactive console")
i.BreakOnStart = flag.Bool("breakonstart", false, "Stop the execution on start")
i.BreakOnError = flag.Bool("breakonerror", false, "Stop the execution when encountering an error")
return i.CLIInterpreter.ParseArgs()
}
/*
Interpret starts the ECAL code interpreter with debug capabilities.
*/
func (i *CLIDebugInterpreter) Interpret() error {
if i.ParseArgs() {
return nil
}
err := i.CreateRuntimeProvider("debug console")
if err == nil {
// Set custom messages
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"
// Set debug object on the runtime provider
i.RuntimeProvider.Debugger = interpreter.NewECALDebugger(i.GlobalVS)
i.RuntimeProvider.Debugger.BreakOnStart(*i.BreakOnStart)
i.RuntimeProvider.Debugger.BreakOnError(*i.BreakOnError)
// Set this object as a custom handler to deal with input.
i.CustomHandler = i
if *i.RunDebugServer {
// Start the debug server
i.debugServer = &debugTelnetServer{*i.DebugServerAddr, "ECALDebugServer: ",
nil, true, *i.EchoDebugServer, i, i.RuntimeProvider.Logger}
wg := &sync.WaitGroup{}
wg.Add(1)
go i.debugServer.Run(wg)
wg.Wait()
if *i.Interactive {
defer i.StopDebugServer()
}
}
err = i.CLIInterpreter.Interpret(*i.Interactive)
}
return err
}
/*
StopDebugServer stops the debug server if it was started.
*/
func (i *CLIDebugInterpreter) StopDebugServer() {
if i.debugServer != nil && i.debugServer.listener != nil {
i.debugServer.listen = false
i.debugServer.listener.Close() // Attempt to cleanup
}
}
/*
LoadInitialFile clears the global scope and reloads the initial file.
*/
func (i *CLIDebugInterpreter) LoadInitialFile(tid uint64) error {
i.RuntimeProvider.Debugger.StopThreads(500 * time.Millisecond)
i.RuntimeProvider.Debugger.BreakOnStart(*i.BreakOnStart)
i.RuntimeProvider.Debugger.BreakOnError(*i.BreakOnError)
return nil
}
/*
CanHandle checks if a given string can be handled by this handler.
*/
func (i *CLIDebugInterpreter) CanHandle(s string) bool {
return strings.HasPrefix(s, "##") || strings.HasPrefix(s, "@dbg")
}
/*
Handle handles a given input string.
*/
func (i *CLIDebugInterpreter) Handle(ot OutputTerminal, line string) {
if strings.HasPrefix(line, "@dbg") {
args := strings.Fields(line)[1:]
tabData := []string{"Debug command", "Description"}
for name, f := range interpreter.DebugCommandsMap {
ds := f.DocString()
if len(args) > 0 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", name, ds), args[0]) {
continue
}
tabData = fillTableRow(tabData, name, ds)
}
if len(tabData) > 2 {
ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
stringutil.SingleDoubleLineTable))
}
ot.WriteString(fmt.Sprintln(fmt.Sprintln()))
} else {
res, err := i.RuntimeProvider.Debugger.HandleInput(strings.TrimSpace(line[2:]))
if err == nil {
var outBytes []byte
outBytes, err = json.MarshalIndent(res, "", " ")
if err == nil {
ot.WriteString(fmt.Sprintln(fmt.Sprintln(string(outBytes))))
}
}
if err != nil {
var outBytes []byte
outBytes, err = json.MarshalIndent(map[string]interface{}{
"DebuggerError": err.Error(),
}, "", " ")
errorutil.AssertOk(err)
ot.WriteString(fmt.Sprintln(fmt.Sprintln(string(outBytes))))
}
}
}
/*
debugTelnetServer is a simple telnet server to send and receive debug data.
*/
type debugTelnetServer struct {
address string
logPrefix string
listener *net.TCPListener
listen bool
echo bool
interpreter *CLIDebugInterpreter
logger util.Logger
}
/*
Run runs the debug server.
*/
func (s *debugTelnetServer) Run(wg *sync.WaitGroup) {
tcpaddr, err := net.ResolveTCPAddr("tcp", s.address)
if 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())
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)
wg.Done()
}
}
/*
HandleConnection handles an incoming connection.
*/
func (s *debugTelnetServer) HandleConnection(conn net.Conn) {
tid := s.interpreter.RuntimeProvider.NewThreadID()
inputReader := bufio.NewReader(conn)
outputTerminal := OutputTerminal(&bufioWriterShim{fmt.Sprint(conn.RemoteAddr()),
bufio.NewWriter(conn), s.echo, s.interpreter.LogOut})
line := ""
s.logger.LogDebug(s.logPrefix, "Connect ", conn.RemoteAddr())
if s.echo {
fmt.Fprintln(s.interpreter.LogOut, fmt.Sprintf("%v : Connected", conn.RemoteAddr()))
}
for {
var outBytes []byte
var err error
if line, err = inputReader.ReadString('\n'); err == nil {
line = strings.TrimSpace(line)
if s.echo {
fmt.Fprintln(s.interpreter.LogOut, fmt.Sprintf("%v > %v", conn.RemoteAddr(), line))
}
if line == "exit" || line == "q" || line == "quit" || line == "bye" || line == "\x04" {
break
}
isHelpTable := strings.HasPrefix(line, "@")
if !s.interpreter.CanHandle(line) || isHelpTable {
buffer := bytes.NewBuffer(nil)
s.interpreter.HandleInput(&bufioWriterShim{"tmpbuffer",
bufio.NewWriter(buffer), false, s.interpreter.LogOut}, line, tid)
if isHelpTable {
// Special case we have tables which should be transformed
r := strings.NewReplacer("═", "*", "│", "*", "╪", "*", "╒", "*",
"╕", "*", "╘", "*", "╛", "*", "╤", "*", "╞", "*", "╡", "*", "╧", "*")
outBytes = []byte(r.Replace(buffer.String()))
} else {
outBytes = buffer.Bytes()
}
outBytes, err = json.MarshalIndent(map[string]interface{}{
"EncodedOutput": base64.StdEncoding.EncodeToString(outBytes),
}, "", " ")
errorutil.AssertOk(err)
outputTerminal.WriteString(fmt.Sprintln(fmt.Sprintln(string(outBytes))))
} else {
s.interpreter.HandleInput(outputTerminal, line, tid)
}
}
if err != nil {
if s.echo {
fmt.Fprintln(s.interpreter.LogOut, fmt.Sprintf("%v : Disconnected", conn.RemoteAddr()))
}
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 {
id string
writer *bufio.Writer
echo bool
logOut io.Writer
}
/*
WriteString write a string to the writer.
*/
func (shim *bufioWriterShim) WriteString(s string) {
if shim.echo {
fmt.Fprintln(shim.logOut, fmt.Sprintf("%v < %v", shim.id, s))
}
shim.writer.WriteString(s)
shim.writer.Flush()
}
/*
* 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 (
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"devt.de/krotik/ecal/parser"
)
/*
Format formats a given set of ECAL files.
*/
func Format() error {
wd, _ := os.Getwd()
dir := flag.String("dir", wd, "Root directory for ECAL files")
ext := flag.String("ext", ".ecal", "Extension for ECAL files")
showHelp := flag.Bool("help", false, "Show this help message")
flag.Usage = func() {
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.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(osArgs[2:])
if *showHelp {
flag.Usage()
return nil
}
}
fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Formatting all %v files in %v", *ext, *dir))
return FormatFiles(*dir, *ext)
}
/*
FormatFiles formats all ECAL files in a given directory with a given ending.
*/
func FormatFiles(dir string, ext string) error {
var err error
// Try to resolve symbolic links
scanDir, lerr := os.Readlink(dir)
if lerr != nil {
scanDir = dir
}
if err == nil {
err = filepath.Walk(scanDir,
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 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(fmt.Sprintln(srcFormatted)), i.Mode())
}
}
if ferr != nil {
fmt.Fprintln(flag.CommandLine.Output(), fmt.Sprintf("Could not format %v: %v", path, ferr))
}
}
}
}
return err
})
}
return err
}
/*
* 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 (
"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.
*/
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 OutputTerminal, input string)
}
/*
OutputTerminal is a generic output terminal which can write strings.
*/
type OutputTerminal interface {
/*
WriteString write a string on this terminal.
*/
WriteString(s string)
}
/*
matchesFulltextSearch checks if a given text matches a given glob expression. Returns
true if an error occurs.
*/
func matchesFulltextSearch(ot OutputTerminal, text string, glob string) bool {
var res bool
re, err := stringutil.GlobToRegex(glob)
if err == nil {
res, err = regexp.MatchString(re, text)
}
if err != nil {
ot.WriteString(fmt.Sprintln("Invalid search expression:", err.Error()))
res = true
}
return res
}
/*
fillTableRow fills a table row of a display table.
*/
func fillTableRow(tabData []string, key string, value string) []string {
tabData = append(tabData, key)
valSplit := stringutil.ChunkSplit(value, 80, true)
tabData = append(tabData, strings.TrimSpace(valSplit[0]))
for _, valPart := range valSplit[1:] {
tabData = append(tabData, "")
tabData = append(tabData, strings.TrimSpace(valPart))
}
// Insert empty rows to ease reading
tabData = append(tabData, "")
tabData = append(tabData, "")
return tabData
}
/*
* 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"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime/pprof"
"strings"
"devt.de/krotik/common/errorutil"
"devt.de/krotik/common/fileutil"
"devt.de/krotik/common/stringutil"
"devt.de/krotik/common/termutil"
"devt.de/krotik/ecal/config"
"devt.de/krotik/ecal/engine"
"devt.de/krotik/ecal/interpreter"
"devt.de/krotik/ecal/parser"
"devt.de/krotik/ecal/scope"
"devt.de/krotik/ecal/stdlib"
"devt.de/krotik/ecal/util"
)
/*
CLICustomHandler is a handler for custom operations.
*/
type CLICustomHandler interface {
CLIInputHandler
/*
LoadInitialFile clears the global scope and reloads the initial file.
*/
LoadInitialFile(tid uint64) error
}
/*
CLIInterpreter is a commandline interpreter for ECAL.
*/
type CLIInterpreter struct {
GlobalVS parser.Scope // Global variable scope
RuntimeProvider *interpreter.ECALRuntimeProvider // Runtime provider of the interpreter
// Customizations of output and input handling
CustomHandler CLICustomHandler
CustomWelcomeMessage string
CustomHelpString string
CustomRules []*engine.Rule
EntryFile string // Entry file for the program
LoadPlugins bool // Flag if stdlib plugins should be loaded
// 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)
// User terminal
Term termutil.ConsoleLineTerminal
// Log output
LogOut io.Writer
}
/*
NewCLIInterpreter creates a new commandline interpreter for ECAL.
*/
func NewCLIInterpreter() *CLIInterpreter {
return &CLIInterpreter{scope.NewScope(scope.GlobalScope), nil, nil, "", "",
[]*engine.Rule{}, "", true, nil, nil, nil, nil, os.Stdout}
}
/*
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()
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.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.Fprintln(flag.CommandLine.Output())
}
if len(osArgs) >= 2 {
flag.CommandLine.Parse(osArgs[2:])
if cargs := flag.Args(); len(cargs) > 0 {
i.EntryFile = flag.Arg(0)
}
if *showHelp {
flag.Usage()
}
}
return *showHelp
}
/*
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 {
var logger util.Logger
var err error
if i.RuntimeProvider != nil {
return nil
}
// Check if we should log to a file
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)
} else {
// Log to the console by default
logger = util.NewStdOutLogger()
}
// Set the log level
if err == nil {
if i.LogLevel != nil && *i.LogLevel != "" {
logger, err = util.NewLogLevelLogger(logger, *i.LogLevel)
}
if err == nil {
// Get the import locator
importLocator := &util.FileImportLocator{Root: *i.Dir}
// Create interpreter
i.RuntimeProvider = interpreter.NewECALRuntimeProvider(name, importLocator, logger)
}
}
return err
}
/*
LoadInitialFile clears the global scope and reloads the initial file.
*/
func (i *CLIInterpreter) LoadInitialFile(tid uint64) error {
var err error
i.RuntimeProvider.Processor.Finish()
i.RuntimeProvider.Processor.Reset()
// Add custom rules
for _, r := range i.CustomRules {
errorutil.AssertOk(i.RuntimeProvider.Processor.AddRule(r))
}
if i.CustomHandler != nil {
i.CustomHandler.LoadInitialFile(tid)
}
i.GlobalVS.Clear()
if i.EntryFile != "" {
var ast *parser.ASTNode
var initFile []byte
initFile, err = ioutil.ReadFile(i.EntryFile)
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)
}
}()
}
}
}
i.RuntimeProvider.Processor.Start()
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.
*/
func (i *CLIInterpreter) Interpret(interactive bool) error {
if i.ParseArgs() {
return nil
}
err := i.LoadStdlibPlugins(interactive)
if err == nil {
err = i.CreateTerm()
if interactive {
fmt.Fprintln(i.LogOut, fmt.Sprintf("ECAL %v", config.ProductVersion))
}
// Create Runtime Provider
if err == nil {
if err = i.CreateRuntimeProvider("console"); err == nil {
tid := i.RuntimeProvider.NewThreadID()
if interactive {
if lll, ok := i.RuntimeProvider.Logger.(*util.LogLevelLogger); ok {
fmt.Fprint(i.LogOut, fmt.Sprintf("Log level: %v - ", lll.Level()))
}
fmt.Fprintln(i.LogOut, fmt.Sprintf("Root directory: %v", *i.Dir))
if i.CustomWelcomeMessage != "" {
fmt.Fprintln(i.LogOut, fmt.Sprintf(i.CustomWelcomeMessage))
}
}
// Execute file if given
if err = i.LoadInitialFile(tid); err == nil {
// Drop into interactive shell
if interactive {
// Add history functionality without file persistence
i.Term, err = termutil.AddHistoryMixin(i.Term, "",
func(s string) bool {
return i.isExitLine(s)
})
if err == nil {
if err = i.Term.StartTerm(); err == nil {
var line string
defer i.Term.StopTerm()
fmt.Fprintln(i.LogOut, "Type 'q' or 'quit' to exit the shell and '?' to get help")
line, err = i.Term.NextLine()
for err == nil && !i.isExitLine(line) {
trimmedLine := strings.TrimSpace(line)
i.HandleInput(i.Term, trimmedLine, tid)
line, err = i.Term.NextLine()
}
}
}
}
}
}
}
}
return err
}
/*
LoadStdlibPlugins load plugins from .ecal.json.
*/
func (i *CLIInterpreter) LoadStdlibPlugins(interactive bool) error {
var err error
if i.LoadPlugins {
confFile := filepath.Join(*i.Dir, ".ecal.json")
if ok, _ := fileutil.PathExists(confFile); ok {
if interactive {
fmt.Fprintln(i.LogOut, fmt.Sprintf("Loading stdlib plugins from %v", confFile))
}
var content []byte
if content, err = ioutil.ReadFile(confFile); err == nil {
var conf map[string]interface{}
if err = json.Unmarshal(content, &conf); err == nil {
if stdlibPlugins, ok := conf["stdlibPlugins"]; ok {
err = fmt.Errorf("Config stdlibPlugins should be a list")
if plugins, ok := stdlibPlugins.([]interface{}); ok {
err = nil
if errs := stdlib.LoadStdlibPlugins(plugins); len(errs) > 0 {
for _, e := range errs {
fmt.Fprintln(i.LogOut, fmt.Sprintf("Error loading plugins: %v", e))
}
err = fmt.Errorf("Could not load plugins defined in .ecal.json")
}
}
}
}
}
}
}
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
thread - use the RuntimeProvider to generate a unique one.
*/
func (i *CLIInterpreter) HandleInput(ot OutputTerminal, line string, tid uint64) {
// Process the entered line
if line == "?" {
// Show help
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(" @format - Format all .ecal files in the current root directory.\n"))
ot.WriteString(fmt.Sprint(" @prof [profile] - Output profiling information (supports any of Go's pprof profiles).\n"))
ot.WriteString(fmt.Sprint(" @reload - Clear the interpreter and reload the initial file if it was given.\n"))
ot.WriteString(fmt.Sprint(" @std <package> [glob] - List all available constants and functions of a stdlib package.\n"))
ot.WriteString(fmt.Sprint(" @sym [glob] - List all available inbuild functions and available stdlib packages of ECAL.\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"))
} else if i.handleSpecialStatements(ot, line) {
return
} 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, line)
} else {
var ierr error
var ast *parser.ASTNode
var res interface{}
if line != "" {
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{}), tid); ierr == nil && res != nil {
ot.WriteString(fmt.Sprintln(stringutil.ConvertToString(res)))
}
defer func() {
if i.RuntimeProvider.Debugger != nil {
i.RuntimeProvider.Debugger.RecordThreadFinished(tid)
}
}()
}
}
if ierr != nil {
ot.WriteString(fmt.Sprintln(ierr.Error()))
}
}
}
}
/*
handleSpecialStatements handles inbuild special statements.
*/
func (i *CLIInterpreter) handleSpecialStatements(ot OutputTerminal, line string) bool {
if strings.HasPrefix(line, "@prof") {
args := strings.Split(line, " ")[1:]
profile := "goroutine"
if len(args) > 0 {
profile = args[0]
}
var buf bytes.Buffer
pprof.Lookup(profile).WriteTo(&buf, 1)
ot.WriteString(buf.String())
return true
} else if strings.HasPrefix(line, "@format") {
err := FormatFiles(*i.Dir, ".ecal")
ot.WriteString(fmt.Sprintln(fmt.Sprintln("Files formatted:", err)))
return true
} else if strings.HasPrefix(line, "@reload") {
// Reload happens in a separate thread as it may be suspended on start
go func() {
err := i.LoadInitialFile(i.RuntimeProvider.NewThreadID())
ot.WriteString(fmt.Sprintln(fmt.Sprintln("Interpreter reloaded:", err)))
}()
ot.WriteString(fmt.Sprintln(fmt.Sprintln("Reloading interpreter state")))
return true
}
return false
}
/*
displaySymbols lists all available inbuild functions and available stdlib packages of ECAL.
*/
func (i *CLIInterpreter) displaySymbols(ot OutputTerminal, args []string) {
tabData := []string{"Inbuild function", "Description"}
for name, f := range interpreter.InbuildFuncMap {
ds, _ := f.DocString()
if len(args) > 0 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", name, ds), args[0]) {
continue
}
tabData = fillTableRow(tabData, name, ds)
}
if len(tabData) > 2 {
ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
stringutil.SingleDoubleLineTable))
}
packageNames, _, _ := stdlib.GetStdlibSymbols()
tabData = []string{"Package name", "Description"}
for _, p := range packageNames {
ps, _ := stdlib.GetPkgDocString(p)
if len(args) > 0 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", p, ps), args[0]) {
continue
}
tabData = fillTableRow(tabData, p, ps)
}
if len(tabData) > 2 {
ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
stringutil.SingleDoubleLineTable))
}
}
/*
displayPackage list all available constants and functions of a stdlib package.
*/
func (i *CLIInterpreter) displayPackage(ot OutputTerminal, args []string) {
_, constSymbols, funcSymbols := stdlib.GetStdlibSymbols()
tabData := []string{"Constant", "Value"}
for _, s := range constSymbols {
if len(args) > 0 && !strings.HasPrefix(s, args[0]) {
continue
}
val, _ := stdlib.GetStdlibConst(s)
tabData = fillTableRow(tabData, s, fmt.Sprint(val))
}
if len(tabData) > 2 {
ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
stringutil.SingleDoubleLineTable))
}
tabData = []string{"Function", "Description"}
for _, f := range funcSymbols {
if len(args) > 0 && !strings.HasPrefix(f, args[0]) {
continue
}
fObj, _ := stdlib.GetStdlibFunc(f)
fDoc, _ := fObj.DocString()
fDoc = strings.Replace(fDoc, "\n", " ", -1)
fDoc = strings.Replace(fDoc, "\t", " ", -1)
if len(args) > 1 && !matchesFulltextSearch(ot, fmt.Sprintf("%v %v", f, fDoc), args[1]) {
continue
}
tabData = fillTableRow(tabData, f, fDoc)
}
if len(tabData) > 2 {
ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
stringutil.SingleDoubleLineTable))
}
}
/*
* 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 (
"archive/zip"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"unicode"
"devt.de/krotik/common/errorutil"
"devt.de/krotik/common/fileutil"
"devt.de/krotik/ecal/interpreter"
"devt.de/krotik/ecal/parser"
"devt.de/krotik/ecal/scope"
"devt.de/krotik/ecal/util"
)
/*
CLIPacker is a commandline packing tool for ECAL. This tool can build a self
contained executable.
*/
type CLIPacker struct {
EntryFile string // Entry file for the program
// Parameter these can either be set programmatically or via CLI args
Dir *string // Root dir for interpreter (all files will be collected)
SourceBinary *string // Binary which is used by the packer
TargetBinary *string // Binary which will be build by the packer
// Log output
LogOut io.Writer
}
var packmarkerend = "####"
var packmarker = fmt.Sprintf("\n%v%v%v\n", packmarkerend, "ECALSRC", packmarkerend)
/*
NewCLIPacker creates a new commandline packer.
*/
func NewCLIPacker() *CLIPacker {
return &CLIPacker{"", nil, nil, nil, os.Stdout}
}
/*
ParseArgs parses the command line arguments. Returns true if the program should exit.
*/
func (p *CLIPacker) ParseArgs() bool {
if p.Dir != nil && p.TargetBinary != nil && p.EntryFile != "" {
return false
}
binname, err := filepath.Abs(osArgs[0])
errorutil.AssertOk(err)
wd, _ := os.Getwd()
p.Dir = flag.String("dir", wd, "Root directory for ECAL interpreter")
p.SourceBinary = flag.String("source", binname, "Filename for source binary")
p.TargetBinary = flag.String("target", "out.bin", "Filename for target binary")
showHelp := flag.Bool("help", false, "Show this help message")
flag.Usage = func() {
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.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.Fprintln(flag.CommandLine.Output())
}
if len(os.Args) >= 2 {
flag.CommandLine.Parse(osArgs[2:])
if cargs := flag.Args(); len(cargs) > 0 {
p.EntryFile = flag.Arg(0)
} else {
*showHelp = true
}
if *showHelp {
flag.Usage()
}
}
return *showHelp
}
/*
Pack builds a standalone executable from a given source binary and collected files.
*/
func (p *CLIPacker) Pack() error {
if p.ParseArgs() {
return nil
}
fmt.Fprintln(p.LogOut, fmt.Sprintf("Packing %v -> %v from %v with entry: %v", *p.Dir,
*p.TargetBinary, *p.SourceBinary, p.EntryFile))
source, err := os.Open(*p.SourceBinary)
if err == nil {
var dest *os.File
defer source.Close()
if dest, err = os.Create(*p.TargetBinary); err == nil {
var bytes int64
defer dest.Close()
// First copy the binary
if bytes, err = io.Copy(dest, source); err == nil {
fmt.Fprintln(p.LogOut, fmt.Sprintf("Copied %v bytes for interpreter.", bytes))
var bytes int
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))
// Create a new zip archive.
w := zip.NewWriter(dest)
if data, err = ioutil.ReadFile(p.EntryFile); err == nil {
var f io.Writer
if f, err = w.Create(".ecalsrc-entry"); err == nil {
if bytes, err = f.Write(data); err == nil {
fmt.Fprintln(p.LogOut, fmt.Sprintf("Writing %v bytes for intro", bytes))
// Add files to the archive
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, "")
}
}
}
}
}
}
}
return err
}
/*
packFiles walk through a given file structure and copies all files into a given zip writer.
*/
func (p *CLIPacker) packFiles(w *zip.Writer, filePath string, zipPath string) error {
var bytes int
files, err := ioutil.ReadDir(filePath)
if err == nil {
for _, file := range files {
if !file.IsDir() {
var data []byte
diskfile := filepath.Join(filePath, file.Name())
if data, err = ioutil.ReadFile(diskfile); err == nil {
var f io.Writer
if f, err = w.Create(path.Join(zipPath, file.Name())); err == nil {
if bytes, err = f.Write(data); err == nil {
fmt.Fprintln(p.LogOut, fmt.Sprintf("Writing %v bytes for %v",
bytes, diskfile))
}
}
}
} else if file.IsDir() {
// Path separator in zipfile is always '/'
p.packFiles(w, filepath.Join(filePath, file.Name()),
path.Join(zipPath, file.Name()))
}
}
}
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.
*/
func RunPackedBinary() {
var retCode = 0
var result bool
exename, err := filepath.Abs(osArgs[0])
errorutil.AssertOk(err)
if ok, _ := fileutil.PathExists(exename); !ok {
// Try an optional .exe suffix which might work on Windows
exename += ".exe"
}
stat, err := os.Stat(exename)
if err == nil {
var f *os.File
if f, err = os.Open(exename); err == nil {
var pos int64
defer f.Close()
found := false
buf := make([]byte, b1)
buf2 := make([]byte, b2)
// Look for the marker which marks the beginning of the attached zip file
for i, err := f.Read(buf); err == nil; i, err = f.Read(buf) {
// Check if the marker could be in the read string
if strings.Contains(string(buf), "#") {
// Marker was found - read a bit more to ensure we got the full marker
if i2, err := f.Read(buf2); err == nil || err == io.EOF {
candidateString := string(append(buf, buf2...))
// Now determine the position of the zip file
markerIndex := strings.Index(candidateString, packmarker)
if found = markerIndex >= 0; found {
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
}
pos += start
break
}
pos += int64(i2)
}
}
pos += int64(i)
}
if err == nil && found {
// Extract the zip
if _, err = f.Seek(pos, 0); err == nil {
var ret interface{}
zipLen := stat.Size() - pos
ret, err = runInterpreter(io.NewSectionReader(f, pos, zipLen), zipLen)
retNum, _ := ret.(float64)
retCode = int(retNum)
result = err == nil
}
}
}
}
handleError(err)
if result {
osExit(retCode)
}
}
func runInterpreter(reader io.ReaderAt, size int64) (interface{}, error) {
var res interface{}
var rc io.ReadCloser
il := &util.MemoryImportLocator{Files: make(map[string]string)}
r, err := zip.NewReader(reader, size)
if err == nil {
for _, f := range r.File {
if err == nil {
if rc, err = f.Open(); err == nil {
var data []byte
defer rc.Close()
if data, err = ioutil.ReadAll(rc); err == nil {
il.Files[f.Name] = string(data)
}
}
}
}
}
if err == nil {
var ast *parser.ASTNode
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 {
var osArgs []interface{}
vs := scope.NewScope(scope.GlobalScope)
for _, arg := range os.Args {
osArgs = append(osArgs, arg)
}
vs.SetValue("osArgs", osArgs)
res, err = ast.Runtime.Eval(vs, make(map[string]interface{}), erp.NewThreadID())
if err != nil {
fmt.Fprintln(osStderr, err.Error())
if terr, ok := err.(util.TraceableRuntimeError); ok {
fmt.Fprintln(osStderr, fmt.Sprint(" ", strings.Join(terr.GetTraceString(), fmt.Sprint(fmt.Sprintln(), " "))))
}
err = nil
}
}
}
}
return res, err
}
/*
* 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 config
import (
"fmt"
"strconv"
"devt.de/krotik/common/errorutil"
)
// Global variables
// ================
/*
ProductVersion is the current version of ECAL
*/
const ProductVersion = "1.6.2"
/*
Known configuration options for ECAL
*/
const (
WorkerCount = "WorkerCount"
)
/*
DefaultConfig is the defaut configuration
*/
var DefaultConfig = map[string]interface{}{
/*
Number of worker threads in ECAL's ECA engine. This number is also the maximum
number of concurrent operations - e.g. how often addEventAndWait can be called
in a single event chain.
*/
WorkerCount: 4,
}
/*
Config is the actual config which is used
*/
var Config map[string]interface{}
/*
Initialise the config
*/
func init() {
data := make(map[string]interface{})
for k, v := range DefaultConfig {
data[k] = v
}
Config = data
}
// Helper functions
// ================
/*
Str reads a config value as a string value.
*/
func Str(key string) string {
return fmt.Sprint(Config[key])
}
/*
Int reads a config value as an int value.
*/
func Int(key string) int {
ret, err := strconv.ParseInt(fmt.Sprint(Config[key]), 10, 64)
errorutil.AssertTrue(err == nil,
fmt.Sprintf("Could not parse config key %v: %v", key, err))
return int(ret)
}
/*
Bool reads a config value as a boolean value.
*/
func Bool(key string) bool {
ret, err := strconv.ParseBool(fmt.Sprint(Config[key]))
errorutil.AssertTrue(err == nil,
fmt.Sprintf("Could not parse config key %v: %v", key, err))
return ret
}
/*
* 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 engine
import (
"fmt"
"io"
"os"
"regexp"
"strings"
"sync"
"devt.de/krotik/common/stringutil"
)
/*
EventTracer is a debugging interface to the engine
*/
var EventTracer = &eventTrace{lock: &sync.Mutex{}, out: os.Stdout}
/*
eventTrace handles low-level event tracing for debugging purposes
*/
type eventTrace struct {
lock *sync.Mutex
eventTraceKind []string
eventTraceState []map[interface{}]interface{}
out io.Writer
}
/*
MonitorEvent adds a request to monitor certain events. The events to monitor
should match the given kind and have the given state values (nil values match
only on the key).
*/
func (et *eventTrace) MonitorEvent(kind string, state map[interface{}]interface{}) {
et.lock.Lock()
defer et.lock.Unlock()
et.eventTraceKind = append(et.eventTraceKind, kind)
et.eventTraceState = append(et.eventTraceState, state)
}
/*
Reset removes all added monitoring requests.
*/
func (et *eventTrace) Reset() {
et.lock.Lock()
defer et.lock.Unlock()
et.eventTraceKind = nil
et.eventTraceState = nil
}
/*
record records an event action.
*/
func (et *eventTrace) record(which *Event, where string, what ...interface{}) {
et.lock.Lock()
defer et.lock.Unlock()
if et.eventTraceKind == nil {
// Return in the normal case
return
}
whichKind := strings.Join(which.Kind(), ".")
// Check if the event matches
for i, tkind := range et.eventTraceKind {
tstate := et.eventTraceState[i]
regexMatch, _ := regexp.MatchString(tkind, whichKind)
if whichKind == tkind || regexMatch {
if tstate == nil || stateMatch(tstate, which.State()) {
fmt.Fprintln(et.out, fmt.Sprintf("%v %v", tkind, where))
for _, w := range what {
fmt.Fprintln(et.out, fmt.Sprintf(" %v",
stringutil.ConvertToString(w)))
}
fmt.Fprintln(et.out, fmt.Sprintf(" %v", which))
}
}
}
}
// Helper functions
// ================
/*
stateMatch checks if a given template matches a given event state.
*/
func stateMatch(template, state map[interface{}]interface{}) bool {
for k, v := range template {
if sv, ok := state[k]; !ok {
return false
} else if v != nil {
regexMatch, _ := regexp.MatchString(fmt.Sprint(v), fmt.Sprint(sv))
if v != sv && !regexMatch {
return false
}
}
}
return true
}
/*
* 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 engine
import (
"fmt"
"strings"
"devt.de/krotik/common/stringutil"
)
/*
Event data structure
*/
type Event struct {
name string // Name of the event
kind []string // Kind of the event (dot notation expressed as array)
state map[interface{}]interface{} // Event state
}
/*
NewEvent returns a new event object.
*/
func NewEvent(name string, kind []string, state map[interface{}]interface{}) *Event {
return &Event{name, kind, state}
}
/*
Name returns the event name.
*/
func (e *Event) Name() string {
return e.name
}
/*
Kind returns the event kind.
*/
func (e *Event) Kind() []string {
return e.kind
}
/*
State returns the event state.
*/
func (e *Event) State() map[interface{}]interface{} {
return e.state
}
func (e *Event) String() string {
return fmt.Sprintf("Event: %v %v %v", e.name, strings.Join(e.kind, "."),
stringutil.ConvertToString(e.state))
}
/*
* 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 engine
import (
"bytes"
"container/heap"
"fmt"
"sync"
"devt.de/krotik/common/errorutil"
"devt.de/krotik/common/sortutil"
"devt.de/krotik/ecal/engine/pubsub"
)
/*
Monitor monitors events as they are cascading. Event cascades will produce tree
structures.
*/
type Monitor interface {
/*
ID returns the monitor ID.
*/
ID() uint64
/*
NewChildMonitor creates a new child monitor of this monitor.
*/
NewChildMonitor(priority int) Monitor
/*
Scope returns the rule scope of this monitor.
*/
Scope() *RuleScope
/*
Priority returns the monitors priority.
*/
Priority() int
/*
Activated returns if this monitor has been activated.
*/
IsActivated() bool
/*
Activate activates this monitor with a given event.
*/
Activate(e *Event)
/*
Skip finishes this monitor without activation.
*/
Skip(e *Event)
/*
Finish finishes this monitor.
*/
Finish()
/*
Returns the root monitor of this monitor.
*/
RootMonitor() *RootMonitor
/*
Errors returns the error object of this monitor.
*/
Errors() *TaskError
/*
SetErrors adds an error object to this monitor.
*/
SetErrors(e *TaskError)
/*
EventPath returns the chain of events which created this monitor.
*/
EventPath() []*Event
/*
EventPathString returns the event path as a string.
*/
EventPathString() string
/*
String returns a string representation of this monitor.
*/
String() string
}
/*
monitorBase provides the basic functions and fields for any monitor.
*/
type monitorBase struct {
id uint64 // Monitor ID
Parent *monitorBase // Parent monitor
Context map[string]interface{} // Context object of the monitor
Err *TaskError // Errors which occurred during event processing
priority int // Priority of the monitor
rootMonitor *RootMonitor // Root monitor
event *Event // Event which activated this monitor
activated bool // Flag indicating if the monitor was activated
finished bool // Flag indicating if the monitor has finished
}
/*
NewMonitor creates a new monitor.
*/
func newMonitorBase(priority int, parent *monitorBase, context map[string]interface{}) *monitorBase {
var ret *monitorBase
if parent != nil {
ret = &monitorBase{newMonID(), parent, context, nil, priority, parent.rootMonitor, nil, false, false}
} else {
ret = &monitorBase{newMonID(), nil, context, nil, priority, nil, nil, false, false}
}
return ret
}
/*
NewChildMonitor creates a new child monitor of this monitor.
*/
func (mb *monitorBase) NewChildMonitor(priority int) Monitor {
child := &ChildMonitor{newMonitorBase(priority, mb, mb.Context)}
mb.rootMonitor.descendantCreated(child)
return child
}
/*
ID returns the monitor ID.
*/
func (mb *monitorBase) ID() uint64 {
return mb.id
}
/*
RootMonitor returns the root monitor of this monitor.
*/
func (mb *monitorBase) RootMonitor() *RootMonitor {
return mb.rootMonitor
}
/*
Scope returns the rule scope of this monitor.
*/
func (mb *monitorBase) Scope() *RuleScope {
return mb.rootMonitor.ruleScope
}
/*
Priority returns the priority of this monitor.
*/
func (mb *monitorBase) Priority() int {
return mb.priority
}
/*
IsActivated returns if this monitor has been activated.
*/
func (mb *monitorBase) IsActivated() bool {
return mb.activated
}
/*
IsFinished returns if this monitor has finished.
*/
func (mb *monitorBase) IsFinished() bool {
return mb.finished
}
/*
Activate activates this monitor.
*/
func (mb *monitorBase) Activate(e *Event) {
errorutil.AssertTrue(!mb.finished, "Cannot activate a finished monitor")
errorutil.AssertTrue(!mb.activated, "Cannot activate an active monitor")
errorutil.AssertTrue(e != nil, "Monitor can only be activated with an event")
mb.event = e
mb.rootMonitor.descendantActivated(mb.priority)
mb.activated = true
}
/*
Skip finishes this monitor without activation.
*/
func (mb *monitorBase) Skip(e *Event) {
errorutil.AssertTrue(!mb.finished, "Cannot skip a finished monitor")
errorutil.AssertTrue(!mb.activated, "Cannot skip an active monitor")
mb.event = e
mb.activated = true
mb.Finish()
}
/*
Finish finishes this monitor.
*/
func (mb *monitorBase) Finish() {
errorutil.AssertTrue(mb.activated, "Cannot finish a not active monitor")
errorutil.AssertTrue(!mb.finished, "Cannot finish a finished monitor")
mb.finished = true
mb.rootMonitor.descendantFinished(mb)
}
/*
Errors returns the error object of this monitor.
*/
func (mb *monitorBase) Errors() *TaskError {
errorutil.AssertTrue(mb.finished, "Cannot get errors on an unfinished monitor")
return mb.Err
}
/*
SetErrors adds an error object to this monitor.
*/
func (mb *monitorBase) SetErrors(e *TaskError) {
mb.Err = e
mb.rootMonitor.descendantFailed(mb)
}
/*
EventPath returns the chain of events which created this monitor.
*/
func (mb *monitorBase) EventPath() []*Event {
errorutil.AssertTrue(mb.finished, "Cannot get event path on an unfinished monitor")
path := []*Event{mb.event}
child := mb.Parent
for child != nil {
path = append(path, child.event)
child = child.Parent
}
// Reverse path
for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
path[i], path[j] = path[j], path[i]
}
return path
}
/*
EventPathString returns the event path as a string.
*/
func (mb *monitorBase) EventPathString() string {
var buf bytes.Buffer
ep := mb.EventPath()
last := len(ep) - 1
for i, e := range mb.EventPath() {
buf.WriteString(e.name)
if i < last {
buf.WriteString(" -> ")
}
}
return buf.String()
}
/*
String returns a string representation of this monitor.
*/
func (mb *monitorBase) String() string {
return fmt.Sprintf("Monitor %v (parent: %v priority: %v activated: %v finished: %v)",
mb.ID(), mb.Parent, mb.priority, mb.activated, mb.finished)
}
// Root Monitor
// ============
/*
RootMonitor is a monitor which is at a beginning of an event cascade.
*/
type RootMonitor struct {
*monitorBase
lock *sync.Mutex // Lock for datastructures
incomplete map[int]int // Priority -> Counters of incomplete trackers
priorities *sortutil.IntHeap // List of handled priorities
ruleScope *RuleScope // Rule scope definitions
unfinished int // Counter of all unfinished trackers
messageQueue *pubsub.EventPump // Message passing queue of the processor
errors map[uint64]*monitorBase // Monitors which got errors
finished func(Processor) // Finish handler (can be used externally)
}
/*
NewRootMonitor creates a new root monitor.
*/
func newRootMonitor(context map[string]interface{}, scope *RuleScope,
messageQueue *pubsub.EventPump) *RootMonitor {
ret := &RootMonitor{newMonitorBase(0, nil, context), &sync.Mutex{},
make(map[int]int), &sortutil.IntHeap{}, scope, 1, messageQueue,
make(map[uint64]*monitorBase), nil}
// A root monitor is its own parent
ret.rootMonitor = ret
heap.Init(ret.priorities)
return ret
}
/*
SetFinishHandler adds a handler function to this monitor which is called once
this monitor has finished.
*/
func (rm *RootMonitor) SetFinishHandler(fh func(Processor)) {
rm.finished = fh
}
/*
HighestPriority returns the highest priority which is handled by this monitor.
*/
func (rm *RootMonitor) HighestPriority() int {
rm.lock.Lock()
defer rm.lock.Unlock()
if len(*rm.priorities) > 0 {
return (*rm.priorities)[0]
}
return -1
}
/*
AllErrors returns all error which have been collected in this root monitor.
*/
func (rm *RootMonitor) AllErrors() []*TaskError {
rm.lock.Lock()
defer rm.lock.Unlock()
ret := make([]*TaskError, 0, len(rm.errors))
// Sort by monitor id - this should give the corrent order timewise
var ids []uint64
for id := range rm.errors {
ids = append(ids, id)
}
sortutil.UInt64s(ids)
for _, id := range ids {
m := rm.errors[id]
ret = append(ret, m.Errors())
}
return ret
}
/*
descendantCreated notifies this root monitor that a descendant has been created.
*/
func (rm *RootMonitor) descendantCreated(monitor Monitor) {
rm.lock.Lock()
defer rm.lock.Unlock()
rm.unfinished++
}
/*
descendantActivated notifies this root monitor that a descendant has been activated.
*/
func (rm *RootMonitor) descendantActivated(priority int) {
rm.lock.Lock()
defer rm.lock.Unlock()
val, ok := rm.incomplete[priority]
if !ok {
val = 0
heap.Push(rm.priorities, priority)
}
rm.incomplete[priority] = val + 1
}
/*
descendantFailed notifies this root monitor that a descendant has failed.
*/
func (rm *RootMonitor) descendantFailed(monitor *monitorBase) {
rm.lock.Lock()
defer rm.lock.Unlock()
rm.errors[monitor.ID()] = monitor
}
/*
descendantFinished records that this monitor has finished. If it is the last
active monitor in the event tree then send a notification.
*/
func (rm *RootMonitor) descendantFinished(m Monitor) {
rm.lock.Lock()
rm.unfinished--
finished := rm.unfinished == 0
if m.IsActivated() {
priority := m.Priority()
rm.incomplete[priority]--
if rm.incomplete[priority] == 0 {
rm.priorities.RemoveFirst(priority)
delete(rm.incomplete, priority)
}
}
rm.lock.Unlock()
// Post notification
if finished {
rm.messageQueue.PostEvent(MessageRootMonitorFinished, rm)
}
}
// Child Monitor
// =============
/*
ChildMonitor is a monitor which is a descendant of a root monitor.
*/
type ChildMonitor struct {
*monitorBase
}
// Unique id creation
// ==================
var midcounter uint64 = 1
var midcounterLock = &sync.Mutex{}
/*
newId returns a new unique id.
*/
func newMonID() uint64 {
midcounterLock.Lock()
defer midcounterLock.Unlock()
ret := midcounter
midcounter++
return ret
}
/*
* Public Domain Software
*
* I (Matthias Ladkau) am the author of the source code in this file.
* I have placed the source code in this file in the public domain.
*
* For further information see: http://creativecommons.org/publicdomain/zero/1.0/
*/
/*
Package pool contains a thread pool implementation.
*/
package pool
import (
"math"
"sync"
"time"
)
/*
Different states of a thread pool.
*/
const (
StatusRunning = "Running"
StatusStopping = "Stopping"
StatusStopped = "Stopped"
)
/*
Task is a task which should be run in a thread.
*/
type Task interface {
/*
Run the task. The function gets the unique thread ID of the worker
which executes the task.
*/
Run(tid uint64) error
/*
HandleError handles an error which occurred during the run method.
*/
HandleError(e error)
}
/*
TaskQueue is a queue of tasks for a thread pool.
*/
type TaskQueue interface {
/*
Clear the queue of all pending tasks
*/
Clear()
/*
Pop returns the next task from the queue.
*/
Pop() Task
/*
Push adds another task to the queue.
*/
Push(t Task)
/*
Size returns the size of the queue.
*/
Size() int
}
/*
DefaultTaskQueue implements a simple (FIFO) task queue for a thread pool.
*/
type DefaultTaskQueue struct {
queue []Task
}
/*
Clear the queue of all pending tasks
*/
func (tq *DefaultTaskQueue) Clear() {
tq.queue = make([]Task, 0)
}
/*
Pop returns the next task from the queue.
*/
func (tq *DefaultTaskQueue) Pop() Task {
var task Task
if len(tq.queue) > 0 {
task = tq.queue[0]
tq.queue = tq.queue[1:]
}
return task
}
/*
Push adds another task to the queue.
*/
func (tq *DefaultTaskQueue) Push(t Task) {
tq.queue = append(tq.queue, t)
}
/*
Size returns the size of the queue.
*/
func (tq *DefaultTaskQueue) Size() int {
return len(tq.queue)
}
/*
ThreadPool creates a pool of threads which process tasks according to a given
task queue. The threads are kept in an idle state if no more tasks are available.
They resume immediately once a new task is added.
*/
type ThreadPool struct {
// Task regulation
queue TaskQueue // Task queue for thread pool
queueLock *sync.Mutex // Lock for queue
// Worker regulation
workerIDCount uint64 // Id counter for worker tasks
workerIDLock *sync.Mutex // Lock for ID generation
workerMap map[uint64]*ThreadPoolWorker // Map of all workers
workerIdleMap map[uint64]*ThreadPoolWorker // Map of all idle workers
workerMapLock *sync.Mutex // Lock for worker map
workerKill int // Count of workers which should die
newTaskCond *sync.Cond // Waiting condition for new tasks
// Callbacks to regulate load
RegulationLock *sync.Mutex // Lock for regulation variables
TooManyThreshold int // Threshold for too many tasks
TooManyCallback func() // Callback for too many tasks
tooManyTriggered bool // Flag if too many tasks threshold was passed
TooFewThreshold int // Threshold for too few tasks
TooFewCallback func() // Callback for too few tasks
tooFewTriggered bool // Flag if too many tasks threshold was passed
}
/*
State returns the current state of the ThreadPool.
*/
func (tp *ThreadPool) State() map[string]interface{} {
getIdsFromWorkerMap := func(m map[uint64]*ThreadPoolWorker) []uint64 {
var ret []uint64
for k := range m {
ret = append(ret, k)
}
return ret
}
tp.workerMapLock.Lock()
defer tp.workerMapLock.Unlock()
return map[string]interface{}{
"TaskQueueSize": tp.queue.Size(),
"TotalWorkerThreads": getIdsFromWorkerMap(tp.workerMap),
"IdleWorkerThreads": getIdsFromWorkerMap(tp.workerIdleMap),
}
}
/*
NewThreadPool creates a new thread pool.
*/
func NewThreadPool() *ThreadPool {
return NewThreadPoolWithQueue(&DefaultTaskQueue{})
}
/*
NewThreadPoolWithQueue creates a new thread pool with a specific task queue.
*/
func NewThreadPoolWithQueue(q TaskQueue) *ThreadPool {
return &ThreadPool{q, &sync.Mutex{},
1, &sync.Mutex{}, make(map[uint64]*ThreadPoolWorker),
make(map[uint64]*ThreadPoolWorker), &sync.Mutex{},
0, sync.NewCond(&sync.Mutex{}), &sync.Mutex{},
math.MaxInt32, func() {}, false, 0, func() {}, false}
}
/*
AddTask adds a task to the thread pool.
*/
func (tp *ThreadPool) AddTask(t Task) {
tp.queueLock.Lock()
defer tp.queueLock.Unlock()
tp.queue.Push(t)
// Reset too few flag
tp.RegulationLock.Lock()
if tp.tooFewTriggered && tp.TooFewThreshold < tp.queue.Size() {
tp.tooFewTriggered = false
}
// Check too many
if !tp.tooManyTriggered && tp.TooManyThreshold <= tp.queue.Size() {
tp.tooManyTriggered = true
tp.TooManyCallback()
}
tp.RegulationLock.Unlock()
// Wake up a waiting worker
tp.newTaskCond.Signal()
}
/*
getTask is called by a worker to request a new task. The worker is expected to finish
if this function returns nil.
*/
func (tp *ThreadPool) getTask() Task {
var returnIdleTask = true
// Check if tasks should be stopped
tp.workerMapLock.Lock()
if tp.workerKill > 0 {
tp.workerKill--
tp.workerMapLock.Unlock()
return nil
} else if tp.workerKill == -1 {
// Check for special worker kill value which is used when workers should
// be killed when no more tasks are available.
returnIdleTask = false
}
tp.workerMapLock.Unlock()
// Check if there is a task available
tp.queueLock.Lock()
task := tp.queue.Pop()
tp.queueLock.Unlock()
if task != nil {
return task
}
tp.RegulationLock.Lock()
// Reset too many flag
if tp.tooManyTriggered && tp.TooManyThreshold > tp.queue.Size() {
tp.tooManyTriggered = false
}
// Check too few
if !tp.tooFewTriggered && tp.TooFewThreshold >= tp.queue.Size() {
tp.tooFewTriggered = true
tp.TooFewCallback()
}
tp.RegulationLock.Unlock()
if returnIdleTask {
// No new task available return idle task
return &idleTask{tp}
}
return nil
}
/*
NewThreadID creates a new thread ID unique to this pool.
*/
func (tp *ThreadPool) NewThreadID() uint64 {
tp.workerIDLock.Lock()
res := tp.workerIDCount
tp.workerIDCount++
tp.workerIDLock.Unlock()
return res
}
/*
SetWorkerCount sets the worker count of this pool. If the wait flag is true then
this call will return after the pool has reached the requested worker count.
*/
func (tp *ThreadPool) SetWorkerCount(count int, wait bool) {
tp.workerMapLock.Lock()
workerCount := len(tp.workerMap)
tp.workerMapLock.Unlock()
if count < 0 {
count = 0
}
if workerCount < count {
// More workers are needed
tp.workerMapLock.Lock()
// Make sure no more workers are killed
tp.workerKill = 0
for len(tp.workerMap) != count {
tid := tp.NewThreadID()
worker := &ThreadPoolWorker{tid, tp}
go worker.run()
tp.workerMap[tid] = worker
}
tp.workerMapLock.Unlock()
} else if workerCount > count {
// Fewer workers are needed
tp.workerMapLock.Lock()
tp.workerKill = workerCount - count
tp.workerMapLock.Unlock()
tp.newTaskCond.Broadcast()
if wait {
for true {
tp.workerMapLock.Lock()
workerCount = len(tp.workerMap)
tp.workerMapLock.Unlock()
if workerCount == count {
break
}
time.Sleep(5 * time.Nanosecond)
// Broadcast again since sine workers might be now waiting
tp.newTaskCond.Broadcast()
}
}
}
// If a count was set wait until at least one worker is idle
for count > 0 && len(tp.workerIdleMap) == 0 {
time.Sleep(5 * time.Nanosecond)
}
}
/*
Status returns the current status of the thread pool.
*/
func (tp *ThreadPool) Status() string {
var status string
tp.workerMapLock.Lock()
workerCount := len(tp.workerMap)
workerKill := tp.workerKill
tp.workerMapLock.Unlock()
if workerCount > 0 {
if workerKill == -1 {
status = StatusStopping
} else {
status = StatusRunning
}
} else {
status = StatusStopped
}
return status
}
/*
WorkerCount returns the current count of workers.
*/
func (tp *ThreadPool) WorkerCount() int {
tp.workerMapLock.Lock()
defer tp.workerMapLock.Unlock()
return len(tp.workerMap)
}
/*
WaitAll waits for all workers to become idle.
*/
func (tp *ThreadPool) WaitAll() {
// Wake up all workers
tp.newTaskCond.Broadcast()
time.Sleep(5 * time.Nanosecond)
for true {
tp.workerMapLock.Lock()
tp.queueLock.Lock()
// Get total number of workers and idle workers
workerCount := len(tp.workerMap)
workerIdleCount := len(tp.workerIdleMap)
// Get number of pending tasks
tasks := tp.queue.Size()
tp.queueLock.Unlock()
tp.workerMapLock.Unlock()
// Only leave this loop if either no workers are left or if all
// tasks are done and all workers are idle
if workerCount == 0 || (workerCount == workerIdleCount && tasks == 0) {
break
}
time.Sleep(5 * time.Nanosecond)
// Broadcast again and again until all workers are idle
tp.newTaskCond.Broadcast()
}
}
/*
JoinAll processes all remaining tasks and kills off all workers afterwards.
*/
func (tp *ThreadPool) JoinAll() {
// Tell all workers to die
tp.workerMapLock.Lock()
tp.workerKill = -1
tp.workerMapLock.Unlock()
tp.newTaskCond.Broadcast()
for true {
tp.workerMapLock.Lock()
tp.queueLock.Lock()
// Get total number of workers
workerCount := len(tp.workerMap)
// Get number of pending tasks
tasks := tp.queue.Size()
tp.queueLock.Unlock()
tp.workerMapLock.Unlock()
// Only leave this loop if no workers are existing and all tasks are done
if workerCount == 0 && tasks == 0 {
break
}
time.Sleep(5 * time.Nanosecond)
// Broadcast again and again until all workers are dead
tp.newTaskCond.Broadcast()
}
}
/*
ThreadPoolWorker models a worker in the thread pool.
*/
type ThreadPoolWorker struct {
id uint64 // ID of the thread pool worker
pool *ThreadPool // Thread pool of this worker
}
/*
run lets this worker run tasks.
*/
func (w *ThreadPoolWorker) run() {
defer func() {
// Remove worker from workerMap
w.pool.workerMapLock.Lock()
delete(w.pool.workerMap, w.id)
w.pool.workerMapLock.Unlock()
}()
for true {
// Try to get the next task
task := w.pool.getTask()
// Exit if there is not new task
if task == nil {
break
}
_, isIdleTask := task.(*idleTask)
if isIdleTask {
// Register this worker as idle
w.pool.workerMapLock.Lock()
w.pool.workerIdleMap[w.id] = w
w.pool.workerMapLock.Unlock()
}
// Run the task
if err := task.Run(w.id); err != nil {
task.HandleError(err)
}
if isIdleTask {
w.pool.workerMapLock.Lock()
delete(w.pool.workerIdleMap, w.id)
w.pool.workerMapLock.Unlock()
}
}
}
/*
idleTask is the internal idle task.
*/
type idleTask struct {
tp *ThreadPool
}
/*
Run the idle task.
*/
func (t *idleTask) Run(tid uint64) error {
t.tp.newTaskCond.L.Lock()
defer t.tp.newTaskCond.L.Unlock()
t.tp.newTaskCond.Wait()
return nil
}
func (t *idleTask) HandleError(e error) {
panic(e.Error())
}
/*
* 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 engine
import (
"fmt"
"os"
"sync"
"devt.de/krotik/ecal/engine/pool"
"devt.de/krotik/ecal/engine/pubsub"
)
/*
Processor is the main object of the event engine. It coordinates the thread pool
and rule index. Rules can only be added if the processor is stopped. Events
can only be added if the processor is not stopped.
*/
type Processor interface {
/*
ID returns the processor ID.
*/
ID() uint64
/*
ThreadPool returns the thread pool which this processor is using.
*/
ThreadPool() *pool.ThreadPool
/*
Workers returns the number of threads of this processor.
*/
Workers() int
/*
Reset removes all stored rules from this processor.
*/
Reset() error
/*
AddRule adds a new rule to the processor.
*/
AddRule(rule *Rule) error
/*
Rules returns all loaded rules.
*/
Rules() map[string]*Rule
/*
Start starts this processor.
*/
Start()
/*
Finish will finish all remaining tasks and then stop the processor.
*/
Finish()
/*
Stopped returns if the processor is stopped.
*/
Stopped() bool
/*
Status returns the status of the processor (Running / Stopping / Stopped).
*/
Status() string
/*
NewRootMonitor creates a new root monitor for this processor. This monitor is used to add initial
root events.
*/
NewRootMonitor(context map[string]interface{}, scope *RuleScope) *RootMonitor
/*
SetRootMonitorErrorObserver specifies an observer which is triggered
when a root monitor of this processor has finished and returns errors.
By default this is set to nil (no observer).
*/
SetRootMonitorErrorObserver(func(rm *RootMonitor))
/*
SetFailOnFirstErrorInTriggerSequence sets the behavior when rules return errors.
If set to false (default) then all rules in a trigger sequence for a specific event
are executed. If set to true then the first rule which returns an error will stop
the trigger sequence. Events which have been added by the failing rule are still processed.
*/
SetFailOnFirstErrorInTriggerSequence(bool)
/*
AddEventAndWait adds a new event to the processor and waits for the resulting event cascade
to finish. If a monitor is passed then it must be a RootMonitor.
*/
AddEventAndWait(event *Event, monitor *RootMonitor) (Monitor, error)
/*
AddEvent adds a new event to the processor. Returns the monitor if the event
triggered a rule and nil if the event was skipped.
*/
AddEvent(event *Event, parentMonitor Monitor) (Monitor, error)
/*
IsTriggering checks if a given event triggers a loaded rule. This does not the
actual state matching for speed.
*/
IsTriggering(event *Event) bool
/*
ProcessEvent processes an event by determining which rules trigger and match
the given event. This function must receive a unique thread ID from the
executing thread.
*/
ProcessEvent(tid uint64, event *Event, parent Monitor) map[string]error
/*
String returns a string representation the processor.
*/
String() string
}
/*
eventProcessor main implementation of the Processor interface.
Event cycle:
Process -> Triggering -> Matching -> Fire Rule
*/
type eventProcessor struct {
id uint64 // Processor ID
pool *pool.ThreadPool // Thread pool of this processor
workerCount int // Number of threads for this processor
failOnFirstError bool // Stop rule execution on first error in an event trigger sequence
ruleIndex RuleIndex // Container for loaded rules
triggeringCache map[string]bool // Cache which remembers which events are triggering
triggeringCacheLock sync.Mutex // Lock for triggeringg cache
messageQueue *pubsub.EventPump // Queue for message passing between components
rmErrorObserver func(rm *RootMonitor) // Error observer for root monitors
}
/*
NewProcessor creates a new event processor with a given number of workers.
*/
func NewProcessor(workerCount int) Processor {
ep := pubsub.NewEventPump()
pool := pool.NewThreadPoolWithQueue(NewTaskQueue(ep))
pool.TooManyThreshold = 10
pool.TooManyCallback = func() {
fmt.Fprintf(os.Stderr, "Warning: The thread pool queue is filling up ...")
}
return &eventProcessor{newProcID(), pool,
workerCount, false, NewRuleIndex(), nil, sync.Mutex{}, ep, nil}
}
/*
ID returns the processor ID.
*/
func (p *eventProcessor) ID() uint64 {
return p.id
}
/*
ThreadPool returns the thread pool which this processor is using.
*/
func (p *eventProcessor) ThreadPool() *pool.ThreadPool {
return p.pool
}
/*
Workers returns the number of threads of this processor.
*/
func (p *eventProcessor) Workers() int {
return p.workerCount
}
/*
Reset removes all stored rules from this processor.
*/
func (p *eventProcessor) Reset() error {
// Check that the thread pool is stopped
if p.pool.Status() != pool.StatusStopped {
return fmt.Errorf("Cannot reset processor if it has not stopped")
}
// Invalidate triggering cache
p.triggeringCacheLock.Lock()
p.triggeringCache = nil
p.triggeringCacheLock.Unlock()
// Create a new rule index
p.ruleIndex = NewRuleIndex()
return nil
}
/*
AddRule adds a new rule to the processor.
*/
func (p *eventProcessor) AddRule(rule *Rule) error {
// Check that the thread pool is stopped
if p.pool.Status() != pool.StatusStopped {
return fmt.Errorf("Cannot add rule if the processor has not stopped")
}
// Invalidate triggering cache
p.triggeringCacheLock.Lock()
p.triggeringCache = nil
p.triggeringCacheLock.Unlock()
return p.ruleIndex.AddRule(rule)
}
/*
Rules returns all loaded rules.
*/
func (p *eventProcessor) Rules() map[string]*Rule {
return p.ruleIndex.Rules()
}
/*
Start starts this processor.
*/
func (p *eventProcessor) Start() {
p.pool.SetWorkerCount(p.workerCount, false)
}
/*
Finish will finish all remaining tasks and then stop the processor.
*/
func (p *eventProcessor) Finish() {
p.pool.JoinAll()
}
/*
Stopped returns if the processor is stopped.
*/
func (p *eventProcessor) Stopped() bool {
return p.pool.Status() == pool.StatusStopped
}
/*
Status returns the status of the processor (Running / Stopping / Stopped).
*/
func (p *eventProcessor) Status() string {
return p.pool.Status()
}
/*
NewRootMonitor creates a new root monitor for this processor. This monitor is used to add initial
root events.
*/
func (p *eventProcessor) NewRootMonitor(context map[string]interface{}, scope *RuleScope) *RootMonitor {
if scope == nil {
scope = NewRuleScope(map[string]bool{
"": true, // Default root monitor has global scope
})
}
return newRootMonitor(context, scope, p.messageQueue)
}
/*
SetRootMonitorErrorObserver specifies an observer which is triggered
when a root monitor of this processor has finished and returns errors.
By default this is set to nil (no observer).
*/
func (p *eventProcessor) SetRootMonitorErrorObserver(rmErrorObserver func(rm *RootMonitor)) {
p.rmErrorObserver = rmErrorObserver
}
/*
SetFailOnFirstErrorInTriggerSequence sets the behavior when rules return errors.
If set to false (default) then all rules in a trigger sequence for a specific event
are executed. If set to true then the first rule which returns an error will stop
the trigger sequence. Events which have been added by the failing rule are still processed.
*/
func (p *eventProcessor) SetFailOnFirstErrorInTriggerSequence(v bool) {
p.failOnFirstError = v
}
/*
Notify the root monitor error observer that an error occurred.
*/
func (p *eventProcessor) notifyRootMonitorErrors(rm *RootMonitor) {
if p.rmErrorObserver != nil {
p.rmErrorObserver(rm)
}
}
/*
AddEventAndWait adds a new event to the processor and waits for the resulting event cascade
to finish. If a monitor is passed then it must be a RootMonitor.
*/
func (p *eventProcessor) AddEventAndWait(event *Event, monitor *RootMonitor) (Monitor, error) {
var wg sync.WaitGroup
wg.Add(1)
if monitor == nil {
monitor = p.NewRootMonitor(nil, nil)
}
p.messageQueue.AddObserver(MessageRootMonitorFinished, monitor,
func(event string, eventSource interface{}) {
// Everything has finished
wg.Done()
p.messageQueue.RemoveObservers(event, eventSource)
})
resMonitor, err := p.AddEvent(event, monitor)
if resMonitor == nil {
// Event was not added
p.messageQueue.RemoveObservers(MessageRootMonitorFinished, monitor)
} else {
// Event was added now wait for it to finish
wg.Wait()
}
return resMonitor, err
}
/*
AddEvent adds a new event to the processor. Returns the monitor if the event
triggered a rule and nil if the event was skipped.
*/
func (p *eventProcessor) AddEvent(event *Event, eventMonitor Monitor) (Monitor, error) {
// Check that the thread pool is running
if s := p.pool.Status(); s == pool.StatusStopped || s == pool.StatusStopping {
return nil, fmt.Errorf("Cannot add event if the processor is stopping or not running")
}
EventTracer.record(event, "eventProcessor.AddEvent", "Event added to the processor")
// First check if the event is triggering any rules at all
if !p.IsTriggering(event) {
EventTracer.record(event, "eventProcessor.AddEvent", "Event was skipped")
if eventMonitor != nil {
eventMonitor.Skip(event)
}
return nil, nil
}
// Check if we need to construct a new root monitor
if eventMonitor == nil {
eventMonitor = p.NewRootMonitor(nil, nil)
}
if rootMonitor, ok := eventMonitor.(*RootMonitor); ok {
p.messageQueue.AddObserver(MessageRootMonitorFinished, rootMonitor,
func(event string, eventSource interface{}) {
// Call finish handler if there is one
if rm := eventSource.(*RootMonitor); rm.finished != nil {
rm.finished(p)
}
p.messageQueue.RemoveObservers(event, eventSource)
})
}
eventMonitor.Activate(event)
EventTracer.record(event, "eventProcessor.AddEvent", "Adding task to thread pool")
// Kick off event processing (see Processor.ProcessEvent)
p.pool.AddTask(&Task{p, eventMonitor, event})
return eventMonitor, nil
}
/*
IsTriggering checks if a given event triggers a loaded rule. This does not the
actual state matching for speed.
*/
func (p *eventProcessor) IsTriggering(event *Event) bool {
var res, ok bool
p.triggeringCacheLock.Lock()
defer p.triggeringCacheLock.Unlock()
// Ensure the triggering cache exists
if p.triggeringCache == nil {
p.triggeringCache = make(map[string]bool)
}
name := event.Name()
if res, ok = p.triggeringCache[name]; !ok {
res = p.ruleIndex.IsTriggering(event)
p.triggeringCache[name] = res
}
return res
}
/*
ProcessEvent processes an event by determining which rules trigger and match
the given event.
*/
func (p *eventProcessor) ProcessEvent(tid uint64, event *Event, parent Monitor) map[string]error {
var rulesTriggering []*Rule
var rulesExecuting []*Rule
scope := parent.Scope()
ruleCandidates := p.ruleIndex.Match(event)
suppressedRules := make(map[string]bool)
EventTracer.record(event, "eventProcessor.ProcessEvent", "Processing event")
// Remove candidates which are out of scope
for _, ruleCandidate := range ruleCandidates {
if scope.IsAllowedAll(ruleCandidate.ScopeMatch) {
rulesTriggering = append(rulesTriggering, ruleCandidate)
// Build up a suppression list
for _, suppressedRule := range ruleCandidate.SuppressionList {
suppressedRules[suppressedRule] = true
}
}
}
// Remove suppressed rules
for _, ruleTriggers := range rulesTriggering {
if _, ok := suppressedRules[ruleTriggers.Name]; ok {
continue
}
rulesExecuting = append(rulesExecuting, ruleTriggers)
}
// Sort rules according to their priority (0 is the highest)
SortRuleSlice(rulesExecuting)
// Run rules which are not suppressed
errors := make(map[string]error)
EventTracer.record(event, "eventProcessor.ProcessEvent", "Running rules: ", rulesExecuting)
for _, rule := range rulesExecuting {
if err := rule.Action(p, parent, event, tid); err != nil {
errors[rule.Name] = err
}
if p.failOnFirstError && len(errors) > 0 {
break
}
}
return errors
}
/*
String returns a string representation the processor.
*/
func (p *eventProcessor) String() string {
return fmt.Sprintf("EventProcessor %v (workers:%v)", p.ID(), p.workerCount)
}
// Unique id creation
// ==================
var pidcounter uint64 = 1
var pidcounterLock = &sync.Mutex{}
/*
newProcId returns a new unique id or processors.
*/
func newProcID() uint64 {
pidcounterLock.Lock()
defer pidcounterLock.Unlock()
ret := pidcounter
pidcounter++
return ret
}
/*
* 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 pubsub contains a pub/sub event handling implementation.
*/
package pubsub
import "sync"
/*
EventPump implements the observer pattern. Observers can subscribe to receive
notifications on certain events. Observed objects can send notifications.
*/
type EventPump struct {
eventsObservers map[string]map[interface{}][]EventCallback
eventsObserversLock *sync.Mutex
}
/*
EventCallback is the callback function which is called when an event was observed.
*/
type EventCallback func(event string, eventSource interface{})
/*
NewEventPump creates a new event pump.
*/
func NewEventPump() *EventPump {
return &EventPump{make(map[string]map[interface{}][]EventCallback), &sync.Mutex{}}
}
/*
AddObserver adds a new observer to the event pump. An observer can subscribe to
a given event from a given event source. If the event is an empty string then
the observer subscribes to all events from the event source. If the
eventSource is nil then the observer subscribes to all event sources.
*/
func (ep *EventPump) AddObserver(event string, eventSource interface{}, callback EventCallback) {
// Ignore requests with non-existent callbacks
if callback == nil {
return
}
ep.eventsObserversLock.Lock()
defer ep.eventsObserversLock.Unlock()
sources, ok := ep.eventsObservers[event]
if !ok {
sources = make(map[interface{}][]EventCallback)
ep.eventsObservers[event] = sources
}
callbacks, ok := sources[eventSource]
if !ok {
callbacks = []EventCallback{callback}
sources[eventSource] = callbacks
} else {
sources[eventSource] = append(callbacks, callback)
}
}
/*
PostEvent posts an event to this event pump from a given event source.
*/
func (ep *EventPump) PostEvent(event string, eventSource interface{}) {
if event == "" || eventSource == nil {
panic("Posting an event requires the event and its source")
}
postEvent := func(event string, eventSource interface{}) {
ep.eventsObserversLock.Lock()
sources, ok := ep.eventsObservers[event]
if ok {
// Create a local copy of sources before executing the callbacks
origsources := sources
sources = make(map[interface{}][]EventCallback)
for source, callbacks := range origsources {
sources[source] = callbacks
}
}
ep.eventsObserversLock.Unlock()
if ok {
for source, callbacks := range sources {
if source == eventSource || source == nil {
for _, callback := range callbacks {
callback(event, eventSource)
}
}
}
}
}
postEvent(event, eventSource)
postEvent("", eventSource)
}
/*
RemoveObservers removes observers from the event pump. If the event is an
empty string then the observer is removed from all events. If the
eventSource is nil then all observers of the event are dropped.
*/
func (ep *EventPump) RemoveObservers(event string, eventSource interface{}) {
ep.eventsObserversLock.Lock()
defer ep.eventsObserversLock.Unlock()
// Clear everything
if event == "" && eventSource == nil {
ep.eventsObservers = make(map[string]map[interface{}][]EventCallback)
} else if eventSource == nil {
delete(ep.eventsObservers, event)
} else if event == "" {
for _, sources := range ep.eventsObservers {
delete(sources, eventSource)
}
} else {
if sources, ok := ep.eventsObservers[event]; ok {
delete(sources, eventSource)
}
}
}
/*
* 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 engine
import (
"bytes"
"encoding/json"
"fmt"
"regexp"
"sort"
"strings"
"sync"
"devt.de/krotik/common/errorutil"
"devt.de/krotik/common/sortutil"
)
/*
Rule models a matching rule for event receivers (actions). A rule has 3 possible
matching criteria:
- Match on event kinds: A list of strings in dot notation which describes event kinds. May
contain '*' characters as wildcards (e.g. core.tests.*).
- Match on event cascade scope: A list of strings in dot notation which describe the
required scopes of an event cascade.
- Match on event state: A simple list of required key / value states in the event
state. Nil values can be used as wildcards (i.e. match is only on key).
Rules have priorities (0 being the highest) and may suppress each other.
*/
type Rule struct {
Name string // Name of the rule
Desc string // Description of the rule (optional)
KindMatch []string // Match on event kinds
ScopeMatch []string // Match on event cascade scope
StateMatch map[string]interface{} // Match on event state
Priority int // Priority of the rule
SuppressionList []string // List of suppressed rules by this rule
Action RuleAction // Action of the rule
}
/*
CopyAs returns a shallow copy of this rule with a new name.
*/
func (r *Rule) CopyAs(newName string) *Rule {
return &Rule{
Name: newName,
Desc: r.Desc,
KindMatch: r.KindMatch,
ScopeMatch: r.ScopeMatch,
StateMatch: r.StateMatch,
Priority: r.Priority,
SuppressionList: r.SuppressionList,
Action: r.Action,
}
}
func (r *Rule) String() string {
sm, _ := json.Marshal(r.StateMatch)
return fmt.Sprintf("Rule:%s [%s] (Priority:%v Kind:%v Scope:%v StateMatch:%s Suppress:%v)",
r.Name, strings.TrimSpace(r.Desc), r.Priority, r.KindMatch, r.ScopeMatch, sm, r.SuppressionList)
}
/*
RuleAction is an action which is executed by a matching rule. The action gets
a unique thread ID from the executing thread.
*/
type RuleAction func(p Processor, m Monitor, e *Event, tid uint64) error
/*
RuleIndex is an index for rules. It takes the form of a tree structure in which
incoming events are matched level by level (e.g. event of kind core.task1.step1
is first matched by kind "core" then "task1" and then "step1". At the leaf of
the index tree it may then be matched on a state condition).
*/
type RuleIndex interface {
/*
AddRule adds a new rule to the index.
*/
AddRule(rule *Rule) error
/*
IsTriggering checks if a given event triggers a rule in this index.
*/
IsTriggering(event *Event) bool
/*
Match returns all rules in this index which match a given event. This
method does a full matching check including state matching.
*/
Match(event *Event) []*Rule
/*
String returns a string representation of this rule index and all subindexes.
*/
String() string
/*
Rules returns all rules with the given prefix in the name. Use the empty
string to return all rules.
*/
Rules() map[string]*Rule
}
/*
ruleSubIndex is a sub index used by a rule index.
*/
type ruleSubIndex interface {
/*
type returns the type of the rule sub index.
*/
Type() string
/*
addRuleAtLevel adds a new rule to the index at a specific level. The
level is described by a part of the rule kind match.
*/
addRuleAtLevel(rule *Rule, kindMatchLevel []string)
/*
isTriggeringAtLevel checks if a given event triggers a rule at the given
level of the index.
*/
isTriggeringAtLevel(event *Event, level int) bool
/*
matchAtLevel returns all rules in this index which match a given event
at the given level. This method does a full matching check including
state matching.
*/
matchAtLevel(event *Event, level int) []*Rule
/*
stringIndent returns a string representation with a given indentation of this
rule index and all subindexes.
*/
stringIndent(indent string) string
}
/*
ruleIndexRoot models the index root node.
*/
type ruleIndexRoot struct {
*RuleIndexKind
rules map[string]*Rule
}
/*
AddRule adds a new rule to the index.
*/
func (r *ruleIndexRoot) AddRule(rule *Rule) error {
if _, ok := r.rules[rule.Name]; ok {
return fmt.Errorf("Cannot add rule %v twice", rule.Name)
}
r.rules[rule.Name] = rule
return r.RuleIndexKind.AddRule(rule)
}
/*
Rules returns all rules with the given prefix in the name. Use the empty
string to return all rules.
*/
func (r *ruleIndexRoot) Rules() map[string]*Rule {
return r.rules
}
/*
NewRuleIndex creates a new rule container for efficient event matching.
*/
func NewRuleIndex() RuleIndex {
return &ruleIndexRoot{newRuleIndexKind(), make(map[string]*Rule)}
}
/*
Rule index types
*/
const (
typeRuleIndexKind = "RuleIndexKind"
typeRuleIndexState = "RuleIndexState"
typeRuleIndexAll = "RuleIndexAll"
)
// Rule Index Kind
// ===============
/*
RuleIndexKind data structure.
*/
type RuleIndexKind struct {
id uint64 // Id of this rule index
kindAllMatch []ruleSubIndex // Rules with target all events of a specific category
kindSingleMatch map[string][]ruleSubIndex // Rules which target specific event kinds
count int // Number of loaded rules
}
/*
newRuleIndexKind creates a new rule index matching on event kind.
*/
func newRuleIndexKind() *RuleIndexKind {
return &RuleIndexKind{
newRuleIndexID(),
make([]ruleSubIndex, 0),
make(map[string][]ruleSubIndex),
0,
}
}
/*
Type returns the type of the rule sub index.
*/
func (ri *RuleIndexKind) Type() string {
return typeRuleIndexKind
}
/*
AddRule adds a new rule to the index.
*/
func (ri *RuleIndexKind) AddRule(rule *Rule) error {
// Check essential rule attributes
if rule.KindMatch == nil || len(rule.KindMatch) == 0 {
return fmt.Errorf("Cannot add rule without a kind match: %v", rule.Name)
} else if rule.ScopeMatch == nil {
return fmt.Errorf("Cannot add rule without a scope match: %v", rule.Name)
}
// Add rule to the index for all kind matches
for _, kindMatch := range rule.KindMatch {
ri.addRuleAtLevel(rule, strings.Split(kindMatch, RuleKindSeparator))
ri.count++
}
return nil
}
/*
addRuleAtLevel adds a new rule to the index at a specific level. The
level is described by a part of the rule kind match.
*/
func (ri *RuleIndexKind) addRuleAtLevel(rule *Rule, kindMatchLevel []string) {
var indexType string
var index ruleSubIndex
var ruleSubIndexList []ruleSubIndex
var ok bool
// Pick the right index type
if len(kindMatchLevel) == 1 {
if rule.StateMatch != nil {
indexType = typeRuleIndexState
} else {
indexType = typeRuleIndexAll
}
} else {
indexType = typeRuleIndexKind
}
// Get (create when necessary) a sub index of a specific type for the
// match item of this level
matchItem := kindMatchLevel[0]
// Select the correct ruleSubIndexList
if matchItem == RuleKindWildcard {
ruleSubIndexList = ri.kindAllMatch
} else {
if ruleSubIndexList, ok = ri.kindSingleMatch[matchItem]; !ok {
ruleSubIndexList = make([]ruleSubIndex, 0)
ri.kindSingleMatch[matchItem] = ruleSubIndexList
}
}
// Check if the required index is already existing
for _, item := range ruleSubIndexList {
if item.Type() == indexType {
index = item
break
}
}
// Create a new index if no index was found
if index == nil {
switch indexType {
case typeRuleIndexState:
index = newRuleIndexState()
case typeRuleIndexAll:
index = newRuleIndexAll()
case typeRuleIndexKind:
index = newRuleIndexKind()
}
// Add the new index to the correct list
if matchItem == RuleKindWildcard {
ri.kindAllMatch = append(ruleSubIndexList, index)
} else {
ri.kindSingleMatch[matchItem] = append(ruleSubIndexList, index)
}
}
// Recurse into the next level
index.addRuleAtLevel(rule, kindMatchLevel[1:])
}
/*
IsTriggering checks if a given event triggers a rule in this index.
*/
func (ri *RuleIndexKind) IsTriggering(event *Event) bool {
return ri.isTriggeringAtLevel(event, 0)
}
/*
isTriggeringAtLevel checks if a given event triggers a rule at the given
level of the index.
*/
func (ri *RuleIndexKind) isTriggeringAtLevel(event *Event, level int) bool {
// Check if the event kind is too general (e.g. rule is defined as a.b.c
// and the event kind is a.b)
if len(event.kind) <= level {
return false
}
levelKind := event.kind[level]
nextLevel := level + 1
// Check rules targeting all events
for _, index := range ri.kindAllMatch {
if index.isTriggeringAtLevel(event, nextLevel) {
return true
}
}
// Check rules targeting specific events
if ruleSubIndexList, ok := ri.kindSingleMatch[levelKind]; ok {
for _, index := range ruleSubIndexList {
if index.isTriggeringAtLevel(event, nextLevel) {
return true
}
}
}
return false
}
/*
Match returns all rules in this index which match a given event. This method
does a full matching check including state matching.
*/
func (ri *RuleIndexKind) Match(event *Event) []*Rule {
return ri.matchAtLevel(event, 0)
}
/*
matchAtLevel returns all rules in this index which match a given event
at the given level. This method does a full matching check including
state matching.
*/
func (ri *RuleIndexKind) matchAtLevel(event *Event, level int) []*Rule {
// Check if the event kind is too general (e.g. rule is defined as a.b.c
// and the event kind is a.b)
if len(event.kind) <= level {
return nil
}
var ret []*Rule
levelKind := event.kind[level]
nextLevel := level + 1
// Check rules targeting all events
for _, index := range ri.kindAllMatch {
ret = append(ret, index.matchAtLevel(event, nextLevel)...)
}
// Check rules targeting specific events
if ruleSubIndexList, ok := ri.kindSingleMatch[levelKind]; ok {
for _, index := range ruleSubIndexList {
ret = append(ret, index.matchAtLevel(event, nextLevel)...)
}
}
return ret
}
/*
String returns a string representation of this rule index and all subindexes.
*/
func (ri *RuleIndexKind) String() string {
return ri.stringIndent("")
}
/*
stringIndent returns a string representation with a given indentation of this
rule index and all subindexes.
*/
func (ri *RuleIndexKind) stringIndent(indent string) string {
var buf bytes.Buffer
newIndent := indent + " "
writeIndexList := func(name string, indexList []ruleSubIndex) {
if len(indexList) > 0 {
buf.WriteString(fmt.Sprint(indent, name))
buf.WriteString(fmt.Sprintf(" - %v (%v)\n", ri.Type(), ri.id))
for _, index := range indexList {
buf.WriteString(index.stringIndent(newIndent))
}
}
}
writeIndexList("*", ri.kindAllMatch)
var keys []string
for k := range ri.kindSingleMatch {
keys = append(keys, k)
}
sort.Strings(keys)
for _, key := range keys {
indexList := ri.kindSingleMatch[key]
writeIndexList(key, indexList)
}
return buf.String()
}
// Rule Index State
// ================
/*
RuleMatcherKey is used for pure key - value state matches.
*/
type RuleMatcherKey struct {
bits uint64
bitsAny uint64
bitsValue map[interface{}]uint64
bitsRegexes map[uint64]*regexp.Regexp
}
/*
addRule adds a new rule to this key matcher.
*/
func (rm *RuleMatcherKey) addRule(num uint, bit uint64, key string, value interface{}) {
// Register rule bit
rm.bits |= bit
if value == nil {
rm.bitsAny |= bit
} else if regex, ok := value.(*regexp.Regexp); ok {
// For regex match we add a bit to the any mask so the presence of
// the key is checked before the actual regex is checked
rm.bitsAny |= bit
rm.bitsRegexes[bit] = regex
} else {
rm.bitsValue[value] |= bit
}
}
/*
match adds matching rules to a given bit mask.
*/
func (rm *RuleMatcherKey) match(bits uint64, value interface{}) uint64 {
toRemove := rm.bitsAny ^ rm.bits
if value != nil {
if additionalBits, ok := rm.bitsValue[value]; ok {
toRemove = rm.bitsAny | additionalBits ^ rm.bits
}
}
keyMatchedBits := bits ^ (bits & toRemove)
for bm, r := range rm.bitsRegexes {
if keyMatchedBits&bm > 0 && !r.MatchString(fmt.Sprint(value)) {
// Regex does not match remove the bit
keyMatchedBits ^= keyMatchedBits & bm
}
}
return keyMatchedBits
}
/*
unmatch removes all registered rules in this
*/
func (rm *RuleMatcherKey) unmatch(bits uint64) uint64 {
return bits ^ (bits & rm.bits)
}
/*
String returns a string representation of this key matcher.
*/
func (rm *RuleMatcherKey) String() string {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("%08X *:%08X", rm.bits, rm.bitsAny))
buf.WriteString(" [")
var keys []interface{}
for k := range rm.bitsValue {
keys = append(keys, k)
}
sortutil.InterfaceStrings(keys)
for _, k := range keys {
m := rm.bitsValue[k]
buf.WriteString(fmt.Sprintf("%v:%08X ", k, m))
}
buf.WriteString("] [")
var rkeys []uint64
for k := range rm.bitsRegexes {
rkeys = append(rkeys, k)
}
sortutil.UInt64s(rkeys)
for _, k := range rkeys {
r := rm.bitsRegexes[k]
buf.WriteString(fmt.Sprintf("%08X:%v ", k, r))
}
buf.WriteString("]")
return buf.String()
}
/*
RuleIndexState data structure
*/
type RuleIndexState struct {
id uint64 // Id of this rule index
rules []*Rule // All rules stored in this index
keyMap map[string]*RuleMatcherKey // Map of keys (key or key and value) to KeyMatcher
}
/*
newRuleIndexState creates a new rule index matching on event state.
*/
func newRuleIndexState() *RuleIndexState {
return &RuleIndexState{newRuleIndexID(), make([]*Rule, 0),
make(map[string]*RuleMatcherKey)}
}
/*
Type returns the type of the rule sub index.
*/
func (ri *RuleIndexState) Type() string {
return typeRuleIndexState
}
/*
addRuleAtLevel adds a new rule to the index at a specific level. The
level is described by a part of the rule kind match.
*/
func (ri *RuleIndexState) addRuleAtLevel(rule *Rule, kindMatchLevel []string) {
errorutil.AssertTrue(len(kindMatchLevel) == 0,
fmt.Sprint("RuleIndexState must be a leaf - level is:", kindMatchLevel))
num := uint(len(ri.rules))
var bit uint64 = 1 << num
ri.rules = append(ri.rules, rule)
for k, v := range rule.StateMatch {
var ok bool
var keyMatcher *RuleMatcherKey
if keyMatcher, ok = ri.keyMap[k]; !ok {
keyMatcher = &RuleMatcherKey{0, 0, make(map[interface{}]uint64), make(map[uint64]*regexp.Regexp)}
ri.keyMap[k] = keyMatcher
}
keyMatcher.addRule(num, bit, k, v)
}
}
/*
isTriggeringAtLevel checks if a given event triggers a rule at the given
level of the index.
*/
func (ri *RuleIndexState) isTriggeringAtLevel(event *Event, level int) bool {
return len(event.kind) == level
}
/*
matchAtLevel returns all rules in this index which match a given event
at the given level. This method does a full matching check including
state matching.
*/
func (ri *RuleIndexState) matchAtLevel(event *Event, level int) []*Rule {
if len(event.kind) != level {
return nil
}
// Assume all rules match and remove the ones with don't
var matchBits uint64 = (1 << uint(len(ri.rules))) - 1
// Match key and values
for key, matcher := range ri.keyMap {
if val, ok := event.state[key]; ok {
// Key is present in event
matchBits = matcher.match(matchBits, val)
} else {
// Key is not present in event - remove all rules which require the key
matchBits = matcher.unmatch(matchBits)
}
if matchBits == 0 {
// All rules have been excluded
return nil
}
}
var ret []*Rule
var collectionBits uint64 = 1
// Collect matched rules
for i := 0; collectionBits <= matchBits; i++ {
if matchBits&collectionBits > 0 {
ret = append(ret, ri.rules[i])
}
collectionBits <<= 1
}
return ret
}
/*
stringIndent returns a string representation with a given indentation of this
rule index and all subindexes.
*/
func (ri *RuleIndexState) stringIndent(indent string) string {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("%v%v (%v) ", indent, ri.Type(), ri.id))
buf.WriteString("[")
for _, r := range ri.rules {
buf.WriteString(fmt.Sprintf("%v ", r.Name))
}
buf.WriteString("]\n")
newIndent := indent + " "
var keys []string
for k := range ri.keyMap {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
m := ri.keyMap[k]
buf.WriteString(fmt.Sprintf("%v%v - %v\n", newIndent, k, m))
}
return buf.String()
}
// Rule Index All
// ==============
/*
RuleIndexAll data structure.
*/
type RuleIndexAll struct {
id uint64 // Id of this rule index
rules []*Rule // Rules with target all events of a specific category
}
/*
newRuleIndexAll creates a new leaf rule index matching on all events.
*/
func newRuleIndexAll() *RuleIndexAll {
return &RuleIndexAll{newRuleIndexID(), make([]*Rule, 0)}
}
/*
Type returns the type of the rule sub index.
*/
func (ri *RuleIndexAll) Type() string {
return typeRuleIndexAll
}
/*
addRuleAtLevel adds a new rule to the index at a specific level. The
level is described by a part of the rule kind match.
*/
func (ri *RuleIndexAll) addRuleAtLevel(rule *Rule, kindMatchLevel []string) {
ri.rules = append(ri.rules, rule)
}
/*
isTriggeringAtLevel checks if a given event triggers a rule at the given
level of the index.
*/
func (ri *RuleIndexAll) isTriggeringAtLevel(event *Event, level int) bool {
return len(event.kind) == level
}
/*
matchAtLevel returns all rules in this index which match a given event
at the given level. This method does a full matching check including
state matching.
*/
func (ri *RuleIndexAll) matchAtLevel(event *Event, level int) []*Rule {
if len(event.kind) != level {
return nil
}
return ri.rules
}
/*
stringIndent returns a string representation with a given indentation of this
rule index and all subindexes.
*/
func (ri *RuleIndexAll) stringIndent(indent string) string {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("%v%v (%v)\n", indent, ri.Type(), ri.id))
newIndent := indent + " "
for _, rule := range ri.rules {
buf.WriteString(fmt.Sprintf("%v%v\n", newIndent, rule))
}
return buf.String()
}
// Unique id creation
// ==================
var ruleindexidcounter uint64 = 1
var ruleindexidcounterLock = &sync.Mutex{}
/*
newId returns a new unique id.
*/
func newRuleIndexID() uint64 {
ruleindexidcounterLock.Lock()
defer ruleindexidcounterLock.Unlock()
ret := ruleindexidcounter
ruleindexidcounter++
return ret
}
/*
* 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 engine
import (
"bytes"
"fmt"
"math/rand"
"sort"
"sync"
"devt.de/krotik/common/errorutil"
"devt.de/krotik/common/sortutil"
"devt.de/krotik/common/stringutil"
"devt.de/krotik/ecal/engine/pool"
"devt.de/krotik/ecal/engine/pubsub"
)
/*
TaskError datastructure to collect all rule errors of an event.
*/
type TaskError struct {
ErrorMap map[string]error // Rule errors (rule name -> error)
Event *Event // Event which caused the error
Monitor Monitor // Event monitor
}
/*
Error returns a string representation of this error.
*/
func (te *TaskError) Error() string {
var ret bytes.Buffer
// Collect all errors and sort them by name
errNames := make([]string, 0, len(te.ErrorMap))
for name := range te.ErrorMap {
errNames = append(errNames, name)
}
sort.Strings(errNames)
ret.WriteString(fmt.Sprintf("Taskerror%v:\n", stringutil.Plural(len(errNames))))
for i, name := range errNames {
ret.WriteString(te.Monitor.EventPathString())
ret.WriteString(fmt.Sprintf(" -> %v : %v", name, te.ErrorMap[name]))
if i < len(errNames)-1 {
ret.WriteString("\n")
}
}
return ret.String()
}
/*
Task models a task which is created and executed by the processor.
*/
type Task struct {
p Processor // Processor which created the task
m Monitor // Monitor which observes the task execution
e *Event // Event which caused the task creation
}
/*
Run the task.
*/
func (t *Task) Run(tid uint64) error {
EventTracer.record(t.e, "Task.Run", "Running task")
errors := t.p.ProcessEvent(tid, t.e, t.m)
if len(errors) > 0 {
// Monitor is not declared finished until the errors have been handled
EventTracer.record(t.e, "Task.Run", fmt.Sprint("Task had errors:", errors))
return &TaskError{errors, t.e, t.m}
}
t.m.Finish()
return nil
}
/*
Returns a string representation of this task.
*/
func (t *Task) String() string {
return fmt.Sprintf("Task: %v %v %v", t.p, t.m, t.e)
}
/*
HandleError handles an error which occurred during the run method.
*/
func (t *Task) HandleError(e error) {
t.m.SetErrors(e.(*TaskError))
t.m.Finish()
t.p.(*eventProcessor).notifyRootMonitorErrors(t.m.RootMonitor())
}
/*
TaskQueue models the queue of tasks for a processor.
*/
type TaskQueue struct {
lock *sync.Mutex // Lock for queue
queues map[uint64]*sortutil.PriorityQueue // Map from root monitor id to priority queue
messageQueue *pubsub.EventPump // Queue for message passing between components
}
/*
NewTaskQueue creates a new TaskQueue object.
*/
func NewTaskQueue(ep *pubsub.EventPump) *TaskQueue {
return &TaskQueue{&sync.Mutex{}, make(map[uint64]*sortutil.PriorityQueue), ep}
}
/*
Clear the queue of all pending tasks.
*/
func (tq *TaskQueue) Clear() {
tq.lock.Lock()
defer tq.lock.Unlock()
tq.queues = make(map[uint64]*sortutil.PriorityQueue)
}
/*
Pop returns the next task from the queue.
*/
func (tq *TaskQueue) Pop() pool.Task {
tq.lock.Lock()
defer tq.lock.Unlock()
var popQueue *sortutil.PriorityQueue
var idx int
// Pick a random number between 0 and len(tq.queues) - 1
if lq := len(tq.queues); lq > 0 {
idx = rand.Intn(lq)
}
// Go through all queues and pick one - clean up while we are at it
for k, v := range tq.queues {
if v.Size() > 0 {
// Pick a random queue - pick the last if idx does not
// reach 0 before the end of the iteration.
idx--
popQueue = v
if idx <= 0 {
break
}
} else {
// Remove empty queues
delete(tq.queues, k)
}
}
if popQueue != nil {
if res := popQueue.Pop(); res != nil {
return res.(*Task)
}
}
return nil
}
/*
Push adds another task to the queue.
*/
func (tq *TaskQueue) Push(t pool.Task) {
tq.lock.Lock()
defer tq.lock.Unlock()
var q *sortutil.PriorityQueue
var ok bool
task := t.(*Task)
rm := task.m.RootMonitor()
id := rm.ID()
if q, ok = tq.queues[id]; !ok {
q = sortutil.NewPriorityQueue()
tq.queues[id] = q
// Add listener for finish
tq.messageQueue.AddObserver(MessageRootMonitorFinished, rm,
func(event string, eventSource interface{}) {
tq.lock.Lock()
defer tq.lock.Unlock()
rm := eventSource.(*RootMonitor)
q := tq.queues[rm.ID()]
// Safeguard that no tasks are ever left over
errorutil.AssertTrue(q == nil || q.Size() == 0,
"Finished monitor left events behind")
tq.messageQueue.RemoveObservers(event, eventSource)
})
}
q.Push(task, task.m.Priority())
}
/*
Size returns the size of the queue.
*/
func (tq *TaskQueue) Size() int {
tq.lock.Lock()
defer tq.lock.Unlock()
var ret int
for _, q := range tq.queues {
ret += q.Size()
}
return ret
}
/*
* 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 engine
import (
"sort"
"strings"
)
// Globals
// =======
/*
RuleKindSeparator is the separator for rule kinds
*/
const RuleKindSeparator = "."
/*
RuleKindWildcard is a wildcard for rule kinds
*/
const RuleKindWildcard = "*"
// Messages
// ========
/*
MessageRootMonitorFinished is send from a root monitor when it has finished
*/
const MessageRootMonitorFinished = "MessageRootMonitorFinished"
// Rule Scope
// ==========
/*
RuleScope is a set of scope definitions for rules. Each definition allows or disallows
a set of rule types. Scope definitions and rule sets are usually expressed with
named paths (scope paths) in dot notation (e.g. core.data.read).
*/
type RuleScope struct {
scopeDefs map[string]interface{}
}
const ruleScopeAllowFlag = "."
/*
NewRuleScope creates a new rule scope object with an initial set of definitions.
*/
func NewRuleScope(allows map[string]bool) *RuleScope {
rs := &RuleScope{make(map[string]interface{})}
rs.AddAll(allows)
return rs
}
/*
IsAllowedAll checks if all given scopes are allowed.
*/
func (rs *RuleScope) IsAllowedAll(scopePaths []string) bool {
for _, path := range scopePaths {
if !rs.IsAllowed(path) {
return false
}
}
return true
}
/*
IsAllowed checks if a given scope path is allowed within this rule scope.
*/
func (rs *RuleScope) IsAllowed(scopePath string) bool {
allowed := false
scopeDefs := rs.scopeDefs
if a, ok := scopeDefs[ruleScopeAllowFlag]; ok {
allowed = a.(bool)
}
for _, scopeStep := range strings.Split(scopePath, ".") {
val, ok := scopeDefs[scopeStep]
if !ok {
break
}
scopeDefs = val.(map[string]interface{})
if a, ok := scopeDefs[ruleScopeAllowFlag]; ok {
allowed = a.(bool)
}
}
return allowed
}
/*
AddAll adds all given definitions to the rule scope.
*/
func (rs *RuleScope) AddAll(allows map[string]bool) {
for scopePath, allow := range allows {
rs.Add(scopePath, allow)
}
}
/*
Add adds a given definition to the rule scope.
*/
func (rs *RuleScope) Add(scopePath string, allow bool) {
scopeDefs := rs.scopeDefs
if scopePath != "" {
for _, scopeStep := range strings.Split(scopePath, ".") {
val, ok := scopeDefs[scopeStep]
if !ok {
val = make(map[string]interface{})
scopeDefs[scopeStep] = val
}
scopeDefs = val.(map[string]interface{})
}
}
scopeDefs[ruleScopeAllowFlag] = allow
}
// Rule sorting
// ============
/*
RuleSlice is a slice of rules
*/
type RuleSlice []*Rule
func (s RuleSlice) Len() int { return len(s) }
func (s RuleSlice) Less(i, j int) bool { return s[i].Priority < s[j].Priority }
func (s RuleSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
/*
SortRuleSlice sorts a slice of rules.
*/
func SortRuleSlice(a []*Rule) { sort.Sort(RuleSlice(a)) }
// Unit testing
// ============
/*
UnitTestResetIDs reset all counting IDs.
THIS FUNCTION SHOULD ONLY BE CALLED BY UNIT TESTS!
*/
func UnitTestResetIDs() {
pidcounter = 1
ruleindexidcounter = 1
midcounter = 1
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
/*
Package interpreter contains the ECAL interpreter.
*/
package interpreter
import (
"fmt"
"runtime"
"strings"
"sync"
"time"
"devt.de/krotik/common/datautil"
"devt.de/krotik/common/errorutil"
"devt.de/krotik/ecal/engine/pool"
"devt.de/krotik/ecal/parser"
"devt.de/krotik/ecal/scope"
"devt.de/krotik/ecal/util"
)
/*
ecalDebugger is the inbuild default debugger.
*/
type ecalDebugger struct {
breakPoints map[string]bool // Break points (active or not)
interrogationStates map[uint64]*interrogationState // Collection of threads which are interrogated
callStacks map[uint64][]*parser.ASTNode // Call stack locations of threads
callStackVsSnapshots map[uint64][]map[string]interface{} // Call stack variable scope snapshots of threads
callStackGlobalVsSnapshots map[uint64][]map[string]interface{} // Call stack global variable scope snapshots of threads
sources map[string]bool // All known sources
breakOnStart bool // Flag to stop at the start of the next execution
breakOnError bool // Flag to stop if an error occurs
globalScope parser.Scope // Global variable scope which can be used to transfer data
lock *sync.RWMutex // Lock for this debugger
lastVisit int64 // Last time the debugger had a state visit
mutexeOwners map[string]uint64 // A map of current mutex owners
mutexLog *datautil.RingBuffer // A log of taken mutexes
threadpool *pool.ThreadPool // Reference to the thread pool of the processor
}
/*
interrogationState contains state information of a thread interrogation.
*/
type interrogationState struct {
cond *sync.Cond // Condition on which the thread is waiting when suspended
running bool // Flag if the thread is running or waiting
cmd interrogationCmd // Next interrogation command for the thread
stepOutStack []*parser.ASTNode // Target stack when doing a step out
node *parser.ASTNode // Node on which the thread was last stopped
vs parser.Scope // Variable scope of the thread when it was last stopped
err error // Error which was returned by a function call
}
/*
interrogationCmd represents a command for a thread interrogation.
*/
type interrogationCmd int
/*
Interrogation commands
*/
const (
Stop interrogationCmd = iota // Stop the execution (default)
StepIn // Step into the next function
StepOut // Step out of the current function
StepOver // Step over the next function
Resume // Resume execution - do not break again on the same line
Kill // Resume execution - and kill the thread on the next state change
)
/*
newInterrogationState creates a new interrogation state.
*/
func newInterrogationState(node *parser.ASTNode, vs parser.Scope) *interrogationState {
return &interrogationState{
sync.NewCond(&sync.Mutex{}),
false,
Stop,
nil,
node,
vs,
nil,
}
}
/*
NewECALDebugger returns a new debugger object.
*/
func NewECALDebugger(globalVS parser.Scope) util.ECALDebugger {
return &ecalDebugger{
breakPoints: make(map[string]bool),
interrogationStates: make(map[uint64]*interrogationState),
callStacks: make(map[uint64][]*parser.ASTNode),
callStackVsSnapshots: make(map[uint64][]map[string]interface{}),
callStackGlobalVsSnapshots: make(map[uint64][]map[string]interface{}),
sources: make(map[string]bool),
breakOnStart: false,
breakOnError: true,
globalScope: globalVS,
lock: &sync.RWMutex{},
lastVisit: 0,
mutexeOwners: nil,
mutexLog: nil,
threadpool: nil,
}
}
/*
HandleInput handles a given debug instruction from a console.
*/
func (ed *ecalDebugger) HandleInput(input string) (interface{}, error) {
var res interface{}
var err error
args := strings.Fields(input)
if len(args) > 0 {
if cmd, ok := DebugCommandsMap[args[0]]; ok {
if len(args) > 1 {
res, err = cmd.Run(ed, args[1:])
} else {
res, err = cmd.Run(ed, nil)
}
} else {
err = fmt.Errorf("Unknown command: %v", args[0])
}
}
return res, err
}
/*
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 occurred.
*/
func (ed *ecalDebugger) StopThreads(d time.Duration) bool {
var ret = false
for _, is := range ed.interrogationStates {
if is.running == false {
ret = true
is.cmd = Kill
is.running = true
is.cond.L.Lock()
is.cond.Broadcast()
is.cond.L.Unlock()
}
}
if ret && d > 0 {
var lastVisit int64 = -1
for lastVisit != ed.lastVisit {
lastVisit = ed.lastVisit
time.Sleep(d)
}
}
return ret
}
/*
BreakOnStart breaks on the start of the next execution.
*/
func (ed *ecalDebugger) BreakOnStart(flag bool) {
ed.lock.Lock()
defer ed.lock.Unlock()
ed.breakOnStart = flag
}
/*
BreakOnError breaks if an error occurs.
*/
func (ed *ecalDebugger) BreakOnError(flag bool) {
ed.lock.Lock()
defer ed.lock.Unlock()
ed.breakOnError = flag
}
/*
SetLockingState sets locking status information.
*/
func (ed *ecalDebugger) SetLockingState(mutexeOwners map[string]uint64, mutexLog *datautil.RingBuffer) {
if ed.mutexeOwners == nil {
ed.mutexeOwners = mutexeOwners
ed.mutexLog = mutexLog
}
}
/*
SetThreadPool sets the reference to the current used thread pool.
*/
func (ed *ecalDebugger) SetThreadPool(tp *pool.ThreadPool) {
if ed.threadpool == nil {
ed.threadpool = tp
}
}
/*
VisitState is called for every state during the execution of a program.
*/
func (ed *ecalDebugger) VisitState(node *parser.ASTNode, vs parser.Scope, tid uint64) util.TraceableRuntimeError {
ed.lock.RLock()
_, ok := ed.callStacks[tid]
ed.lastVisit = time.Now().UnixNano()
ed.lock.RUnlock()
if !ok {
// Make the debugger aware of running threads
ed.lock.Lock()
ed.callStacks[tid] = make([]*parser.ASTNode, 0, 10)
ed.callStackVsSnapshots[tid] = make([]map[string]interface{}, 0, 10)
ed.callStackGlobalVsSnapshots[tid] = make([]map[string]interface{}, 0, 10)
ed.lock.Unlock()
}
if node.Token != nil { // Statements are excluded here
targetIdentifier := fmt.Sprintf("%v:%v", node.Token.Lsource, node.Token.Lline)
ed.lock.RLock()
is, ok := ed.interrogationStates[tid]
_, sourceKnown := ed.sources[node.Token.Lsource]
ed.lock.RUnlock()
if !sourceKnown {
ed.RecordSource(node.Token.Lsource)
}
if ok {
// The thread is being interrogated
switch is.cmd {
case Resume, Kill:
if is.node.Token.Lline != node.Token.Lline {
// Remove the resume command once we are on a different line
ed.lock.Lock()
delete(ed.interrogationStates, tid)
ed.lock.Unlock()
if is.cmd == Kill {
runtime.Goexit()
}
return ed.VisitState(node, vs, tid)
}
case Stop, StepIn, StepOver:
if is.node.Token.Lline != node.Token.Lline || is.cmd == Stop {
is.node = node
is.vs = vs
is.running = false
is.cond.L.Lock()
is.cond.Wait()
is.cond.L.Unlock()
}
}
} else if active, ok := ed.breakPoints[targetIdentifier]; (ok && active) || ed.breakOnStart {
// A globally defined breakpoint has been hit - note the position
// in the thread specific map and wait
is := newInterrogationState(node, vs)
ed.lock.Lock()
ed.breakOnStart = false
ed.interrogationStates[tid] = is
ed.lock.Unlock()
is.cond.L.Lock()
is.cond.Wait()
is.cond.L.Unlock()
}
}
return nil
}
/*
VisitStepInState is called before entering a function call.
*/
func (ed *ecalDebugger) VisitStepInState(node *parser.ASTNode, vs parser.Scope, tid uint64) util.TraceableRuntimeError {
ed.lock.Lock()
defer ed.lock.Unlock()
var err util.TraceableRuntimeError
threadCallStack := ed.callStacks[tid]
threadCallStackVs := ed.callStackVsSnapshots[tid]
threadCallStackGlobalVs := ed.callStackGlobalVsSnapshots[tid]
is, ok := ed.interrogationStates[tid]
if ok {
if is.cmd == Stop {
// Special case a parameter of a function was resolved by another
// function call - the debugger should stop before entering
ed.lock.Unlock()
err = ed.VisitState(node, vs, tid)
ed.lock.Lock()
}
if err == nil {
// The thread is being interrogated
switch is.cmd {
case StepIn:
is.cmd = Stop
case StepOver:
is.cmd = StepOut
is.stepOutStack = threadCallStack
}
}
}
ed.callStacks[tid] = append(threadCallStack, node)
ed.callStackVsSnapshots[tid] = append(threadCallStackVs, ed.buildVsSnapshot(vs))
ed.callStackGlobalVsSnapshots[tid] = append(threadCallStackGlobalVs, ed.buildGlobalVsSnapshot(vs))
return err
}
/*
VisitStepOutState is called after returning from a function call.
*/
func (ed *ecalDebugger) VisitStepOutState(node *parser.ASTNode, vs parser.Scope, tid uint64, soErr error) util.TraceableRuntimeError {
ed.lock.Lock()
defer ed.lock.Unlock()
threadCallStack := ed.callStacks[tid]
threadCallStackVs := ed.callStackVsSnapshots[tid]
threadCallStackGlobalVs := ed.callStackGlobalVsSnapshots[tid]
lastIndex := len(threadCallStack) - 1
ok, cerr := threadCallStack[lastIndex].Equals(node, false) // Sanity check step in node must be the same as step out node
errorutil.AssertTrue(ok,
fmt.Sprintf("Unexpected callstack when stepping out - callstack: %v - funccall: %v - comparison error: %v",
threadCallStack, node, cerr))
ed.callStacks[tid] = threadCallStack[:lastIndex] // Remove the last item
ed.callStackVsSnapshots[tid] = threadCallStackVs[:lastIndex]
ed.callStackGlobalVsSnapshots[tid] = threadCallStackGlobalVs[:lastIndex]
is, ok := ed.interrogationStates[tid]
if ed.breakOnError && soErr != nil {
if !ok {
is = newInterrogationState(node, vs)
ed.breakOnStart = false
ed.interrogationStates[tid] = is
} else {
is.node = node
is.vs = vs
is.running = false
}
if is.err == nil {
// Only stop if the error is being set
is.err = soErr
ed.lock.Unlock()
is.cond.L.Lock()
is.cond.Wait()
is.cond.L.Unlock()
ed.lock.Lock()
}
} else if ok {
is.err = soErr
// The thread is being interrogated
switch is.cmd {
case StepOver, StepOut:
if len(ed.callStacks[tid]) == len(is.stepOutStack) {
is.cmd = Stop
}
}
}
return nil
}
/*
RecordSource records a code source.
*/
func (ed *ecalDebugger) RecordSource(source string) {
ed.lock.Lock()
defer ed.lock.Unlock()
ed.sources[source] = true
}
/*
RecordThreadFinished lets the debugger know that a thread has finished.
*/
func (ed *ecalDebugger) RecordThreadFinished(tid uint64) {
ed.lock.Lock()
defer ed.lock.Unlock()
if is, ok := ed.interrogationStates[tid]; !ok || !is.running {
delete(ed.interrogationStates, tid)
delete(ed.callStacks, tid)
delete(ed.callStackVsSnapshots, tid)
delete(ed.callStackGlobalVsSnapshots, tid)
}
}
/*
SetBreakPoint sets a break point.
*/
func (ed *ecalDebugger) SetBreakPoint(source string, line int) {
ed.lock.Lock()
defer ed.lock.Unlock()
ed.breakPoints[fmt.Sprintf("%v:%v", source, line)] = true
}
/*
DisableBreakPoint disables a break point but keeps the code reference.
*/
func (ed *ecalDebugger) DisableBreakPoint(source string, line int) {
ed.lock.Lock()
defer ed.lock.Unlock()
ed.breakPoints[fmt.Sprintf("%v:%v", source, line)] = false
}
/*
RemoveBreakPoint removes a break point.
*/
func (ed *ecalDebugger) RemoveBreakPoint(source string, line int) {
ed.lock.Lock()
defer ed.lock.Unlock()
if line > 0 {
delete(ed.breakPoints, fmt.Sprintf("%v:%v", source, line))
} else {
for k := range ed.breakPoints {
if ksource := strings.Split(k, ":")[0]; ksource == source {
delete(ed.breakPoints, k)
}
}
}
}
/*
ExtractValue copies a value from a suspended thread into the
global variable scope.
*/
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)
ed.lock.Lock()
defer ed.lock.Unlock()
is, ok := ed.interrogationStates[threadID]
if ok && !is.running {
var val interface{}
var ok bool
if val, ok, err = is.vs.GetValue(varName); ok {
err = ed.globalScope.SetValue(destVarName, val)
} else if err == nil {
err = fmt.Errorf("No such value %v", varName)
}
}
return err
}
/*
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 {
if ed.globalScope == nil {
return fmt.Errorf("Cannot access global scope")
}
err := fmt.Errorf("Cannot find suspended thread %v", threadID)
ed.lock.Lock()
defer ed.lock.Unlock()
is, ok := ed.interrogationStates[threadID]
if ok && !is.running {
var ast *parser.ASTNode
var val interface{}
// Eval expression
ast, err = parser.ParseWithRuntime("InjectValueExpression", expression,
NewECALRuntimeProvider("InjectValueExpression2", nil, nil))
if err == nil {
if err = ast.Runtime.Validate(); err == nil {
ivs := scope.NewScopeWithParent("InjectValueExpressionScope", ed.globalScope)
val, err = ast.Runtime.Eval(ivs, make(map[string]interface{}), 999)
if err == nil {
err = is.vs.SetValue(varName, val)
}
}
}
}
return err
}
/*
Continue will continue a suspended thread.
*/
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 {
switch contType {
case util.Resume:
is.cmd = Resume
case util.StepIn:
is.cmd = StepIn
case util.StepOver:
is.cmd = StepOver
case util.StepOut:
is.cmd = StepOut
stack := ed.callStacks[threadID]
is.stepOutStack = stack[:len(stack)-1]
}
is.running = true
is.cond.L.Lock()
is.cond.Broadcast()
is.cond.L.Unlock()
}
}
/*
Status returns the current status of the debugger.
*/
func (ed *ecalDebugger) Status() interface{} {
ed.lock.RLock()
defer ed.lock.RUnlock()
var sources []string
threadStates := make(map[string]map[string]interface{})
res := map[string]interface{}{
"breakpoints": ed.breakPoints,
"breakonstart": ed.breakOnStart,
"threads": threadStates,
}
for k := range ed.sources {
sources = append(sources, k)
}
res["sources"] = sources
for k, v := range ed.callStacks {
s := map[string]interface{}{
"callStack": ed.prettyPrintCallStack(v),
}
if is, ok := ed.interrogationStates[k]; ok {
s["threadRunning"] = is.running
s["error"] = is.err
}
threadStates[fmt.Sprint(k)] = s
}
return res
}
/*
LockState returns the current locking state.
*/
func (ed *ecalDebugger) LockState() interface{} {
return map[string]interface{}{
"log": ed.mutexLog.StringSlice(),
"owners": ed.mutexeOwners,
"threads": ed.threadpool.State(),
}
}
/*
Describe describes a thread currently observed by the debugger.
*/
func (ed *ecalDebugger) Describe(threadID uint64) interface{} {
ed.lock.RLock()
defer ed.lock.RUnlock()
var res map[string]interface{}
threadCallStack, ok1 := ed.callStacks[threadID]
if is, ok2 := ed.interrogationStates[threadID]; ok1 && ok2 {
callStackNode := make([]map[string]interface{}, 0)
for _, sn := range threadCallStack {
callStackNode = append(callStackNode, sn.ToJSONObject())
}
res = map[string]interface{}{
"threadRunning": is.running,
"error": is.err,
"callStack": ed.prettyPrintCallStack(threadCallStack),
"callStackNode": callStackNode,
"callStackVsSnapshot": ed.callStackVsSnapshots[threadID],
"callStackVsSnapshotGlobal": ed.callStackGlobalVsSnapshots[threadID],
}
if !is.running {
codeString, _ := parser.PrettyPrint(is.node)
res["code"] = codeString
res["node"] = is.node.ToJSONObject()
res["vs"] = ed.buildVsSnapshot(is.vs)
res["vsGlobal"] = ed.buildGlobalVsSnapshot(is.vs)
}
}
return res
}
func (ed *ecalDebugger) buildVsSnapshot(vs parser.Scope) map[string]interface{} {
vsValues := make(map[string]interface{})
// Collect all parent scopes except the global scope
parent := vs.Parent()
for parent != nil &&
parent.Name() != scope.GlobalScope {
vsValues = datautil.MergeMaps(vsValues, parent.ToJSONObject())
parent = parent.Parent()
}
return ed.MergeMaps(vsValues, vs.ToJSONObject())
}
func (ed *ecalDebugger) buildGlobalVsSnapshot(vs parser.Scope) map[string]interface{} {
vsValues := make(map[string]interface{})
globalVs := vs
for globalVs != nil &&
globalVs.Name() != scope.GlobalScope {
globalVs = globalVs.Parent()
}
if globalVs != nil && globalVs.Name() == scope.GlobalScope {
vsValues = globalVs.ToJSONObject()
}
return vsValues
}
/*
MergeMaps merges all given maps into a new map. Contents are shallow copies
and conflicts are resolved as first-one-wins.
*/
func (ed *ecalDebugger) MergeMaps(maps ...map[string]interface{}) map[string]interface{} {
ret := make(map[string]interface{})
for _, m := range maps {
for k, v := range m {
if _, ok := ret[k]; !ok {
ret[k] = v
}
}
}
return ret
}
/*
Describe describes a thread currently observed by the debugger.
*/
func (ed *ecalDebugger) prettyPrintCallStack(threadCallStack []*parser.ASTNode) []string {
cs := []string{}
for _, s := range threadCallStack {
pp, _ := parser.PrettyPrint(s)
cs = append(cs, fmt.Sprintf("%v (%v:%v)",
pp, s.Token.Lsource, s.Token.Lline))
}
return cs
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
/*
Package interpreter contains the ECAL interpreter.
*/
package interpreter
import (
"fmt"
"strconv"
"strings"
"devt.de/krotik/ecal/parser"
"devt.de/krotik/ecal/util"
)
/*
DebugCommandsMap contains the mapping of inbuild debug commands.
*/
var DebugCommandsMap = map[string]util.DebugCommand{
"breakonstart": &breakOnStartCommand{&inbuildDebugCommand{}},
"break": &setBreakpointCommand{&inbuildDebugCommand{}},
"rmbreak": &rmBreakpointCommand{&inbuildDebugCommand{}},
"disablebreak": &disableBreakpointCommand{&inbuildDebugCommand{}},
"cont": &contCommand{&inbuildDebugCommand{}},
"describe": &describeCommand{&inbuildDebugCommand{}},
"status": &statusCommand{&inbuildDebugCommand{}},
"extract": &extractCommand{&inbuildDebugCommand{}},
"inject": &injectCommand{&inbuildDebugCommand{}},
"lockstate": &lockstateCommand{&inbuildDebugCommand{}},
}
/*
inbuildDebugCommand is the base structure for inbuild debug commands providing some
utility functions.
*/
type inbuildDebugCommand struct {
}
/*
AssertNumParam converts a parameter into a number.
*/
func (ibf *inbuildDebugCommand) AssertNumParam(index int, val string) (uint64, error) {
if resNum, err := strconv.ParseInt(fmt.Sprint(val), 10, 0); err == nil {
return uint64(resNum), nil
}
return 0, fmt.Errorf("Parameter %v should be a number", index)
}
// break
// =====
/*
setBreakpointCommand sets a breakpoint
*/
type setBreakpointCommand struct {
*inbuildDebugCommand
}
/*
Execute the debug command and return its result. It must be possible to
convert the output data into a JSON string.
*/
func (c *setBreakpointCommand) Run(debugger util.ECALDebugger, args []string) (interface{}, error) {
if len(args) == 0 {
return nil, fmt.Errorf("Need a break target (<source>:<line>) as first parameter")
}
targetSplit := strings.Split(args[0], ":")
if len(targetSplit) > 1 {
if line, err := strconv.Atoi(targetSplit[1]); err == nil {
debugger.SetBreakPoint(targetSplit[0], line)
return nil, nil
}
}
return nil, fmt.Errorf("Invalid break target - should be <source>:<line>")
}
/*
DocString returns a descriptive text about this command.
*/
func (c *setBreakpointCommand) DocString() string {
return "Set a breakpoint specifying <source>:<line>"
}
// breakOnStartCommand
// ===================
/*
breakOnStartCommand breaks on the start of the next execution.
*/
type breakOnStartCommand struct {
*inbuildDebugCommand
}
/*
Execute the debug command and return its result. It must be possible to
convert the output data into a JSON string.
*/
func (c *breakOnStartCommand) Run(debugger util.ECALDebugger, args []string) (interface{}, error) {
b := true
if len(args) > 0 {
b, _ = strconv.ParseBool(args[0])
}
debugger.BreakOnStart(b)
return nil, nil
}
/*
DocString returns a descriptive text about this command.
*/
func (c *breakOnStartCommand) DocString() string {
return "Break on the start of the next execution."
}
// rmbreak
// =======
/*
rmBreakpointCommand removes a breakpoint
*/
type rmBreakpointCommand struct {
*inbuildDebugCommand
}
/*
Execute the debug command and return its result. It must be possible to
convert the output data into a JSON string.
*/
func (c *rmBreakpointCommand) Run(debugger util.ECALDebugger, args []string) (interface{}, error) {
if len(args) == 0 {
return nil, fmt.Errorf("Need a break target (<source>[:<line>]) as first parameter")
}
targetSplit := strings.Split(args[0], ":")
if len(targetSplit) > 1 {
if line, err := strconv.Atoi(targetSplit[1]); err == nil {
debugger.RemoveBreakPoint(targetSplit[0], line)
return nil, nil
}
} else {
debugger.RemoveBreakPoint(args[0], -1)
}
return nil, nil
}
/*
DocString returns a descriptive text about this command.
*/
func (c *rmBreakpointCommand) DocString() string {
return "Remove a breakpoint specifying <source>:<line>"
}
// disablebreak
// ============
/*
disableBreakpointCommand temporarily disables a breakpoint
*/
type disableBreakpointCommand struct {
*inbuildDebugCommand
}
/*
Execute the debug command and return its result. It must be possible to
convert the output data into a JSON string.
*/
func (c *disableBreakpointCommand) Run(debugger util.ECALDebugger, args []string) (interface{}, error) {
if len(args) == 0 {
return nil, fmt.Errorf("Need a break target (<source>:<line>) as first parameter")
}
targetSplit := strings.Split(args[0], ":")
if len(targetSplit) > 1 {
if line, err := strconv.Atoi(targetSplit[1]); err == nil {
debugger.DisableBreakPoint(targetSplit[0], line)
return nil, nil
}
}
return nil, fmt.Errorf("Invalid break target - should be <source>:<line>")
}
/*
DocString returns a descriptive text about this command.
*/
func (c *disableBreakpointCommand) DocString() string {
return "Temporarily disable a breakpoint specifying <source>:<line>"
}
// cont
// ====
/*
contCommand continues a suspended thread
*/
type contCommand struct {
*inbuildDebugCommand
}
/*
Execute the debug command and return its result. It must be possible to
convert the output data into a JSON string.
*/
func (c *contCommand) Run(debugger util.ECALDebugger, args []string) (interface{}, error) {
var cmd util.ContType
if len(args) != 2 {
return nil, fmt.Errorf("Need a thread ID and a command Resume, StepIn, StepOver or StepOut")
}
threadID, err := c.AssertNumParam(1, args[0])
if err == nil {
cmdString := strings.ToLower(args[1])
switch cmdString {
case "resume":
cmd = util.Resume
case "stepin":
cmd = util.StepIn
case "stepover":
cmd = util.StepOver
case "stepout":
cmd = util.StepOut
default:
return nil, fmt.Errorf("Invalid command %v - must be resume, stepin, stepover or stepout", cmdString)
}
debugger.Continue(threadID, cmd)
}
return nil, err
}
/*
DocString returns a descriptive text about this command.
*/
func (c *contCommand) DocString() string {
return "Continues a suspended thread. Specify <threadID> <Resume | StepIn | StepOver | StepOut>"
}
// describe
// ========
/*
describeCommand describes a suspended thread
*/
type describeCommand struct {
*inbuildDebugCommand
}
/*
Execute the debug command and return its result. It must be possible to
convert the output data into a JSON string.
*/
func (c *describeCommand) Run(debugger util.ECALDebugger, args []string) (interface{}, error) {
var res interface{}
if len(args) != 1 {
return nil, fmt.Errorf("Need a thread ID")
}
threadID, err := c.AssertNumParam(1, args[0])
if err == nil {
res = debugger.Describe(threadID)
}
return res, err
}
/*
DocString returns a descriptive text about this command.
*/
func (c *describeCommand) DocString() string {
return "Describes a suspended thread."
}
// status
// ======
/*
statusCommand shows breakpoints and suspended threads
*/
type statusCommand struct {
*inbuildDebugCommand
}
/*
Execute the debug command and return its result. It must be possible to
convert the output data into a JSON string.
*/
func (c *statusCommand) Run(debugger util.ECALDebugger, args []string) (interface{}, error) {
return debugger.Status(), nil
}
/*
DocString returns a descriptive text about this command.
*/
func (c *statusCommand) DocString() string {
return "Shows breakpoints and suspended threads."
}
// extract
// =======
/*
extractCommand copies a value from a suspended thread into the
global variable scope
*/
type extractCommand struct {
*inbuildDebugCommand
}
/*
Execute the debug command and return its result. It must be possible to
convert the output data into a JSON string.
*/
func (c *extractCommand) Run(debugger util.ECALDebugger, args []string) (interface{}, error) {
if len(args) != 3 {
return nil, fmt.Errorf("Need a thread ID, a variable name and a destination variable name")
}
threadID, err := c.AssertNumParam(1, args[0])
if err == nil {
if !parser.NamePattern.MatchString(args[1]) || !parser.NamePattern.MatchString(args[2]) {
err = fmt.Errorf("Variable names may only contain [a-zA-Z] and [a-zA-Z0-9] from the second character")
}
if err == nil {
err = debugger.ExtractValue(threadID, args[1], args[2])
}
}
return nil, err
}
/*
DocString returns a descriptive text about this command.
*/
func (c *extractCommand) DocString() string {
return "Copies a value from a suspended thread into the global variable scope."
}
// inject
// =======
/*
injectCommand copies a value from the global variable scope into
a suspended thread
*/
type injectCommand struct {
*inbuildDebugCommand
}
/*
Execute the debug command and return its result. It must be possible to
convert the output data into a JSON string.
*/
func (c *injectCommand) Run(debugger util.ECALDebugger, args []string) (interface{}, error) {
if len(args) < 3 {
return nil, fmt.Errorf("Need a thread ID, a variable name and an expression")
}
threadID, err := c.AssertNumParam(1, args[0])
if err == nil {
varName := args[1]
expression := strings.Join(args[2:], " ")
err = debugger.InjectValue(threadID, varName, expression)
}
return nil, err
}
/*
DocString returns a descriptive text about this command.
*/
func (c *injectCommand) DocString() string {
return "Copies a value from the global variable scope into a suspended thread."
}
// lockstate
// =========
/*
lockstateCommand inspects the locking state
*/
type lockstateCommand struct {
*inbuildDebugCommand
}
/*
Execute the debug command and return its result. It must be possible to
convert the output data into a JSON string.
*/
func (c *lockstateCommand) Run(debugger util.ECALDebugger, args []string) (interface{}, error) {
return debugger.LockState(), nil
}
/*
DocString returns a descriptive text about this command.
*/
func (c *lockstateCommand) DocString() string {
return "Inspects the locking state."
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package interpreter
import (
"fmt"
"math/rand"
"strconv"
"strings"
"time"
"devt.de/krotik/common/errorutil"
"devt.de/krotik/common/timeutil"
"devt.de/krotik/ecal/engine"
"devt.de/krotik/ecal/parser"
"devt.de/krotik/ecal/scope"
"devt.de/krotik/ecal/stdlib"
"devt.de/krotik/ecal/util"
)
/*
InbuildFuncMap contains the mapping of inbuild functions.
*/
var InbuildFuncMap = map[string]util.ECALFunction{
"range": &rangeFunc{&inbuildBaseFunc{}},
"new": &newFunc{&inbuildBaseFunc{}},
"type": &typeFunc{&inbuildBaseFunc{}},
"len": &lenFunc{&inbuildBaseFunc{}},
"del": &delFunc{&inbuildBaseFunc{}},
"add": &addFunc{&inbuildBaseFunc{}},
"concat": &concatFunc{&inbuildBaseFunc{}},
"now": &nowFunc{&inbuildBaseFunc{}},
"rand": &randFunc{&inbuildBaseFunc{}},
"timestamp": ×tampFunc{&inbuildBaseFunc{}},
"dumpenv": &dumpenvFunc{&inbuildBaseFunc{}},
"doc": &docFunc{&inbuildBaseFunc{}},
"sleep": &sleepFunc{&inbuildBaseFunc{}},
"raise": &raise{&inbuildBaseFunc{}},
"addEvent": &addevent{&inbuildBaseFunc{}},
"addEventAndWait": &addeventandwait{&addevent{&inbuildBaseFunc{}}},
"setCronTrigger": &setCronTrigger{&inbuildBaseFunc{}},
"setPulseTrigger": &setPulseTrigger{&inbuildBaseFunc{}},
}
/*
inbuildBaseFunc is the base structure for inbuild functions providing some
utility functions.
*/
type inbuildBaseFunc struct {
}
/*
AssertNumParam converts a general interface{} parameter into a number.
*/
func (ibf *inbuildBaseFunc) AssertNumParam(index int, val interface{}) (float64, error) {
var err error
resNum, ok := val.(float64)
if !ok {
resNum, err = strconv.ParseFloat(fmt.Sprint(val), 64)
if err != nil {
err = fmt.Errorf("Parameter %v should be a number", index)
}
}
return resNum, err
}
/*
AssertMapParam converts a general interface{} parameter into a map.
*/
func (ibf *inbuildBaseFunc) AssertMapParam(index int, val interface{}) (map[interface{}]interface{}, error) {
valMap, ok := val.(map[interface{}]interface{})
if ok {
return valMap, nil
}
return nil, fmt.Errorf("Parameter %v should be a map", index)
}
/*
AssertListParam converts a general interface{} parameter into a list.
*/
func (ibf *inbuildBaseFunc) AssertListParam(index int, val interface{}) ([]interface{}, error) {
valList, ok := val.([]interface{})
if ok {
return valList, nil
}
return nil, fmt.Errorf("Parameter %v should be a list", index)
}
// Range
// =====
/*
rangeFunc is an interator function which returns a range of numbers.
*/
type rangeFunc struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (rf *rangeFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
var currVal, to float64
var err error
lenargs := len(args)
from := 0.
step := 1.
if lenargs == 0 {
err = fmt.Errorf("Need at least an end range as first parameter")
}
if err == nil {
if stepVal, ok := is[instanceID+"step"]; ok {
step = stepVal.(float64)
from = is[instanceID+"from"].(float64)
to = is[instanceID+"to"].(float64)
currVal = is[instanceID+"currVal"].(float64)
is[instanceID+"currVal"] = currVal + step
// Check for end of iteration
if (from < to && currVal > to) || (from > to && currVal < to) || from == to {
err = util.ErrEndOfIteration
}
} else {
if lenargs == 1 {
to, err = rf.AssertNumParam(1, args[0])
} else {
from, err = rf.AssertNumParam(1, args[0])
if err == nil {
to, err = rf.AssertNumParam(2, args[1])
}
if err == nil && lenargs > 2 {
step, err = rf.AssertNumParam(3, args[2])
}
}
if err == nil {
is[instanceID+"from"] = from
is[instanceID+"to"] = to
is[instanceID+"step"] = step
is[instanceID+"currVal"] = from
currVal = from
}
}
}
if err == nil {
err = util.ErrIsIterator // Identify as iterator
}
return currVal, err
}
/*
DocString returns a descriptive string.
*/
func (rf *rangeFunc) DocString() (string, error) {
return "Iterates over number ranges. Parameters are start, end and step.", nil
}
// New
// ===
/*
newFunc instantiates a new object.
*/
type newFunc struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (rf *newFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
var res interface{}
err := fmt.Errorf("Need a map as first parameter")
if len(args) > 0 {
var argMap map[interface{}]interface{}
if argMap, err = rf.AssertMapParam(1, args[0]); err == nil {
obj := make(map[interface{}]interface{})
res = obj
_, err = rf.addSuperClasses(vs, is, obj, argMap)
if initObj, ok := obj["init"]; ok {
if initFunc, ok := initObj.(*function); ok {
initvs := scope.NewScope(fmt.Sprintf("newfunc: %v", instanceID))
initis := make(map[string]interface{})
_, err = initFunc.Run(instanceID, initvs, initis, tid, args[1:])
}
}
}
}
return res, err
}
/*
addSuperClasses adds super class functions to a given object.
*/
func (rf *newFunc) addSuperClasses(vs parser.Scope, is map[string]interface{},
obj map[interface{}]interface{}, template map[interface{}]interface{}) (interface{}, error) {
var err error
var initFunc interface{}
var initSuperList []interface{}
// First loop into the base classes (i.e. top-most classes)
if super, ok := template["super"]; ok {
if superList, ok := super.([]interface{}); ok {
for _, superObj := range superList {
var superInit interface{}
if superTemplate, ok := superObj.(map[interface{}]interface{}); ok {
superInit, err = rf.addSuperClasses(vs, is, obj, superTemplate)
initSuperList = append(initSuperList, superInit) // Build up the list of super functions
}
}
} else {
err = fmt.Errorf("Property _super must be a list of super classes")
}
}
// Copy all properties from template to obj
for k, v := range template {
// Save previous init function
if funcVal, ok := v.(*function); ok {
newFunction := &function{funcVal.name, nil, obj, funcVal.declaration, funcVal.declarationVS}
if k == "init" {
newFunction.super = initSuperList
initFunc = newFunction
}
obj[k] = newFunction
} else {
obj[k] = v
}
}
return initFunc, err
}
/*
DocString returns a descriptive string.
*/
func (rf *newFunc) DocString() (string, error) {
return "Creates a new object instance.", nil
}
// Type
// =====
/*
typeFunc returns the underlying types and values of an object.
*/
type typeFunc struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (rf *typeFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
var res interface{}
err := fmt.Errorf("Need a value as first parameter")
if len(args) > 0 {
res = fmt.Sprintf("%#v", args[0])
err = nil
}
return res, err
}
/*
DocString returns a descriptive string.
*/
func (rf *typeFunc) DocString() (string, error) {
return "Returns the underlying types and values of an object.", nil
}
// Len
// ===
/*
lenFunc returns the size of a list or map.
*/
type lenFunc struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (rf *lenFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
var res float64
err := fmt.Errorf("Need a list or a map as first parameter")
if len(args) > 0 {
argList, ok1 := args[0].([]interface{})
argMap, ok2 := args[0].(map[interface{}]interface{})
if ok1 {
res = float64(len(argList))
err = nil
} else if ok2 {
res = float64(len(argMap))
err = nil
}
}
return res, err
}
/*
DocString returns a descriptive string.
*/
func (rf *lenFunc) DocString() (string, error) {
return "Returns the size of a list or map.", nil
}
// Del
// ===
/*
delFunc removes an element from a list or map.
*/
type delFunc struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (rf *delFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
var res interface{}
err := fmt.Errorf("Need a list or a map as first parameter and an index or key as second parameter")
if len(args) == 2 {
if argList, ok1 := args[0].([]interface{}); ok1 {
var index float64
index, err = rf.AssertNumParam(2, args[1])
if err == nil {
res = append(argList[:int(index)], argList[int(index+1):]...)
}
}
if argMap, ok2 := args[0].(map[interface{}]interface{}); ok2 {
key := fmt.Sprint(args[1])
delete(argMap, key)
res = argMap
err = nil
}
}
return res, err
}
/*
DocString returns a descriptive string.
*/
func (rf *delFunc) DocString() (string, error) {
return "Removes an item from a list or map.", nil
}
// Add
// ===
/*
addFunc adds an element to a list.
*/
type addFunc struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (rf *addFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
var res interface{}
err := fmt.Errorf("Need a list as first parameter and a value as second parameter")
if len(args) > 1 {
var argList []interface{}
if argList, err = rf.AssertListParam(1, args[0]); err == nil {
if len(args) == 3 {
var index float64
if index, err = rf.AssertNumParam(3, args[2]); err == nil {
argList = append(argList, 0)
copy(argList[int(index+1):], argList[int(index):])
argList[int(index)] = args[1]
res = argList
}
} else {
res = append(argList, args[1])
}
}
}
return res, err
}
/*
DocString returns a descriptive string.
*/
func (rf *addFunc) DocString() (string, error) {
return "Adds an item to a list. The item is added at the optionally given index or at the end if no index is specified.", nil
}
// Concat
// ======
/*
concatFunc joins one or more lists together.
*/
type concatFunc struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (rf *concatFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
var res interface{}
err := fmt.Errorf("Need at least two lists as parameters")
if len(args) > 1 {
var argList []interface{}
resList := make([]interface{}, 0)
err = nil
for _, a := range args {
if err == nil {
if argList, err = rf.AssertListParam(1, a); err == nil {
resList = append(resList, argList...)
}
}
}
if err == nil {
res = resList
}
}
return res, err
}
/*
DocString returns a descriptive string.
*/
func (rf *concatFunc) DocString() (string, error) {
return "Joins one or more lists together. The result is a new list.", nil
}
// dumpenv
// =======
/*
dumpenvFunc returns the current variable environment as a string.
*/
type dumpenvFunc struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (rf *dumpenvFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
return vs.String(), nil
}
/*
DocString returns a descriptive string.
*/
func (rf *dumpenvFunc) DocString() (string, error) {
return "Returns the current variable environment as a string.", nil
}
// now
// ===
/*
nowFunc returns the current time in microseconds from 1st of January 1970 UTC.
*/
type nowFunc struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (rf *nowFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
t := time.Now().UnixNano() / int64(time.Microsecond)
return float64(t), nil
}
/*
DocString returns a descriptive string.
*/
func (rf *nowFunc) DocString() (string, error) {
return "Returns the current time in microseconds from 1st of January 1970 UTC.", nil
}
// rand
// ====
/*
randFunc returns a pseudo-random number between 0 and 1 from the default source.
*/
type randFunc struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (rf *randFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
return rand.Float64(), nil
}
/*
DocString returns a descriptive string.
*/
func (rf *randFunc) DocString() (string, error) {
return "Returns a pseudo-random number between 0 and 1 from the default source.", nil
}
// timestamp
// ===
/*
timestampFunc returns a human readable time stamp string from a given number of microseconds since posix epoch time.
*/
type timestampFunc struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (rf *timestampFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
var ret string
var err error
micros := float64(time.Now().UnixNano() / int64(time.Microsecond))
loc := "UTC"
if len(args) > 0 {
micros, err = rf.AssertNumParam(1, args[0])
if len(args) > 1 {
loc = fmt.Sprint(args[1])
}
}
if err == nil {
var l *time.Location
tsTime := time.Unix(0, int64(micros*1000))
if l, err = time.LoadLocation(loc); err == nil {
ret = tsTime.In(l).Format("2006-01-02T15:04:05.999999Z07:MST")
}
}
return ret, err
}
/*
DocString returns a descriptive string.
*/
func (rf *timestampFunc) DocString() (string, error) {
return "Returns a human readable time stamp string from a given number of microseconds since posix epoch time.", nil
}
// doc
// ===
/*
docFunc returns the docstring of a function.
*/
type docFunc struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (rf *docFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
var res interface{}
err := fmt.Errorf("Need a function as parameter")
if len(args) > 0 {
funcObj, ok := args[0].(util.ECALFunction)
if args[0] == nil {
// Try to lookup by the given identifier
c := is["astnode"].(*parser.ASTNode).Children[0].Children[0]
astring := c.Token.Val
if len(c.Children) > 0 {
astring = fmt.Sprintf("%v.%v", astring, c.Children[0].Token.Val)
}
// Check for stdlib function
if funcObj, ok = stdlib.GetStdlibFunc(astring); !ok {
// Check for inbuild function
funcObj, ok = InbuildFuncMap[astring]
}
}
if ok {
res, err = funcObj.DocString()
}
}
return res, err
}
/*
DocString returns a descriptive string.
*/
func (rf *docFunc) DocString() (string, error) {
return "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{}, tid uint64, 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 "Pauses the current thread for a number of micro seconds.", nil
}
// raise
// =====
/*
raise returns an error. Outside of sinks this will stop the code execution
if the error is not handled by try / except. Inside a sink only the specific sink
will fail. This error can be used to break trigger sequences of sinks if
FailOnFirstErrorInTriggerSequence is set.
*/
type raise struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (rf *raise) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
var err error
var detailMsg string
var detail interface{}
if len(args) > 0 {
err = fmt.Errorf("%v", args[0])
if len(args) > 1 {
if args[1] != nil {
detailMsg = fmt.Sprint(args[1])
}
if len(args) > 2 {
detail = args[2]
}
}
}
erp := is["erp"].(*ECALRuntimeProvider)
node := is["astnode"].(*parser.ASTNode)
return nil, &util.RuntimeErrorWithDetail{
RuntimeError: erp.NewRuntimeError(err, detailMsg, node).(*util.RuntimeError),
Environment: vs,
Data: detail,
}
}
/*
DocString returns a descriptive string.
*/
func (rf *raise) DocString() (string, error) {
return "Raise an error which stops the execution unless it is handled by a try/except block.", nil
}
// addEvent
// ========
/*
addevent adds an event to trigger sinks. This function will return immediately
and not wait for the event cascade to finish. Use this function for event cascades.
*/
type addevent struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (rf *addevent) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
return rf.addEvent(func(proc engine.Processor, event *engine.Event, scope *engine.RuleScope) (interface{}, error) {
var monitor engine.Monitor
parentMonitor, ok := is["monitor"]
if scope != nil || !ok {
monitor = proc.NewRootMonitor(nil, scope)
} else {
monitor = parentMonitor.(engine.Monitor).NewChildMonitor(0)
}
_, err := proc.AddEvent(event, monitor)
return nil, err
}, is, args)
}
func (rf *addevent) addEvent(addFunc func(engine.Processor, *engine.Event, *engine.RuleScope) (interface{}, error),
is map[string]interface{}, args []interface{}) (interface{}, error) {
var res interface{}
var stateMap map[interface{}]interface{}
erp := is["erp"].(*ECALRuntimeProvider)
proc := erp.Processor
if proc.Stopped() {
proc.Start()
}
err := fmt.Errorf("Need at least three parameters: name, kind and state")
if len(args) > 2 {
if stateMap, err = rf.AssertMapParam(3, args[2]); err == nil {
var scope *engine.RuleScope
event := engine.NewEvent(
fmt.Sprint(args[0]),
strings.Split(fmt.Sprint(args[1]), "."),
stateMap,
)
if len(args) > 3 {
var scopeMap map[interface{}]interface{}
// Add optional scope - if not specified it is { "": true }
if scopeMap, err = rf.AssertMapParam(4, args[3]); err == nil {
var scopeData = map[string]bool{}
for k, v := range scopeMap {
b, _ := strconv.ParseBool(fmt.Sprint(v))
scopeData[fmt.Sprint(k)] = b
}
scope = engine.NewRuleScope(scopeData)
}
}
if err == nil {
res, err = addFunc(proc, event, scope)
}
}
}
return res, err
}
/*
DocString returns a descriptive string.
*/
func (rf *addevent) DocString() (string, error) {
return "Adds an event to trigger sinks. This function will return " +
"immediately and not wait for the event cascade to finish.", nil
}
// addEventAndWait
// ===============
/*
addeventandwait adds an event to trigger sinks. This function will return once
the event cascade has finished and return all errors.
*/
type addeventandwait struct {
*addevent
}
/*
Run executes this function.
*/
func (rf *addeventandwait) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
return rf.addEvent(func(proc engine.Processor, event *engine.Event, scope *engine.RuleScope) (interface{}, error) {
var res []interface{}
rm := proc.NewRootMonitor(nil, scope)
m, err := proc.AddEventAndWait(event, rm)
if m != nil {
allErrors := m.(*engine.RootMonitor).AllErrors()
for _, e := range allErrors {
errors := map[interface{}]interface{}{}
for k, v := range e.ErrorMap {
// Note: The variable scope of the sink (se.environment)
// was also captured - for now it is not exposed to the
// language environment
errorItem := map[interface{}]interface{}{
"error": v.Error(),
}
if se, ok := v.(*util.RuntimeErrorWithDetail); ok {
errorItem["type"] = se.Type.Error()
errorItem["detail"] = se.Detail
errorItem["data"] = se.Data
}
errors[k] = errorItem
}
item := map[interface{}]interface{}{
"event": map[interface{}]interface{}{
"name": e.Event.Name(),
"kind": strings.Join(e.Event.Kind(), "."),
"state": e.Event.State(),
},
"errors": errors,
}
res = append(res, item)
}
}
return res, err
}, is, args)
}
/*
DocString returns a descriptive string.
*/
func (rf *addeventandwait) DocString() (string, error) {
return "Adds an event to trigger sinks. This function will " +
"return once the event cascade has finished.", nil
}
// setCronTrigger
// ==============
/*
setCronTrigger adds a periodic cron job which fires events.
*/
type setCronTrigger struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (ct *setCronTrigger) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
var res interface{}
err := fmt.Errorf("Need a cronspec, an event name and an event scope as parameters")
if len(args) > 2 {
var cs *timeutil.CronSpec
cronspec := fmt.Sprint(args[0])
eventname := fmt.Sprint(args[1])
eventkind := strings.Split(fmt.Sprint(args[2]), ".")
erp := is["erp"].(*ECALRuntimeProvider)
proc := erp.Processor
if proc.Stopped() {
proc.Start()
}
if cs, err = timeutil.NewCronSpec(cronspec); err == nil {
res = cs.String()
tick := 0
erp.Cron.RegisterSpec(cs, func() {
tick++
now := erp.Cron.NowFunc()
event := engine.NewEvent(eventname, eventkind, map[interface{}]interface{}{
"time": now,
"timestamp": fmt.Sprintf("%d", now.UnixNano()/int64(time.Millisecond)),
"tick": float64(tick),
})
monitor := proc.NewRootMonitor(nil, nil)
_, err := proc.AddEvent(event, monitor)
if status := proc.Status(); status != "Stopped" && status != "Stopping" {
errorutil.AssertTrue(err == nil,
fmt.Sprintf("Could not add cron event for trigger %v %v %v: %v",
cronspec, eventname, eventkind, err))
}
})
}
}
return res, err
}
/*
DocString returns a descriptive string.
*/
func (ct *setCronTrigger) DocString() (string, error) {
return "Adds a periodic cron job which fires events.", nil
}
// setPulseTrigger
// ==============
/*
setPulseTrigger adds recurring events in very short intervals.
*/
type setPulseTrigger struct {
*inbuildBaseFunc
}
/*
Run executes this function.
*/
func (pt *setPulseTrigger) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
err := fmt.Errorf("Need micro second interval, an event name and an event scope as parameters")
if len(args) > 2 {
var micros float64
micros, err = pt.AssertNumParam(1, args[0])
if err == nil {
eventname := fmt.Sprint(args[1])
eventkind := strings.Split(fmt.Sprint(args[2]), ".")
erp := is["erp"].(*ECALRuntimeProvider)
proc := erp.Processor
if proc.Stopped() {
proc.Start()
}
tick := 0
go func() {
var lastmicros int64
for {
time.Sleep(time.Duration(micros) * time.Microsecond)
tick++
now := time.Now()
micros := now.UnixNano() / int64(time.Microsecond)
event := engine.NewEvent(eventname, eventkind, map[interface{}]interface{}{
"currentMicros": float64(micros),
"lastMicros": float64(lastmicros),
"timestamp": fmt.Sprintf("%d", now.UnixNano()/int64(time.Microsecond)),
"tick": float64(tick),
})
lastmicros = micros
monitor := proc.NewRootMonitor(nil, nil)
_, err := proc.AddEventAndWait(event, monitor)
if status := proc.Status(); status == "Stopped" || status == "Stopping" {
break
}
errorutil.AssertTrue(err == nil,
fmt.Sprintf("Could not add pulse event for trigger %v %v %v: %v",
micros, eventname, eventkind, err))
}
}()
}
}
return nil, err
}
/*
DocString returns a descriptive string.
*/
func (pt *setPulseTrigger) DocString() (string, error) {
return "Adds recurring events in microsecond intervals.", nil
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package interpreter
import (
"fmt"
"os"
"path/filepath"
"sync"
"devt.de/krotik/common/datautil"
"devt.de/krotik/common/timeutil"
"devt.de/krotik/ecal/config"
"devt.de/krotik/ecal/engine"
"devt.de/krotik/ecal/parser"
"devt.de/krotik/ecal/util"
)
/*
ecalRuntimeNew is used to instantiate ECAL runtime components.
*/
type ecalRuntimeNew func(*ECALRuntimeProvider, *parser.ASTNode) parser.Runtime
/*
providerMap contains the mapping of AST nodes to runtime components for ECAL ASTs.
*/
var providerMap = map[string]ecalRuntimeNew{
parser.NodeEOF: invalidRuntimeInst,
parser.NodeSTRING: stringValueRuntimeInst, // String constant
parser.NodeNUMBER: numberValueRuntimeInst, // Number constant
parser.NodeIDENTIFIER: identifierRuntimeInst, // Idendifier
// Constructed tokens
parser.NodeSTATEMENTS: statementsRuntimeInst, // List of statements
parser.NodeFUNCCALL: voidRuntimeInst, // Function call
parser.NodeCOMPACCESS: voidRuntimeInst, // Composition structure access
parser.NodeLIST: listValueRuntimeInst, // List value
parser.NodeMAP: mapValueRuntimeInst, // Map value
parser.NodePARAMS: voidRuntimeInst, // Function parameters
parser.NodeGUARD: guardRuntimeInst, // Guard expressions for conditional statements
// Condition operators
parser.NodeGEQ: greaterequalOpRuntimeInst,
parser.NodeLEQ: lessequalOpRuntimeInst,
parser.NodeNEQ: notequalOpRuntimeInst,
parser.NodeEQ: equalOpRuntimeInst,
parser.NodeGT: greaterOpRuntimeInst,
parser.NodeLT: lessOpRuntimeInst,
// Separators
parser.NodeKVP: voidRuntimeInst, // Key-value pair
parser.NodePRESET: voidRuntimeInst, // Preset value
// Arithmetic operators
parser.NodePLUS: plusOpRuntimeInst,
parser.NodeMINUS: minusOpRuntimeInst,
parser.NodeTIMES: timesOpRuntimeInst,
parser.NodeDIV: divOpRuntimeInst,
parser.NodeMODINT: modintOpRuntimeInst,
parser.NodeDIVINT: divintOpRuntimeInst,
// Assignment statement
parser.NodeASSIGN: assignmentRuntimeInst,
parser.NodeLET: letRuntimeInst,
// Import statement
parser.NodeIMPORT: importRuntimeInst,
parser.NodeAS: voidRuntimeInst,
// Sink definition
parser.NodeSINK: sinkRuntimeInst,
parser.NodeKINDMATCH: kindMatchRuntimeInst,
parser.NodeSCOPEMATCH: scopeMatchRuntimeInst,
parser.NodeSTATEMATCH: stateMatchRuntimeInst,
parser.NodePRIORITY: priorityRuntimeInst,
parser.NodeSUPPRESSES: suppressesRuntimeInst,
// Function definition
parser.NodeFUNC: funcRuntimeInst,
parser.NodeRETURN: returnRuntimeInst,
// Boolean operators
parser.NodeOR: orOpRuntimeInst,
parser.NodeAND: andOpRuntimeInst,
parser.NodeNOT: notOpRuntimeInst,
// Condition operators
parser.NodeLIKE: likeOpRuntimeInst,
parser.NodeIN: inOpRuntimeInst,
parser.NodeHASPREFIX: beginswithOpRuntimeInst,
parser.NodeHASSUFFIX: endswithOpRuntimeInst,
parser.NodeNOTIN: notinOpRuntimeInst,
// Constant terminals
parser.NodeFALSE: falseRuntimeInst,
parser.NodeTRUE: trueRuntimeInst,
parser.NodeNULL: nullRuntimeInst,
// Conditional statements
parser.NodeIF: ifRuntimeInst,
// Loop statements
parser.NodeLOOP: loopRuntimeInst,
parser.NodeBREAK: breakRuntimeInst,
parser.NodeCONTINUE: continueRuntimeInst,
// Try statement
parser.NodeTRY: tryRuntimeInst,
parser.NodeEXCEPT: voidRuntimeInst,
parser.NodeOTHERWISE: voidRuntimeInst,
parser.NodeFINALLY: voidRuntimeInst,
// Mutex block
parser.NodeMUTEX: mutexRuntimeInst,
}
/*
ECALRuntimeProvider is the factory object producing runtime objects for ECAL ASTs.
*/
type ECALRuntimeProvider struct {
Name string // Name to identify the input
ImportLocator util.ECALImportLocator // Locator object for imports
Logger util.Logger // Logger object for log messages
Processor engine.Processor // Processor of the ECA engine
Mutexes map[string]*sync.Mutex // Map of named mutexes
MutexLog *datautil.RingBuffer // Ringbuffer to track locking events
MutexeOwners map[string]uint64 // Map of mutex owners
MutexesMutex *sync.Mutex // Mutex for mutexes map
Cron *timeutil.Cron // Cron object for scheduled execution
Debugger util.ECALDebugger // Optional: ECAL Debugger object
}
/*
NewECALRuntimeProvider returns a new instance of a ECAL runtime provider.
*/
func NewECALRuntimeProvider(name string, importLocator util.ECALImportLocator, logger util.Logger) *ECALRuntimeProvider {
if importLocator == nil {
// By default imports are located in the current directory
importLocator = &util.FileImportLocator{Root: filepath.Dir(os.Args[0])}
}
if logger == nil {
// By default we just have a memory logger
logger = util.NewMemoryLogger(100)
}
proc := engine.NewProcessor(config.Int(config.WorkerCount))
// By default ECAL should stop the triggering sequence of sinks after the
// first sink that returns a sinkerror.
proc.SetFailOnFirstErrorInTriggerSequence(true)
cron := timeutil.NewCron()
cron.Start()
return &ECALRuntimeProvider{name, importLocator, logger, proc,
make(map[string]*sync.Mutex), datautil.NewRingBuffer(1024), make(map[string]uint64), &sync.Mutex{}, cron, nil}
}
/*
Runtime returns a runtime component for a given ASTNode.
*/
func (erp *ECALRuntimeProvider) Runtime(node *parser.ASTNode) parser.Runtime {
if instFunc, ok := providerMap[node.Name]; ok {
return instFunc(erp, node)
}
return invalidRuntimeInst(erp, node)
}
/*
NewRuntimeError creates a new RuntimeError object.
*/
func (erp *ECALRuntimeProvider) NewRuntimeError(t error, d string, node *parser.ASTNode) error {
source := erp.Name
if node.Token != nil {
source = fmt.Sprintf("%v (%v)", source, node.Token.Lsource)
}
return util.NewRuntimeError(source, t, d, node)
}
/*
NewThreadID creates a new thread ID unique to this runtime provider instance.
This ID can be safely used for the thread ID when calling Eval on a
parser.Runtime instance.
*/
func (erp *ECALRuntimeProvider) NewThreadID() uint64 {
return erp.Processor.ThreadPool().NewThreadID()
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package interpreter
import (
"math"
"devt.de/krotik/ecal/parser"
)
// Basic Arithmetic Operator Runtimes
// ==================================
type plusOpRuntime struct {
*operatorRuntime
}
/*
plusOpRuntimeInst returns a new runtime component instance.
*/
func plusOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &plusOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *plusOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
// Use as prefix
if len(rt.node.Children) == 1 {
return rt.numVal(func(n float64) interface{} {
return n
}, vs, is, tid)
}
// Use as operation
res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
return n1 + n2
}, vs, is, tid)
}
return res, err
}
type minusOpRuntime struct {
*operatorRuntime
}
/*
minusOpRuntimeInst returns a new runtime component instance.
*/
func minusOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &minusOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *minusOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
// Use as prefix
if len(rt.node.Children) == 1 {
return rt.numVal(func(n float64) interface{} {
return -n
}, vs, is, tid)
}
// Use as operation
res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
return n1 - n2
}, vs, is, tid)
}
return res, err
}
type timesOpRuntime struct {
*operatorRuntime
}
/*
timesOpRuntimeInst returns a new runtime component instance.
*/
func timesOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return ×OpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *timesOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
return n1 * n2
}, vs, is, tid)
}
return res, err
}
type divOpRuntime struct {
*operatorRuntime
}
/*
divOpRuntimeInst returns a new runtime component instance.
*/
func divOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &divOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *divOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
return n1 / n2
}, vs, is, tid)
}
return res, err
}
type divintOpRuntime struct {
*operatorRuntime
}
/*
divintOpRuntimeInst returns a new runtime component instance.
*/
func divintOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &divintOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *divintOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
return math.Floor(n1 / n2)
}, vs, is, tid)
}
return res, err
}
type modintOpRuntime struct {
*operatorRuntime
}
/*
divOpRuntimeInst returns a new runtime component instance.
*/
func modintOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &modintOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *modintOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
return float64(int64(n1) % int64(n2))
}, vs, is, tid)
}
return res, err
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package interpreter
import (
"fmt"
"devt.de/krotik/ecal/parser"
"devt.de/krotik/ecal/util"
)
/*
assignmentRuntime is the runtime component for assignment of values.
*/
type assignmentRuntime struct {
*baseRuntime
leftSide []*identifierRuntime
}
/*
assignmentRuntimeInst returns a new runtime component instance.
*/
func assignmentRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &assignmentRuntime{newBaseRuntime(erp, node), nil}
}
/*
Validate this node and all its child nodes.
*/
func (rt *assignmentRuntime) Validate() error {
err := rt.baseRuntime.Validate()
if err == nil {
leftVar := rt.node.Children[0]
if _, ok := leftVar.Runtime.(*letRuntime); ok {
leftVar = leftVar.Children[0]
}
if leftRuntime, ok := leftVar.Runtime.(*identifierRuntime); ok {
rt.leftSide = []*identifierRuntime{leftRuntime}
} else if leftVar.Name == parser.NodeLIST {
rt.leftSide = make([]*identifierRuntime, 0, len(leftVar.Children))
for _, child := range leftVar.Children {
childRuntime, ok := child.Runtime.(*identifierRuntime)
if !ok {
err = rt.erp.NewRuntimeError(util.ErrVarAccess,
"Must have a list of variables on the left side of the assignment", rt.node)
break
}
rt.leftSide = append(rt.leftSide, childRuntime)
}
} else {
err = rt.erp.NewRuntimeError(util.ErrVarAccess,
"Must have a variable or list of variables on the left side of the assignment", rt.node)
}
}
return err
}
/*
Eval evaluate this runtime component.
*/
func (rt *assignmentRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
// Execute let statements on the right before evaluating the left side
if _, err = rt.node.Children[0].Runtime.Eval(vs, is, tid); err == nil {
var val interface{}
val, err = rt.node.Children[1].Runtime.Eval(vs, is, tid)
if err == nil {
if len(rt.leftSide) == 1 {
err = rt.leftSide[0].Set(vs, is, tid, val)
} else if valList, ok := val.([]interface{}); ok {
if len(rt.leftSide) != len(valList) {
err = rt.erp.NewRuntimeError(util.ErrInvalidState,
fmt.Sprintf("Assigned number of variables is different to "+
"number of values (%v variables vs %v values)",
len(rt.leftSide), len(valList)), rt.node)
} else {
for i, v := range rt.leftSide {
if err = v.Set(vs, is, tid, valList[i]); err != nil {
err = rt.erp.NewRuntimeError(util.ErrVarAccess,
err.Error(), rt.node)
break
}
}
}
} else {
err = rt.erp.NewRuntimeError(util.ErrInvalidState,
fmt.Sprintf("Result is not a list (value is %v)", val),
rt.node)
}
}
}
}
return nil, err
}
/*
letRuntime is the runtime component for let statements
*/
type letRuntime struct {
*baseRuntime
declared []*identifierRuntime
}
/*
letRuntimeInst returns a new runtime component instance.
*/
func letRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &letRuntime{newBaseRuntime(erp, node), nil}
}
/*
Validate this node and all its child nodes.
*/
func (rt *letRuntime) Validate() error {
err := rt.baseRuntime.Validate()
if err == nil {
leftVar := rt.node.Children[0]
if leftRuntime, ok := leftVar.Runtime.(*identifierRuntime); ok {
rt.declared = []*identifierRuntime{leftRuntime}
} else if leftVar.Name == parser.NodeLIST {
rt.declared = make([]*identifierRuntime, 0, len(leftVar.Children))
for _, child := range leftVar.Children {
childRuntime, ok := child.Runtime.(*identifierRuntime)
if !ok {
err = rt.erp.NewRuntimeError(util.ErrInvalidConstruct,
"Let can only declare variables within a list", rt.node)
break
}
rt.declared = append(rt.declared, childRuntime)
}
} else {
err = rt.erp.NewRuntimeError(util.ErrInvalidConstruct,
"Let must declare a variable or list of variables", rt.node)
}
}
return err
}
/*
Eval evaluate this runtime component.
*/
func (rt *letRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
for _, v := range rt.declared {
if len(v.node.Children) == 0 {
vs.SetLocalValue(v.node.Token.Val, nil)
} else {
err = rt.erp.NewRuntimeError(util.ErrInvalidConstruct,
"Let can only declare simple variables", rt.node)
break
}
}
if err == nil {
res, err = rt.node.Children[0].Runtime.Eval(vs, is, tid)
}
}
return res, err
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package interpreter
import (
"fmt"
"regexp"
"strings"
"devt.de/krotik/common/errorutil"
"devt.de/krotik/ecal/parser"
)
// Basic Boolean Operator Runtimes
// ===============================
type greaterequalOpRuntime struct {
*operatorRuntime
}
/*
greaterequalOpRuntimeInst returns a new runtime component instance.
*/
func greaterequalOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &greaterequalOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *greaterequalOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
return n1 >= n2
}, vs, is, tid)
if err != nil {
res, err = rt.strOp(func(n1 string, n2 string) interface{} {
return n1 >= n2
}, vs, is, tid)
}
}
return res, err
}
type greaterOpRuntime struct {
*operatorRuntime
}
/*
greaterOpRuntimeInst returns a new runtime component instance.
*/
func greaterOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &greaterOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *greaterOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
return n1 > n2
}, vs, is, tid)
if err != nil {
res, err = rt.strOp(func(n1 string, n2 string) interface{} {
return n1 > n2
}, vs, is, tid)
}
}
return res, err
}
type lessequalOpRuntime struct {
*operatorRuntime
}
/*
lessequalOpRuntimeInst returns a new runtime component instance.
*/
func lessequalOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &lessequalOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *lessequalOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
return n1 <= n2
}, vs, is, tid)
if err != nil {
res, err = rt.strOp(func(n1 string, n2 string) interface{} {
return n1 <= n2
}, vs, is, tid)
}
}
return res, err
}
type lessOpRuntime struct {
*operatorRuntime
}
/*
lessOpRuntimeInst returns a new runtime component instance.
*/
func lessOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &lessOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *lessOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.numOp(func(n1 float64, n2 float64) interface{} {
return n1 < n2
}, vs, is, tid)
if err != nil {
res, err = rt.strOp(func(n1 string, n2 string) interface{} {
return n1 < n2
}, vs, is, tid)
}
}
return res, err
}
type equalOpRuntime struct {
*operatorRuntime
}
/*
equalOpRuntimeInst returns a new runtime component instance.
*/
func equalOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &equalOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *equalOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.genOp(func(n1 interface{}, n2 interface{}) interface{} {
return n1 == n2
}, vs, is, tid)
}
return res, err
}
type notequalOpRuntime struct {
*operatorRuntime
}
/*
notequalOpRuntimeInst returns a new runtime component instance.
*/
func notequalOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return ¬equalOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *notequalOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.genOp(func(n1 interface{}, n2 interface{}) interface{} {
return n1 != n2
}, vs, is, tid)
}
return res, err
}
type andOpRuntime struct {
*operatorRuntime
}
/*
andOpRuntimeInst returns a new runtime component instance.
*/
func andOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &andOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *andOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.boolOp(func(b1 bool, b2 bool) interface{} {
return b1 && b2
}, vs, is, tid)
}
return res, err
}
type orOpRuntime struct {
*operatorRuntime
}
/*
orOpRuntimeInst returns a new runtime component instance.
*/
func orOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &orOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *orOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.boolOp(func(b1 bool, b2 bool) interface{} {
return b1 || b2
}, vs, is, tid)
}
return res, err
}
type notOpRuntime struct {
*operatorRuntime
}
/*
notOpRuntimeInst returns a new runtime component instance.
*/
func notOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return ¬OpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *notOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.boolVal(func(b bool) interface{} {
return !b
}, vs, is, tid)
}
return res, err
}
// In-build condition operators
// ============================
/*
likeOpRuntime is the pattern matching operator. The syntax of the regular
expressions accepted is the same general syntax used by Go, Perl, Python, and
other languages.
*/
type likeOpRuntime struct {
*operatorRuntime
}
/*
likeOpRuntimeInst returns a new runtime component instance.
*/
func likeOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &likeOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *likeOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
errorutil.AssertTrue(len(rt.node.Children) == 2,
fmt.Sprint("Operation requires 2 operands", rt.node))
str, err := rt.node.Children[0].Runtime.Eval(vs, is, tid)
if err == nil {
var pattern interface{}
pattern, err = rt.node.Children[1].Runtime.Eval(vs, is, tid)
if err == nil {
var re *regexp.Regexp
re, err = regexp.Compile(fmt.Sprint(pattern))
if err == nil {
res = re.MatchString(fmt.Sprint(str))
}
}
}
}
return res, err
}
type beginswithOpRuntime struct {
*operatorRuntime
}
/*
beginswithOpRuntimeInst returns a new runtime component instance.
*/
func beginswithOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &beginswithOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *beginswithOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.strOp(func(s1 string, s2 string) interface{} {
return strings.HasPrefix(s1, s2)
}, vs, is, tid)
}
return res, err
}
type endswithOpRuntime struct {
*operatorRuntime
}
/*
endswithOpRuntimeInst returns a new runtime component instance.
*/
func endswithOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &endswithOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *endswithOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.strOp(func(s1 string, s2 string) interface{} {
return strings.HasSuffix(s1, s2)
}, vs, is, tid)
}
return res, err
}
type inOpRuntime struct {
*operatorRuntime
}
/*
inOpRuntimeInst returns a new runtime component instance.
*/
func inOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &inOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *inOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.listOp(func(val interface{}, list []interface{}) interface{} {
for _, i := range list {
if val == i {
return true
}
}
return false
}, vs, is, tid)
}
return res, err
}
type notinOpRuntime struct {
*inOpRuntime
}
/*
notinOpRuntimeInst returns a new runtime component instance.
*/
func notinOpRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return ¬inOpRuntime{&inOpRuntime{&operatorRuntime{newBaseRuntime(erp, node)}}}
}
/*
Eval evaluate this runtime component.
*/
func (rt *notinOpRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
if res, err = rt.inOpRuntime.Eval(vs, is, tid); err == nil {
res = !res.(bool)
}
}
return res, err
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package interpreter
import "devt.de/krotik/ecal/parser"
/*
trueRuntime is the runtime component for the true constant.
*/
type trueRuntime struct {
*baseRuntime
}
/*
trueRuntimeInst returns a new runtime component instance.
*/
func trueRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &trueRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *trueRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
return true, err
}
/*
falseRuntime is the runtime component for the false constant.
*/
type falseRuntime struct {
*baseRuntime
}
/*
falseRuntimeInst returns a new runtime component instance.
*/
func falseRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &falseRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *falseRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
return false, err
}
/*
nullRuntime is the runtime component for the null constant.
*/
type nullRuntime struct {
*baseRuntime
}
/*
nullRuntimeInst returns a new runtime component instance.
*/
func nullRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &nullRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *nullRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
return nil, err
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package interpreter
import (
"encoding/json"
"fmt"
"strings"
"devt.de/krotik/ecal/parser"
"devt.de/krotik/ecal/scope"
"devt.de/krotik/ecal/util"
)
/*
returnRuntime is a special runtime for return statements in functions.
*/
type returnRuntime struct {
*baseRuntime
}
/*
voidRuntimeInst returns a new runtime component instance.
*/
func returnRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &returnRuntime{newBaseRuntime(erp, node)}
}
/*
Validate this node and all its child nodes.
*/
func (rt *returnRuntime) Validate() error {
return rt.baseRuntime.Validate()
}
/*
Eval evaluate this runtime component.
*/
func (rt *returnRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
var res interface{}
if len(rt.node.Children) > 0 {
res, err = rt.node.Children[0].Runtime.Eval(vs, is, tid)
} else {
res = nil
}
if err == nil {
rerr := rt.erp.NewRuntimeError(util.ErrReturn, fmt.Sprintf("Return value: %v", res), rt.node)
err = &returnValue{
rerr.(*util.RuntimeError),
res,
}
}
}
return nil, err
}
type returnValue struct {
*util.RuntimeError
returnValue interface{}
}
/*
funcRuntime is the runtime component for function declarations.
*/
type funcRuntime struct {
*baseRuntime
}
/*
funcRuntimeInst returns a new runtime component instance.
*/
func funcRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &funcRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *funcRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var fc interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
name := ""
if rt.node.Children[0].Name == parser.NodeIDENTIFIER {
name = rt.node.Children[0].Token.Val
}
fc = &function{name, nil, nil, rt.node, vs}
if name != "" {
vs.SetValue(name, fc)
}
}
return fc, err
}
/*
function models a function in ECAL. It can have a context object attached - this.
*/
type function struct {
name string
super []interface{} // Super function pointer
this interface{} // Function context
declaration *parser.ASTNode // Function declaration node
declarationVS parser.Scope // Function declaration scope
}
/*
Run executes this function. The function is called with parameters and might also
have a reference to a context state - this.
*/
func (f *function) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {
var res interface{}
var err error
nameOffset := 0
if f.declaration.Children[0].Name == parser.NodeIDENTIFIER {
nameOffset = 1
}
params := f.declaration.Children[0+nameOffset].Children
body := f.declaration.Children[1+nameOffset]
// Create varscope for the body - not a child scope but a new root
fvs := scope.NewScope(fmt.Sprintf("%v %v", scope.FuncPrefix, f.name))
if f.this != nil {
fvs.SetValue("this", f.this)
}
if f.super != nil {
fvs.SetValue("super", f.super)
}
for i, p := range params {
var name string
var val interface{}
if err == nil {
name = ""
if p.Name == parser.NodeIDENTIFIER {
name = p.Token.Val
if i < len(args) {
val = args[i]
}
} else if p.Name == parser.NodePRESET {
name = p.Children[0].Token.Val
if i < len(args) {
val = args[i]
} else {
val, err = p.Children[1].Runtime.Eval(vs, is, tid)
}
}
if name != "" {
fvs.SetValue(name, val)
}
}
}
if err == nil {
scope.SetParentOfScope(fvs, f.declarationVS)
res, err = body.Runtime.Eval(fvs, make(map[string]interface{}), tid)
// Check for return value (delivered as error object)
if rval, ok := err.(*returnValue); ok {
res = rval.returnValue
err = nil
}
}
return res, err
}
/*
DocString returns a descriptive string.
*/
func (f *function) DocString() (string, error) {
if len(f.declaration.Meta) > 0 {
return strings.TrimSpace(f.declaration.Meta[0].Value()), nil
}
return fmt.Sprintf("Declared function: %v (%v)", f.name, f.declaration.Token.PosString()), nil
}
/*
String returns a string representation of this function.
*/
func (f *function) String() string {
return fmt.Sprintf("ecal.function: %v (%v)", f.name, f.declaration.Token.PosString())
}
/*
MarshalJSON returns a string representation of this function - a function cannot
be JSON encoded.
*/
func (f *function) MarshalJSON() ([]byte, error) {
return json.Marshal(f.String())
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package interpreter
import (
"fmt"
"devt.de/krotik/common/errorutil"
"devt.de/krotik/ecal/parser"
"devt.de/krotik/ecal/scope"
"devt.de/krotik/ecal/util"
)
// Base Runtime
// ============
/*
baseRuntime models a base runtime component which provides the essential fields and functions.
*/
type baseRuntime struct {
instanceID string // Unique identifier (should be used when instance state is stored)
erp *ECALRuntimeProvider // Runtime provider
node *parser.ASTNode // AST node which this runtime component is servicing
validated bool
}
var instanceCounter uint64 // Global instance counter to create unique identifiers for every runtime component instance
/*
Validate this node and all its child nodes.
*/
func (rt *baseRuntime) Validate() error {
rt.validated = true
// Validate all children
for _, child := range rt.node.Children {
if err := child.Runtime.Validate(); err != nil {
return err
}
}
return nil
}
/*
Eval evaluate this runtime component.
*/
func (rt *baseRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var err error
errorutil.AssertTrue(rt.validated, "Runtime component has not been validated - please call Validate() before Eval()")
if rt.erp.Debugger != nil {
err = rt.erp.Debugger.VisitState(rt.node, vs, tid)
rt.erp.Debugger.SetLockingState(rt.erp.MutexeOwners, rt.erp.MutexLog)
rt.erp.Debugger.SetThreadPool(rt.erp.Processor.ThreadPool())
}
return nil, err
}
/*
newBaseRuntime returns a new instance of baseRuntime.
*/
func newBaseRuntime(erp *ECALRuntimeProvider, node *parser.ASTNode) *baseRuntime {
instanceCounter++
return &baseRuntime{fmt.Sprint(instanceCounter), erp, node, false}
}
// Void Runtime
// ============
/*
voidRuntime is a special runtime for constructs which are only evaluated as part
of other components.
*/
type voidRuntime struct {
*baseRuntime
}
/*
voidRuntimeInst returns a new runtime component instance.
*/
func voidRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &voidRuntime{newBaseRuntime(erp, node)}
}
/*
Validate this node and all its child nodes.
*/
func (rt *voidRuntime) Validate() error {
return rt.baseRuntime.Validate()
}
/*
Eval evaluate this runtime component.
*/
func (rt *voidRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
return rt.baseRuntime.Eval(vs, is, tid)
}
// Import Runtime
// ==============
/*
importRuntime handles import statements.
*/
type importRuntime struct {
*baseRuntime
}
/*
importRuntimeInst returns a new runtime component instance.
*/
func importRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &importRuntime{newBaseRuntime(erp, node)}
}
/*
Validate this node and all its child nodes.
*/
func (rt *importRuntime) Validate() error {
return rt.baseRuntime.Validate()
}
/*
Eval evaluate this runtime component.
*/
func (rt *importRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
if rt.erp.ImportLocator == nil {
err = rt.erp.NewRuntimeError(util.ErrRuntimeError, "No import locator was specified", rt.node)
}
if err == nil {
var importPath interface{}
if importPath, err = rt.node.Children[0].Runtime.Eval(vs, is, tid); err == nil {
var codeText string
if codeText, err = rt.erp.ImportLocator.Resolve(fmt.Sprint(importPath)); err == nil {
var ast *parser.ASTNode
if ast, err = parser.ParseWithRuntime(fmt.Sprint(importPath), codeText, rt.erp); err == nil {
if err = ast.Runtime.Validate(); err == nil {
ivs := scope.NewScope(scope.GlobalScope)
if _, err = ast.Runtime.Eval(ivs, make(map[string]interface{}), tid); err == nil {
irt := rt.node.Children[1].Runtime.(*identifierRuntime)
irt.Set(vs, is, tid, scope.ToObject(ivs))
}
}
}
}
}
}
return nil, err
}
// Not Implemented Runtime
// =======================
/*
invalidRuntime is a special runtime for not implemented constructs.
*/
type invalidRuntime struct {
*baseRuntime
}
/*
invalidRuntimeInst returns a new runtime component instance.
*/
func invalidRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &invalidRuntime{newBaseRuntime(erp, node)}
}
/*
Validate this node and all its child nodes.
*/
func (rt *invalidRuntime) Validate() error {
err := rt.baseRuntime.Validate()
if err == nil {
err = rt.erp.NewRuntimeError(util.ErrInvalidConstruct,
fmt.Sprintf("Unknown node: %s", rt.node.Name), rt.node)
}
return err
}
/*
Eval evaluate this runtime component.
*/
func (rt *invalidRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
err = rt.erp.NewRuntimeError(util.ErrInvalidConstruct, fmt.Sprintf("Unknown node: %s", rt.node.Name), rt.node)
}
return nil, err
}
// General Operator Runtime
// ========================
/*
operatorRuntime is a general operator operation. Used for embedding.
*/
type operatorRuntime struct {
*baseRuntime
}
/*
errorDetailString produces a detail string for errors.
*/
func (rt *operatorRuntime) errorDetailString(token *parser.LexToken, opVal interface{}) string {
if !token.Identifier {
return token.Val
}
if opVal == nil {
opVal = "NULL"
}
return fmt.Sprintf("%v=%v", token.Val, opVal)
}
/*
numVal returns a transformed number value.
*/
func (rt *operatorRuntime) numVal(op func(float64) interface{}, vs parser.Scope,
is map[string]interface{}, tid uint64) (interface{}, error) {
var ret interface{}
errorutil.AssertTrue(len(rt.node.Children) == 1,
fmt.Sprint("Operation requires 1 operand", rt.node))
res, err := rt.node.Children[0].Runtime.Eval(vs, is, tid)
if err == nil {
// Check if the value is a number
resNum, ok := res.(float64)
if !ok {
// Produce a runtime error if the value is not a number
return nil, rt.erp.NewRuntimeError(util.ErrNotANumber,
rt.errorDetailString(rt.node.Children[0].Token, res), rt.node.Children[0])
}
ret = op(resNum)
}
return ret, err
}
/*
boolVal returns a transformed boolean value.
*/
func (rt *operatorRuntime) boolVal(op func(bool) interface{},
vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var ret interface{}
errorutil.AssertTrue(len(rt.node.Children) == 1,
fmt.Sprint("Operation requires 1 operand", rt.node))
res, err := rt.node.Children[0].Runtime.Eval(vs, is, tid)
if err == nil {
resBool, ok := res.(bool)
if !ok {
return nil, rt.erp.NewRuntimeError(util.ErrNotABoolean,
rt.errorDetailString(rt.node.Children[0].Token, res), rt.node.Children[0])
}
ret = op(resBool)
}
return ret, err
}
/*
numOp executes an operation on two number values.
*/
func (rt *operatorRuntime) numOp(op func(float64, float64) interface{},
vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var ok bool
var res1, res2 interface{}
var err error
errorutil.AssertTrue(len(rt.node.Children) == 2,
fmt.Sprint("Operation requires 2 operands", rt.node))
if res1, err = rt.node.Children[0].Runtime.Eval(vs, is, tid); err == nil {
if res2, err = rt.node.Children[1].Runtime.Eval(vs, is, tid); err == nil {
var res1Num, res2Num float64
if res1Num, ok = res1.(float64); !ok {
err = rt.erp.NewRuntimeError(util.ErrNotANumber,
rt.errorDetailString(rt.node.Children[0].Token, res1), rt.node.Children[0])
} else {
if res2Num, ok = res2.(float64); !ok {
err = rt.erp.NewRuntimeError(util.ErrNotANumber,
rt.errorDetailString(rt.node.Children[1].Token, res2), rt.node.Children[1])
} else {
return op(res1Num, res2Num), err
}
}
}
}
return nil, err
}
/*
genOp executes an operation on two general values.
*/
func (rt *operatorRuntime) genOp(op func(interface{}, interface{}) interface{},
vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var ret interface{}
errorutil.AssertTrue(len(rt.node.Children) == 2,
fmt.Sprint("Operation requires 2 operands", rt.node))
res1, err := rt.node.Children[0].Runtime.Eval(vs, is, tid)
if err == nil {
var res2 interface{}
if res2, err = rt.node.Children[1].Runtime.Eval(vs, is, tid); err == nil {
ret = op(res1, res2)
}
}
return ret, err
}
/*
strOp executes an operation on two string values.
*/
func (rt *operatorRuntime) strOp(op func(string, string) interface{},
vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var ret interface{}
errorutil.AssertTrue(len(rt.node.Children) == 2,
fmt.Sprint("Operation requires 2 operands", rt.node))
res1, err := rt.node.Children[0].Runtime.Eval(vs, is, tid)
if err == nil {
var res2 interface{}
if res2, err = rt.node.Children[1].Runtime.Eval(vs, is, tid); err == nil {
ret = op(fmt.Sprint(res1), fmt.Sprint(res2))
}
}
return ret, err
}
/*
boolOp executes an operation on two boolean values.
*/
func (rt *operatorRuntime) boolOp(op func(bool, bool) interface{},
vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
errorutil.AssertTrue(len(rt.node.Children) == 2,
fmt.Sprint("Operation requires 2 operands", rt.node))
res1, err := rt.node.Children[0].Runtime.Eval(vs, is, tid)
if err == nil {
var res2 interface{}
if res2, err = rt.node.Children[1].Runtime.Eval(vs, is, tid); err == nil {
res1bool, ok := res1.(bool)
if !ok {
return nil, rt.erp.NewRuntimeError(util.ErrNotABoolean,
rt.errorDetailString(rt.node.Children[0].Token, res1), rt.node.Children[0])
}
res2bool, ok := res2.(bool)
if !ok {
return nil, rt.erp.NewRuntimeError(util.ErrNotABoolean,
rt.errorDetailString(rt.node.Children[1].Token, res2), rt.node.Children[0])
}
res = op(res1bool, res2bool)
}
}
return res, err
}
/*
listOp executes an operation on a value and a list.
*/
func (rt *operatorRuntime) listOp(op func(interface{}, []interface{}) interface{},
vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
errorutil.AssertTrue(len(rt.node.Children) == 2,
fmt.Sprint("Operation requires 2 operands", rt.node))
res1, err := rt.node.Children[0].Runtime.Eval(vs, is, tid)
if err == nil {
var res2 interface{}
if res2, err = rt.node.Children[1].Runtime.Eval(vs, is, tid); err == nil {
res2list, ok := res2.([]interface{})
if !ok {
err = rt.erp.NewRuntimeError(util.ErrNotAList,
rt.errorDetailString(rt.node.Children[1].Token, res2), rt.node.Children[0])
} else {
res = op(res1, res2list)
}
}
}
return res, err
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package interpreter
import (
"fmt"
"strings"
"devt.de/krotik/common/stringutil"
"devt.de/krotik/ecal/parser"
"devt.de/krotik/ecal/scope"
"devt.de/krotik/ecal/stdlib"
"devt.de/krotik/ecal/util"
)
/*
identifierRuntime is the runtime component for identifiers.
*/
type identifierRuntime struct {
*baseRuntime
}
/*
identifierRuntimeInst returns a new runtime component instance.
*/
func identifierRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &identifierRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *identifierRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
res, err = rt.resolveValue(vs, is, tid, rt.node)
}
return res, err
}
/*
resolveValue resolves the value of this identifier.
*/
func (rt *identifierRuntime) resolveValue(vs parser.Scope, is map[string]interface{}, tid uint64, node *parser.ASTNode) (interface{}, error) {
var anode *parser.ASTNode
var astring string
var result interface{}
var err error
functionResolved := func(astring string, rnode *parser.ASTNode) *parser.ASTNode {
res := &parser.ASTNode{ // Create a dummy identifier which models the value evaluation so far
Name: parser.NodeIDENTIFIER,
Token: &parser.LexToken{
ID: node.Token.ID,
Identifier: node.Token.Identifier,
Lline: node.Token.Lline,
Lpos: node.Token.Lpos,
Pos: node.Token.Pos,
Val: strings.Replace(astring, ".", ">", -1),
},
Children: nil,
}
for i, c := range rnode.Children {
if c.Name == parser.NodeFUNCCALL {
res.Children = rnode.Children[i+1:]
}
}
return res
}
anode, astring, err = buildAccessString(rt.erp, vs, is, tid, node, node.Token.Val)
if len(node.Children) == 0 {
// Simple case we just have a variable
result, _, err = vs.GetValue(node.Token.Val)
} else if cval, ok := stdlib.GetStdlibConst(astring); ok {
result = cval
} else {
if rerr, ok := err.(*util.RuntimeError); err == nil || ok && rerr.Type == util.ErrInvalidConstruct {
funcCallInAccessStringExecuted := ok && rerr.Type == util.ErrInvalidConstruct
if result, _, err = vs.GetValue(astring); err == nil {
if funcCallInAccessStringExecuted {
result, err = rt.resolveFunction(astring, vs, is, tid, rerr.Node, result, err)
node = functionResolved(astring, anode)
if len(node.Children) > 0 {
// We have more identifiers after the func call - there is more to do ...
vs = scope.NewScope("funcresult")
vs.SetValue(node.Token.Val, result)
result, err = rt.resolveValue(vs, is, tid, node)
}
} else {
result, err = rt.resolveFunction(astring, vs, is, tid, node, result, err)
}
}
}
}
return result, err
}
/*
resolveFunction execute function calls and return the result.
*/
func (rt *identifierRuntime) resolveFunction(astring string, vs parser.Scope, is map[string]interface{},
tid uint64, node *parser.ASTNode, result interface{}, err error) (interface{}, error) {
is["erp"] = rt.erp // All functions have access to the ECAL Runtime Provider
is["astnode"] = rt.node // ... and the AST node
for _, funccall := range node.Children {
if funccall.Name == parser.NodeFUNCCALL {
funcObj, ok := rt.resolveFunctionObject(astring, result)
if ok {
var args []interface{}
// Collect the parameter values
for _, c := range funccall.Children {
var val interface{}
if err == nil {
val, err = c.Runtime.Eval(vs, make(map[string]interface{}), tid)
args = append(args, val)
}
}
if err == nil {
result, err = rt.executeFunction(astring, funcObj, args, vs, is, tid, node)
}
} else {
err = rt.erp.NewRuntimeError(util.ErrUnknownConstruct,
fmt.Sprintf("Unknown function: %v", node.Token.Val), node)
}
break
}
}
return result, err
}
/*
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
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]
}
}
}
return funcObj, 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) {
var result interface{}
var err error
if stringutil.IndexOf(astring, []string{"log", "error", "debug"}) != -1 {
// Convert non-string structures
for i, a := range args {
if _, ok := a.(string); !ok {
args[i] = stringutil.ConvertToPrettyString(a)
}
}
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)
}
}
return result, err
}
/*
Set sets a value to this identifier.
*/
func (rt *identifierRuntime) Set(vs parser.Scope, is map[string]interface{}, tid uint64, value interface{}) error {
var err error
if len(rt.node.Children) == 0 {
// Simple case we just have a variable
err = vs.SetValue(rt.node.Token.Val, value)
} else {
var as string
_, as, err = buildAccessString(rt.erp, vs, is, tid, rt.node, rt.node.Token.Val)
if err == nil {
// Collect all the children and find the right spot
err = vs.SetValue(as, value)
}
}
return err
}
/*
buildAccessString builds an access string using a given node and a prefix.
*/
func buildAccessString(erp *ECALRuntimeProvider, vs parser.Scope, is map[string]interface{},
tid uint64, node *parser.ASTNode, prefix string) (*parser.ASTNode, string, error) {
var err error
res := prefix
for i, c := range node.Children {
if err == nil {
// The unexpected construct error is used in two ways:
// 1. Error message when a function call is used on the left hand of
// an assignment.
// 2. Signalling there is a function call involved on the right hand
// of an assignment.
if c.Name == parser.NodeCOMPACCESS {
var val interface{}
val, err = c.Children[0].Runtime.Eval(vs, is, tid)
res = fmt.Sprintf("%v.%v", res, val)
if len(node.Children) > i+1 && node.Children[i+1].Name == parser.NodeFUNCCALL {
err = erp.NewRuntimeError(util.ErrInvalidConstruct,
"Unexpected construct", node)
break
}
} else if c.Name == parser.NodeIDENTIFIER {
res = fmt.Sprintf("%v.%v", res, c.Token.Val)
if len(c.Children) > 0 && c.Children[0].Name == parser.NodeFUNCCALL {
node = c
err = erp.NewRuntimeError(util.ErrInvalidConstruct,
"Unexpected construct", node)
break
}
node, res, err = buildAccessString(erp, vs, is, tid, c, res)
}
}
}
return node, res, err
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package interpreter
import (
"fmt"
"math"
"strings"
"devt.de/krotik/ecal/engine"
"devt.de/krotik/ecal/parser"
"devt.de/krotik/ecal/scope"
"devt.de/krotik/ecal/util"
)
/*
sinkRuntime is the runtime for sink declarations.
*/
type sinkRuntime struct {
*baseRuntime
}
/*
sinkRuntimeInst returns a new runtime component instance.
*/
func sinkRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &sinkRuntime{newBaseRuntime(erp, node)}
}
/*
Validate this node and all its child nodes.
*/
func (rt *sinkRuntime) Validate() error {
err := rt.baseRuntime.Validate()
if err == nil {
// Check that all children are valid
for _, child := range rt.node.Children[1:] {
switch child.Name {
case parser.NodeKINDMATCH:
case parser.NodeSCOPEMATCH:
case parser.NodeSTATEMATCH:
case parser.NodePRIORITY:
case parser.NodeSUPPRESSES:
case parser.NodeSTATEMENTS:
continue
default:
err = rt.erp.NewRuntimeError(util.ErrInvalidConstruct,
fmt.Sprintf("Unknown expression in sink declaration %v", child.Token.Val),
child)
}
if err != nil {
break
}
}
}
return err
}
/*
Eval evaluate this runtime component.
*/
func (rt *sinkRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
var rule *engine.Rule
var statements *parser.ASTNode
rule, statements, err = rt.createRule(vs, is, tid)
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())
}
rule.Action = func(p engine.Processor, m engine.Monitor, e *engine.Event, tid uint64) error { // Action of the rule
// Create a new root variable scope
sinkVS := scope.NewScope(fmt.Sprintf("sink: %v", rule.Name))
// Create a new instance state with the monitor - everything called
// by the rule will have access to the current monitor.
sinkIs := map[string]interface{}{
"monitor": m,
}
err = sinkVS.SetValue("event", map[interface{}]interface{}{
"name": e.Name(),
"kind": strings.Join(e.Kind(), engine.RuleKindSeparator),
"state": e.State(),
})
if err == nil {
scope.SetParentOfScope(sinkVS, vs)
if _, err = statements.Runtime.Eval(sinkVS, sinkIs, tid); err != nil {
if sre, ok := err.(*util.RuntimeErrorWithDetail); ok {
sre.Environment = sinkVS
} else {
var data interface{}
rerr := rt.erp.NewRuntimeError(util.ErrSink, err.Error(), rt.node).(*util.RuntimeError)
if e, ok := err.(*util.RuntimeError); ok {
rerr = e
} else if r, ok := err.(*returnValue); ok {
rerr = r.RuntimeError
data = r.returnValue
}
// Provide additional information for unexpected errors
err = &util.RuntimeErrorWithDetail{
RuntimeError: rerr,
Environment: sinkVS,
Data: data,
}
}
}
}
return err
}
if err = rt.erp.Processor.AddRule(rule); err != nil {
err = rt.erp.NewRuntimeError(util.ErrInvalidState, err.Error(), rt.node)
}
}
}
return nil, err
}
/*
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) {
var kindMatch, scopeMatch, suppresses []string
var stateMatch map[string]interface{}
var priority int
var statements *parser.ASTNode
var err error
// Create default scope
scopeMatch = []string{}
// Get sink name
sinkName := fmt.Sprint(rt.node.Children[0].Token.Val)
// Collect values from children
for _, child := range rt.node.Children[1:] {
switch child.Name {
case parser.NodeKINDMATCH:
kindMatch, err = rt.makeStringList(child, vs, is, tid)
break
case parser.NodeSCOPEMATCH:
scopeMatch, err = rt.makeStringList(child, vs, is, tid)
break
case parser.NodeSTATEMATCH:
var val interface{}
stateMatch = make(map[string]interface{})
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
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 = rt.makeStringList(child, vs, is, tid)
break
case parser.NodeSTATEMENTS:
statements = child
break
}
if err != nil {
break
}
}
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
// ================
/*
sinkDetailRuntime is the runtime for sink detail declarations.
*/
type sinkDetailRuntime struct {
*baseRuntime
valType string
}
/*
Eval evaluate this runtime component.
*/
func (rt *sinkDetailRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var ret interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
if ret, err = rt.node.Children[0].Runtime.Eval(vs, is, tid); err == nil {
// Check value is of expected type
if rt.valType == "list" {
if _, ok := ret.([]interface{}); !ok {
return nil, rt.erp.NewRuntimeError(util.ErrInvalidConstruct,
fmt.Sprintf("Expected a list as value"),
rt.node)
}
} else if rt.valType == "map" {
if _, ok := ret.(map[interface{}]interface{}); !ok {
return nil, rt.erp.NewRuntimeError(util.ErrInvalidConstruct,
fmt.Sprintf("Expected a map as value"),
rt.node)
}
} else if rt.valType == "int" {
if _, ok := ret.(float64); !ok {
return nil, rt.erp.NewRuntimeError(util.ErrInvalidConstruct,
fmt.Sprintf("Expected a number as value"),
rt.node)
}
}
}
}
return ret, err
}
/*
kindMatchRuntimeInst returns a new runtime component instance.
*/
func kindMatchRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &sinkDetailRuntime{newBaseRuntime(erp, node), "list"}
}
/*
scopeMatchRuntimeInst returns a new runtime component instance.
*/
func scopeMatchRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &sinkDetailRuntime{newBaseRuntime(erp, node), "list"}
}
/*
stateMatchRuntimeInst returns a new runtime component instance.
*/
func stateMatchRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &sinkDetailRuntime{newBaseRuntime(erp, node), "map"}
}
/*
priorityRuntimeInst returns a new runtime component instance.
*/
func priorityRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &sinkDetailRuntime{newBaseRuntime(erp, node), "int"}
}
/*
suppressesRuntimeInst returns a new runtime component instance.
*/
func suppressesRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &sinkDetailRuntime{newBaseRuntime(erp, node), "list"}
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package interpreter
import (
"fmt"
"sync"
"devt.de/krotik/common/errorutil"
"devt.de/krotik/common/sortutil"
"devt.de/krotik/ecal/parser"
"devt.de/krotik/ecal/scope"
"devt.de/krotik/ecal/util"
)
// Statements Runtime
// ==================
/*
statementsRuntime is the runtime component for sequences of statements.
*/
type statementsRuntime struct {
*baseRuntime
}
/*
statementsRuntimeInst returns a new runtime component instance.
*/
func statementsRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &statementsRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *statementsRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
for _, child := range rt.node.Children {
if res, err = child.Runtime.Eval(vs, is, tid); err != nil {
return nil, err
}
}
}
return res, err
}
// Condition statement
// ===================
/*
ifRuntime is the runtime for the if condition statement.
*/
type ifRuntime struct {
*baseRuntime
}
/*
ifRuntimeInst returns a new runtime component instance.
*/
func ifRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &ifRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *ifRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
// Create a new variable scope
vs = vs.NewChild(scope.NameFromASTNode(rt.node))
for offset := 0; offset < len(rt.node.Children); offset += 2 {
var guardres interface{}
// Evaluate guard
if err == nil {
guardres, err = rt.node.Children[offset].Runtime.Eval(vs, is, tid)
if err == nil && guardres.(bool) {
// The guard holds true so we execture its statements
return rt.node.Children[offset+1].Runtime.Eval(vs, is, tid)
}
}
}
}
return nil, err
}
// Guard Runtime
// =============
/*
guardRuntime is the runtime for any guard condition (used in if, for, etc...).
*/
type guardRuntime struct {
*baseRuntime
}
/*
guardRuntimeInst returns a new runtime component instance.
*/
func guardRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &guardRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *guardRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
var ret interface{}
// Evaluate the condition
ret, err = rt.node.Children[0].Runtime.Eval(vs, is, tid)
// Guard returns always a boolean
res = ret != nil && ret != false && ret != 0
}
return res, err
}
// Loop statement
// ==============
/*
loopRuntime is the runtime for the loop statement (for).
*/
type loopRuntime struct {
*baseRuntime
leftInVarName []string
}
/*
loopRuntimeInst returns a new runtime component instance.
*/
func loopRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &loopRuntime{newBaseRuntime(erp, node), nil}
}
/*
Validate this node and all its child nodes.
*/
func (rt *loopRuntime) Validate() error {
err := rt.baseRuntime.Validate()
if err == nil {
if rt.node.Children[0].Name == parser.NodeIN {
inVar := rt.node.Children[0].Children[0]
if inVar.Name == parser.NodeIDENTIFIER {
if len(inVar.Children) != 0 {
return rt.erp.NewRuntimeError(util.ErrInvalidConstruct,
"Must have a simple variable on the left side of the In expression", rt.node)
}
rt.leftInVarName = []string{inVar.Token.Val}
} else if inVar.Name == parser.NodeLIST {
rt.leftInVarName = make([]string, 0, len(inVar.Children))
for _, child := range inVar.Children {
if child.Name != parser.NodeIDENTIFIER || len(child.Children) != 0 {
return rt.erp.NewRuntimeError(util.ErrInvalidConstruct,
"Must have a list of simple variables on the left side of the In expression", rt.node)
}
rt.leftInVarName = append(rt.leftInVarName, child.Token.Val)
}
}
}
}
return err
}
/*
Eval evaluate this runtime component.
*/
func (rt *loopRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
var guardres interface{}
// Create a new variable scope
vs = vs.NewChild(scope.NameFromASTNode(rt.node))
// Create a new instance scope - elements in each loop iteration start from scratch
is = make(map[string]interface{})
if rt.node.Children[0].Name == parser.NodeGUARD {
// Evaluate guard
guardres, err = rt.node.Children[0].Runtime.Eval(vs, is, tid)
for err == nil && guardres.(bool) {
// Execute block
_, err = rt.node.Children[1].Runtime.Eval(vs, is, tid)
// Check for continue
if err != nil {
if eoi, ok := err.(*util.RuntimeError); ok {
if eoi.Type == util.ErrContinueIteration {
err = nil
}
}
}
if err == nil {
// Evaluate guard
guardres, err = rt.node.Children[0].Runtime.Eval(vs, is, tid)
}
}
} else if rt.node.Children[0].Name == parser.NodeIN {
err = rt.handleIterator(vs, is, tid)
}
}
return nil, err
}
/*
handleIterator handles iterator functions for loops.
*/
func (rt *loopRuntime) handleIterator(vs parser.Scope, is map[string]interface{}, tid uint64) error {
var res interface{}
iterator, err := rt.getIterator(vs, is, tid)
vars := rt.leftInVarName
for err == nil {
if res, err = rt.getIteratorValue(iterator); err == nil {
if len(vars) == 1 {
err = vs.SetValue(vars[0], res)
} else if resList, ok := res.([]interface{}); ok {
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 err == nil {
for i, v := range vars {
if err == nil {
err = vs.SetValue(v, resList[i])
}
}
}
} else {
err = fmt.Errorf("Result for loop variable is not a list (value is %v)", res)
}
if err != nil {
return rt.erp.NewRuntimeError(util.ErrRuntimeError,
err.Error(), rt.node)
}
// Execute block
_, err = rt.node.Children[1].Runtime.Eval(vs, is, tid)
}
// Check for continue
if err != nil {
if eoi, ok := err.(*util.RuntimeError); ok {
if eoi.Type == util.ErrContinueIteration {
err = nil
}
}
}
}
// Check for end of iteration error
if eoi, ok := err.(*util.RuntimeError); ok {
if eoi.Type == util.ErrEndOfIteration {
err = nil
}
}
return err
}
/*
getIteratorValue gets the next iterator value.
*/
func (rt *loopRuntime) getIteratorValue(iterator func() (interface{}, error)) (interface{}, error) {
var err error
var res interface{}
if res, err = iterator(); err != nil {
if eoi, ok := err.(*util.RuntimeError); ok {
if eoi.Type == util.ErrIsIterator {
err = nil
}
}
}
return res, err
}
/*
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)
it := rt.node.Children[0].Children[1]
val, err := it.Runtime.Eval(vs, is, tid)
// Create an iterator object
if rterr, ok := err.(*util.RuntimeError); ok && rterr.Type == util.ErrIsIterator {
// We got an iterator - all subsequent calls will return values
iterator = func() (interface{}, error) {
return it.Runtime.Eval(vs, is, tid)
}
err = nil
} else {
// We got a value over which we need to iterate
if valList, isList := val.([]interface{}); isList {
index := -1
end := len(valList)
iterator = func() (interface{}, error) {
index++
if index >= end {
return nil, rt.erp.NewRuntimeError(util.ErrEndOfIteration, "", rt.node)
}
return valList[index], nil
}
} else if valMap, isMap := val.(map[interface{}]interface{}); isMap {
var keys []interface{}
index := -1
for k := range valMap {
keys = append(keys, k)
}
end := len(keys)
// Try to sort according to string value
sortutil.InterfaceStrings(keys)
iterator = func() (interface{}, error) {
index++
if index >= end {
return nil, rt.erp.NewRuntimeError(util.ErrEndOfIteration, "", rt.node)
}
key := keys[index]
return []interface{}{key, valMap[key]}, nil
}
} else {
// A single value will do exactly one iteration
index := -1
iterator = func() (interface{}, error) {
index++
if index > 0 {
return nil, rt.erp.NewRuntimeError(util.ErrEndOfIteration, "", rt.node)
}
return val, nil
}
}
}
return iterator, err
}
// Break statement
// ===============
/*
breakRuntime is the runtime for the break statement.
*/
type breakRuntime struct {
*baseRuntime
}
/*
breakRuntimeInst returns a new runtime component instance.
*/
func breakRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &breakRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *breakRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
err = rt.erp.NewRuntimeError(util.ErrEndOfIteration, "", rt.node)
}
return nil, err
}
// Continue statement
// ==================
/*
continueRuntime is the runtime for the continue statement.
*/
type continueRuntime struct {
*baseRuntime
}
/*
continueRuntimeInst returns a new runtime component instance.
*/
func continueRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &continueRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *continueRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
err = rt.erp.NewRuntimeError(util.ErrContinueIteration, "", rt.node)
}
return nil, err
}
// Try Runtime
// ===========
/*
tryRuntime is the runtime for try blocks.
*/
type tryRuntime struct {
*baseRuntime
}
/*
tryRuntimeInst returns a new runtime component instance.
*/
func tryRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &tryRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *tryRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
// Make sure the finally block is executed in any case
if finally := rt.node.Children[len(rt.node.Children)-1]; finally.Name == parser.NodeFINALLY {
fvs := vs.NewChild(scope.NameFromASTNode(finally))
defer finally.Children[0].Runtime.Eval(fvs, is, tid)
}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
tvs := vs.NewChild(scope.NameFromASTNode(rt.node))
res, err = rt.node.Children[0].Runtime.Eval(tvs, is, tid)
// Evaluate except clauses
if err != nil {
errObj := map[interface{}]interface{}{
"type": "UnexpectedError",
"error": err.Error(),
}
if rtError, ok := err.(*util.RuntimeError); ok {
errObj["type"] = rtError.Type.Error()
errObj["detail"] = rtError.Detail
errObj["pos"] = rtError.Pos
errObj["line"] = rtError.Line
errObj["source"] = rtError.Source
} else if rtError, ok := err.(*util.RuntimeErrorWithDetail); ok {
errObj["type"] = rtError.Type.Error()
errObj["detail"] = rtError.Detail
errObj["pos"] = rtError.Pos
errObj["line"] = rtError.Line
errObj["source"] = rtError.Source
errObj["data"] = rtError.Data
}
if te, ok := err.(util.TraceableRuntimeError); ok {
if ts := te.GetTraceString(); ts != nil {
errObj["trace"] = ts
}
}
res = nil
for i := 1; i < len(rt.node.Children); i++ {
if child := rt.node.Children[i]; child.Name == parser.NodeEXCEPT {
if ok, newerror := rt.evalExcept(vs, is, tid, errObj, child); ok {
err = newerror
break
}
}
}
} else {
// Evaluate otherwise clause
for i := 1; i < len(rt.node.Children); i++ {
if child := rt.node.Children[i]; child.Name == parser.NodeOTHERWISE {
ovs := vs.NewChild(scope.NameFromASTNode(child))
_, err = child.Children[0].Runtime.Eval(ovs, is, tid)
break
}
}
}
}
return res, err
}
func (rt *tryRuntime) evalExcept(vs parser.Scope, is map[string]interface{},
tid uint64, errObj map[interface{}]interface{}, except *parser.ASTNode) (bool, error) {
var newerror error
ret := false
if len(except.Children) == 1 {
// We only have statements - any exception is handled here
evs := vs.NewChild(scope.NameFromASTNode(except))
_, newerror = 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)
_, newerror = 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)
}
_, newerror = child.Runtime.Eval(evs, is, tid)
}
}
}
return ret, newerror
}
// Mutex Runtime
// =============
/*
mutexRuntime is the runtime for mutex blocks.
*/
type mutexRuntime struct {
*baseRuntime
}
/*
mutexRuntimeInst returns a new runtime component instance.
*/
func mutexRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &mutexRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *mutexRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
var res interface{}
_, err := rt.baseRuntime.Eval(vs, is, tid)
if err == nil {
// Get the name of the mutex
name := rt.node.Children[0].Token.Val
// Take mutex to modify the mutex map
rt.erp.MutexesMutex.Lock()
// Lookup the mutex
mutex, ok := rt.erp.Mutexes[name]
if !ok {
mutex = &sync.Mutex{}
rt.erp.Mutexes[name] = mutex
}
// Try to take the mutex if this thread does not already own it
owner, ok := rt.erp.MutexeOwners[name]
rt.erp.MutexesMutex.Unlock()
if !ok || owner != tid {
rt.erp.MutexLog.Add(fmt.Sprintf("Thread: %v - attempting to take lock %v with owner %v at %v:%v",
tid, name, owner, rt.node.Token.Lsource, rt.node.Token.Lline))
mutex.Lock()
rt.erp.MutexLog.Add(fmt.Sprintf("Thread: %v - took lock %v with owner %v", tid, name, owner))
// Register ownership on mutex
rt.erp.MutexesMutex.Lock()
rt.erp.MutexeOwners[name] = tid
rt.erp.MutexesMutex.Unlock()
defer func() {
rt.erp.MutexLog.Add(fmt.Sprintf("Thread: %v - releasing lock %v", tid, name))
// Unregister ownership on mutex
rt.erp.MutexesMutex.Lock()
rt.erp.MutexeOwners[name] = 0
rt.erp.MutexesMutex.Unlock()
mutex.Unlock()
}()
} else if owner == tid {
rt.erp.MutexLog.Add(fmt.Sprintf("Thread: %v - attempted to take lock %v twice", tid, name))
}
rt.erp.MutexLog.Add(fmt.Sprintf("Thread: %v - execute critical section %v", tid, name))
tvs := vs.NewChild(scope.NameFromASTNode(rt.node))
res, err = rt.node.Children[1].Runtime.Eval(tvs, is, tid)
}
return res, err
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package interpreter
import (
"fmt"
"strconv"
"strings"
"devt.de/krotik/ecal/parser"
"devt.de/krotik/ecal/scope"
)
/*
numberValueRuntime is the runtime component for constant numeric values.
*/
type numberValueRuntime struct {
*baseRuntime
numValue float64 // Numeric value
}
/*
numberValueRuntimeInst returns a new runtime component instance.
*/
func numberValueRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &numberValueRuntime{newBaseRuntime(erp, node), 0}
}
/*
Validate this node and all its child nodes.
*/
func (rt *numberValueRuntime) Validate() error {
err := rt.baseRuntime.Validate()
if err == nil {
rt.numValue, err = strconv.ParseFloat(rt.node.Token.Val, 64)
}
return err
}
/*
Eval evaluate this runtime component.
*/
func (rt *numberValueRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
return rt.numValue, err
}
/*
stringValueRuntime is the runtime component for constant string values.
*/
type stringValueRuntime struct {
*baseRuntime
}
/*
stringValueRuntimeInst returns a new runtime component instance.
*/
func stringValueRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &stringValueRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *stringValueRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
ret := rt.node.Token.Val
if rt.node.Token.AllowEscapes {
// Do allow string interpolation if escape sequences are allowed
for {
var replace string
code, ok := rt.GetInfix(ret, "{{", "}}")
if !ok {
break
}
ast, ierr := parser.ParseWithRuntime(
fmt.Sprintf("String interpolation: %v", code), code, rt.erp)
if ierr == nil {
if ierr = ast.Runtime.Validate(); ierr == nil {
var res interface{}
res, ierr = ast.Runtime.Eval(
vs.NewChild(scope.NameFromASTNode(rt.node)),
make(map[string]interface{}), tid)
if ierr == nil {
replace = fmt.Sprint(res)
}
}
}
if ierr != nil {
replace = fmt.Sprintf("#%v", ierr.Error())
}
ret = strings.Replace(ret, fmt.Sprintf("{{%v}}", code), replace, 1)
}
}
return ret, err
}
func (rt *stringValueRuntime) GetInfix(str string, start string, end string) (string, bool) {
res := str
if s := strings.Index(str, start); s >= 0 {
s += len(start)
if e := strings.Index(str, end); e >= 0 {
res = str[s:e]
}
}
return res, res != str
}
/*
mapValueRuntime is the runtime component for map values.
*/
type mapValueRuntime struct {
*baseRuntime
}
/*
mapValueRuntimeInst returns a new runtime component instance.
*/
func mapValueRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &mapValueRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *mapValueRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
m := make(map[interface{}]interface{})
if err == nil {
for _, kvp := range rt.node.Children {
var key, val interface{}
if err == nil {
if key, err = kvp.Children[0].Runtime.Eval(vs, is, tid); err == nil {
if val, err = kvp.Children[1].Runtime.Eval(vs, is, tid); err == nil {
m[key] = val
}
}
}
}
}
return m, err
}
/*
listValueRuntime is the runtime component for list values.
*/
type listValueRuntime struct {
*baseRuntime
}
/*
listValueRuntimeInst returns a new runtime component instance.
*/
func listValueRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
return &listValueRuntime{newBaseRuntime(erp, node)}
}
/*
Eval evaluate this runtime component.
*/
func (rt *listValueRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint64) (interface{}, error) {
_, err := rt.baseRuntime.Eval(vs, is, tid)
var l []interface{}
if err == nil {
for _, item := range rt.node.Children {
if err == nil {
var val interface{}
if val, err = item.Runtime.Eval(vs, is, tid); err == nil {
l = append(l, val)
}
}
}
}
return l, err
}
/*
* 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 parser contains a ECAL parser.
Lexer for Source Text
Lex() is a lexer function to convert a given search query into a list of tokens.
Based on a talk by Rob Pike: Lexical Scanning in Go
https://www.youtube.com/watch?v=HxaD_trXwRE
The lexer's output is pushed into a channel which is consumed by the parser.
This design enables the concurrent processing of the input text by lexer and
parser.
Parser
Parse() is a parser which produces a parse tree from a given set of lexer tokens.
Based on an article by Douglas Crockford: Top Down Operator Precedence
http://crockford.com/javascript/tdop/tdop.html
which is based on the ideas of Vaughan Pratt and his paper: Top Down Operator Precedence
http://portal.acm.org/citation.cfm?id=512931
https://tdop.github.io/
ParseWithRuntime() parses a given input and decorates the resulting parse tree
with runtime components which can be used to interpret the parsed query.
*/
package parser
/*
LexTokenID represents a unique lexer token ID
*/
type LexTokenID int
/*
Available meta data types
*/
const (
MetaDataPreComment = "MetaDataPreComment"
MetaDataPostComment = "MetaDataPostComment"
MetaDataGeneral = "MetaDataGeneral"
)
/*
Available lexer token types
*/
const (
TokenError LexTokenID = iota // Lexing error token with a message as val
TokenEOF // End-of-file token
TokenANY // Unspecified token (used when building an AST from a Go map structure)
TokenPRECOMMENT // Comment /* ... */
TokenPOSTCOMMENT // Comment # ...
// Value tokens
TokenSTRING // String constant
TokenNUMBER // Number constant
TokenIDENTIFIER // Idendifier
// Constructed tokens which are generated by the parser not the lexer
TokenSTATEMENTS // A code block
TokenFUNCCALL // A function call
TokenCOMPACCESS // Access to a composition structure
TokenLIST // List value
TokenMAP // MAP value
TokenPARAMS // Function parameters
TokenGUARD // Conditional statements
TOKENodeSYMBOLS // Used to separate symbols from other tokens in this list
// Condition operators
TokenGEQ
TokenLEQ
TokenNEQ
TokenEQ
TokenGT
TokenLT
// Grouping symbols
TokenLPAREN
TokenRPAREN
TokenLBRACK
TokenRBRACK
TokenLBRACE
TokenRBRACE
// Separators
TokenDOT
TokenCOMMA
TokenSEMICOLON
// Grouping
TokenCOLON
TokenEQUAL
// Arithmetic operators
TokenPLUS
TokenMINUS
TokenTIMES
TokenDIV
TokenDIVINT
TokenMODINT
// Assignment statement
TokenASSIGN
TokenLET
TOKENodeKEYWORDS // Used to separate keywords from other tokens in this list
// Import statement
TokenIMPORT
TokenAS
// Sink definition
TokenSINK
TokenKINDMATCH
TokenSCOPEMATCH
TokenSTATEMATCH
TokenPRIORITY
TokenSUPPRESSES
// Function definition
TokenFUNC
TokenRETURN
// Boolean operators
TokenAND
TokenOR
TokenNOT
// Condition operators
TokenLIKE
TokenIN
TokenHASPREFIX
TokenHASSUFFIX
TokenNOTIN
// Constant terminals
TokenFALSE
TokenTRUE
TokenNULL
// Conditional statements
TokenIF
TokenELIF
TokenELSE
// Loop statements
TokenFOR
TokenBREAK
TokenCONTINUE
// Try block
TokenTRY
TokenEXCEPT
TokenOTHERWISE
TokenFINALLY
// Mutex block
TokenMUTEX
TokenENDLIST
)
/*
IsValidTokenID check if a given token ID is valid.
*/
func IsValidTokenID(value int) bool {
return value < int(TokenENDLIST)
}
/*
Available parser AST node types
*/
const (
NodeEOF = "EOF"
NodeSTRING = "string" // String constant
NodeNUMBER = "number" // Number constant
NodeIDENTIFIER = "identifier" // Idendifier
// Constructed tokens
NodeSTATEMENTS = "statements" // List of statements
NodeFUNCCALL = "funccall" // Function call
NodeCOMPACCESS = "compaccess" // Composition structure access
NodeLIST = "list" // List value
NodeMAP = "map" // Map value
NodePARAMS = "params" // Function parameters
NodeGUARD = "guard" // Guard expressions for conditional statements
// Condition operators
NodeGEQ = ">="
NodeLEQ = "<="
NodeNEQ = "!="
NodeEQ = "=="
NodeGT = ">"
NodeLT = "<"
// Separators
NodeKVP = "kvp" // Key-value pair
NodePRESET = "preset" // Preset value
// Arithmetic operators
NodePLUS = "plus"
NodeMINUS = "minus"
NodeTIMES = "times"
NodeDIV = "div"
NodeMODINT = "modint"
NodeDIVINT = "divint"
// Assignment statement
NodeASSIGN = ":="
NodeLET = "let"
// Import statement
NodeIMPORT = "import"
// Sink definition
NodeSINK = "sink"
NodeKINDMATCH = "kindmatch"
NodeSCOPEMATCH = "scopematch"
NodeSTATEMATCH = "statematch"
NodePRIORITY = "priority"
NodeSUPPRESSES = "suppresses"
// Function definition
NodeFUNC = "function"
NodeRETURN = "return"
// Boolean operators
NodeAND = "and"
NodeOR = "or"
NodeNOT = "not"
// Condition operators
NodeLIKE = "like"
NodeIN = "in"
NodeHASPREFIX = "hasprefix"
NodeHASSUFFIX = "hassuffix"
NodeNOTIN = "notin"
// Constant terminals
NodeTRUE = "true"
NodeFALSE = "false"
NodeNULL = "null"
// Conditional statements
NodeIF = "if"
// Loop statements
NodeLOOP = "loop"
NodeBREAK = "break"
NodeCONTINUE = "continue"
// Try block
NodeTRY = "try"
NodeEXCEPT = "except"
NodeAS = "as"
NodeOTHERWISE = "otherwise"
NodeFINALLY = "finally"
// Mutex block
NodeMUTEX = "mutex"
)
/*
* 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 parser
import (
"bytes"
"fmt"
"strconv"
"devt.de/krotik/common/datautil"
"devt.de/krotik/common/stringutil"
)
// AST Nodes
// =========
/*
MetaData is auxiliary data which can be attached to ASTs.
*/
type MetaData interface {
/*
Type returns the type of the meta data.
*/
Type() string
/*
Value returns the value of the meta data.
*/
Value() string
}
/*
metaData is a minimal MetaData implementation.
*/
type metaData struct {
metatype string
metavalue string
}
/*
Type returns the type of the meta data.
*/
func (m *metaData) Type() string {
return m.metatype
}
/*
Value returns the value of the meta data.
*/
func (m *metaData) Value() string {
return m.metavalue
}
/*
ASTNode models a node in the AST
*/
type ASTNode struct {
Name string // Name of the node
Token *LexToken // Lexer token of this ASTNode
Meta []MetaData // Meta data for this ASTNode (e.g. comments)
Children []*ASTNode // Child nodes
Runtime Runtime // Runtime component for this ASTNode
binding int // Binding power of this node
nullDenotation func(p *parser, self *ASTNode) (*ASTNode, error) // Configure token as beginning node
leftDenotation func(p *parser, self *ASTNode, left *ASTNode) (*ASTNode, error) // Configure token as left node
}
/*
Create a new instance of this ASTNode which is connected to a concrete lexer token.
*/
func (n *ASTNode) instance(p *parser, t *LexToken) *ASTNode {
ret := &ASTNode{n.Name, t, nil, make([]*ASTNode, 0, 2), nil, n.binding, n.nullDenotation, n.leftDenotation}
if p.rp != nil {
ret.Runtime = p.rp.Runtime(ret)
}
return ret
}
/*
Equals checks if this AST data equals another AST data. Returns also a message describing
what is the found difference.
*/
func (n *ASTNode) Equals(other *ASTNode, ignoreTokenPosition bool) (bool, string) {
return n.equalsPath(n.Name, other, ignoreTokenPosition)
}
/*
equalsPath checks if this AST data equals another AST data while preserving the search path.
Returns also a message describing what is the found difference.
*/
func (n *ASTNode) equalsPath(path string, other *ASTNode, ignoreTokenPosition bool) (bool, string) {
var res = true
var msg = ""
if n.Name != other.Name {
res = false
msg = fmt.Sprintf("Name is different %v vs %v\n", n.Name, other.Name)
}
if n.Token != nil && other.Token != nil {
if ok, tokenMSG := n.Token.Equals(*other.Token, ignoreTokenPosition); !ok {
res = false
msg += fmt.Sprintf("Token is different:\n%v\n", tokenMSG)
}
}
if len(n.Meta) != len(other.Meta) {
res = false
msg = fmt.Sprintf("Number of meta data entries is different %v vs %v\n",
len(n.Meta), len(other.Meta))
} else {
for i, meta := range n.Meta {
// Check for different in meta entries
if meta.Type() != other.Meta[i].Type() {
res = false
msg += fmt.Sprintf("Meta data type is different %v vs %v\n", meta.Type(), other.Meta[i].Type())
} else if meta.Value() != other.Meta[i].Value() {
res = false
msg += fmt.Sprintf("Meta data value is different %v vs %v\n", meta.Value(), other.Meta[i].Value())
}
}
}
if len(n.Children) != len(other.Children) {
res = false
msg = fmt.Sprintf("Number of children is different %v vs %v\n",
len(n.Children), len(other.Children))
} else {
for i, child := range n.Children {
// Check for different in children
if ok, childMSG := child.equalsPath(fmt.Sprintf("%v > %v", path, child.Name),
other.Children[i], ignoreTokenPosition); !ok {
return ok, childMSG
}
}
}
if msg != "" {
var buf bytes.Buffer
buf.WriteString("AST Nodes:\n")
n.levelString(0, &buf, 1)
buf.WriteString("vs\n")
other.levelString(0, &buf, 1)
msg = fmt.Sprintf("Path to difference: %v\n\n%v\n%v", path, msg, buf.String())
}
return res, msg
}
/*
String returns a string representation of this token.
*/
func (n *ASTNode) String() string {
var buf bytes.Buffer
n.levelString(0, &buf, -1)
return buf.String()
}
/*
levelString function to recursively print the tree.
*/
func (n *ASTNode) levelString(indent int, buf *bytes.Buffer, printChildren int) {
// Print current level
buf.WriteString(stringutil.GenerateRollingString(" ", indent*2))
if n.Name == NodeSTRING {
buf.WriteString(fmt.Sprintf("%v: '%v'", n.Name, n.Token.Val))
} else if n.Name == NodeNUMBER {
buf.WriteString(fmt.Sprintf("%v: %v", n.Name, n.Token.Val))
} else if n.Name == NodeIDENTIFIER {
buf.WriteString(fmt.Sprintf("%v: %v", n.Name, n.Token.Val))
} else {
buf.WriteString(n.Name)
}
if len(n.Meta) > 0 {
buf.WriteString(" # ")
for i, c := range n.Meta {
buf.WriteString(c.Value())
if i < len(n.Meta)-1 {
buf.WriteString(" ")
}
}
}
buf.WriteString("\n")
if printChildren == -1 || printChildren > 0 {
if printChildren != -1 {
printChildren--
}
// Print children
for _, child := range n.Children {
child.levelString(indent+1, buf, printChildren)
}
}
}
/*
ToJSONObject returns this ASTNode and all its children as a JSON object.
*/
func (n *ASTNode) ToJSONObject() map[string]interface{} {
ret := make(map[string]interface{})
ret["name"] = n.Name
lenMeta := len(n.Meta)
if lenMeta > 0 {
meta := make([]map[string]interface{}, lenMeta)
for i, metaChild := range n.Meta {
meta[i] = map[string]interface{}{
"type": metaChild.Type(),
"value": metaChild.Value(),
}
}
ret["meta"] = meta
}
lenChildren := len(n.Children)
if lenChildren > 0 {
children := make([]map[string]interface{}, lenChildren)
for i, child := range n.Children {
children[i] = child.ToJSONObject()
}
ret["children"] = children
}
// The value is what the lexer found in the source
if n.Token != nil {
ret["id"] = n.Token.ID
if n.Token.Val != "" {
ret["value"] = n.Token.Val
}
ret["identifier"] = n.Token.Identifier
ret["allowescapes"] = n.Token.AllowEscapes
ret["pos"] = n.Token.Pos
ret["source"] = n.Token.Lsource
ret["line"] = n.Token.Lline
ret["linepos"] = n.Token.Lpos
}
return ret
}
/*
ASTFromJSONObject creates an AST from a JSON Object.
The following nested map structure is expected:
{
name : <name of node>
// Optional node information
value : <value of node>
children : [ <child nodes> ]
// Optional token information
id : <token id>
}
*/
func ASTFromJSONObject(jsonAST map[string]interface{}) (*ASTNode, error) {
var astMeta []MetaData
var astChildren []*ASTNode
nodeID := TokenANY
name, ok := jsonAST["name"]
if !ok {
return nil, fmt.Errorf("Found json ast node without a name: %v", jsonAST)
}
if nodeIDString, ok := jsonAST["id"]; ok {
if nodeIDInt, err := strconv.Atoi(fmt.Sprint(nodeIDString)); err == nil && IsValidTokenID(nodeIDInt) {
nodeID = LexTokenID(nodeIDInt)
}
}
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
}
value, _ := getVal("value", "")
identifier, _ := getVal("identifier", false)
allowescapes, _ := getVal("allowescapes", false)
_, prefixnl := getVal("prefixnewlines", "")
_, pos := getVal("pos", "")
_, line := getVal("line", "")
_, linepos := getVal("linepos", "")
source, _ := getVal("source", "")
// Create meta data
if meta, ok := jsonAST["meta"]; ok {
if ic, ok := meta.([]interface{}); ok {
// Do a list conversion if necessary - this is necessary when we parse
// JSON with map[string]interface{}
metaList := make([]map[string]interface{}, len(ic))
for i := range ic {
metaList[i] = ic[i].(map[string]interface{})
}
meta = metaList
}
for _, metaChild := range meta.([]map[string]interface{}) {
astMeta = append(astMeta, &metaData{
fmt.Sprint(metaChild["type"]), fmt.Sprint(metaChild["value"])})
}
}
// Create children
if children, ok := jsonAST["children"]; ok {
if ic, ok := children.([]interface{}); ok {
// Do a list conversion if necessary - this is necessary when we parse
// JSON with map[string]interface{}
childrenList := make([]map[string]interface{}, len(ic))
for i := range ic {
childrenList[i] = ic[i].(map[string]interface{})
}
children = childrenList
}
for _, child := range children.([]map[string]interface{}) {
astChild, err := ASTFromJSONObject(child)
if err != nil {
return nil, err
}
astChildren = append(astChildren, astChild)
}
}
token := &LexToken{
nodeID, // ID
pos, // Pos
fmt.Sprint(value), // Val
identifier == true, // Identifier
allowescapes == true, // AllowEscapes
prefixnl, // PrefixNewlines
fmt.Sprint(source), // Lsource
line, // Lline
linepos, // Lpos
}
return &ASTNode{fmt.Sprint(name), token, astMeta, astChildren, nil, 0, nil, nil}, nil
}
// Look ahead buffer
// =================
/*
LABuffer models a look-ahead buffer.
*/
type LABuffer struct {
tokens chan LexToken
buffer *datautil.RingBuffer
}
/*
NewLABuffer creates a new NewLABuffer instance.
*/
func NewLABuffer(c chan LexToken, size int) *LABuffer {
if size < 1 {
size = 1
}
ret := &LABuffer{c, datautil.NewRingBuffer(size)}
v, more := <-ret.tokens
ret.buffer.Add(v)
for ret.buffer.Size() < size && more && v.ID != TokenEOF {
v, more = <-ret.tokens
ret.buffer.Add(v)
}
return ret
}
/*
Next returns the next item.
*/
func (b *LABuffer) Next() (LexToken, bool) {
ret := b.buffer.Poll()
if v, more := <-b.tokens; more {
b.buffer.Add(v)
}
if ret == nil {
return LexToken{ID: TokenEOF}, false
}
return ret.(LexToken), true
}
/*
Peek looks inside the buffer starting with 0 as the next item.
*/
func (b *LABuffer) Peek(pos int) (LexToken, bool) {
if pos >= b.buffer.Size() {
return LexToken{ID: TokenEOF}, false
}
return b.buffer.Get(pos).(LexToken), true
}
/*
* 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 parser
import (
"bytes"
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"unicode"
"unicode/utf8"
)
/*
NamePattern is the pattern for valid names.
*/
var NamePattern = regexp.MustCompile("^[A-Za-z][A-Za-z0-9]*$")
/*
numberPattern is a hint pattern for numbers.
*/
var numberPattern = regexp.MustCompile("^[0-9].*$")
/*
LexToken represents a token which is returned by the lexer.
*/
type LexToken struct {
ID LexTokenID // Token kind
Pos int // Starting position (in bytes)
Val string // Token value
Identifier bool // Flag if the value is an identifier (not quoted and not a number)
AllowEscapes bool // Flag if the value did interpret escape charaters
PrefixNewlines int // Number of newlines which precede this token
Lsource string // Input source label (e.g. filename)
Lline int // Line in the input this token appears
Lpos int // Position in the input line this token appears
}
/*
NewLexTokenInstance creates a new LexToken object instance from given LexToken values.
*/
func NewLexTokenInstance(t LexToken) *LexToken {
return &LexToken{
t.ID,
t.Pos,
t.Val,
t.Identifier,
t.AllowEscapes,
t.PrefixNewlines,
t.Lsource,
t.Lline,
t.Lpos,
}
}
/*
Equals checks if this LexToken equals another LexToken. Returns also a message describing
what is the found difference.
*/
func (t LexToken) Equals(other LexToken, ignorePosition bool) (bool, string) {
var res = true
var msg = ""
if t.ID != other.ID {
res = false
msg += fmt.Sprintf("ID is different %v vs %v\n", t.ID, other.ID)
}
if !ignorePosition && t.Pos != other.Pos {
res = false
msg += fmt.Sprintf("Pos is different %v vs %v\n", t.Pos, other.Pos)
}
if t.Val != other.Val {
res = false
msg += fmt.Sprintf("Val is different %v vs %v\n", t.Val, other.Val)
}
if t.Identifier != other.Identifier {
res = false
msg += fmt.Sprintf("Identifier is different %v vs %v\n", t.Identifier, other.Identifier)
}
if !ignorePosition && t.Lline != other.Lline {
res = false
msg += fmt.Sprintf("Lline is different %v vs %v\n", t.Lline, other.Lline)
}
if !ignorePosition && t.Lpos != other.Lpos {
res = false
msg += fmt.Sprintf("Lpos is different %v vs %v\n", t.Lpos, other.Lpos)
}
if msg != "" {
var buf bytes.Buffer
out, _ := json.MarshalIndent(t, "", " ")
buf.WriteString(string(out))
buf.WriteString("\nvs\n")
out, _ = json.MarshalIndent(other, "", " ")
buf.WriteString(string(out))
msg = fmt.Sprintf("%v%v", msg, buf.String())
}
return res, msg
}
/*
PosString returns the position of this token in the origianl input as a string.
*/
func (t LexToken) PosString() string {
return fmt.Sprintf("Line %v, Pos %v", t.Lline, t.Lpos)
}
/*
String returns a string representation of a token.
*/
func (t LexToken) String() string {
prefix := ""
if !t.Identifier {
prefix = "v:" // Value is not an identifier
}
switch {
case t.ID == TokenEOF:
return "EOF"
case t.ID == TokenError:
return fmt.Sprintf("Error: %s (%s)", t.Val, t.PosString())
case t.ID == TokenPRECOMMENT:
return fmt.Sprintf("/* %s */", t.Val)
case t.ID == TokenPOSTCOMMENT:
return fmt.Sprintf("# %s", t.Val)
case t.ID > TOKENodeSYMBOLS && t.ID < TOKENodeKEYWORDS:
return fmt.Sprintf("%s", strings.ToUpper(t.Val))
case t.ID > TOKENodeKEYWORDS:
return fmt.Sprintf("<%s>", strings.ToUpper(t.Val))
case len(t.Val) > 20:
// Special case for very long values
return fmt.Sprintf("%s%.10q...", prefix, t.Val)
}
return fmt.Sprintf("%s%q", prefix, t.Val)
}
// Meta data interface
/*
Type returns the meta data type.
*/
func (t LexToken) Type() string {
if t.ID == TokenPRECOMMENT {
return MetaDataPreComment
} else if t.ID == TokenPOSTCOMMENT {
return MetaDataPostComment
}
return MetaDataGeneral
}
/*
Value returns the meta data value.
*/
func (t LexToken) Value() string {
return t.Val
}
/*
KeywordMap is a map of keywords - these require spaces between them
*/
var KeywordMap = map[string]LexTokenID{
// Assign statement
"let": TokenLET,
// Import statement
"import": TokenIMPORT,
"as": TokenAS,
// Sink definition
"sink": TokenSINK,
"kindmatch": TokenKINDMATCH,
"scopematch": TokenSCOPEMATCH,
"statematch": TokenSTATEMATCH,
"priority": TokenPRIORITY,
"suppresses": TokenSUPPRESSES,
// Function definition
"func": TokenFUNC,
"return": TokenRETURN,
// Boolean operators
"and": TokenAND,
"or": TokenOR,
"not": TokenNOT,
// String operators
"like": TokenLIKE,
"hasprefix": TokenHASPREFIX,
"hassuffix": TokenHASSUFFIX,
// List operators
"in": TokenIN,
"notin": TokenNOTIN,
// Constant terminals
"false": TokenFALSE,
"true": TokenTRUE,
"null": TokenNULL,
// Conditional statements
"if": TokenIF,
"elif": TokenELIF,
"else": TokenELSE,
// Loop statements
"for": TokenFOR,
"break": TokenBREAK,
"continue": TokenCONTINUE,
// Try block
"try": TokenTRY,
"except": TokenEXCEPT,
"otherwise": TokenOTHERWISE,
"finally": TokenFINALLY,
// Mutex block
"mutex": TokenMUTEX,
}
/*
SymbolMap is a map of special symbols which will always be unique - these will separate unquoted strings
Symbols can be maximal 2 characters long.
*/
var SymbolMap = map[string]LexTokenID{
// Condition operators
">=": TokenGEQ,
"<=": TokenLEQ,
"!=": TokenNEQ,
"==": TokenEQ,
">": TokenGT,
"<": TokenLT,
// Grouping symbols
"(": TokenLPAREN,
")": TokenRPAREN,
"[": TokenLBRACK,
"]": TokenRBRACK,
"{": TokenLBRACE,
"}": TokenRBRACE,
// Separators
".": TokenDOT,
",": TokenCOMMA,
";": TokenSEMICOLON,
// Grouping
":": TokenCOLON,
"=": TokenEQUAL,
// Arithmetic operators
"+": TokenPLUS,
"-": TokenMINUS,
"*": TokenTIMES,
"/": TokenDIV,
"//": TokenDIVINT,
"%": TokenMODINT,
// Assignment statement
":=": TokenASSIGN,
}
// Lexer
// =====
/*
RuneEOF is a special rune which represents the end of the input
*/
const RuneEOF = -1
/*
Function which represents the current state of the lexer and returns the next state
*/
type lexFunc func(*lexer) lexFunc
/*
Lexer data structure
*/
type lexer struct {
name string // Name to identify the input
input string // Input string of the lexer
pos int // Current rune pointer
line int // Current line pointer
lastnl int // Last newline position
skippedNewline int // Number of skipped newlines
width int // Width of last rune
start int // Start position of the current red token
tokens chan LexToken // Channel for lexer output
}
/*
Lex lexes a given input. Returns a channel which contains tokens.
*/
func Lex(name string, input string) chan LexToken {
l := &lexer{name, input, 0, 0, 0, 0, 0, 0, make(chan LexToken)}
go l.run()
return l.tokens
}
/*
LexToList lexes a given input. Returns a list of tokens.
*/
func LexToList(name string, input string) []LexToken {
var tokens []LexToken
for t := range Lex(name, input) {
tokens = append(tokens, t)
}
return tokens
}
/*
Main loop of the lexer.
*/
func (l *lexer) run() {
if skipWhiteSpace(l) {
for state := lexToken; state != nil; {
state = state(l)
if !skipWhiteSpace(l) {
break
}
}
}
close(l.tokens)
}
/*
next returns the next rune in the input and advances the current rune pointer
if peek is 0. If peek is >0 then the nth character is returned without advancing
the rune pointer.
*/
func (l *lexer) next(peek int) rune {
// Check if we reached the end
if int(l.pos) >= len(l.input) {
return RuneEOF
}
// Decode the next rune
pos := l.pos
if peek > 0 {
pos += peek - 1
}
r, w := utf8.DecodeRuneInString(l.input[pos:])
if peek == 0 {
l.width = w
l.pos += l.width
}
return r
}
/*
backup sets the pointer one rune back. Can only be called once per next call.
*/
func (l *lexer) backup(width int) {
if width == 0 {
width = l.width
}
l.pos -= width
}
/*
startNew starts a new token.
*/
func (l *lexer) startNew() {
l.start = l.pos
}
/*
emitToken passes a token back to the client.
*/
func (l *lexer) emitToken(t LexTokenID) {
if t == TokenEOF {
l.emitTokenAndValue(t, "", false, false)
return
}
if l.tokens != nil {
l.tokens <- LexToken{t, l.start, l.input[l.start:l.pos], false, false, l.skippedNewline, l.name,
l.line + 1, l.start - l.lastnl + 1}
}
}
/*
emitTokenAndValue passes a token with a given value back to the client.
*/
func (l *lexer) emitTokenAndValue(t LexTokenID, val string, identifier bool, allowEscapes bool) {
if l.tokens != nil {
l.tokens <- LexToken{t, l.start, val, identifier, allowEscapes, l.skippedNewline,
l.name, l.line + 1, l.start - l.lastnl + 1}
}
}
/*
emitError passes an error token back to the client.
*/
func (l *lexer) emitError(msg string) {
if l.tokens != nil {
l.tokens <- LexToken{TokenError, l.start, msg, false, false, l.skippedNewline,
l.name, l.line + 1, l.start - l.lastnl + 1}
}
}
// Helper functions
// ================
/*
skipWhiteSpace skips any number of whitespace characters. Returns false if the parser
reaches EOF while skipping whitespaces.
*/
func skipWhiteSpace(l *lexer) bool {
r := l.next(0)
l.skippedNewline = 0
for unicode.IsSpace(r) || unicode.IsControl(r) || r == RuneEOF {
if r == '\n' {
l.line++
l.skippedNewline++
l.lastnl = l.pos
}
r = l.next(0)
if r == RuneEOF {
l.emitToken(TokenEOF)
return false
}
}
l.backup(0)
return true
}
/*
lexTextBlock lexes a block of text without whitespaces. Interprets
optionally all one or two letter tokens.
*/
func lexTextBlock(l *lexer, interpretToken bool) {
r := l.next(0)
if interpretToken {
// Check if we start with a known symbol
nr := l.next(1)
if _, ok := SymbolMap[strings.ToLower(string(r)+string(nr))]; ok {
l.next(0)
return
}
if _, ok := SymbolMap[strings.ToLower(string(r))]; ok {
return
}
}
for !unicode.IsSpace(r) && !unicode.IsControl(r) && r != RuneEOF {
if interpretToken {
// Check if we find a token in the block
if _, ok := SymbolMap[strings.ToLower(string(r))]; ok {
l.backup(0)
return
}
nr := l.next(1)
if _, ok := SymbolMap[strings.ToLower(string(r)+string(nr))]; ok {
l.backup(0)
return
}
}
r = l.next(0)
}
if r != RuneEOF {
l.backup(0)
}
}
/*
lexNumberBlock lexes a block potentially containing a number.
*/
func lexNumberBlock(l *lexer) {
r := l.next(0)
for !unicode.IsSpace(r) && !unicode.IsControl(r) && r != RuneEOF {
if !unicode.IsNumber(r) && r != '.' {
if r == 'e' {
l1 := l.next(1)
l2 := l.next(2)
if l1 != '+' || !unicode.IsNumber(l2) {
break
}
l.next(0)
l.next(0)
} else {
break
}
}
r = l.next(0)
}
if r != RuneEOF {
l.backup(0)
}
}
// State functions
// ===============
/*
lexToken is the main entry function for the lexer.
*/
func lexToken(l *lexer) lexFunc {
// Check if we got a quoted value or a comment
n1 := l.next(1)
n2 := l.next(2)
// Parse comments
if (n1 == '/' && n2 == '*') || n1 == '#' {
return lexComment
}
// Parse strings
if (n1 == '"' || n1 == '\'') || (n1 == 'r' && (n2 == '"' || n2 == '\'')) {
return lexValue
}
// Lex a block of text and emit any found tokens
l.startNew()
// First try to parse a number
lexNumberBlock(l)
identifierCandidate := l.input[l.start:l.pos]
keywordCandidate := strings.ToLower(identifierCandidate)
// Check for number
if numberPattern.MatchString(keywordCandidate) {
_, err := strconv.ParseFloat(keywordCandidate, 64)
if err == nil {
l.emitTokenAndValue(TokenNUMBER, keywordCandidate, false, false)
return lexToken
}
}
if len(keywordCandidate) > 0 {
l.backup(l.pos - l.start)
}
lexTextBlock(l, true)
identifierCandidate = l.input[l.start:l.pos]
keywordCandidate = strings.ToLower(identifierCandidate)
// Check for keyword
token, ok := KeywordMap[keywordCandidate]
if !ok {
// Check for symbol
token, ok = SymbolMap[keywordCandidate]
}
if ok {
// A known token was found
l.emitToken(token)
} else {
if !NamePattern.MatchString(keywordCandidate) {
l.emitError(fmt.Sprintf("Cannot parse identifier '%v'. Identifies may only contain [a-zA-Z] and [a-zA-Z0-9] from the second character", keywordCandidate))
return nil
}
// An identifier was found
l.emitTokenAndValue(TokenIDENTIFIER, identifierCandidate, true, false)
}
return lexToken
}
/*
lexValue lexes a string value.
Values can be declared in different ways:
' ... ' or " ... "
Characters are parsed between quotes (escape sequences are interpreted)
r' ... ' or r" ... "
Characters are parsed plain between quote
*/
func lexValue(l *lexer) lexFunc {
var endToken rune
l.startNew()
allowEscapes := false
r := l.next(0)
// Check if we have a raw quoted string
if q := l.next(1); r == 'r' && (q == '"' || q == '\'') {
endToken = q
l.next(0)
} else {
allowEscapes = true
endToken = r
}
r = l.next(0)
rprev := ' '
lLine := l.line
lLastnl := l.lastnl
for (!allowEscapes && r != endToken) ||
(allowEscapes && (r != endToken || rprev == '\\')) {
if r == '\n' {
lLine++
lLastnl = l.pos
}
rprev = r
r = l.next(0)
if r == RuneEOF {
l.emitError("Unexpected end while reading string value (unclosed quotes)")
return nil
}
}
if allowEscapes {
val := l.input[l.start+1 : l.pos-1]
// Interpret escape sequences right away
if endToken == '\'' {
// Escape double quotes in a single quoted string
val = strings.Replace(val, "\"", "\\\"", -1)
}
s, err := strconv.Unquote("\"" + val + "\"")
if err != nil {
l.emitError(err.Error() + " while parsing string")
return nil
}
l.emitTokenAndValue(TokenSTRING, s, false, true)
} else {
l.emitTokenAndValue(TokenSTRING, l.input[l.start+2:l.pos-1], false, false)
}
// Set newline
l.line = lLine
l.lastnl = lLastnl
return lexToken
}
/*
lexComment lexes comments.
*/
func lexComment(l *lexer) lexFunc {
// Consume initial /*
r := l.next(0)
if r == '#' {
l.startNew()
for r != '\n' && r != RuneEOF {
r = l.next(0)
}
l.emitTokenAndValue(TokenPOSTCOMMENT, l.input[l.start:l.pos], false, false)
if r == RuneEOF {
return nil
}
l.line++
} else {
l.next(0)
lLine := l.line
lLastnl := l.lastnl
l.startNew()
r = l.next(0)
for r != '*' || l.next(1) != '/' {
if r == '\n' {
lLine++
lLastnl = l.pos
}
r = l.next(0)
if r == RuneEOF {
l.emitError("Unexpected end while reading comment")
return nil
}
}
l.emitTokenAndValue(TokenPRECOMMENT, l.input[l.start:l.pos-1], false, false)
// Consume final /
l.next(0)
// Set newline
l.line = lLine
l.lastnl = lLastnl
}
return lexToken
}
/*
* 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 parser
import (
"fmt"
)
/*
Map of AST nodes corresponding to lexer tokens. The map determines how a given
sequence of lexer tokens are organized into an AST.
*/
var astNodeMap map[LexTokenID]*ASTNode
func init() {
astNodeMap = map[LexTokenID]*ASTNode{
TokenEOF: {NodeEOF, nil, nil, nil, nil, 0, ndTerm, nil},
// Value tokens
TokenSTRING: {NodeSTRING, nil, nil, nil, nil, 0, ndTerm, nil},
TokenNUMBER: {NodeNUMBER, nil, nil, nil, nil, 0, ndTerm, nil},
TokenIDENTIFIER: {NodeIDENTIFIER, nil, nil, nil, nil, 0, ndIdentifier, nil},
// Constructed tokens
TokenSTATEMENTS: {NodeSTATEMENTS, nil, nil, nil, nil, 0, nil, nil},
TokenFUNCCALL: {NodeFUNCCALL, nil, nil, nil, nil, 0, nil, nil},
TokenCOMPACCESS: {NodeCOMPACCESS, nil, nil, nil, nil, 0, nil, nil},
TokenLIST: {NodeLIST, nil, nil, nil, nil, 0, nil, nil},
TokenMAP: {NodeMAP, nil, nil, nil, nil, 0, nil, nil},
TokenPARAMS: {NodePARAMS, nil, nil, nil, nil, 0, nil, nil},
TokenGUARD: {NodeGUARD, nil, nil, nil, nil, 0, nil, nil},
// Condition operators
TokenGEQ: {NodeGEQ, nil, nil, nil, nil, 60, nil, ldInfix},
TokenLEQ: {NodeLEQ, nil, nil, nil, nil, 60, nil, ldInfix},
TokenNEQ: {NodeNEQ, nil, nil, nil, nil, 60, nil, ldInfix},
TokenEQ: {NodeEQ, nil, nil, nil, nil, 60, nil, ldInfix},
TokenGT: {NodeGT, nil, nil, nil, nil, 60, nil, ldInfix},
TokenLT: {NodeLT, nil, nil, nil, nil, 60, nil, ldInfix},
// Grouping symbols
TokenLPAREN: {"", nil, nil, nil, nil, 150, ndInner, nil},
TokenRPAREN: {"", nil, nil, nil, nil, 0, nil, nil},
TokenLBRACK: {"", nil, nil, nil, nil, 150, ndList, nil},
TokenRBRACK: {"", nil, nil, nil, nil, 0, nil, nil},
TokenLBRACE: {"", nil, nil, nil, nil, 150, ndMap, nil},
TokenRBRACE: {"", nil, nil, nil, nil, 0, nil, nil},
// Separators
TokenDOT: {"", nil, nil, nil, nil, 0, nil, nil},
TokenCOMMA: {"", nil, nil, nil, nil, 0, nil, nil},
TokenSEMICOLON: {"", nil, nil, nil, nil, 0, nil, nil},
// Grouping
TokenCOLON: {NodeKVP, nil, nil, nil, nil, 60, nil, ldInfix},
TokenEQUAL: {NodePRESET, nil, nil, nil, nil, 60, nil, ldInfix},
// Arithmetic operators
TokenPLUS: {NodePLUS, nil, nil, nil, nil, 110, ndPrefix, ldInfix},
TokenMINUS: {NodeMINUS, nil, nil, nil, nil, 110, ndPrefix, ldInfix},
TokenTIMES: {NodeTIMES, nil, nil, nil, nil, 120, nil, ldInfix},
TokenDIV: {NodeDIV, nil, nil, nil, nil, 120, nil, ldInfix},
TokenDIVINT: {NodeDIVINT, nil, nil, nil, nil, 120, nil, ldInfix},
TokenMODINT: {NodeMODINT, nil, nil, nil, nil, 120, nil, ldInfix},
// Assignment statement
TokenASSIGN: {NodeASSIGN, nil, nil, nil, nil, 10, nil, ldInfix},
TokenLET: {NodeLET, nil, nil, nil, nil, 0, ndPrefix, nil},
// Import statement
TokenIMPORT: {NodeIMPORT, nil, nil, nil, nil, 0, ndImport, nil},
TokenAS: {NodeAS, nil, nil, nil, nil, 0, nil, nil},
// Sink definition
TokenSINK: {NodeSINK, nil, nil, nil, nil, 0, ndSkink, nil},
TokenKINDMATCH: {NodeKINDMATCH, nil, nil, nil, nil, 150, ndPrefix, nil},
TokenSCOPEMATCH: {NodeSCOPEMATCH, nil, nil, nil, nil, 150, ndPrefix, nil},
TokenSTATEMATCH: {NodeSTATEMATCH, nil, nil, nil, nil, 150, ndPrefix, nil},
TokenPRIORITY: {NodePRIORITY, nil, nil, nil, nil, 150, ndPrefix, nil},
TokenSUPPRESSES: {NodeSUPPRESSES, nil, nil, nil, nil, 150, ndPrefix, nil},
// Function definition
TokenFUNC: {NodeFUNC, nil, nil, nil, nil, 0, ndFunc, nil},
TokenRETURN: {NodeRETURN, nil, nil, nil, nil, 0, ndReturn, nil},
// Boolean operators
TokenAND: {NodeAND, nil, nil, nil, nil, 40, nil, ldInfix},
TokenOR: {NodeOR, nil, nil, nil, nil, 30, nil, ldInfix},
TokenNOT: {NodeNOT, nil, nil, nil, nil, 20, ndPrefix, nil},
// Condition operators
TokenLIKE: {NodeLIKE, nil, nil, nil, nil, 60, nil, ldInfix},
TokenIN: {NodeIN, nil, nil, nil, nil, 60, nil, ldInfix},
TokenHASPREFIX: {NodeHASPREFIX, nil, nil, nil, nil, 60, nil, ldInfix},
TokenHASSUFFIX: {NodeHASSUFFIX, nil, nil, nil, nil, 60, nil, ldInfix},
TokenNOTIN: {NodeNOTIN, nil, nil, nil, nil, 60, nil, ldInfix},
// Constant terminals
TokenFALSE: {NodeFALSE, nil, nil, nil, nil, 0, ndTerm, nil},
TokenTRUE: {NodeTRUE, nil, nil, nil, nil, 0, ndTerm, nil},
TokenNULL: {NodeNULL, nil, nil, nil, nil, 0, ndTerm, nil},
// Conditional statements
TokenIF: {NodeIF, nil, nil, nil, nil, 0, ndGuard, nil},
TokenELIF: {"", nil, nil, nil, nil, 0, nil, nil},
TokenELSE: {"", nil, nil, nil, nil, 0, nil, nil},
// Loop statement
TokenFOR: {NodeLOOP, nil, nil, nil, nil, 0, ndLoop, nil},
TokenBREAK: {NodeBREAK, nil, nil, nil, nil, 0, ndTerm, nil},
TokenCONTINUE: {NodeCONTINUE, nil, nil, nil, nil, 0, ndTerm, nil},
// Try statement
TokenTRY: {NodeTRY, nil, nil, nil, nil, 0, ndTry, nil},
TokenEXCEPT: {NodeEXCEPT, nil, nil, nil, nil, 0, nil, nil},
TokenOTHERWISE: {NodeOTHERWISE, nil, nil, nil, nil, 0, nil, nil},
TokenFINALLY: {NodeFINALLY, nil, nil, nil, nil, 0, nil, nil},
// Mutex statement
TokenMUTEX: {NodeMUTEX, nil, nil, nil, nil, 0, ndMutex, nil},
}
}
// Parser
// ======
/*
Parser data structure
*/
type parser struct {
name string // Name to identify the input
node *ASTNode // Current ast node
tokens *LABuffer // Buffer which is connected to the channel which contains lex tokens
rp RuntimeProvider // Runtime provider which creates runtime components
}
/*
Parse parses a given input string and returns an AST.
*/
func Parse(name string, input string) (*ASTNode, error) {
return ParseWithRuntime(name, input, nil)
}
/*
ParseWithRuntime parses a given input string and returns an AST decorated with
runtime components.
*/
func ParseWithRuntime(name string, input string, rp RuntimeProvider) (*ASTNode, error) {
// Create a new parser with a look-ahead buffer of 3
p := &parser{name, nil, NewLABuffer(Lex(name, input), 3), rp}
// Read and set initial AST node
node, err := p.next()
if err != nil {
return nil, err
}
p.node = node
n, err := p.run(0)
if err == nil && hasMoreStatements(p, n) {
st := astNodeMap[TokenSTATEMENTS].instance(p, nil)
st.Children = append(st.Children, n)
for err == nil && hasMoreStatements(p, n) {
// Skip semicolons
if p.node.Token.ID == TokenSEMICOLON {
skipToken(p, TokenSEMICOLON)
}
n, err = p.run(0)
st.Children = append(st.Children, n)
}
n = st
}
if err == nil && p.node != nil && p.node.Token.ID != TokenEOF {
token := *p.node.Token
err = p.newParserError(ErrUnexpectedEnd, fmt.Sprintf("extra token id:%v (%v)",
token.ID, token), token)
}
return n, err
}
/*
run models the main parser function.
*/
func (p *parser) run(rightBinding int) (*ASTNode, error) {
var err error
n := p.node
p.node, err = p.next()
if err != nil {
return nil, err
}
// Start with the null denotation of this statement / expression
if n.nullDenotation == nil {
return nil, p.newParserError(ErrImpossibleNullDenotation,
n.Token.String(), *n.Token)
}
left, err := n.nullDenotation(p, n)
if err != nil {
return nil, err
}
// Collect left denotations as long as the left binding power is greater
// than the initial right one
for rightBinding < p.node.binding {
var nleft *ASTNode
n = p.node
if n.leftDenotation == nil {
if left.Token.Lline < n.Token.Lline {
// If the impossible left denotation is on a new line
// we might be parsing a new statement
return left, nil
}
return nil, p.newParserError(ErrImpossibleLeftDenotation,
n.Token.String(), *n.Token)
}
p.node, err = p.next()
if err != nil {
return nil, err
}
// Get the next left denotation
nleft, err = n.leftDenotation(p, n, left)
left = nleft
if err != nil {
return nil, err
}
}
return left, nil
}
/*
next retrieves the next lexer token.
*/
func (p *parser) next() (*ASTNode, error) {
var preComments []MetaData
var postComments []MetaData
token, more := p.tokens.Next()
for more && (token.ID == TokenPRECOMMENT || token.ID == TokenPOSTCOMMENT) {
if token.ID == TokenPRECOMMENT {
// Skip over pre comment token
preComments = append(preComments, NewLexTokenInstance(token))
token, more = p.tokens.Next()
}
if token.ID == TokenPOSTCOMMENT {
// Skip over post comment token
postComments = append(postComments, NewLexTokenInstance(token))
token, more = p.tokens.Next()
}
}
if !more {
// Unexpected end of input - the associated token is an empty error token
return nil, p.newParserError(ErrUnexpectedEnd, "", token)
} else if token.ID == TokenError {
// There was a lexer error wrap it in a parser error
return nil, p.newParserError(ErrLexicalError, token.Val, token)
} else if node, ok := astNodeMap[token.ID]; ok {
// We got a normal AST component
ret := node.instance(p, &token)
ret.Meta = append(ret.Meta, preComments...) // Attach pre comments to the next AST node
if len(postComments) > 0 && p.node != nil {
p.node.Meta = append(p.node.Meta, postComments...) // Attach post comments to the previous AST node
}
return ret, nil
}
return nil, p.newParserError(ErrUnknownToken, fmt.Sprintf("id:%v (%v)", token.ID, token), token)
}
// Standard null denotation functions
// ==================================
/*
ndTerm is used for terminals.
*/
func ndTerm(p *parser, self *ASTNode) (*ASTNode, error) {
return self, nil
}
/*
ndInner returns the inner expression of an enclosed block and discard the
block token. This method is used for brackets.
*/
func ndInner(p *parser, self *ASTNode) (*ASTNode, error) {
// Get the inner expression
exp, err := p.run(0)
if err != nil {
return nil, err
}
// We return here the inner expression - discarding the bracket tokens
return exp, skipToken(p, TokenRPAREN)
}
/*
ndPrefix is used for prefix operators.
*/
func ndPrefix(p *parser, self *ASTNode) (*ASTNode, error) {
// Make sure a prefix will only prefix the next item
val, err := p.run(self.binding + 20)
if err != nil {
return nil, err
}
self.Children = append(self.Children, val)
return self, nil
}
// Null denotation functions for specific expressions
// ==================================================
/*
ndImport is used to parse imports.
*/
func ndImport(p *parser, self *ASTNode) (*ASTNode, error) {
// Must specify a file path
err := acceptChild(p, self, TokenSTRING)
if err == nil {
// Must specify AS
if err = skipToken(p, TokenAS); err == nil {
// Must specify an identifier
err = acceptChild(p, self, TokenIDENTIFIER)
}
}
return self, err
}
/*
ndSink is used to parse sinks.
*/
func ndSkink(p *parser, self *ASTNode) (*ASTNode, error) {
var exp, ret *ASTNode
// Must specify a name
err := acceptChild(p, self, TokenIDENTIFIER)
if err == nil {
// Parse the rest of the parameters as children until we reach the body
for err == nil && IsNotEndAndNotTokens(p, []LexTokenID{TokenLBRACE}) {
if exp, err = p.run(150); err == nil {
self.Children = append(self.Children, exp)
// Skip commas
if p.node.Token.ID == TokenCOMMA {
err = skipToken(p, TokenCOMMA)
}
}
}
if err == nil {
// Parse the body
ret, err = parseInnerStatements(p, self)
}
}
return ret, err
}
/*
ndFunc is used to parse function definitions.
*/
func ndFunc(p *parser, self *ASTNode) (*ASTNode, error) {
var exp *ASTNode
var err error
// Might specify a function name
if p.node.Token.ID == TokenIDENTIFIER {
err = acceptChild(p, self, TokenIDENTIFIER)
}
// Read in parameters
if err == nil {
err = skipToken(p, TokenLPAREN)
params := astNodeMap[TokenPARAMS].instance(p, nil)
self.Children = append(self.Children, params)
for err == nil && IsNotEndAndNotTokens(p, []LexTokenID{TokenRPAREN}) {
// Parse all the expressions inside
if exp, err = p.run(0); err == nil {
params.Children = append(params.Children, exp)
if p.node.Token.ID == TokenCOMMA {
err = skipToken(p, TokenCOMMA)
}
}
}
if err == nil {
err = skipToken(p, TokenRPAREN)
}
}
if err == nil {
// Parse the body
self, err = parseInnerStatements(p, self)
}
return self, err
}
/*
ndReturn is used to parse return statements.
*/
func ndReturn(p *parser, self *ASTNode) (*ASTNode, error) {
var err error
if self.Token.Lline == p.node.Token.Lline {
var val *ASTNode
// Consume the next expression only if it is on the same line
val, err = p.run(0)
if err == nil {
self.Children = append(self.Children, val)
}
}
return self, err
}
/*
ndIdentifier is to parse identifiers and function calls.
*/
func ndIdentifier(p *parser, self *ASTNode) (*ASTNode, error) {
var parseMore, parseSegment, parseFuncCall, parseCompositionAccess func(parent *ASTNode) error
parseMore = func(current *ASTNode) error {
var err error
if p.node.Token.ID == TokenDOT {
err = parseSegment(current)
} else if p.node.Token.ID == TokenLPAREN {
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
err = parseCompositionAccess(current)
}
return err
}
parseSegment = func(current *ASTNode) error {
var err error
var next *ASTNode
if err = skipToken(p, TokenDOT); err == nil {
next = p.node
if err = acceptChild(p, current, TokenIDENTIFIER); err == nil {
err = parseMore(next)
}
}
return err
}
parseFuncCall = func(current *ASTNode) error {
var exp *ASTNode
err := skipToken(p, TokenLPAREN)
fc := astNodeMap[TokenFUNCCALL].instance(p, nil)
current.Children = append(current.Children, fc)
// Read in parameters
for err == nil && IsNotEndAndNotTokens(p, []LexTokenID{TokenRPAREN}) {
// Parse all the expressions inside the directives
if exp, err = p.run(0); err == nil {
fc.Children = append(fc.Children, exp)
if p.node.Token.ID == TokenCOMMA {
err = skipToken(p, TokenCOMMA)
}
}
}
if err == nil {
if err = skipToken(p, TokenRPAREN); err == nil {
err = parseMore(current)
}
}
return err
}
parseCompositionAccess = func(current *ASTNode) error {
var exp *ASTNode
var err error
ca := astNodeMap[TokenCOMPACCESS].instance(p, nil)
current.Children = append(current.Children, ca)
// Parse all the expressions inside the directives
if exp, err = p.run(0); err == nil {
ca.Children = append(ca.Children, exp)
if err = skipToken(p, TokenRBRACK); err == nil {
err = parseMore(current)
}
}
return err
}
return self, parseMore(self)
}
/*
ndList is used to collect elements of a list.
*/
func ndList(p *parser, self *ASTNode) (*ASTNode, error) {
var err error
var exp *ASTNode
// Create a list token
st := astNodeMap[TokenLIST].instance(p, self.Token)
// Get the inner expression
for err == nil && IsNotEndAndNotTokens(p, []LexTokenID{TokenRBRACK}) {
// Parse all the expressions inside
if exp, err = p.run(0); err == nil {
st.Children = append(st.Children, exp)
if p.node.Token.ID == TokenCOMMA {
err = skipToken(p, TokenCOMMA)
}
}
}
if err == nil {
err = skipToken(p, TokenRBRACK)
}
// Must have a closing bracket
return st, err
}
/*
ndMap is used to collect elements of a map.
*/
func ndMap(p *parser, self *ASTNode) (*ASTNode, error) {
var err error
var exp *ASTNode
// Create a map token
st := astNodeMap[TokenMAP].instance(p, self.Token)
// Get the inner expression
for err == nil && IsNotEndAndNotTokens(p, []LexTokenID{TokenRBRACE}) {
// Parse all the expressions inside
if exp, err = p.run(0); err == nil {
st.Children = append(st.Children, exp)
if p.node.Token.ID == TokenCOMMA {
err = skipToken(p, TokenCOMMA)
}
}
}
if err == nil {
err = skipToken(p, TokenRBRACE)
}
// Must have a closing brace
return st, err
}
/*
ndGuard is used to parse a conditional statement.
*/
func ndGuard(p *parser, self *ASTNode) (*ASTNode, error) {
var err error
parseGuardAndStatements := func() error {
// The brace starts statements while parsing the expression of an if statement
nodeMapEntryBak := astNodeMap[TokenLBRACE]
astNodeMap[TokenLBRACE] = &ASTNode{"", nil, nil, nil, nil, 0, parseInnerStatements, nil}
exp, err := p.run(0)
astNodeMap[TokenLBRACE] = nodeMapEntryBak
if err == nil {
g := astNodeMap[TokenGUARD].instance(p, nil)
g.Children = append(g.Children, exp)
self.Children = append(self.Children, g)
_, err = parseInnerStatements(p, self)
}
return err
}
if err = parseGuardAndStatements(); err == nil {
for err == nil && IsNotEndAndToken(p, TokenELIF) {
// Parse an elif
if err = skipToken(p, TokenELIF); err == nil {
err = parseGuardAndStatements()
}
}
if err == nil && p.node.Token.ID == TokenELSE {
// Parse else
if err = skipToken(p, TokenELSE); err == nil {
g := astNodeMap[TokenGUARD].instance(p, nil)
g.Children = append(g.Children, astNodeMap[TokenTRUE].instance(p, nil))
self.Children = append(self.Children, g)
_, err = parseInnerStatements(p, self)
}
}
}
return self, err
}
/*
ndLoop is used to parse a loop statement.
*/
func ndLoop(p *parser, self *ASTNode) (*ASTNode, error) {
// The brace starts statements while parsing the expression of a for statement
nodeMapEntryBak := astNodeMap[TokenLBRACE]
astNodeMap[TokenLBRACE] = &ASTNode{"", nil, nil, nil, nil, 0, parseInnerStatements, nil}
exp, err := p.run(0)
astNodeMap[TokenLBRACE] = nodeMapEntryBak
if err == nil {
g := exp
if exp.Token.ID != TokenIN {
g = astNodeMap[TokenGUARD].instance(p, nil)
g.Children = append(g.Children, exp)
}
self.Children = append(self.Children, g)
_, err = parseInnerStatements(p, self)
}
return self, err
}
/*
ndTry is used to parse a try block.
*/
func ndTry(p *parser, self *ASTNode) (*ASTNode, error) {
try, err := parseInnerStatements(p, self)
for err == nil && IsNotEndAndToken(p, TokenEXCEPT) {
except := p.node
err = acceptChild(p, try, TokenEXCEPT)
for err == nil &&
IsNotEndAndNotTokens(p, []LexTokenID{TokenAS, TokenIDENTIFIER, TokenLBRACE}) {
if err = acceptChild(p, except, TokenSTRING); err == nil {
// Skip commas
if p.node.Token.ID == TokenCOMMA {
err = skipToken(p, TokenCOMMA)
}
}
}
if err == nil {
if p.node.Token.ID == TokenAS {
as := p.node
if err = acceptChild(p, except, TokenAS); err == nil {
err = acceptChild(p, as, TokenIDENTIFIER)
}
} else if p.node.Token.ID == TokenIDENTIFIER {
err = acceptChild(p, except, TokenIDENTIFIER)
}
}
if err == nil {
_, err = parseInnerStatements(p, except)
}
}
return ndOtherwiseFinally(p, try, err)
}
/*
ndOtherwiseFinally is used to parse otherwise and finally blocks.
*/
func ndOtherwiseFinally(p *parser, try *ASTNode, err error) (*ASTNode, error) {
if err == nil && p.node.Token.ID == TokenOTHERWISE {
otherwise := p.node
if err = acceptChild(p, try, TokenOTHERWISE); err == nil {
_, err = parseInnerStatements(p, otherwise)
}
}
if err == nil && p.node.Token.ID == TokenFINALLY {
finally := p.node
if err = acceptChild(p, try, TokenFINALLY); err == nil {
_, err = parseInnerStatements(p, finally)
}
}
return try, err
}
/*
ndMutex is used to parse a mutex block.
*/
func ndMutex(p *parser, self *ASTNode) (*ASTNode, error) {
var block *ASTNode
err := acceptChild(p, self, TokenIDENTIFIER)
if err == nil {
block, err = parseInnerStatements(p, self)
}
return block, err
}
// Standard left denotation functions
// ==================================
/*
ldInfix is used for infix operators.
*/
func ldInfix(p *parser, self *ASTNode, left *ASTNode) (*ASTNode, error) {
right, err := p.run(self.binding)
if err != nil {
return nil, err
}
self.Children = append(self.Children, left)
self.Children = append(self.Children, right)
return self, nil
}
// Helper functions
// ================
/*
IsNotEndAndToken checks if the next token is of a specific type or the end has been reached.
*/
func IsNotEndAndToken(p *parser, i LexTokenID) bool {
return p.node != nil && p.node.Name != NodeEOF && p.node.Token.ID == i
}
/*
IsNotEndAndNotTokens checks if the next token is not of a specific type or the end has been reached.
*/
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
}
/*
hasMoreStatements returns true if there are more statements to parse.
*/
func hasMoreStatements(p *parser, currentNode *ASTNode) bool {
nextNode := p.node
if nextNode == nil || nextNode.Token.ID == TokenEOF {
return false
} else if nextNode.Token.ID == TokenSEMICOLON {
return true
}
return currentNode != nil && currentNode.Token.Lline < nextNode.Token.Lline
}
/*
skipToken skips over a given token.
*/
func skipToken(p *parser, ids ...LexTokenID) error {
var err error
canSkip := func(id LexTokenID) bool {
for _, i := range ids {
if i == id {
return true
}
}
return false
}
if !canSkip(p.node.Token.ID) {
if p.node.Token.ID == TokenEOF {
return p.newParserError(ErrUnexpectedEnd, "", *p.node.Token)
}
return p.newParserError(ErrUnexpectedToken, p.node.Token.Val, *p.node.Token)
}
// This should never return an error unless we skip over EOF or complex tokens
// like values
p.node, err = p.next()
return err
}
/*
acceptChild accepts the current token as a child.
*/
func acceptChild(p *parser, self *ASTNode, id LexTokenID) error {
var err error
current := p.node
if p.node, err = p.next(); err == nil {
if current.Token.ID == id {
self.Children = append(self.Children, current)
} else {
err = p.newParserError(ErrUnexpectedToken, current.Token.Val, *current.Token)
}
}
return err
}
/*
parseInnerStatements collects the inner statements of a block statement. It
is assumed that a block statement starts with a left brace '{' and ends with
a right brace '}'.
*/
func parseInnerStatements(p *parser, self *ASTNode) (*ASTNode, error) {
// Must start with an opening brace
if err := skipToken(p, TokenLBRACE); err != nil {
return nil, err
}
// Always create a statements node
st := astNodeMap[TokenSTATEMENTS].instance(p, nil)
self.Children = append(self.Children, st)
// Check if there are actually children
if p.node != nil && p.node.Token.ID != TokenRBRACE {
n, err := p.run(0)
if p.node != nil && p.node.Token.ID != TokenEOF {
st.Children = append(st.Children, n)
for hasMoreStatements(p, n) {
if p.node.Token.ID == TokenSEMICOLON {
skipToken(p, TokenSEMICOLON)
} else if p.node.Token.ID == TokenRBRACE {
break
}
n, err = p.run(0)
st.Children = append(st.Children, n)
}
}
if err != nil {
return nil, err
}
}
// Must end with a closing brace
return self, skipToken(p, TokenRBRACE)
}
/*
* 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 parser
import (
"errors"
"fmt"
)
/*
newParserError creates a new ParserError object.
*/
func (p *parser) newParserError(t error, d string, token LexToken) error {
return &Error{p.name, t, d, token.Lline, token.Lpos}
}
/*
Error models a parser related error.
*/
type Error struct {
Source string // Name of the source which was given to the parser
Type error // Error type (to be used for equal checks)
Detail string // Details of this error
Line int // Line of the error
Pos int // Position of the error
}
/*
Error returns a human-readable string representation of this error.
*/
func (pe *Error) Error() string {
var ret string
if pe.Detail != "" {
ret = fmt.Sprintf("Parse error in %s: %v (%v)", pe.Source, pe.Type, pe.Detail)
} else {
ret = fmt.Sprintf("Parse error in %s: %v", pe.Source, pe.Type)
}
if pe.Line != 0 {
return fmt.Sprintf("%s (Line:%d Pos:%d)", ret, pe.Line, pe.Pos)
}
return ret
}
/*
Parser related error types
*/
var (
ErrUnexpectedEnd = errors.New("Unexpected end")
ErrLexicalError = errors.New("Lexical error")
ErrUnknownToken = errors.New("Unknown term")
ErrImpossibleNullDenotation = errors.New("Term cannot start an expression")
ErrImpossibleLeftDenotation = errors.New("Term can only start an expression")
ErrUnexpectedToken = errors.New("Unexpected term")
)
/*
* 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 parser
import (
"bufio"
"bytes"
"fmt"
"strconv"
"strings"
"text/template"
"unicode"
"devt.de/krotik/common/errorutil"
"devt.de/krotik/common/stringutil"
)
/*
IndentationLevel is the level of indentation which the pretty printer should use
*/
const IndentationLevel = 4
/*
Map of AST nodes corresponding to lexer tokens
*/
var prettyPrinterMap map[string]*template.Template
/*
Map of nodes where the precedence might have changed because of parentheses
*/
var bracketPrecedenceMap map[string]bool
func init() {
prettyPrinterMap = map[string]*template.Template{
NodeSTRING: template.Must(template.New(NodeSTRING).Parse("{{.qval}}")),
NodeNUMBER: template.Must(template.New(NodeNUMBER).Parse("{{.val}}")),
// NodeIDENTIFIER - Special case (handled in code)
// Constructed tokens
// NodeSTATEMENTS - Special case (handled in code)
// NodeFUNCCALL - Special case (handled in code)
NodeCOMPACCESS + "_1": template.Must(template.New(NodeCOMPACCESS).Parse("[{{.c1}}]")),
// TokenLIST - Special case (handled in code)
// TokenMAP - Special case (handled in code)
// TokenPARAMS - Special case (handled in code)
NodeGUARD + "_1": template.Must(template.New(NodeGUARD).Parse("{{.c1}}")),
// Condition operators
NodeGEQ + "_2": template.Must(template.New(NodeGEQ).Parse("{{.c1}} >= {{.c2}}")),
NodeLEQ + "_2": template.Must(template.New(NodeLEQ).Parse("{{.c1}} <= {{.c2}}")),
NodeNEQ + "_2": template.Must(template.New(NodeNEQ).Parse("{{.c1}} != {{.c2}}")),
NodeEQ + "_2": template.Must(template.New(NodeEQ).Parse("{{.c1}} == {{.c2}}")),
NodeGT + "_2": template.Must(template.New(NodeGT).Parse("{{.c1}} > {{.c2}}")),
NodeLT + "_2": template.Must(template.New(NodeLT).Parse("{{.c1}} < {{.c2}}")),
// Separators
NodeKVP + "_2": template.Must(template.New(NodeKVP).Parse("{{.c1}} : {{.c2}}")),
NodePRESET + "_2": template.Must(template.New(NodePRESET).Parse("{{.c1}}={{.c2}}")),
// Arithmetic operators
NodePLUS + "_1": template.Must(template.New(NodePLUS).Parse("+{{.c1}}")),
NodePLUS + "_2": template.Must(template.New(NodePLUS).Parse("{{.c1}} + {{.c2}}")),
NodeMINUS + "_1": template.Must(template.New(NodeMINUS).Parse("-{{.c1}}")),
NodeMINUS + "_2": template.Must(template.New(NodeMINUS).Parse("{{.c1}} - {{.c2}}")),
NodeTIMES + "_2": template.Must(template.New(NodeTIMES).Parse("{{.c1}} * {{.c2}}")),
NodeDIV + "_2": template.Must(template.New(NodeDIV).Parse("{{.c1}} / {{.c2}}")),
NodeMODINT + "_2": template.Must(template.New(NodeMODINT).Parse("{{.c1}} % {{.c2}}")),
NodeDIVINT + "_2": template.Must(template.New(NodeDIVINT).Parse("{{.c1}} // {{.c2}}")),
// Assignment statement
NodeASSIGN + "_2": template.Must(template.New(NodeASSIGN).Parse("{{.c1}} := {{.c2}}")),
NodeLET + "_1": template.Must(template.New(NodeASSIGN).Parse("let {{.c1}}")),
// Import statement
NodeIMPORT + "_2": template.Must(template.New(NodeIMPORT).Parse("import {{.c1}} as {{.c2}}")),
NodeAS + "_1": template.Must(template.New(NodeRETURN).Parse("as {{.c1}}")),
// Sink definition
// NodeSINK - Special case (handled in code)
NodeKINDMATCH + "_1": template.Must(template.New(NodeKINDMATCH).Parse("kindmatch {{.c1}}")),
NodeSCOPEMATCH + "_1": template.Must(template.New(NodeSCOPEMATCH).Parse("scopematch {{.c1}}")),
NodeSTATEMATCH + "_1": template.Must(template.New(NodeSTATEMATCH).Parse("statematch {{.c1}}")),
NodePRIORITY + "_1": template.Must(template.New(NodePRIORITY).Parse("priority {{.c1}}")),
NodeSUPPRESSES + "_1": template.Must(template.New(NodeSUPPRESSES).Parse("suppresses {{.c1}}")),
// Function definition
NodeFUNC + "_2": template.Must(template.New(NodeFUNC).Parse("func {{.c1}} {\n{{.c2}}}")),
NodeFUNC + "_3": template.Must(template.New(NodeFUNC).Parse("func {{.c1}}{{.c2}} {\n{{.c3}}}")),
NodeRETURN: template.Must(template.New(NodeRETURN).Parse("return")),
NodeRETURN + "_1": template.Must(template.New(NodeRETURN).Parse("return {{.c1}}")),
// Boolean operators
NodeOR + "_2": template.Must(template.New(NodeOR).Parse("{{.c1}} or {{.c2}}")),
NodeAND + "_2": template.Must(template.New(NodeAND).Parse("{{.c1}} and {{.c2}}")),
NodeNOT + "_1": template.Must(template.New(NodeNOT).Parse("not {{.c1}}")),
// Condition operators
NodeLIKE + "_2": template.Must(template.New(NodeLIKE).Parse("{{.c1}} like {{.c2}}")),
NodeIN + "_2": template.Must(template.New(NodeIN).Parse("{{.c1}} in {{.c2}}")),
NodeHASPREFIX + "_2": template.Must(template.New(NodeHASPREFIX).Parse("{{.c1}} hasprefix {{.c2}}")),
NodeHASSUFFIX + "_2": template.Must(template.New(NodeHASSUFFIX).Parse("{{.c1}} hassuffix {{.c2}}")),
NodeNOTIN + "_2": template.Must(template.New(NodeNOTIN).Parse("{{.c1}} notin {{.c2}}")),
// Constant terminals
NodeTRUE: template.Must(template.New(NodeTRUE).Parse("true")),
NodeFALSE: template.Must(template.New(NodeFALSE).Parse("false")),
NodeNULL: template.Must(template.New(NodeNULL).Parse("null")),
// Conditional statements
// TokenIF - Special case (handled in code)
// TokenELIF - Special case (handled in code)
// TokenELSE - Special case (handled in code)
// Loop statement
NodeLOOP + "_2": template.Must(template.New(NodeLOOP).Parse("for {{.c1}} {\n{{.c2}}}")),
NodeBREAK: template.Must(template.New(NodeBREAK).Parse("break")),
NodeCONTINUE: template.Must(template.New(NodeCONTINUE).Parse("continue")),
// Try statement
// TokenTRY - Special case (handled in code)
// TokenEXCEPT - Special case (handled in code)
NodeOTHERWISE + "_1": template.Must(template.New(NodeOTHERWISE).Parse(" otherwise {\n{{.c1}}}")),
NodeFINALLY + "_1": template.Must(template.New(NodeFINALLY).Parse(" finally {\n{{.c1}}}")),
// Mutex block
NodeMUTEX + "_2": template.Must(template.New(NodeLOOP).Parse("mutex {{.c1}} {\n{{.c2}}}\n")),
}
bracketPrecedenceMap = map[string]bool{
NodePLUS: true,
NodeMINUS: true,
NodeAND: true,
NodeOR: true,
}
}
/*
PrettyPrint produces pretty printed code from a given AST.
*/
func PrettyPrint(ast *ASTNode) (string, error) {
var visit func(ast *ASTNode, path []*ASTNode) (string, error)
visit = func(ast *ASTNode, path []*ASTNode) (string, error) {
var buf bytes.Buffer
if ast == nil {
return "", fmt.Errorf("Nil pointer in AST")
}
numChildren := len(ast.Children)
tempKey := ast.Name
tempParam := make(map[string]string)
// First pretty print children
if numChildren > 0 {
for i, child := range ast.Children {
res, err := visit(child, append(path, child))
if err != nil {
return "", err
}
if _, ok := bracketPrecedenceMap[child.Name]; ok && ast.binding > child.binding {
// Put the expression in brackets iff (if and only if) the binding would
// normally order things differently
res = fmt.Sprintf("(%v)", res)
}
tempParam[fmt.Sprint("c", i+1)] = res
}
tempKey += fmt.Sprint("_", len(tempParam))
}
if res, ok := ppSpecialDefs(ast, path, tempParam, &buf); ok {
return res, nil
} else if res, ok := ppSpecialBlocks(ast, path, tempParam, &buf); ok {
return res, nil
} else if res, ok := ppContainerBlocks(ast, path, tempParam, &buf); ok {
return res, nil
} else if res, ok := ppSpecialStatements(ast, path, tempParam, &buf); ok {
return res, nil
}
if ast.Token != nil {
// Adding node value to template parameters
tempParam["val"] = ast.Token.Val
tempParam["qval"] = strconv.Quote(ast.Token.Val)
}
// Retrieve the template
temp, ok := prettyPrinterMap[tempKey]
errorutil.AssertTrue(ok,
fmt.Sprintf("Could not find template for %v (tempkey: %v)",
ast.Name, tempKey))
// Use the children as parameters for template
errorutil.AssertOk(temp.Execute(&buf, tempParam))
return ppPostProcessing(ast, path, buf.String()), nil
}
res, err := visit(ast, []*ASTNode{ast})
return strings.TrimSpace(res), err
}
/*
ppPostProcessing applies post processing rules.
*/
func ppPostProcessing(ast *ASTNode, path []*ASTNode, ppString string) string {
// Add meta data
ret := ppMetaData(ast, path, ppString)
// Apply indentation
if len(path) > 1 {
if stringutil.IndexOf(ast.Name, []string{
NodeSTATEMENTS,
NodeMAP,
NodeLIST,
NodeKINDMATCH,
NodeSTATEMATCH,
NodeSCOPEMATCH,
NodePRIORITY,
NodeSUPPRESSES,
}) != -1 {
parent := path[len(path)-2]
indentSpaces := stringutil.GenerateRollingString(" ", IndentationLevel)
ret = strings.ReplaceAll(ret, "\n", "\n"+indentSpaces)
// Add initial indent only if we are inside a block statement
if stringutil.IndexOf(parent.Name, []string{
NodeRETURN,
NodeIN,
NodeASSIGN,
NodePRESET,
NodeKVP,
NodeLIST,
NodeFUNCCALL,
NodeKINDMATCH,
NodeSTATEMATCH,
NodeSCOPEMATCH,
NodePRIORITY,
NodeSUPPRESSES,
}) == -1 {
ret = fmt.Sprintf("%v%v", indentSpaces, ret)
}
// Remove indentation from last line unless we have a special case
if stringutil.IndexOf(parent.Name, []string{
NodeSINK,
}) == -1 || ast.Name == NodeSTATEMENTS {
if idx := strings.LastIndex(ret, "\n"); idx != -1 {
ret = ret[:idx+1] + ret[idx+IndentationLevel+1:]
}
}
}
}
if ast.Token != nil {
// Calculate number of extra newlines which should be prefixed the default
// pretty printer assumes a single one which produces very compact code
// the previous formatting might give a hint where to add extra newlines.
// The pretty printer only ever adds a maximum of 1 additional line per
// statement
if ast.Token.PrefixNewlines-1 > 0 {
ret = fmt.Sprintf("\n%v", ret)
}
}
// Remove all trailing spaces
newlineSplit := strings.Split(ret, "\n")
for i, s := range newlineSplit {
newlineSplit[i] = strings.TrimRightFunc(s, unicode.IsSpace)
}
return strings.Join(newlineSplit, "\n")
}
/*
ppMetaData pretty prints comments.
*/
func ppMetaData(ast *ASTNode, path []*ASTNode, ppString string) string {
ret := ppString
if len(ast.Meta) > 0 {
for _, meta := range ast.Meta {
metaValue := meta.Value()
if meta.Type() == MetaDataPreComment {
var buf bytes.Buffer
scanner := bufio.NewScanner(strings.NewReader(metaValue))
for scanner.Scan() {
buf.WriteString(fmt.Sprintf(" %v\n", strings.TrimSpace(scanner.Text())))
}
if ast.Token.Lpos != 1 || strings.Index(metaValue, "\n") == -1 {
// Remove the last newline if we are not on the root level
// or we didn't have any newlines in the original comment
buf.Truncate(buf.Len() - 1)
}
if strings.Index(buf.String(), "\n") == -1 {
// If the pretty printed comment does not have any newlines
// Add at least a space at the end
buf.WriteString(" ")
}
ret = fmt.Sprintf("/*%v*/\n%v", buf.String(), ret)
if ast.Token.Lline > 1 {
ret = fmt.Sprintf("\n%v", ret)
}
} else if meta.Type() == MetaDataPostComment {
metaValue = strings.TrimSpace(strings.ReplaceAll(metaValue, "\n", ""))
ret = fmt.Sprintf("%v # %v", ret, metaValue)
}
}
}
return ret
}
/*
ppSpecialDefs pretty prints special cases.
*/
func ppSpecialDefs(ast *ASTNode, path []*ASTNode, tempParam map[string]string, buf *bytes.Buffer) (string, bool) {
numChildren := len(ast.Children)
if ast.Name == NodeFUNCCALL {
for i := 0; i < numChildren; i++ {
buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
if i < numChildren-1 {
buf.WriteString(", ")
}
}
return ppPostProcessing(ast, path, buf.String()), true
} else if ast.Name == NodeSINK {
buf.WriteString("sink ")
buf.WriteString(tempParam["c1"])
buf.WriteString("\n")
for i := 1; i < len(ast.Children)-1; i++ {
buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
buf.WriteString("\n")
}
buf.WriteString("{\n")
buf.WriteString(tempParam[fmt.Sprint("c", len(ast.Children))])
buf.WriteString("}\n")
return ppPostProcessing(ast, path, buf.String()), true
}
return "", false
}
/*
ppContainerBlocks pretty prints container structures.
*/
func ppContainerBlocks(ast *ASTNode, path []*ASTNode, tempParam map[string]string, buf *bytes.Buffer) (string, bool) {
numChildren := len(ast.Children)
if ast.Name == NodeLIST {
multilineThreshold := 4
buf.WriteString("[")
if numChildren > multilineThreshold {
buf.WriteString("\n")
}
for i := 0; i < numChildren; i++ {
buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
if i < numChildren-1 {
if numChildren > multilineThreshold {
buf.WriteString(",")
} else {
buf.WriteString(", ")
}
}
if numChildren > multilineThreshold {
buf.WriteString("\n")
}
}
buf.WriteString("]")
return ppPostProcessing(ast, path, buf.String()), true
} else if ast.Name == NodeMAP {
multilineThreshold := 2
buf.WriteString("{")
if numChildren > multilineThreshold {
buf.WriteString("\n")
}
for i := 0; i < numChildren; i++ {
buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
if i < numChildren-1 {
if numChildren > multilineThreshold {
buf.WriteString(",")
} else {
buf.WriteString(", ")
}
}
if numChildren > multilineThreshold {
buf.WriteString("\n")
}
}
buf.WriteString("}")
return ppPostProcessing(ast, path, buf.String()), true
}
return "", false
}
/*
ppSpecialBlocks pretty prints special cases.
*/
func ppSpecialBlocks(ast *ASTNode, path []*ASTNode, tempParam map[string]string, buf *bytes.Buffer) (string, bool) {
numChildren := len(ast.Children)
// Handle special cases - children in tempParam have been resolved
if ast.Name == NodeSTATEMENTS {
// For statements just concat all children
for i := 0; i < numChildren; i++ {
buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
buf.WriteString("\n")
}
return ppPostProcessing(ast, path, buf.String()), true
} else if ast.Name == NodeTRY {
buf.WriteString("try {\n")
buf.WriteString(tempParam[fmt.Sprint("c1")])
buf.WriteString("}")
for i := 1; i < len(ast.Children); i++ {
buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
}
return ppPostProcessing(ast, path, buf.String()), true
} else if ast.Name == NodeEXCEPT {
buf.WriteString(" except ")
for i := 0; i < len(ast.Children)-1; i++ {
buf.WriteString(tempParam[fmt.Sprint("c", i+1)])
if ast.Children[i+1].Name != NodeAS && i < len(ast.Children)-2 {
buf.WriteString(",")
}
buf.WriteString(" ")
}
buf.WriteString("{\n")
buf.WriteString(tempParam[fmt.Sprint("c", len(ast.Children))])
buf.WriteString("}")
return ppPostProcessing(ast, path, buf.String()), true
}
return "", false
}
/*
ppSpecialStatements pretty prints special cases.
*/
func ppSpecialStatements(ast *ASTNode, path []*ASTNode, tempParam map[string]string, buf *bytes.Buffer) (string, bool) {
numChildren := len(ast.Children)
if ast.Name == NodeIDENTIFIER {
buf.WriteString(ast.Token.Val)
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)])
}
}
return ppPostProcessing(ast, path, buf.String()), true
} else if ast.Name == NodePARAMS {
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 ppPostProcessing(ast, path, buf.String()), true
} else if ast.Name == NodeIF {
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("if ")
writeGUARD(1)
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)
}
}
return ppPostProcessing(ast, path, buf.String()), true
}
return "", false
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
/*
Package scope contains the block scope implementation for the event condition language ECAL.
*/
package scope
import (
"fmt"
"devt.de/krotik/common/stringutil"
"devt.de/krotik/ecal/parser"
)
/*
Default scope names
*/
const (
GlobalScope = "GlobalScope"
FuncPrefix = "func:"
)
/*
NameFromASTNode returns a scope name from a given ASTNode.
*/
func NameFromASTNode(node *parser.ASTNode) string {
return fmt.Sprintf("block: %v (Line:%d Pos:%d)", node.Name, node.Token.Lline, node.Token.Lpos)
}
/*
EvalToString should be used if a value should be converted into a string.
*/
func EvalToString(v interface{}) string {
return stringutil.ConvertToString(v)
}
/*
ToObject converts a Scope into an object.
*/
func ToObject(vs parser.Scope) map[interface{}]interface{} {
res := make(map[interface{}]interface{})
for k, v := range vs.(*varsScope).storage {
res[k] = v
}
return res
}
/*
ToScope converts a given object into a Scope.
*/
func ToScope(name string, o map[interface{}]interface{}) parser.Scope {
vs := NewScope(name)
for k, v := range o {
vs.SetValue(fmt.Sprint(k), v)
}
return vs
}
/*
ConvertJSONToECALObject converts a JSON container structure into an object which
can be used by ECAL.
*/
func ConvertJSONToECALObject(v interface{}) interface{} {
res := v
if mapContainer, ok := v.(map[interface{}]interface{}); ok {
newRes := make(map[interface{}]interface{})
for mk, mv := range mapContainer {
newRes[mk] = ConvertJSONToECALObject(mv)
}
res = newRes
} else if mapContainer, ok := v.(map[string]interface{}); ok {
newRes := make(map[interface{}]interface{})
for mk, mv := range mapContainer {
newRes[mk] = ConvertJSONToECALObject(mv)
}
res = newRes
} else if mapList, ok := v.([]interface{}); ok {
newRes := make([]interface{}, len(mapList))
for i, lv := range mapList {
newRes[i] = ConvertJSONToECALObject(lv)
}
res = newRes
} else if mapList, ok := v.([]map[string]interface{}); ok {
newRes := make([]interface{}, len(mapList))
for i, lv := range mapList {
newRes[i] = ConvertJSONToECALObject(lv)
}
res = newRes
}
return res
}
/*
ConvertECALToJSONObject converts an ECAL container structure into an object which
can be marshalled into a JSON string.
*/
func ConvertECALToJSONObject(v interface{}) interface{} {
res := v
if mapContainer, ok := v.(map[interface{}]interface{}); ok {
newRes := make(map[string]interface{})
for mk, mv := range mapContainer {
newRes[fmt.Sprint(mk)] = ConvertECALToJSONObject(mv)
}
res = newRes
} else if mapList, ok := v.([]interface{}); ok {
newRes := make([]interface{}, len(mapList))
for i, lv := range mapList {
newRes[i] = ConvertECALToJSONObject(lv)
}
res = newRes
}
return res
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package scope
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"sync"
"devt.de/krotik/common/stringutil"
"devt.de/krotik/ecal/parser"
)
/*
varsScope models a scope for variables in ECAL.
*/
type varsScope struct {
name string // Name of the scope
parent parser.Scope // Parent scope
children []*varsScope // Children of this scope (only if tracking is enabled)
storage map[string]interface{} // Storage for variables
lock *sync.RWMutex // Lock for this scope
}
/*
NewScope creates a new variable scope.
*/
func NewScope(name string) parser.Scope {
return NewScopeWithParent(name, nil)
}
/*
NewScopeWithParent creates a new variable scope with a parent. This can be
used to create scope structures without children links.
*/
func NewScopeWithParent(name string, parent parser.Scope) parser.Scope {
res := &varsScope{name, nil, nil, make(map[string]interface{}), &sync.RWMutex{}}
SetParentOfScope(res, parent)
return res
}
/*
SetParentOfScope sets the parent of a given scope. This assumes that the given scope
is a varsScope.
*/
func SetParentOfScope(scope parser.Scope, parent parser.Scope) {
if pvs, ok := parent.(*varsScope); ok {
if vs, ok := scope.(*varsScope); ok {
vs.lock.Lock()
defer vs.lock.Unlock()
pvs.lock.Lock()
defer pvs.lock.Unlock()
vs.parent = parent
vs.lock = pvs.lock
}
}
}
/*
NewChild creates a new child scope for variables. The new child scope is tracked
by the parent scope. This means it should not be used for global scopes with
many children.
*/
func (s *varsScope) NewChild(name string) parser.Scope {
s.lock.Lock()
defer s.lock.Unlock()
for _, c := range s.children {
if c.name == name {
return c
}
}
child := NewScope(name).(*varsScope)
child.parent = s
child.lock = s.lock
s.children = append(s.children, child)
return child
}
/*
Name returns the name of this scope.
*/
func (s *varsScope) Name() string {
return s.name
}
/*
Clear clears this scope of all stored values. This will clear children scopes
but not remove parent scopes.
*/
func (s *varsScope) Clear() {
s.children = nil
s.storage = make(map[string]interface{})
}
/*
Parent returns the parent scope or nil.
*/
func (s *varsScope) Parent() parser.Scope {
return s.parent
}
/*
SetValue sets a new value for a variable.
*/
func (s *varsScope) SetValue(varName string, varValue interface{}) error {
s.lock.Lock()
defer s.lock.Unlock()
return s.setValue(varName, varValue)
}
/*
SetLocalValue sets a new value for a local variable.
*/
func (s *varsScope) SetLocalValue(varName string, varValue interface{}) error {
s.lock.Lock()
defer s.lock.Unlock()
// Ensure the variable exists in the local scope
localVarName := strings.Split(varName, ".")[0]
s.storage[localVarName] = nil
return s.setValue(varName, varValue)
}
/*
setValue sets a new value for a variable.
*/
func (s *varsScope) setValue(varName string, varValue interface{}) error {
var err error
// Check for dotted names which access a container structure
if cFields := strings.Split(varName, "."); len(cFields) > 1 {
// Get the container
if container, ok, _ := s.getValue(cFields[0]); ok {
if len(cFields) > 2 {
container, err = s.containerAccess(container, cFields, cFields[1:])
}
if err == nil && container != nil {
fieldIndex := cFields[len(cFields)-1]
if mapContainer, ok := container.(map[interface{}]interface{}); ok {
mapContainer[fieldIndex] = varValue
} else if listContainer, ok := container.([]interface{}); ok {
var index int
if index, err = strconv.Atoi(fieldIndex); err == nil {
if index < 0 {
// Handle negative numbers
index = len(listContainer) + index
}
if index < len(listContainer) {
listContainer[index] = varValue
} else {
err = fmt.Errorf("Out of bounds access to list %v with index: %v",
strings.Join(cFields[:len(cFields)-1], "."), index)
}
} else {
err = fmt.Errorf("List %v needs a number index not: %v",
strings.Join(cFields[:len(cFields)-1], "."), fieldIndex)
}
} else {
err = fmt.Errorf("Variable %v is not a container",
strings.Join(cFields[:len(cFields)-1], "."))
}
}
} else {
err = fmt.Errorf("Variable %v is not a container", cFields[0])
}
return err
}
// Check if the variable is already defined in a parent scope
if vs := s.getScopeForVariable(varName); vs != nil {
s = vs
}
// Set value newly in scope
s.storage[varName] = varValue
return err
}
/*
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.
*/
func (s *varsScope) getScopeForVariable(varName string) *varsScope {
_, ok := s.storage[varName]
if ok {
return s
} else if s.parent != nil {
return s.parent.(*varsScope).getScopeForVariable(varName)
}
return nil
}
/*
GetValue gets the current value of a variable.
*/
func (s *varsScope) GetValue(varName string) (interface{}, bool, error) {
s.lock.Lock()
defer s.lock.Unlock()
return s.getValue(varName)
}
/*
getValue gets the current value of a variable.
*/
func (s *varsScope) getValue(varName string) (interface{}, bool, error) {
// Check for dotted names which access a container structure
if cFields := strings.Split(varName, "."); len(cFields) > 1 {
var err error
var containerAccess func(fields []string, container interface{}) (interface{}, bool, error)
// Get the container
container, ok, _ := s.getValue(cFields[0])
if !ok {
return nil, ok, err
}
// Now look into the container and get the value
containerAccess = func(fields []string, container interface{}) (interface{}, bool, error) {
var retContainer interface{}
if mapContainer, ok := container.(map[interface{}]interface{}); ok {
var ok bool
if index, err := strconv.Atoi(fmt.Sprint(fields[0])); err == nil {
// Numbers are usually converted to float64
retContainer, ok = mapContainer[float64(index)]
}
if !ok {
retContainer = mapContainer[fields[0]]
}
} else if listContainer, ok := container.([]interface{}); ok {
var index int
if index, err = strconv.Atoi(fmt.Sprint(fields[0])); err == nil {
if index < 0 {
// Handle negative numbers
index = len(listContainer) + index
}
if index < len(listContainer) {
retContainer = listContainer[index]
} else {
err = fmt.Errorf("Out of bounds access to list %v with index: %v",
strings.Join(cFields[:len(cFields)-len(fields)], "."), index)
}
} else {
err = fmt.Errorf("List %v needs a number index not: %v",
strings.Join(cFields[:len(cFields)-len(fields)], "."), fields[0])
}
} else {
err = fmt.Errorf("Variable %v is not a container",
strings.Join(cFields[:len(cFields)-len(fields)], "."))
}
if err == nil && len(fields) > 1 {
return containerAccess(fields[1:], retContainer)
}
return retContainer, retContainer != nil, err
}
return containerAccess(cFields[1:], container)
}
if vs := s.getScopeForVariable(varName); vs != nil {
ret := vs.storage[varName]
return ret, true, nil
}
return nil, false, nil
}
/*
String returns a string representation of this varsScope and all its
parents.
*/
func (s *varsScope) String() string {
s.lock.RLock()
defer s.lock.RUnlock()
return s.scopeStringParents(s.scopeStringChildren())
}
/*
ToJSONObject returns this ASTNode and all its children as a JSON object.
*/
func (s *varsScope) ToJSONObject() map[string]interface{} {
s.lock.RLock()
defer s.lock.RUnlock()
ret := make(map[string]interface{})
for k, v := range s.storage {
var value interface{}
value = fmt.Sprintf("ComplexDataStructure: %#v", v)
bytes, err := json.Marshal(v)
if err != nil {
bytes, err = json.Marshal(stringutil.ConvertToJSONMarshalableObject(v))
}
if err == nil {
json.Unmarshal(bytes, &value)
}
ret[k] = value
}
return ret
}
/*
scopeStringChildren returns a string representation of all children scopes.
*/
func (s *varsScope) scopeStringChildren() string {
var buf bytes.Buffer
// Write the known child scopes
for i, c := range s.children {
buf.WriteString(c.scopeString(c.scopeStringChildren()))
if i < len(s.children)-1 {
buf.WriteString("\n")
}
}
return buf.String()
}
/*
scopeStringParents returns a string representation of this varsScope
with initial children and all its parents.
*/
func (s *varsScope) scopeStringParents(childrenString string) string {
ss := s.scopeString(childrenString)
if s.parent != nil {
return s.parent.(*varsScope).scopeStringParents(ss)
}
return fmt.Sprint(ss)
}
/*
scopeString returns a string representation of this varsScope.
*/
func (s *varsScope) scopeString(childrenString string) string {
buf := bytes.Buffer{}
varList := []string{}
buf.WriteString(fmt.Sprintf("%v {\n", s.name))
for k := range s.storage {
varList = append(varList, k)
}
sort.Strings(varList)
for _, v := range varList {
buf.WriteString(fmt.Sprintf(" %s (%T) : %v\n", v, s.storage[v],
EvalToString(s.storage[v])))
}
if childrenString != "" {
// Indent all
buf.WriteString(" ")
buf.WriteString(strings.Replace(childrenString, "\n", "\n ", -1))
buf.WriteString("\n")
}
buf.WriteString("}")
return buf.String()
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package stdlib
import (
"fmt"
"reflect"
"devt.de/krotik/ecal/parser"
)
/*
ECALFunctionAdapter models a bridge adapter between an ECAL function to a Go function.
*/
type ECALFunctionAdapter struct {
funcval reflect.Value
docstring string
}
/*
NewECALFunctionAdapter creates a new ECALFunctionAdapter.
*/
func NewECALFunctionAdapter(funcval reflect.Value, docstring string) *ECALFunctionAdapter {
return &ECALFunctionAdapter{funcval, docstring}
}
/*
Run executes this function.
*/
func (ea *ECALFunctionAdapter) Run(instanceID string, vs parser.Scope,
is map[string]interface{}, tid uint64, args []interface{}) (ret interface{}, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("Error: %v", r)
}
}()
funcType := ea.funcval.Type()
// Build arguments
fargs := make([]reflect.Value, 0, len(args))
for i, arg := range args {
if i == funcType.NumIn() {
return nil, fmt.Errorf("Too many parameters - got %v expected %v",
len(args), funcType.NumIn())
}
expectedType := funcType.In(i)
// Try to convert into correct number types
if float64Arg, ok := arg.(float64); ok {
arg = ea.convertNumber(arg, float64Arg, expectedType)
}
givenType := reflect.TypeOf(arg)
// Check that the right types were given
if givenType != expectedType &&
!(expectedType.Kind() == reflect.Interface &&
givenType.Kind() == reflect.Interface &&
givenType.Implements(expectedType)) &&
expectedType != reflect.TypeOf([]interface{}{}) {
return nil, fmt.Errorf("Parameter %v should be of type %v but is of type %v",
i+1, expectedType, givenType)
}
fargs = append(fargs, reflect.ValueOf(arg))
}
// Call the function
vals := ea.funcval.Call(fargs)
// Convert result value
results := make([]interface{}, 0, len(vals))
for i, v := range vals {
res := v.Interface()
if i == len(vals)-1 {
// If the last item is an error then it is not part of the resutls
// (it will be wrapped into a proper runtime error later)
if funcType.Out(i) == reflect.TypeOf((*error)(nil)).Elem() {
if res != nil {
err = res.(error)
}
break
}
}
// Convert result if it is a primitive type
results = append(results, ea.convertResultNumber(res, v))
}
ret = results
// Return a single value if results contains only a single item
if len(results) == 1 {
ret = results[0]
}
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.
*/
func (ea *ECALFunctionAdapter) DocString() (string, error) {
return ea.docstring, nil
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package main
import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/build"
"go/doc"
"go/importer"
goparser "go/parser"
"go/token"
"go/types"
"io/ioutil"
"os"
"path/filepath"
"sort"
"unicode"
"devt.de/krotik/common/errorutil"
"devt.de/krotik/common/stringutil"
)
//go:generate echo Generating ECAL stdlib from Go functions ...
//go:generate go run devt.de/krotik/ecal/stdlib/generate $PWD/stdlib
/*
Stdlib candidates modules:
go list std | grep -v internal | grep -v '\.' | grep -v unsafe | grep -v syscall
*/
// List underneath the Go symbols which should be available in ECAL's stdlib
// e.g. var pkgNames = map[string][]string{ "fmt": {"Println", "Sprint"} }
// Turn the generateDoc switch on or off to extract documentation from the
// Go source.
// =============EDIT HERE START=============
var pkgNames = map[string][]string{
// "fmt": {"Println", "Sprint"},
}
var generateDoc = false
// ==============EDIT HERE END==============
var filename = filepath.Join(os.Args[1], "stdlib_gen.go")
var stderrPrint = fmt.Println
var stdoutPrint = fmt.Println
func main() {
var err error
var outbuf bytes.Buffer
synopsis := make(map[string]string)
pkgDocs := make(map[string]*doc.Package)
flag.Parse()
// Make sure we have at least an empty pkgName
if len(pkgNames) == 0 {
pkgNames["math"] = []string{
"E",
"Pi",
"Phi",
"Sqrt2",
"SqrtE",
"SqrtPi",
"SqrtPhi",
"Ln2",
"Log2E",
"Ln10",
"Log10E",
"Abs",
"Acos",
"Acosh",
"Asin",
"Asinh",
"Atan",
"Atan2",
"Atanh",
"Cbrt",
"Ceil",
"Copysign",
"Cos",
"Cosh",
"Dim",
"Erf",
"Erfc",
"Erfcinv",
"Erfinv",
"Exp",
"Exp2",
"Expm1",
"Floor",
"Frexp",
"Gamma",
"Hypot",
"Ilogb",
"Inf",
"IsInf",
"IsNaN",
"J0",
"J1",
"Jn",
"Ldexp",
"Lgamma",
"Log",
"Log10",
"Log1p",
"Log2",
"Logb",
"Max",
"Min",
"Mod",
"Modf",
"NaN",
"Nextafter",
"Nextafter32",
"Pow",
"Pow10",
"Remainder",
"Round",
"RoundToEven",
"Signbit",
"Sin",
"Sincos",
"Sinh",
"Sqrt",
"Tan",
"Tanh",
"Trunc",
"Y0",
"Y1",
"Yn",
}
}
// Make sure pkgNames is sorted
var importList []string
for pkgName, names := range pkgNames {
sort.Strings(names)
importList = append(importList, pkgName)
synopsis["math"] = "Mathematics-related constants and functions"
}
sort.Strings(importList)
outbuf.WriteString(`
// Code generated by ecal/stdlib/generate; DO NOT EDIT.
package stdlib
`)
outbuf.WriteString("import (\n")
for _, pkgName := range importList {
if generateDoc {
syn, pkgDoc, err := getPackageDocs(pkgName)
errorutil.AssertOk(err) // If this throws try not generating the docs!
synopsis[pkgName] = syn
pkgDocs[pkgName] = pkgDoc
} else if _, ok := synopsis[pkgName]; !ok {
synopsis[pkgName] = fmt.Sprintf("Package %v", pkgName)
}
outbuf.WriteString(fmt.Sprintf("\t\"%v\"\n", pkgName))
}
if stringutil.IndexOf("fmt", importList) == -1 {
outbuf.WriteString(` "fmt"
`)
}
if stringutil.IndexOf("reflect", importList) == -1 {
outbuf.WriteString(` "reflect"
)
`)
}
outbuf.WriteString(`/*
genStdlib contains all generated stdlib constructs.
*/
`)
outbuf.WriteString("var genStdlib = map[interface{}]interface{}{\n")
for _, pkgName := range importList {
if s, ok := synopsis[pkgName]; ok {
outbuf.WriteString(fmt.Sprintf("\t\"%v-synopsis\" : %#v,\n", pkgName, s))
}
outbuf.WriteString(fmt.Sprintf("\t\"%v-const\" : %vConstMap,\n", pkgName, pkgName))
outbuf.WriteString(fmt.Sprintf("\t\"%v-func\" : %vFuncMap,\n", pkgName, pkgName))
outbuf.WriteString(fmt.Sprintf("\t\"%v-func-doc\" : %vFuncDocMap,\n", pkgName, pkgName))
}
outbuf.WriteString("}\n\n")
for _, pkgName := range importList {
var pkg *types.Package
pkgSymbols := pkgNames[pkgName]
if err == nil {
pkg, err = importer.ForCompiler(fset, "source", nil).Import(pkgName)
if err == nil {
stdoutPrint("Generating adapter functions for", pkg)
scope := pkg.Scope()
// Write constants
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() {
if !containsSymbol(pkgSymbols, name) {
continue
}
switch obj := scope.Lookup(name).(type) {
case *types.Const:
if unicode.IsUpper([]rune(name)[0]) {
line := fmt.Sprintf(` "%v": %v.%v,
`, name, pkgName, obj.Name())
if basicType, ok := obj.Type().(*types.Basic); ok {
// Convert number constants so they can be used in calculations
switch basicType.Kind() {
case types.Int,
types.Int8,
types.Int16,
types.Int32,
types.Int64,
types.Uint,
types.Uint8,
types.Uint16,
types.Uint32,
types.Uint64,
types.Uintptr,
types.Float32,
types.UntypedInt,
types.UntypedFloat:
line = fmt.Sprintf(` "%v": float64(%v.%v),
`, name, pkgName, obj.Name())
}
}
outbuf.WriteString(line)
}
}
}
outbuf.WriteString("}\n\n")
}
/*
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 {
for _, name := range scope.Names() {
if !containsSymbol(pkgSymbols, name) {
continue
}
for _, f := range pkgDoc.Funcs {
if f.Name == name {
outbuf.WriteString(
fmt.Sprintf(` "%v": %#v,
`, name, f.Doc))
}
}
}
} else {
for _, name := range pkgSymbols {
switch scope.Lookup(name).(type) {
case *types.Func:
outbuf.WriteString(
fmt.Sprintf(` "%v": "Function: %v",
`, lcFirst(name), lcFirst(name)))
}
}
}
outbuf.WriteString("}\n\n")
}
/*
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() {
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])},
`, lcFirst(name), obj.FullName(), pkgName, lcFirst(name)))
}
}
}
outbuf.WriteString("}\n\n")
}
/*
getPackageDocs returns the source code documentation of as given Go package.
Returns a short synopsis and a documentation object.
*/
func getPackageDocs(pkgName string) (string, *doc.Package, error) {
var synopsis string
var pkgDoc *doc.Package
var filenames []string
bp, err := ctx.Import(pkgName, ".", 0)
if err == nil {
synopsis = bp.Doc
// Get all go files of the package
filenames = append(filenames, bp.GoFiles...)
filenames = append(filenames, bp.CgoFiles...)
// Build the ast package from Go source
astPkg := &ast.Package{
Name: bp.Name,
Files: make(map[string]*ast.File),
}
for _, filename := range filenames {
filepath := filepath.Join(bp.Dir, filename)
astFile, _ := goparser.ParseFile(fset, filepath, nil, goparser.ParseComments)
astPkg.Files[filepath] = astFile
}
// Build the package doc object
pkgDoc = doc.New(astPkg, bp.Dir, doc.AllDecls)
}
return synopsis, pkgDoc, err
}
/*
containsSymbol checks if a list of strings contains a given item.
*/
func containsSymbol(symbols []string, item string) bool {
i := sort.SearchStrings(symbols, item)
return i < len(symbols) && symbols[i] == item
}
/*
lcFirst lower cases the first rune of a given string
*/
func lcFirst(s string) string {
var ret = ""
for i, v := range s {
ret = string(unicode.ToLower(v)) + s[i+len(string(v)):]
break
}
return ret
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package stdlib
import (
"fmt"
"plugin"
"reflect"
"strings"
"devt.de/krotik/ecal/util"
)
/*
internalStdlibFuncMap holds all registered functions
*/
var internalStdlibFuncMap = make(map[string]util.ECALFunction)
/*
internalStdlibDocMap holds the docstrings for all registered functions
*/
var internalStdlibDocMap = make(map[string]string)
/*
pluginLookup is an interface for required function of the plugin object - only used for unit testing.
*/
type pluginLookup interface {
Lookup(symName string) (plugin.Symbol, error)
}
/*
pluginTestLookup override plugin object - only used for unit testing.
*/
var pluginTestLookup pluginLookup
/*
AddStdlibPkg adds a package to stdlib. A package needs to be added before functions
can be added.
*/
func AddStdlibPkg(pkg string, docstring string) error {
_, ok1 := GetPkgDocString(pkg)
_, ok2 := internalStdlibDocMap[pkg]
if ok1 || ok2 {
return fmt.Errorf("Package %v already exists", pkg)
}
internalStdlibDocMap[pkg] = docstring
return nil
}
/*
AddStdlibFunc adds a function to stdlib.
*/
func AddStdlibFunc(pkg string, name string, funcObj util.ECALFunction) error {
_, ok1 := GetPkgDocString(pkg)
_, ok2 := internalStdlibDocMap[pkg]
if !ok1 && !ok2 {
return fmt.Errorf("Package %v does not exist", pkg)
}
internalStdlibFuncMap[fmt.Sprintf("%v.%v", pkg, name)] = funcObj
return nil
}
/*
LoadStdlibPlugins attempts to load stdlib functions from a given list of definitions.
*/
func LoadStdlibPlugins(jsonObj []interface{}) []error {
var errs []error
for _, i := range jsonObj {
if err := LoadStdlibPlugin(i.(map[string]interface{})); err != nil {
errs = append(errs, err)
}
}
return errs
}
/*
LoadStdlibPlugin attempts to load a stdlib function from a given definition.
*/
func LoadStdlibPlugin(jsonObj map[string]interface{}) error {
pkg := fmt.Sprint(jsonObj["package"])
name := fmt.Sprint(jsonObj["name"])
path := fmt.Sprint(jsonObj["path"])
symName := fmt.Sprint(jsonObj["symbol"])
return AddStdlibPluginFunc(pkg, name, path, symName)
}
/*
AddStdlibPluginFunc adds a function to stdlib via a loaded plugin.
The plugin needs to be build as a Go plugin (https://golang.org/pkg/plugin):
go build -buildmode=plugin -o myfunc.so myfunc.go
And have an exported variable (passed here as symName) which conforms
to util.ECALPluginFunction.
*/
func AddStdlibPluginFunc(pkg string, name string, path string, symName string) error {
var err error
var plug pluginLookup
AddStdlibPkg(pkg, "Functions provided by plugins")
if plug, err = plugin.Open(path); err == nil || pluginTestLookup != nil {
var sym plugin.Symbol
if pluginTestLookup != nil {
plug = pluginTestLookup
}
if sym, err = plug.Lookup(symName); err == nil {
if stdlibPluginFunc, ok := sym.(util.ECALPluginFunction); ok {
adapterFunc := func(a ...interface{}) (interface{}, error) {
return stdlibPluginFunc.Run(a)
}
err = AddStdlibFunc(pkg, name, &ECALFunctionAdapter{
reflect.ValueOf(adapterFunc), stdlibPluginFunc.DocString()})
} else {
err = fmt.Errorf("Symbol %v is not a stdlib function", symName)
}
}
}
return err
}
/*
GetStdlibSymbols returns all available packages of stdlib and their constant
and function symbols.
*/
func GetStdlibSymbols() ([]string, []string, []string) {
var constSymbols, funcSymbols []string
var packageNames []string
packageSet := make(map[string]bool)
addSym := func(sym string, suffix string, symMap map[interface{}]interface{},
ret []string) []string {
if strings.HasSuffix(sym, suffix) {
trimSym := strings.TrimSuffix(sym, suffix)
packageSet[trimSym] = true
for k := range symMap {
ret = append(ret, fmt.Sprintf("%v.%v", trimSym, k))
}
}
return ret
}
for k, v := range genStdlib {
sym := fmt.Sprint(k)
if symMap, ok := v.(map[interface{}]interface{}); ok {
constSymbols = addSym(sym, "-const", symMap, constSymbols)
funcSymbols = addSym(sym, "-func", symMap, funcSymbols)
}
}
for k := range packageSet {
packageNames = append(packageNames, k)
}
// Add internal stuff
for k := range internalStdlibDocMap {
packageNames = append(packageNames, k)
}
for k := range internalStdlibFuncMap {
funcSymbols = append(funcSymbols, k)
}
return packageNames, constSymbols, funcSymbols
}
/*
GetStdlibConst looks up a constant from stdlib.
*/
func GetStdlibConst(name string) (interface{}, bool) {
var res interface{}
var resok bool
if m, n := splitModuleAndName(name); n != "" {
if cmap, ok := genStdlib[fmt.Sprintf("%v-const", m)]; ok {
res, resok = cmap.(map[interface{}]interface{})[n]
}
}
return res, resok
}
/*
GetStdlibFunc looks up a function from stdlib.
*/
func GetStdlibFunc(name string) (util.ECALFunction, bool) {
var res util.ECALFunction
var resok bool
if m, n := splitModuleAndName(name); n != "" {
if fmap, ok := genStdlib[fmt.Sprintf("%v-func", m)]; ok {
if fn, ok := fmap.(map[interface{}]interface{})[n]; ok {
res = fn.(util.ECALFunction)
resok = true
}
}
}
if !resok {
res, resok = internalStdlibFuncMap[name]
}
return res, resok
}
/*
GetPkgDocString returns the docstring of a stdlib package.
*/
func GetPkgDocString(name string) (string, bool) {
var res string
s, ok := genStdlib[fmt.Sprintf("%v-synopsis", name)]
if ok {
res = fmt.Sprint(s)
} else {
res, ok = internalStdlibDocMap[name]
}
return res, ok
}
/*
splitModuleAndName splits up a given full function name in module and function name part.
*/
func splitModuleAndName(fullname string) (string, string) {
var module, name string
ccSplit := strings.SplitN(fullname, ".", 2)
if len(ccSplit) != 0 {
module = ccSplit[0]
name = strings.Join(ccSplit[1:], "")
}
return module, name
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
/*
Package util contains utility definitions and functions for the event condition language ECAL.
*/
package util
import (
"encoding/json"
"errors"
"fmt"
"devt.de/krotik/ecal/parser"
)
/*
TraceableRuntimeError can record and show a stack trace.
*/
type TraceableRuntimeError interface {
error
/*
AddTrace adds a trace step.
*/
AddTrace(*parser.ASTNode)
/*
GetTrace returns the current stacktrace.
*/
GetTrace() []*parser.ASTNode
/*
GetTrace returns the current stacktrace as a string.
*/
GetTraceString() []string
}
/*
RuntimeError is a runtime related error.
*/
type RuntimeError struct {
Source string // Name of the source which was given to the parser
Type error // Error type (to be used for equal checks)
Detail string // Details of this error
Node *parser.ASTNode // AST Node where the error occurred
Line int // Line of the error
Pos int // Position of the error
Trace []*parser.ASTNode // Stacktrace
}
/*
Runtime related error types.
*/
var (
ErrRuntimeError = errors.New("Runtime error")
ErrUnknownConstruct = errors.New("Unknown construct")
ErrInvalidConstruct = errors.New("Invalid construct")
ErrInvalidState = errors.New("Invalid state")
ErrVarAccess = errors.New("Cannot access variable")
ErrNotANumber = errors.New("Operand is not a number")
ErrNotABoolean = errors.New("Operand is not a boolean")
ErrNotAList = errors.New("Operand is not a list")
ErrNotAMap = errors.New("Operand is not a map")
ErrNotAListOrMap = errors.New("Operand is not a list nor a map")
ErrSink = errors.New("Error in sink")
// ErrReturn is not an error. It is used to return when executing a function
ErrReturn = errors.New("*** return ***")
// Error codes for loop operations
ErrIsIterator = errors.New("Function is an iterator")
ErrEndOfIteration = errors.New("End of iteration was reached")
ErrContinueIteration = errors.New("End of iteration step - Continue iteration")
)
/*
NewRuntimeError creates a new RuntimeError object.
*/
func NewRuntimeError(source string, t error, d string, node *parser.ASTNode) error {
if node.Token != nil {
return &RuntimeError{source, t, d, node, node.Token.Lline, node.Token.Lpos, nil}
}
return &RuntimeError{source, t, d, node, 0, 0, nil}
}
/*
Error returns a human-readable string representation of this error.
*/
func (re *RuntimeError) Error() string {
ret := fmt.Sprintf("ECAL error in %s: %v (%v)", re.Source, re.Type, re.Detail)
if re.Line != 0 {
// Add line if available
ret = fmt.Sprintf("%s (Line:%d Pos:%d)", ret, re.Line, re.Pos)
}
return ret
}
/*
AddTrace adds a trace step.
*/
func (re *RuntimeError) AddTrace(n *parser.ASTNode) {
re.Trace = append(re.Trace, n)
}
/*
GetTrace returns the current stacktrace.
*/
func (re *RuntimeError) GetTrace() []*parser.ASTNode {
return re.Trace
}
/*
GetTraceString returns the current stacktrace as a string.
*/
func (re *RuntimeError) GetTraceString() []string {
res := []string{}
for _, t := range re.GetTrace() {
pp, _ := parser.PrettyPrint(t)
res = append(res, fmt.Sprintf("%v (%v:%v)", pp, t.Token.Lsource, t.Token.Lline))
}
return res
}
/*
ToJSONObject returns this RuntimeError and all its children as a JSON object.
*/
func (re *RuntimeError) ToJSONObject() map[string]interface{} {
t := ""
if re.Type != nil {
t = re.Type.Error()
}
return map[string]interface{}{
"Source": re.Source,
"Type": t,
"Detail": re.Detail,
"Node": re.Node,
"Trace": re.Trace,
}
}
/*
MarshalJSON serializes this RuntimeError into a JSON string.
*/
func (re *RuntimeError) MarshalJSON() ([]byte, error) {
return json.Marshal(re.ToJSONObject())
}
/*
RuntimeErrorWithDetail is a runtime error with additional environment information.
*/
type RuntimeErrorWithDetail struct {
*RuntimeError
Environment parser.Scope
Data interface{}
}
/*
ToJSONObject returns this RuntimeErrorWithDetail and all its children as a JSON object.
*/
func (re *RuntimeErrorWithDetail) ToJSONObject() map[string]interface{} {
res := re.RuntimeError.ToJSONObject()
e := map[string]interface{}{}
if re.Environment != nil {
e = re.Environment.ToJSONObject()
}
res["Environment"] = e
res["Data"] = re.Data
return res
}
/*
MarshalJSON serializes this RuntimeErrorWithDetail into a JSON string.
*/
func (re *RuntimeErrorWithDetail) MarshalJSON() ([]byte, error) {
return json.Marshal(re.ToJSONObject())
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package util
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// ImportLocator implementations
// =============================
/*
MemoryImportLocator holds a given set of code in memory and can provide it as imports.
*/
type MemoryImportLocator struct {
Files map[string]string
}
/*
Resolve a given import path and parse the imported file into an AST.
*/
func (il *MemoryImportLocator) Resolve(path string) (string, error) {
res, ok := il.Files[path]
if !ok {
return "", fmt.Errorf("Could not find import path: %v", path)
}
return res, nil
}
/*
FileImportLocator tries to locate files on disk relative to a root directory and provide them as imports.
*/
type FileImportLocator struct {
Root string // Relative root path
}
/*
Resolve a given import path and parse the imported file into an AST.
*/
func (il *FileImportLocator) Resolve(path string) (string, error) {
var res string
importPath := filepath.Clean(filepath.Join(il.Root, path))
ok, err := isSubpath(il.Root, importPath)
if err == nil && !ok {
err = fmt.Errorf("Import path is outside of code root: %v", path)
}
if err == nil {
var b []byte
if b, err = ioutil.ReadFile(importPath); err != nil {
err = fmt.Errorf("Could not import path %v: %v", path, err)
} else {
res = string(b)
}
}
return res, err
}
/*
isSubpath checks if the given sub path is a child path of root.
*/
func isSubpath(root, sub string) (bool, error) {
rel, err := filepath.Rel(root, sub)
return err == nil &&
!strings.HasPrefix(rel, fmt.Sprintf("..%v", string(os.PathSeparator))) &&
rel != "..", err
}
/*
* ECAL
*
* Copyright 2020 Matthias Ladkau. All rights reserved.
*
* This Source Code Form is subject to the terms of the MIT
* License, If a copy of the MIT License was not distributed with this
* file, You can obtain one at https://opensource.org/licenses/MIT.
*/
package util
import (
"fmt"
"io"
"log"
"strings"
"devt.de/krotik/common/datautil"
)
// Loger with loglevel support
// ===========================
/*
LogLevel represents a logging level
*/
type LogLevel string
/*
Log levels
*/
const (
Debug LogLevel = "debug"
Info = "info"
Error = "error"
)
/*
LogLevelLogger is a wrapper around loggers to add log level functionality.
*/
type LogLevelLogger struct {
logger Logger
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))
if llevel != Debug && llevel != Info && llevel != Error {
return nil, fmt.Errorf("Invalid log level: %v", llevel)
}
return &LogLevelLogger{
logger,
llevel,
}, nil
}
/*
Level returns the current log level.
*/
func (ll *LogLevelLogger) Level() LogLevel {
return ll.level
}
/*
LogError adds a new error log message.
*/
func (ll *LogLevelLogger) LogError(m ...interface{}) {
ll.logger.LogError(m...)
}
/*
LogInfo adds a new info log message.
*/
func (ll *LogLevelLogger) LogInfo(m ...interface{}) {
if ll.level == Info || ll.level == Debug {
ll.logger.LogInfo(m...)
}
}
/*
LogDebug adds a new debug log message.
*/
func (ll *LogLevelLogger) LogDebug(m ...interface{}) {
if ll.level == Debug {
ll.logger.LogDebug(m...)
}
}
// Logging implementations
// =======================
/*
MemoryLogger collects log messages in a RingBuffer in memory.
*/
type MemoryLogger struct {
*datautil.RingBuffer
}
/*
NewMemoryLogger returns a new memory logger instance.
*/
func NewMemoryLogger(size int) *MemoryLogger {
return &MemoryLogger{datautil.NewRingBuffer(size)}
}
/*
LogError adds a new error log message.
*/
func (ml *MemoryLogger) LogError(m ...interface{}) {
ml.RingBuffer.Add(fmt.Sprintf("error: %v", fmt.Sprint(m...)))
}
/*
LogInfo adds a new info log message.
*/
func (ml *MemoryLogger) LogInfo(m ...interface{}) {
ml.RingBuffer.Add(fmt.Sprintf("%v", fmt.Sprint(m...)))
}
/*
LogDebug adds a new debug log message.
*/
func (ml *MemoryLogger) LogDebug(m ...interface{}) {
ml.RingBuffer.Add(fmt.Sprintf("debug: %v", fmt.Sprint(m...)))
}
/*
Slice returns the contents of the current log as a slice.
*/
func (ml *MemoryLogger) Slice() []string {
sl := ml.RingBuffer.Slice()
ret := make([]string, len(sl))
for i, lm := range sl {
ret[i] = lm.(string)
}
return ret
}
/*
Reset resets the current log.
*/
func (ml *MemoryLogger) Reset() {
ml.RingBuffer.Reset()
}
/*
Size returns the current log size.
*/
func (ml *MemoryLogger) Size() int {
return ml.RingBuffer.Size()
}
/*
String returns the current log as a string.
*/
func (ml *MemoryLogger) String() string {
return ml.RingBuffer.String()
}
/*
StdOutLogger writes log messages to stdout.
*/
type StdOutLogger struct {
stdlog func(v ...interface{})
}
/*
NewStdOutLogger returns a stdout logger instance.
*/
func NewStdOutLogger() *StdOutLogger {
return &StdOutLogger{log.Print}
}
/*
LogError adds a new error log message.
*/
func (sl *StdOutLogger) LogError(m ...interface{}) {
sl.stdlog(fmt.Sprintf("error: %v", fmt.Sprint(m...)))
}
/*
LogInfo adds a new info log message.
*/
func (sl *StdOutLogger) LogInfo(m ...interface{}) {
sl.stdlog(fmt.Sprintf("%v", fmt.Sprint(m...)))
}
/*
LogDebug adds a new debug log message.
*/
func (sl *StdOutLogger) LogDebug(m ...interface{}) {
sl.stdlog(fmt.Sprintf("debug: %v", fmt.Sprint(m...)))
}
/*
NullLogger discards log messages.
*/
type NullLogger struct {
}
/*
NewNullLogger returns a null logger instance.
*/
func NewNullLogger() *NullLogger {
return &NullLogger{}
}
/*
LogError adds a new error log message.
*/
func (nl *NullLogger) LogError(m ...interface{}) {
}
/*
LogInfo adds a new info log message.
*/
func (nl *NullLogger) LogInfo(m ...interface{}) {
}
/*
LogDebug adds a new debug log message.
*/
func (nl *NullLogger) LogDebug(m ...interface{}) {
}
/*
BufferLogger logs into a buffer.
*/
type BufferLogger struct {
buf io.Writer
}
/*
NewBufferLogger returns a buffer logger instance.
*/
func NewBufferLogger(buf io.Writer) *BufferLogger {
return &BufferLogger{buf}
}
/*
LogError adds a new error log message.
*/
func (bl *BufferLogger) LogError(m ...interface{}) {
fmt.Fprintln(bl.buf, fmt.Sprintf("error: %v", fmt.Sprint(m...)))
}
/*
LogInfo adds a new info log message.
*/
func (bl *BufferLogger) LogInfo(m ...interface{}) {
fmt.Fprintln(bl.buf, fmt.Sprintf("%v", fmt.Sprint(m...)))
}
/*
LogDebug adds a new debug log message.
*/
func (bl *BufferLogger) LogDebug(m ...interface{}) {
fmt.Fprintln(bl.buf, fmt.Sprintf("debug: %v", fmt.Sprint(m...)))
}