Browse Source

feat: Adding ECAL pretty printer

Matthias Ladkau 3 years ago
parent
commit
423a6bfe5f

+ 2 - 2
lang/ecal/parser/const.go

@@ -197,8 +197,8 @@ const (
 
 	NodeLIKE      = "like"
 	NodeIN        = "in"
-	NodeHASPREFIX = "hasPrefix"
-	NodeHASSUFFIX = "hasSuffix"
+	NodeHASPREFIX = "hasprefix"
+	NodeHASSUFFIX = "hassuffix"
 	NodeNOTIN     = "notin"
 
 	NodeGEQ = ">="

+ 2 - 2
lang/ecal/parser/lexer.go

@@ -105,8 +105,8 @@ var KeywordMap = map[string]LexTokenID{
 	// String operators
 
 	"like":      TokenLIKE,
-	"hasPrefix": TokenHASPREFIX,
-	"hasSuffix": TokenHASSUFFIX,
+	"hasprefix": TokenHASPREFIX,
+	"hassuffix": TokenHASSUFFIX,
 
 	// List operators
 

+ 121 - 0
lang/ecal/parser/main_test.go

@@ -0,0 +1,121 @@
+/*
+ * 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 parser
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"testing"
+)
+
+// Main function for all tests in this package
+
+func TestMain(m *testing.M) {
+	flag.Parse()
+
+	res := m.Run()
+
+	// Check if all nodes have been tested
+
+	for _, n := range astNodeMap {
+		if _, ok := usedNodes[n.Name]; !ok {
+			fmt.Println("Not tested node: ", n.Name)
+		}
+	}
+
+	// Check if all nodes have been pretty printed
+
+	for k := range prettyPrinterMap {
+		if _, ok := usedPrettyPrinterNodes[k]; !ok {
+			fmt.Println("Not tested pretty printer: ", k)
+		}
+	}
+
+	os.Exit(res)
+}
+
+// Used nodes map which is filled during unit testing. Prefilled with tokens which
+// will not be generated by the parser
+//
+var usedNodes = map[string]bool{
+	NodeEOF: true, // Only used as end term
+	"":      true, // No node e.g. semicolon - These nodes should never be part of an AST
+}
+
+func UnitTestParse(name string, input string) (*ASTNode, error) {
+	n, err := ParseWithRuntime(name, input, &DummyRuntimeProvider{})
+
+	// TODO Test pretty printing
+
+	// TODO Test AST serialization
+
+	return n, err
+}
+
+// Used nodes map which is filled during unit testing. Prefilled with tokens which
+// will not be generated by the parser
+//
+var usedPrettyPrinterNodes = map[string]bool{}
+
+func UnitTestPrettyPrinting(input, astOutput, ppOutput string) error {
+	var visitAST func(*ASTNode)
+
+	astres, err := ParseWithRuntime("mytest", input, &DummyRuntimeProvider{})
+	if err != nil || fmt.Sprint(astres) != astOutput {
+		return fmt.Errorf("Unexpected parser output:\n%v expected was:\n%v Error: %v", astres, astOutput, err)
+	}
+
+	visitAST = func(n *ASTNode) {
+
+		// Make the encountered node as used
+
+		numChildren := len(n.Children)
+		if numChildren > 0 {
+			usedPrettyPrinterNodes[fmt.Sprintf("%v_%v", n.Name, numChildren)] = true
+		} else {
+			usedPrettyPrinterNodes[n.Name] = true
+		}
+
+		for _, c := range n.Children {
+			visitAST(c)
+		}
+	}
+
+	visitAST(astres)
+
+	ppres, err := PrettyPrint(astres)
+	if err != nil || ppres != ppOutput {
+		return fmt.Errorf("Unexpected result: %v (expected: %v) error: %v", ppres, ppOutput, err)
+	}
+
+	// Make sure the pretty printed result is valid and gets the same parse tree
+
+	astres2, err := ParseWithRuntime("mytest", ppres, &DummyRuntimeProvider{})
+	if err != nil || fmt.Sprint(astres2) != astOutput {
+		return fmt.Errorf("Unexpected parser output from pretty print string:\n%v expected was:\n%v Error: %v", astres2, astOutput, err)
+	}
+
+	return nil
+}
+
+// Helper objects
+
+type DummyRuntimeProvider struct {
+}
+
+func (d *DummyRuntimeProvider) Runtime(n *ASTNode) Runtime {
+
+	// Make the encountered node as used
+
+	usedNodes[n.Name] = true
+
+	return nil
+}

+ 0 - 63
lang/ecal/parser/parser_helper_test.go

@@ -1,63 +0,0 @@
-/*
- * 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 parser
-
-import (
-	"flag"
-	"fmt"
-	"os"
-	"testing"
-)
-
-// Main function for all tests in this package
-
-func TestMain(m *testing.M) {
-	flag.Parse()
-
-	res := m.Run()
-
-	// Check if all nodes have been tested
-
-	for _, n := range astNodeMap {
-		if _, ok := usedNodes[n.Name]; !ok {
-			fmt.Println("Not tested node: ", n.Name)
-		}
-	}
-
-	os.Exit(res)
-}
-
-// Used nodes map which is filled during unit testing. Prefilled with tokens which
-// will not be generated by the parser
-//
-var usedNodes = map[string]bool{
-	NodeEOF: true, // Only used as end term
-	"":      true, // No node e.g. semicolon - These nodes should never be part of an AST
-}
-
-func UnitTestParse(name string, input string) (*ASTNode, error) {
-	n, err := ParseWithRuntime(name, input, &DummyRuntimeProvider{})
-
-	return n, err
-}
-
-// Helper objects
-
-type DummyRuntimeProvider struct {
-}
-
-func (d *DummyRuntimeProvider) Runtime(n *ASTNode) Runtime {
-
-	// Make the encountered node as used
-
-	usedNodes[n.Name] = true
-
-	return nil
-}

+ 142 - 0
lang/ecal/parser/parser_main_test.go

@@ -79,3 +79,145 @@ minus
 	}
 
 }
+
+func TestArithmeticParsing(t *testing.T) {
+	input := "a + b * 5 /2"
+	expectedOutput := `
+plus
+  identifier: a
+  div
+    times
+      identifier: b
+      number: 5
+    number: 2
+`[1:]
+
+	if res, err := UnitTestParse("mytest", input); err != nil || fmt.Sprint(res) != expectedOutput {
+		t.Error("Unexpected parser output:\n", res, "expected was:\n", expectedOutput, "Error:", err)
+		return
+	}
+
+	// Test brackets
+
+	input = "a + 1 * (5 + 6)"
+	expectedOutput = `
+plus
+  identifier: a
+  times
+    number: 1
+    plus
+      number: 5
+      number: 6
+`[1:]
+
+	if res, err := UnitTestParse("mytest", input); err != nil || fmt.Sprint(res) != expectedOutput {
+		t.Error("Unexpected parser output:\n", res, "expected was:\n", expectedOutput, "Error:", err)
+		return
+	}
+
+	input = "(a + 1) * (5 / (6 - 2))"
+	expectedOutput = `
+times
+  plus
+    identifier: a
+    number: 1
+  div
+    number: 5
+    minus
+      number: 6
+      number: 2
+`[1:]
+
+	if res, err := UnitTestParse("mytest", input); err != nil || fmt.Sprint(res) != expectedOutput {
+		t.Error("Unexpected parser output:\n", res, "expected was:\n", expectedOutput, "Error:", err)
+		return
+	}
+}
+
+func TestLogicParsing(t *testing.T) {
+	input := "not (a + 1) * 5 and tRue == false or not 1 - 5 != test"
+	expectedOutput := `
+or
+  and
+    not
+      times
+        plus
+          identifier: a
+          number: 1
+        number: 5
+    ==
+      true
+      false
+  not
+    !=
+      minus
+        number: 1
+        number: 5
+      identifier: test
+`[1:]
+
+	if res, err := UnitTestParse("mytest", input); err != nil || fmt.Sprint(res) != expectedOutput {
+		t.Error("Unexpected parser output:\n", res, "expected was:\n", expectedOutput, "Error:", err)
+		return
+	}
+
+	input = "a > b or a <= p or b hasSuffix 'test' or c hasPrefix 'test' and x < 4 or x >= 10"
+	expectedOutput = `
+or
+  or
+    or
+      or
+        >
+          identifier: a
+          identifier: b
+        <=
+          identifier: a
+          identifier: p
+      hassuffix
+        identifier: b
+        string: 'test'
+    and
+      hasprefix
+        identifier: c
+        string: 'test'
+      <
+        identifier: x
+        number: 4
+  >=
+    identifier: x
+    number: 10
+`[1:]
+
+	if res, err := UnitTestParse("mytest", input); err != nil || fmt.Sprint(res) != expectedOutput {
+		t.Error("Unexpected parser output:\n", res, "expected was:\n", expectedOutput, "Error:", err)
+		return
+	}
+
+	input = "(a in null or c notin d) and false like 9 or x // 6 > 2 % 1"
+	expectedOutput = `
+or
+  and
+    or
+      in
+        identifier: a
+        null
+      notin
+        identifier: c
+        identifier: d
+    like
+      false
+      number: 9
+  >
+    divint
+      identifier: x
+      number: 6
+    modint
+      number: 2
+      number: 1
+`[1:]
+
+	if res, err := UnitTestParse("mytest", input); err != nil || fmt.Sprint(res) != expectedOutput {
+		t.Error("Unexpected parser output:\n", res, "expected was:\n", expectedOutput, "Error:", err)
+		return
+	}
+}

+ 150 - 0
lang/ecal/parser/prettyprinter.go

@@ -0,0 +1,150 @@
+/*
+ * 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 parser
+
+import (
+	"bytes"
+	"fmt"
+	"strconv"
+	"text/template"
+
+	"devt.de/krotik/common/errorutil"
+)
+
+/*
+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{
+
+		NodeCOMMENT:    template.Must(template.New(NodeTRUE).Parse("true")),
+		NodeSTRING:     template.Must(template.New(NodeTRUE).Parse("{{.qval}}")),
+		NodeNUMBER:     template.Must(template.New(NodeTRUE).Parse("{{.val}}")),
+		NodeIDENTIFIER: template.Must(template.New(NodeTRUE).Parse("{{.val}}")),
+
+		/*
+
+			// Constructed tokens
+
+			NodeSTATEMENTS = "statements" // List of statements
+
+			// Assignment statement
+
+			NodeASSIGN = ":="
+		*/
+		// Arithmetic operators
+
+		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}}")),
+
+		// Boolean operators
+
+		NodeOR + "_2":  template.Must(template.New(NodeGEQ).Parse("{{.c1}} or {{.c2}}")),
+		NodeAND + "_2": template.Must(template.New(NodeLEQ).Parse("{{.c1}} and {{.c2}}")),
+		NodeNOT + "_1": template.Must(template.New(NodeNOT).Parse("not {{.c1}}")),
+
+		// Condition operators
+
+		NodeLIKE + "_2":      template.Must(template.New(NodeGEQ).Parse("{{.c1}} like {{.c2}}")),
+		NodeIN + "_2":        template.Must(template.New(NodeLEQ).Parse("{{.c1}} in {{.c2}}")),
+		NodeHASPREFIX + "_2": template.Must(template.New(NodeLEQ).Parse("{{.c1}} hasprefix {{.c2}}")),
+		NodeHASSUFFIX + "_2": template.Must(template.New(NodeLEQ).Parse("{{.c1}} hassuffix {{.c2}}")),
+		NodeNOTIN + "_2":     template.Must(template.New(NodeLEQ).Parse("{{.c1}} notin {{.c2}}")),
+
+		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}}")),
+
+		// Constants
+
+		NodeTRUE:  template.Must(template.New(NodeTRUE).Parse("true")),
+		NodeFALSE: template.Must(template.New(NodeFALSE).Parse("false")),
+		NodeNULL:  template.Must(template.New(NodeNULL).Parse("null")),
+	}
+
+	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, level int) (string, error)
+
+	visit = func(ast *ASTNode, level int) (string, error) {
+		var buf bytes.Buffer
+
+		tempKey := ast.Name
+		tempParam := make(map[string]string)
+
+		// First pretty print children
+
+		if len(ast.Children) > 0 {
+			for i, child := range ast.Children {
+				res, err := visit(child, level+1)
+				if err != nil {
+					return "", err
+				}
+
+				if _, ok := bracketPrecedenceMap[child.Name]; ok && ast.binding > child.binding {
+
+					// Put the expression in brackets 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))
+		}
+
+		// 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]
+		if !ok {
+			return "", fmt.Errorf("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 buf.String(), nil
+	}
+
+	return visit(ast, 0)
+}

+ 187 - 0
lang/ecal/parser/prettyprinter_test.go

@@ -0,0 +1,187 @@
+/*
+ * 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 parser
+
+import (
+	"testing"
+)
+
+func TestArithmeticExpressionPrinting(t *testing.T) {
+
+	input := "a + b * 5 /2-1"
+	expectedOutput := `
+minus
+  plus
+    identifier: a
+    div
+      times
+        identifier: b
+        number: 5
+      number: 2
+  number: 1
+`[1:]
+
+	if err := UnitTestPrettyPrinting(input, expectedOutput,
+		"a + b * 5 / 2 - 1"); err != nil {
+		t.Error(err)
+		return
+	}
+
+	input = `-a + "\"'b"`
+	expectedOutput = `
+plus
+  minus
+    identifier: a
+  string: '"'b'
+`[1:]
+
+	if err := UnitTestPrettyPrinting(input, expectedOutput,
+		`-a + "\"'b"`); err != nil {
+		t.Error(err)
+		return
+	}
+
+	input = `a // 5 % (50 + 1)`
+	expectedOutput = `
+modint
+  divint
+    identifier: a
+    number: 5
+  plus
+    number: 50
+    number: 1
+`[1:]
+
+	if err := UnitTestPrettyPrinting(input, expectedOutput,
+		`a // 5 % (50 + 1)`); err != nil {
+		t.Error(err)
+		return
+	}
+
+	input = "(a + 1) * 5 / (6 - 2)"
+	expectedOutput = `
+div
+  times
+    plus
+      identifier: a
+      number: 1
+    number: 5
+  minus
+    number: 6
+    number: 2
+`[1:]
+
+	if err := UnitTestPrettyPrinting(input, expectedOutput,
+		"(a + 1) * 5 / (6 - 2)"); err != nil {
+		t.Error(err)
+		return
+	}
+
+	input = "a + (1 * 5) / 6 - 2"
+	expectedOutput = `
+minus
+  plus
+    identifier: a
+    div
+      times
+        number: 1
+        number: 5
+      number: 6
+  number: 2
+`[1:]
+
+	if err := UnitTestPrettyPrinting(input, expectedOutput,
+		"a + 1 * 5 / 6 - 2"); err != nil {
+		t.Error(err)
+		return
+	}
+}
+
+func TestLogicalExpressionPrinting(t *testing.T) {
+	input := "not (a + 1) * 5 and tRue or not 1 - 5 != '!test'"
+	expectedOutput := `
+or
+  and
+    not
+      times
+        plus
+          identifier: a
+          number: 1
+        number: 5
+    true
+  not
+    !=
+      minus
+        number: 1
+        number: 5
+      string: '!test'
+`[1:]
+
+	if err := UnitTestPrettyPrinting(input, expectedOutput,
+		"not (a + 1) * 5 and true or not 1 - 5 != \"!test\""); err != nil {
+		t.Error(err)
+		return
+	}
+
+	input = "not x < null and a > b or 1 <= c and 2 >= false or c == true"
+	expectedOutput = `
+or
+  or
+    and
+      not
+        <
+          identifier: x
+          null
+      >
+        identifier: a
+        identifier: b
+    and
+      <=
+        number: 1
+        identifier: c
+      >=
+        number: 2
+        false
+  ==
+    identifier: c
+    true
+`[1:]
+
+	if err := UnitTestPrettyPrinting(input, expectedOutput,
+		"not x < null and a > b or 1 <= c and 2 >= false or c == true"); err != nil {
+		t.Error(err)
+		return
+	}
+
+	input = "a hasPrefix 'a' and b hassuffix 'c' or d like '^.*' and 3 notin x"
+	expectedOutput = `
+or
+  and
+    hasprefix
+      identifier: a
+      string: 'a'
+    hassuffix
+      identifier: b
+      string: 'c'
+  and
+    like
+      identifier: d
+      string: '^.*'
+    notin
+      number: 3
+      identifier: x
+`[1:]
+
+	if err := UnitTestPrettyPrinting(input, expectedOutput,
+		`a hasprefix "a" and b hassuffix "c" or d like "^.*" and 3 notin x`); err != nil {
+		t.Error(err)
+		return
+	}
+}