Skip to content

Commit

Permalink
feat: Merging 1.0.2 release
Browse files Browse the repository at this point in the history
  • Loading branch information
krotik committed Dec 1, 2020
1 parent dc6a6f3 commit 3260fab
Show file tree
Hide file tree
Showing 17 changed files with 385 additions and 6 deletions.
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,50 @@ $ sh run.sh

The interpreter can be run in debug mode which adds debug commands to the console. Run the ECAL program in debug mode with: `sh debug.sh` - this will also start a debug server which external development environments can connect to. There is a [VSCode integration](ecal-support/README.md) available which allows debugging via a graphical interface.

### Embedding ECAL

The primary purpose of ECAL is to be a simple multi-purpose language which can be embedded into other software:
- It has a minimal (quite generic) syntax.
- By default the language can only reach outside the interpreter via return values, injecting events or logging.
- External systems can interact with the code via events which maybe be handled in sink systems with varying complexity.
- A standard library of function can easily be created by either generating proxy code to standard Go functions or by adding simple straight-forward function objects.

The core of the ECAL interpreter is the runtime provider object which is constructed with a given logger and import locator. The import locator is used by the import statement to load other ECAL code at runtime. The logger is used to process log statements from the interpreter.
```
logger := util.NewStdOutLogger()
importLocator := &util.FileImportLocator{Root: "/somedir"}
rtp := interpreter.NewECALRuntimeProvider("Some Program Title", importLocator, logger)
```
The ECALRuntimeProvider provides additionally to the logger and import locator also the following: A cron object to schedule recurring events. An ECA processor which triggers sinks and can be used to inject events into the interpreter. A debugger object which can be used to debug ECAL code supporting thread suspension, thread inspection, value injection and extraction and stepping through statements.

The actual ECAL code has to be first parsed into an Abstract Syntax Tree. The tree is annotated during its construction with runtime components created by the runtime provider.
```
ast, err := parser.ParseWithRuntime("sourcefilename", code, rtp)
```
The code is executed by calling the Validate() and Eval() function.
```
err = ast.Runtime.Validate()
vs := scope.NewScope(scope.GlobalScope)
res, err := ast.Runtime.Eval(vs, make(map[string]interface{}), threadId)
```
Eval is given a variable scope which stores the values of variables, an instance state for internal use and a thread ID identifying the executing thread.

If events are to be used then the processor of the runtime provider needs to be started first.
```
rtp.Processor.Start()
```
Events can then be injected into the interpreter.
```
monitor, err := rtp.Processor.AddEventAndWait(engine.NewEvent("MyEvent", []string{"foo", "bar"}, map[interface{}]interface{}{
"data1": 123,
"data2": "123",
}), nil)
```
All errors are collected in the returned monitor.
```
monitor.RootMonitor().AllErrors()
```

### Further Reading:

- [ECA Language](ecal.md)
Expand Down
2 changes: 1 addition & 1 deletion config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
/*
ProductVersion is the current version of ECAL
*/
const ProductVersion = "1.0.0"
const ProductVersion = "1.0.2"

