Browse Source

feat: Adding import statements

Matthias Ladkau 3 years ago
parent
commit
07b3c799d5

+ 7 - 0
cli/ecal.go

@@ -12,6 +12,13 @@ package main
 
 import "fmt"
 
+/*
+Ideas:
+- auto reload code (watch)
+- cron job support (trigger periodic events)
+- create executable binary (pack into single binary)
+*/
+
 func main() {
 	fmt.Println("ECAL")
 }

+ 7 - 2
interpreter/main_test.go

@@ -19,6 +19,7 @@ import (
 	"devt.de/krotik/common/datautil"
 	"devt.de/krotik/ecal/parser"
 	"devt.de/krotik/ecal/scope"
+	"devt.de/krotik/ecal/util"
 )
 
 // Main function for all tests in this package
@@ -51,8 +52,11 @@ var usedNodes = map[string]bool{
 func UnitTestEval(input string, vs parser.Scope) (interface{}, error) {
 	return UnitTestEvalAndAST(input, vs, "")
 }
-
 func UnitTestEvalAndAST(input string, vs parser.Scope, expectedAST string) (interface{}, error) {
+	return UnitTestEvalAndASTAndImport(input, vs, "", nil)
+}
+
+func UnitTestEvalAndASTAndImport(input string, vs parser.Scope, expectedAST string, importLocator util.ECALImportLocator) (interface{}, error) {
 	var traverseAST func(n *parser.ASTNode)
 
 	traverseAST = func(n *parser.ASTNode) {
@@ -68,7 +72,8 @@ func UnitTestEvalAndAST(input string, vs parser.Scope, expectedAST string) (inte
 
 	// Parse the input
 
-	ast, err := parser.ParseWithRuntime("ECALEvalTest", input, NewECALRuntimeProvider("ECALTestRuntime"))
+	ast, err := parser.ParseWithRuntime("ECALEvalTest", input,
+		NewECALRuntimeProvider("ECALTestRuntime", importLocator))
 	if err != nil {
 		return nil, err
 	}

+ 8 - 7
interpreter/provider.go

@@ -9,7 +9,6 @@
  */
 
 // TODO:
-// Import resolve
 // Event function: event
 // Context supporting final
 // Event handling
@@ -74,11 +73,12 @@ var providerMap = map[string]ecalRuntimeNew{
 	// Assignment statement
 
 	parser.NodeASSIGN: assignmentRuntimeInst,
-	/*
 
-		// Import statement
+	// Import statement
+
+	parser.NodeIMPORT: importRuntimeInst,
 
-		parser.NodeIMPORT
+	/*
 
 		// Sink definition
 
@@ -129,14 +129,15 @@ var providerMap = map[string]ecalRuntimeNew{
 ECALRuntimeProvider is the factory object producing runtime objects for ECAL ASTs.
 */
 type ECALRuntimeProvider struct {
-	Name string // Name to identify the input
+	Name          string                 // Name to identify the input
+	ImportLocator util.ECALImportLocator // Locator object for imports
 }
 
 /*
 NewECALRuntimeProvider returns a new instance of a ECAL runtime provider.
 */
-func NewECALRuntimeProvider(name string) *ECALRuntimeProvider {
-	return &ECALRuntimeProvider{name}
+func NewECALRuntimeProvider(name string, importLocator util.ECALImportLocator) *ECALRuntimeProvider {
+	return &ECALRuntimeProvider{name, importLocator}
 }
 
 /*

+ 61 - 0
interpreter/rt_general.go

@@ -15,6 +15,7 @@ import (
 
 	"devt.de/krotik/common/errorutil"
 	"devt.de/krotik/ecal/parser"
+	"devt.de/krotik/ecal/scope"
 	"devt.de/krotik/ecal/util"
 )
 
@@ -95,6 +96,66 @@ func (rt *voidRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interfa
 	return rt.baseRuntime.Eval(vs, is)
 }
 
+// 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{}) (interface{}, error) {
+	_, err := rt.baseRuntime.Eval(vs, is)
+
+	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); 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{})); err == nil {
+							irt := rt.node.Children[1].Runtime.(*identifierRuntime)
+							irt.Set(vs, is, scope.ToObject(ivs))
+						}
+					}
+				}
+			}
+		}
+	}
+
+	return nil, err
+}
+
 // Not Implemented Runtime
 // =======================
 

+ 36 - 1
interpreter/rt_general_test.go

@@ -14,12 +14,14 @@ import (
 	"testing"
 
 	"devt.de/krotik/ecal/parser"
+	"devt.de/krotik/ecal/scope"
+	"devt.de/krotik/ecal/util"
 )
 
 func TestGeneralErrorCases(t *testing.T) {
 
 	n, _ := parser.Parse("a", "a")
-	inv := &invalidRuntime{newBaseRuntime(NewECALRuntimeProvider("a"), n)}
+	inv := &invalidRuntime{newBaseRuntime(NewECALRuntimeProvider("a", nil), n)}
 
 	if err := inv.Validate().Error(); err != "ECAL error in a: Invalid construct (Unknown node: identifier) (Line:1 Pos:1)" {
 		t.Error("Unexpected result:", err)
@@ -31,3 +33,36 @@ func TestGeneralErrorCases(t *testing.T) {
 		return
 	}
 }
+
+func TestImporting(t *testing.T) {
+
+	vs := scope.NewScope(scope.GlobalScope)
+	il := &util.MemoryImportLocator{make(map[string]string)}
+
+	il.Files["foo/bar"] = `
+b := 123
+`
+
+	res, err := UnitTestEvalAndASTAndImport(
+		`
+	   import "foo/bar" as foobar
+	   a := foobar.b`, vs,
+		`
+statements
+  import
+    string: 'foo/bar'
+    identifier: foobar
+  :=
+    identifier: a
+    identifier: foobar
+      identifier: b
+`[1:], il)
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    a (float64) : 123
+    foobar (map[interface {}]interface {}) : {"b":123}
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+}

+ 4 - 4
scope/helper.go

@@ -44,8 +44,8 @@ func EvalToString(v interface{}) string {
 /*
 ToObject converts a Scope into an object.
 */
-func ToObject(vs parser.Scope) map[string]interface{} {
-	res := make(map[string]interface{})
+func ToObject(vs parser.Scope) map[interface{}]interface{} {
+	res := make(map[interface{}]interface{})
 	for k, v := range vs.(*varsScope).storage {
 		res[k] = v
 	}
@@ -55,10 +55,10 @@ func ToObject(vs parser.Scope) map[string]interface{} {
 /*
 ToScope converts a given object into a Scope.
 */
-func ToScope(name string, o map[string]interface{}) parser.Scope {
+func ToScope(name string, o map[interface{}]interface{}) parser.Scope {
 	vs := NewScope(name)
 	for k, v := range o {
-		vs.SetValue(k, v)
+		vs.SetValue(fmt.Sprint(k), v)
 	}
 	return vs
 }

+ 39 - 0
util/error_test.go

@@ -0,0 +1,39 @@
+/*
+ * ECAL
+ *
+ * Copyright 2020 Matthias Ladkau. All rights reserved.
+ *
+ * This Source Code Form is subject to the terms of the MIT
+ * License, If a copy of the MIT License was not distributed with this
+ * file, You can obtain one at https://opensource.org/licenses/MIT.
+ */
+
+package util
+
+import (
+	"fmt"
+	"testing"
+
+	"devt.de/krotik/ecal/parser"
+)
+
+func TestRuntimeError(t *testing.T) {
+
+	ast, _ := parser.Parse("foo", "a")
+
+	err1 := NewRuntimeError("foo", fmt.Errorf("foo"), "bar", ast)
+
+	if err1.Error() != "ECAL error in foo: foo (bar) (Line:1 Pos:1)" {
+		t.Error("Unexpected result:", err1)
+		return
+	}
+
+	ast.Token = nil
+
+	err2 := NewRuntimeError("foo", fmt.Errorf("foo"), "bar", ast)
+
+	if err2.Error() != "ECAL error in foo: foo (bar)" {
+		t.Error("Unexpected result:", err2)
+		return
+	}
+}

+ 86 - 0
util/import.go

@@ -0,0 +1,86 @@
+/*
+ * 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
+}

+ 116 - 0
util/import_test.go

@@ -0,0 +1,116 @@
+/*
+ * 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"
+	"testing"
+
+	"devt.de/krotik/common/fileutil"
+)
+
+const importTestDir = "importtest"
+
+func TestImportLocater(t *testing.T) {
+	if res, _ := fileutil.PathExists(importTestDir); res {
+		os.RemoveAll(importTestDir)
+	}
+
+	err := os.Mkdir(importTestDir, 0770)
+	if err != nil {
+		t.Error("Could not create test dir:", err)
+		return
+	}
+
+	defer func() {
+
+		// Teardown
+
+		if err := os.RemoveAll(importTestDir); err != nil {
+			t.Error("Could not create test dir:", err)
+			return
+		}
+	}()
+
+	err = os.Mkdir(filepath.Join(importTestDir, "test1"), 0770)
+	if err != nil {
+		t.Error("Could not create test dir:", err)
+		return
+	}
+
+	codecontent := "\na := 1 + 1\n"
+
+	ioutil.WriteFile(filepath.Join(importTestDir, "test1", "myfile.ecal"),
+		[]byte(codecontent), 0770)
+
+	fil := &FileImportLocator{importTestDir}
+
+	res, err := fil.Resolve(filepath.Join("..", "t"))
+
+	expectedError := fmt.Sprintf("Import path is outside of code root: ..%vt",
+		string(os.PathSeparator))
+
+	if res != "" || err == nil || err.Error() != expectedError {
+		t.Error("Unexpected result:", res, err)
+		return
+	}
+
+	res, err = fil.Resolve(filepath.Join("..", importTestDir, "x"))
+
+	if res != "" || err == nil || !strings.HasPrefix(err.Error(), "Could not import path") {
+		t.Error("Unexpected result:", res, err)
+		return
+	}
+
+	res, err = fil.Resolve(filepath.Join("..", importTestDir, "x"))
+
+	if res != "" || err == nil || !strings.HasPrefix(err.Error(), "Could not import path") {
+		t.Error("Unexpected result:", res, err)
+		return
+	}
+
+	res, err = fil.Resolve(filepath.Join("test1", "myfile.ecal"))
+
+	if res != codecontent || err != nil {
+		t.Error("Unexpected result:", res, err)
+		return
+	}
+
+	mil := &MemoryImportLocator{make(map[string]string)}
+
+	mil.Files["foo"] = "bar"
+	mil.Files["test"] = "test1"
+
+	res, err = mil.Resolve("xxx")
+
+	if res != "" || err == nil || err.Error() != "Could not find import path: xxx" {
+		t.Error("Unexpected result:", res, err)
+		return
+	}
+
+	res, err = mil.Resolve("foo")
+
+	if res != "bar" || err != nil {
+		t.Error("Unexpected result:", res, err)
+		return
+	}
+
+	res, err = mil.Resolve("test")
+
+	if res != "test1" || err != nil {
+		t.Error("Unexpected result:", res, err)
+		return
+	}
+}

+ 11 - 0
util/types.go

@@ -18,6 +18,17 @@ Processor models a top level execution instance for ECAL.
 type Processor interface {
 }
 
+/*
+ECALImportLocator is used to resolve imports.
+*/
+type ECALImportLocator interface {
+
+	/*
+		Resolve a given import path and parse the imported file into an AST.
+	*/
+	Resolve(path string) (string, error)
+}
+
 /*
 ECALFunction models a callable function in ECAL.
 */