Browse Source

feat: Adding local variable declaration via let

Matthias Ladkau 3 years ago
parent
commit
0eda3e5bc3

+ 0 - 1
cli/ecal.go

@@ -23,7 +23,6 @@ import (
 TODO:
 - create executable binary (pack into single binary)
 - pretty printer
-- local variable definition (let)
 - reload on start for debugger adapter
 */
 

+ 3 - 1
cli/tool/interpret.go

@@ -158,7 +158,9 @@ LoadInitialFile clears the global scope and reloads the initial file.
 func (i *CLIInterpreter) LoadInitialFile(tid uint64) error {
 	var err error
 
-	i.CustomHandler.LoadInitialFile(tid)
+	if i.CustomHandler != nil {
+		i.CustomHandler.LoadInitialFile(tid)
+	}
 
 	i.GlobalVS.Clear()
 

+ 10 - 2
ecal.md

@@ -96,7 +96,7 @@ The order of execution of sinks can be controlled via their priority. All sinks
 
 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, passed as arguments, immediately invoked or deferred for last execution. Each parameter can have a default value which is by default NULL.
+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.
 
 Example:
 ```
@@ -105,7 +105,15 @@ func myfunc(a, b, c=1) {
 }
 ```
 
-Primitive values are passed by value, composition structures like maps and lists are passed by reference.
+Primitive values are passed by value, composition structures like maps and lists are passed by reference. Local variables should be defined using the `let` statement.
+
+Example:
+```
+a := 1
+func myfunc(a, b, c=1) {
+  let a := 2 # Local to myfunc
+}
+```
 
 Comments
 --

+ 1 - 0
interpreter/provider.go

@@ -74,6 +74,7 @@ var providerMap = map[string]ecalRuntimeNew{
 	// Assignment statement
 
 	parser.NodeASSIGN: assignmentRuntimeInst,
+	parser.NodeLET:    letRuntimeInst,
 
 	// Import statement
 

+ 114 - 22
interpreter/rt_assign.go

@@ -42,6 +42,10 @@ func (rt *assignmentRuntime) Validate() error {
 
 		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}
@@ -79,44 +83,132 @@ func (rt *assignmentRuntime) Eval(vs parser.Scope, is map[string]interface{}, ti
 	_, err := rt.baseRuntime.Eval(vs, is, tid)
 
 	if err == nil {
-		var val interface{}
 
-		val, err = rt.node.Children[1].Runtime.Eval(vs, is, tid)
+		// Execute let statements on the right before evaluating the left side
 
-		if err == nil {
-			if len(rt.leftSide) == 1 {
+		if _, err = rt.node.Children[0].Runtime.Eval(vs, is, tid); err == nil {
+			var val interface{}
 
-				err = rt.leftSide[0].Set(vs, is, tid, val)
+			val, err = rt.node.Children[1].Runtime.Eval(vs, is, tid)
 
-			} else if valList, ok := val.([]interface{}); ok {
+			if err == nil {
+				if len(rt.leftSide) == 1 {
 
-				if len(rt.leftSide) != len(valList) {
+					err = rt.leftSide[0].Set(vs, is, tid, val)
 
-					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 if valList, ok := val.([]interface{}); ok {
 
-				} else {
+					if len(rt.leftSide) != len(valList) {
 
-					for i, v := range rt.leftSide {
+						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)
 
-						if err = v.Set(vs, is, tid, valList[i]); err != nil {
-							err = rt.erp.NewRuntimeError(util.ErrVarAccess,
-								err.Error(), rt.node)
-							break
+					} 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 {
+				} else {
 
-				err = rt.erp.NewRuntimeError(util.ErrInvalidState,
-					fmt.Sprintf("Result is not a list (value is %v)", val),
-					rt.node)
+					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
+}

+ 65 - 6
interpreter/rt_assign_test.go

@@ -21,10 +21,11 @@ func TestSimpleAssignments(t *testing.T) {
 	vs := scope.NewScope(scope.GlobalScope)
 
 	res, err := UnitTestEvalAndAST(
-		`a := 42`, vs,
+		`let a := 42`, vs,
 		`
 :=
-  identifier: a
+  let
+    identifier: a
   number: 42
 `[1:])
 
@@ -146,12 +147,13 @@ func TestComplexAssignments(t *testing.T) {
 	vs := scope.NewScope(scope.GlobalScope)
 
 	res, err := UnitTestEvalAndAST(
-		`[a, b] := ["test", [1,2,3]]`, vs,
+		`let [a, b] := ["test", [1,2,3]]`, vs,
 		`
 :=
-  list
-    identifier: a
-    identifier: b
+  let
+    list
+      identifier: a
+      identifier: b
   list
     string: 'test'
     list
@@ -256,3 +258,60 @@ statements
 	}
 
 }
+
+func TestScopedDeclaration(t *testing.T) {
+
+	vs := scope.NewScope(scope.GlobalScope)
+
+	res, err := UnitTestEval(`
+a := 5
+func foo() {
+	let a := 2
+}
+foo()`, vs)
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    a (float64) : 5
+    foo (*interpreter.function) : ecal.function: foo (Line 3, Pos 1)
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+
+	res, err = UnitTestEval(`
+a := 5
+func foo() {
+	let [a, b, c]
+	a := 2
+}
+foo()`, vs)
+
+	if vsRes := vs.String(); err != nil || res != nil || vsRes != `GlobalScope {
+    a (float64) : 5
+    foo (*interpreter.function) : ecal.function: foo (Line 3, Pos 1)
+}` {
+		t.Error("Unexpected result: ", vsRes, res, err)
+		return
+	}
+
+	res, err = UnitTestEval(`let [1]`, vs)
+
+	if err == nil || err.Error() != "ECAL error in ECALTestRuntime: Invalid construct (Let can only declare variables within a list) (Line:1 Pos:1)" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEval(`let 1`, vs)
+
+	if err == nil || err.Error() != "ECAL error in ECALTestRuntime: Invalid construct (Let must declare a variable or list of variables) (Line:1 Pos:1)" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEval(`let a.b`, vs)
+
+	if err == nil || err.Error() != "ECAL error in ECALTestRuntime: Invalid construct (Let can only declare simple variables) (Line:1 Pos:1)" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+}

+ 2 - 0
parser/const.go

@@ -125,6 +125,7 @@ const (
 	// Assignment statement
 
 	TokenASSIGN
+	TokenLET
 
 	TOKENodeKEYWORDS // Used to separate keywords from other tokens in this list
 
@@ -241,6 +242,7 @@ const (
 	// Assignment statement
 
 	NodeASSIGN = ":="
+	NodeLET    = "let"
 
 	// Import statement
 

+ 4 - 0
parser/lexer.go

@@ -179,6 +179,10 @@ KeywordMap is a map of keywords - these require spaces between them
 */
 var KeywordMap = map[string]LexTokenID{
 
+	// Assign statement
+
+	"let": TokenLET,
+
 	// Import statement
 
 	"import": TokenIMPORT,

+ 2 - 2
parser/lexer_test.go

@@ -70,14 +70,14 @@ func TestEquals(t *testing.T) {
 		return
 	}
 
-	if ok, msg := l[0].Equals(l[1], false); ok || msg != `ID is different 53 vs 7
+	if ok, msg := l[0].Equals(l[1], false); ok || msg != `ID is different 54 vs 7
 Pos is different 0 vs 5
 Val is different not vs test
 Identifier is different false vs true
 Lline is different 1 vs 2
 Lpos is different 1 vs 2
 {
-  "ID": 53,
+  "ID": 54,
   "Pos": 0,
   "Val": "not",
   "Identifier": false,

+ 1 - 0
parser/parser.go

@@ -81,6 +81,7 @@ func init() {
 		// Assignment statement
 
 		TokenASSIGN: {NodeASSIGN, nil, nil, nil, nil, 10, nil, ldInfix},
+		TokenLET:    {NodeLET, nil, nil, nil, nil, 0, ndPrefix, nil},
 
 		// Import statement
 

+ 5 - 3
parser/parser_func_test.go

@@ -18,15 +18,17 @@ import (
 func TestImportParsing(t *testing.T) {
 
 	input := `import "foo/bar.ecal" as fooBar
-	i := fooBar`
+	let i := let fooBar`
 	expectedOutput := `
 statements
   import
     string: 'foo/bar.ecal'
     identifier: fooBar
   :=
-    identifier: i
-    identifier: fooBar
+    let
+      identifier: i
+    let
+      identifier: fooBar
 `[1:]
 
 	if res, err := UnitTestParse("mytest", input); err != nil || fmt.Sprint(res) != expectedOutput {

+ 1 - 0
parser/prettyprinter.go

@@ -75,6 +75,7 @@ func init() {
 		// Assignment statement
 
 		NodeASSIGN + "_2": template.Must(template.New(NodeASSIGN).Parse("{{.c1}} := {{.c2}}")),
+		NodeLET + "_1":    template.Must(template.New(NodeASSIGN).Parse("let {{.c1}}")),
 
 		// Import statement
 

+ 5 - 0
parser/runtime.go

@@ -75,6 +75,11 @@ type Scope interface {
 	*/
 	SetValue(varName string, varValue interface{}) error
 
+	/*
+	   SetLocalValue sets a new value for a local variable.
+	*/
+	SetLocalValue(varName string, varValue interface{}) error
+
 	/*
 	   GetValue gets the current value of a variable.
 	*/

+ 15 - 0
scope/varsscope.go

@@ -126,6 +126,21 @@ func (s *varsScope) SetValue(varName string, varValue interface{}) error {
 	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.
 */

+ 4 - 1
scope/varsscope_test.go

@@ -294,6 +294,8 @@ func TestVarScopeDump(t *testing.T) {
 	for211Vs.SetValue("x", ToObject(for211Vs))
 	sinkVs2.SetValue("g", 2)
 
+	sinkVs1.SetLocalValue("a", 5)
+
 	// Dump the sinkVs1 scope
 
 	if res := sinkVs1.String(); res != `global {
@@ -301,6 +303,7 @@ func TestVarScopeDump(t *testing.T) {
     global2 {
         a (int) : 1
         sink: 1 {
+            a (int) : 5
             b (int) : 2
             block: for1 {
                 c (int) : 3
@@ -323,7 +326,7 @@ func TestVarScopeDump(t *testing.T) {
 	}
 
 	bytes, _ := json.Marshal(sinkVs1.ToJSONObject())
-	if res := string(bytes); res != `{"b":2}` {
+	if res := string(bytes); res != `{"a":5,"b":2}` {
 		t.Error("Unexpected result:", res)
 		return
 	}