/*
Known configuration options for ECAL
Expand Down
11 changes: 10 additions & 1 deletion ecal.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ foobar.doSomething()

Event Sinks
--
Event sinks are the core constructs of ECAL which provide concurrency and the means to respond to events of an external system. Sinks provide ECAL with an interface to an [event condition action engine](engine.md) which coordinates the parallel execution of code. Sinks cannot be scoped into modules or objects and are usually declared at the top level. Sinks must not write to top level variables. They have the following form:
Event sinks are the core constructs of ECAL which provide concurrency and the means to respond to events of an external system. Sinks provide ECAL with an interface to an [event condition action engine](engine.md) which coordinates the parallel execution of code. Sinks cannot be scoped into modules or objects and are usually declared at the top level. They must only access top level variables within mutex blocks. Sinks have the following form:
```
sink mysink
kindmatch [ "foo.bar.*" ],
Expand Down Expand Up @@ -94,6 +94,15 @@ res := addEventAndWait("request", "foo.bar.xxx", {
```
The order of execution of sinks can be controlled via their priority. All sinks which are triggered by a particular event will be executed in order of their priority.

Mutex blocks
--
To protect shared resource when handling concurrent events, ECAL supports mutex blocks. Mutex blocks which share the same name can only be accessed by one thread at a given time:
```
mutex myresource {
globalResource := "new value"
}
```

Functions
--
Functions define reusable pieces of code dedicated to perform a particular task based on a set of given input values. In ECAL functions are first-class citizens in that they can be assigned to variables and passed as arguments. Each parameter can have a default value which is by default NULL.
Expand Down
Binary file added examples/embedding/embedding
Binary file not shown.
5 changes: 5 additions & 0 deletions examples/embedding/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module example.com/ecal/embedding

go 1.12

require devt.de/krotik/ecal v1.0.1
4 changes: 4 additions & 0 deletions examples/embedding/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
devt.de/krotik/common v1.3.9 h1:/DdNkkaaplUXHeA+6juY3f+WMUOkc5zycar2TdPXHB0=
devt.de/krotik/common v1.3.9/go.mod h1:X4nsS85DAxyHkwSg/Tc6+XC2zfmGeaVz+37F61+eSaI=
devt.de/krotik/ecal v1.0.1 h1:V+CqMpT9xAG2+YPfd9CH6DH+MGxELcFXyhBZcN1k44E=
devt.de/krotik/ecal v1.0.1/go.mod h1:eZrJYQRIcWLUwNy6s8f+5N+PEduCu7XUABa8ZD0nBKA=
138 changes: 138 additions & 0 deletions examples/embedding/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* ECAL Embedding Example
*/

package main

import (
"fmt"
"log"

"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"
)

func main() {

// The code to execute

code := `
sink mysink
kindmatch [ "foo.*" ],
{
log("Handling: ", event)
log("Result: ", event.state.op1 + event.state.op2)
}
sink mysink2
kindmatch [ "foo.*" ],
{
raise("Some error")
}
func compute(x) {
let result := x + 1
return result
}
mystuff.add(compute(5), 1)
`

// Add a stdlib function

stdlib.AddStdlibPkg("mystuff", "My special functions")

// A single instance if the ECALFunction struct will be used for all function calls across all threads

stdlib.AddStdlibFunc("mystuff", "add", &AddFunc{})

// Logger for log() statements in the code

logger := util.NewMemoryLogger(100)

// Import locator when using import statements in the code

importLocator := &util.MemoryImportLocator{Files: make(map[string]string)}

// Runtime provider which contains all objects needed by the interpreter

rtp := interpreter.NewECALRuntimeProvider("Embedded Example", importLocator, logger)

// First we need to parse the code into an Abstract Syntax Tree

ast, err := parser.ParseWithRuntime("code1", code, rtp)
if err != nil {
log.Fatal(err)
}

// Then we need to validate the code - this prepares certain runtime bits
// of the AST for execution.

if err = ast.Runtime.Validate(); err != nil {
log.Fatal(err)
}

// We need a global variable scope which contains all declared variables - use
// this object to inject initialization values into the ECAL program.

vs := scope.NewScope(scope.GlobalScope)

// Each thread which evaluates the Runtime of an AST should get a unique thread ID

var threadId uint64 = 1

// Evaluate the Runtime of an AST with a variable scope

res, err := ast.Runtime.Eval(vs, make(map[string]interface{}), threadId)
if err != nil {
log.Fatal(err)
}

// The executed code returns the value of the last statement

fmt.Println("Computation result:", res)

// We can also react to events

rtp.Processor.Start()
monitor, err := rtp.Processor.AddEventAndWait(engine.NewEvent("MyEvent", []string{"foo", "bar"}, map[interface{}]interface{}{
"op1": float64(5.2),
"op2": float64(5.3),
}), nil)

if err != nil {
log.Fatal(err)
}

// All errors can be found on the returned monitor object

fmt.Println("Event result:", monitor.RootMonitor().AllErrors())

// The log messages of a program can be collected

fmt.Println("Log:", logger.String())
}

/*
AddFunc is a simple add function which calculates the sum of two numbers.
*/
type AddFunc struct {
}

func (f *AddFunc) Run(instanceID string, vs parser.Scope, is map[string]interface{}, tid uint64, args []interface{}) (interface{}, error) {

// This should have some proper error checking

// Arguments are either of type string, float64, map[interface{}]interface{}
// or []interface{}

return args[0].(float64) + args[1].(float64), nil
}

func (f *AddFunc) DocString() (string, error) {
return "Sum up two numbers", nil
}
9 changes: 8 additions & 1 deletion interpreter/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ package interpreter
import (
"os"
"path/filepath"
"sync"

"devt.de/krotik/common/timeutil"
"devt.de/krotik/ecal/config"
Expand Down Expand Up @@ -130,6 +131,10 @@ var providerMap = map[string]ecalRuntimeNew{
parser.NodeTRY: tryRuntimeInst,
parser.NodeEXCEPT: voidRuntimeInst,
parser.NodeFINALLY: voidRuntimeInst,

// Mutex block

parser.NodeMUTEX: mutexRuntimeInst,
}

/*
Expand All @@ -140,6 +145,7 @@ type ECALRuntimeProvider struct {
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
Cron *timeutil.Cron // Cron object for scheduled execution
Debugger util.ECALDebugger // Optional: ECAL Debugger object
}
Expand Down Expand Up @@ -173,7 +179,8 @@ func NewECALRuntimeProvider(name string, importLocator util.ECALImportLocator, l
cron := timeutil.NewCron()
cron.Start()

return &ECALRuntimeProvider{name, importLocator, logger, proc, cron, nil}
return &ECALRuntimeProvider{name, importLocator, logger, proc,
make(map[string]*sync.Mutex), cron, nil}
}

/*
Expand Down
13 changes: 11 additions & 2 deletions interpreter/rt_sink.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,22 @@ func (rt *sinkRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint
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: err.(*util.RuntimeError),
RuntimeError: rerr,
Environment: sinkVS,
Data: nil,
Data: data,
}
}
}
Expand Down
21 changes: 20 additions & 1 deletion interpreter/rt_sink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ sink rule3
kindmatch [ "web.log" ],
{
log("rule3 - Logging user:", event.state.user)
return 123
}
res := addEventAndWait("request", "web.page.index", {
Expand Down Expand Up @@ -140,7 +141,25 @@ log("ErrorResult:", res, " ", res == null)
rule1 - Handling request: web.page.index
rule2 - Tracking user:foo
rule3 - Logging user:foo
ErrorResult:null true
ErrorResult:[
{
"errors": {
"rule3": {
"data": 123,
"detail": "Return value: 123",
"error": "ECAL error in ECALTestRuntime: *** return *** (Return value: 123) (Line:26 Pos:9)",
"type": "*** return ***"
}
},
"event": {
"kind": "web.log",
"name": "Rule1Event2",
"state": {
"user": "foo"
}
}
}
] false
rule2 - Tracking user:bar
ErrorResult:[
{
Expand Down
49 changes: 49 additions & 0 deletions interpreter/rt_statements.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ package interpreter

import (
"fmt"
"sync"

"devt.de/krotik/common/errorutil"
"devt.de/krotik/common/sortutil"
Expand Down Expand Up @@ -601,3 +602,51 @@ func (rt *tryRuntime) Eval(vs parser.Scope, is map[string]interface{}, tid uint6

return res, err
}

// 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

mutex, ok := rt.erp.Mutexes[name]
if !ok {
mutex = &sync.Mutex{}
rt.erp.Mutexes[name] = mutex
}

tvs := vs.NewChild(scope.NameFromASTNode(rt.node))

mutex.Lock()
defer mutex.Unlock()

res, err = rt.node.Children[0].Runtime.Eval(tvs, is, tid)
}

return res, err
}
Loading

0 comments on commit 3260fab

Please sign in to comment.