Browse Source

feat: String interpolation

Matthias Ladkau 5 years ago
parent
commit
a1ba9b4d93

+ 7 - 15
ecal.md

@@ -147,34 +147,26 @@ Strings can be normal quoted stings which interpret backslash escape characters:
 \uhhhh → a Unicode character whose codepoint can be expressed in 4 hexadecimal digits. (pad 0 in front)
 \uhhhh → a Unicode character whose codepoint can be expressed in 4 hexadecimal digits. (pad 0 in front)
 ```
 ```
 
 
-Normal quoted strings also interpret inline expressions escaped with `{}`:
+Normal quoted strings also interpret inline expressions and statements escaped with `{{}}`:
 ```
 ```
-"Foo bar {1+2}"
+"Foo bar {{1+2}}"
 ```
 ```
-Inline expression may also specify number formatting:
-```
-"Foo bar {1+2}.2f"
-```
-Formatting|Description
--|-
-{}.f|With decimal point full precision
-{}.3f|Decimal point with precision 3
-{}.5w3f|5 Width with decimal point with precision 3
-{}.e|Scientific notation
 
 
 Strings can also be expressed in raw form which will not interpret any escape characters.
 Strings can also be expressed in raw form which will not interpret any escape characters.
 ```
 ```
-r"Foo bar {1+2}"
+r"Foo bar {{1+2}}"
 ```
 ```
 
 
+Some examples:
+
 Expression|Value
 Expression|Value
 -|-
 -|-
 `"foo'bar"`| `foo'bar`
 `"foo'bar"`| `foo'bar`
 `'foo"bar'`| `foo"bar`
 `'foo"bar'`| `foo"bar`
 `'foo\u0028bar'`| `foo(bar`
 `'foo\u0028bar'`| `foo(bar`
 `"foo\u0028bar"`| `foo(bar`
 `"foo\u0028bar"`| `foo(bar`
-`"Foo bar {1+2}"`| `Foo bar 3`
-`r"Foo bar {1+2}"`| `Foo bar {1+2}`
+`"Foo bar {{1+2}}"`| `Foo bar 3`
+`r"Foo bar {{1+2}}"`| `Foo bar {{1+2}}`
 
 
 Variable Assignments
 Variable Assignments
 --
 --

+ 4 - 4
interpreter/func_provider.go

@@ -514,9 +514,9 @@ func (rf *raise) Run(instanceID string, vs parser.Scope, is map[string]interface
 	node := is["astnode"].(*parser.ASTNode)
 	node := is["astnode"].(*parser.ASTNode)
 
 
 	return nil, &util.RuntimeErrorWithDetail{
 	return nil, &util.RuntimeErrorWithDetail{
-		erp.NewRuntimeError(err, detailMsg, node).(*util.RuntimeError),
-		vs,
-		detail,
+		RuntimeError: erp.NewRuntimeError(err, detailMsg, node).(*util.RuntimeError),
+		Environment:  vs,
+		Data:         detail,
 	}
 	}
 
 
 }
 }
@@ -586,7 +586,7 @@ func (rf *addevent) addEvent(addFunc func(engine.Processor, *engine.Event, *engi
 			)
 			)
 
 
 			if len(args) > 3 {
 			if len(args) > 3 {
-				var scopeMap = map[interface{}]interface{}{}
+				var scopeMap map[interface{}]interface{}
 
 
 				// Add optional scope - if not specified it is { "": true }
 				// Add optional scope - if not specified it is { "": true }
 
 

+ 0 - 4
interpreter/provider.go

@@ -8,10 +8,6 @@
  * file, You can obtain one at https://opensource.org/licenses/MIT.
  * file, You can obtain one at https://opensource.org/licenses/MIT.
  */
  */
 
 
-// TODO:
-// Context supporting final and exception handling
-// Inline escaping in strings "bla {1+1} bla"
-
 package interpreter
 package interpreter
 
 
 import (
 import (

+ 3 - 3
interpreter/rt_sink.go

@@ -212,9 +212,9 @@ func (rt *sinkRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interfa
 								// Provide additional information for unexpected errors
 								// Provide additional information for unexpected errors
 
 
 								err = &util.RuntimeErrorWithDetail{
 								err = &util.RuntimeErrorWithDetail{
-									err.(*util.RuntimeError),
-									sinkVS,
-									nil,
+									RuntimeError: err.(*util.RuntimeError),
+									Environment:  sinkVS,
+									Data:         nil,
 								}
 								}
 							}
 							}
 						}
 						}

+ 3 - 2
interpreter/rt_statements.go

@@ -41,17 +41,18 @@ func statementsRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parse
 Eval evaluate this runtime component.
 Eval evaluate this runtime component.
 */
 */
 func (rt *statementsRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
 func (rt *statementsRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
+	var res interface{}
 	_, err := rt.baseRuntime.Eval(vs, is)
 	_, err := rt.baseRuntime.Eval(vs, is)
 
 
 	if err == nil {
 	if err == nil {
 		for _, child := range rt.node.Children {
 		for _, child := range rt.node.Children {
-			if _, err := child.Runtime.Eval(vs, is); err != nil {
+			if res, err = child.Runtime.Eval(vs, is); err != nil {
 				return nil, err
 				return nil, err
 			}
 			}
 		}
 		}
 	}
 	}
 
 
-	return nil, err
+	return res, err
 }
 }
 
 
 // Condition statement
 // Condition statement

+ 57 - 2
interpreter/rt_value.go

@@ -11,9 +11,12 @@
 package interpreter
 package interpreter
 
 
 import (
 import (
+	"fmt"
 	"strconv"
 	"strconv"
+	"strings"
 
 
 	"devt.de/krotik/ecal/parser"
 	"devt.de/krotik/ecal/parser"
+	"devt.de/krotik/ecal/scope"
 )
 )
 
 
 /*
 /*
@@ -73,9 +76,61 @@ Eval evaluate this runtime component.
 func (rt *stringValueRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
 func (rt *stringValueRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
 	_, err := rt.baseRuntime.Eval(vs, is)
 	_, err := rt.baseRuntime.Eval(vs, is)
 
 
-	// Do some string interpolation
+	ret := rt.node.Token.Val
 
 
-	return rt.node.Token.Val, err
+	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{}))
+
+					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
 }
 }
 
 
 /*
 /*

+ 67 - 0
interpreter/rt_value_test.go

@@ -41,6 +41,73 @@ string: 'test'
 	}
 	}
 }
 }
 
 
+func TestStringInterpolation(t *testing.T) {
+
+	res, err := UnitTestEvalAndAST(
+		`"{{'foo'}}test{{'foo'}}test"`, nil,
+		`
+string: '{{'foo'}}test{{'foo'}}test'
+`[1:])
+
+	if err != nil || res != "footestfootest" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEvalAndAST(
+		`"{{foo'}}test{{'foo'}}test"`, nil,
+		`
+string: '{{foo'}}test{{'foo'}}test'
+`[1:])
+
+	if err != nil || res != "#Parse error in String interpolation: foo': Lexical "+
+		"error (Cannot parse identifier 'foo''. Identifies may only contain [a-zA-Z] "+
+		"and [a-zA-Z0-9] from the second character) (Line:1 Pos:1)testfootest" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEval(
+		`"Foo bar {{1+2}}"`, nil)
+
+	if err != nil || res != "Foo bar 3" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEval(
+		`r"Foo bar {{1+2}}"`, nil)
+
+	if err != nil || res != "Foo bar {{1+2}}" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEval(
+		`b:=1;"test{{a:=1;concat([1,2,3], [4,5], [a,b])}}test"`, nil)
+
+	if err != nil || res != "test[1 2 3 4 5 1 1]test" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEval(
+		`b:="foo";"test{{if b { 1 } else { 2 } }}test"`, nil)
+
+	if err != nil || res != "test1test" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+
+	res, err = UnitTestEval(
+		`b:=null;"test{{if b { 1 } else { 2 } }}test"`, nil)
+
+	if err != nil || res != "test2test" {
+		t.Error("Unexpected result: ", res, err)
+		return
+	}
+}
+
 func TestCompositionValues(t *testing.T) {
 func TestCompositionValues(t *testing.T) {
 
 
 	res, err := UnitTestEvalAndAST(
 	res, err := UnitTestEvalAndAST(

+ 13 - 6
parser/helper.go

@@ -259,6 +259,7 @@ func (n *ASTNode) ToJSONObject() map[string]interface{} {
 			ret["value"] = n.Token.Val
 			ret["value"] = n.Token.Val
 		}
 		}
 		ret["identifier"] = n.Token.Identifier
 		ret["identifier"] = n.Token.Identifier
+		ret["allowescapes"] = n.Token.AllowEscapes
 		ret["pos"] = n.Token.Pos
 		ret["pos"] = n.Token.Pos
 		ret["line"] = n.Token.Lline
 		ret["line"] = n.Token.Lline
 		ret["linepos"] = n.Token.Lpos
 		ret["linepos"] = n.Token.Lpos
@@ -310,6 +311,11 @@ func ASTFromJSONObject(jsonAST map[string]interface{}) (*ASTNode, error) {
 		identifier = false
 		identifier = false
 	}
 	}
 
 
+	allowescapes, ok := jsonAST["allowescapes"]
+	if !ok {
+		allowescapes = false
+	}
+
 	if posString, ok := jsonAST["pos"]; ok {
 	if posString, ok := jsonAST["pos"]; ok {
 		pos, _ = strconv.Atoi(fmt.Sprint(posString))
 		pos, _ = strconv.Atoi(fmt.Sprint(posString))
 	} else {
 	} else {
@@ -380,12 +386,13 @@ func ASTFromJSONObject(jsonAST map[string]interface{}) (*ASTNode, error) {
 	}
 	}
 
 
 	token := &LexToken{
 	token := &LexToken{
-		nodeID,             // ID
-		pos,                // Pos
-		fmt.Sprint(value),  // Val
-		identifier == true, // Identifier
-		line,               // Lline
-		linepos,            // Lpos
+		nodeID,               // ID
+		pos,                  // Pos
+		fmt.Sprint(value),    // Val
+		identifier == true,   // Identifier
+		allowescapes == true, // AllowEscapes
+		line,                 // Lline
+		linepos,              // Lpos
 	}
 	}
 
 
 	return &ASTNode{fmt.Sprint(name), token, astMeta, astChildren, nil, 0, nil, nil}, nil
 	return &ASTNode{fmt.Sprint(name), token, astMeta, astChildren, nil, 0, nil, nil}, nil

+ 6 - 0
parser/helper_test.go

@@ -39,6 +39,7 @@ Lpos is different 3 vs 2
   "Pos": 2,
   "Pos": 2,
   "Val": "1",
   "Val": "1",
   "Identifier": false,
   "Identifier": false,
+  "AllowEscapes": false,
   "Lline": 1,
   "Lline": 1,
   "Lpos": 3
   "Lpos": 3
 }
 }
@@ -48,6 +49,7 @@ vs
   "Pos": 1,
   "Pos": 1,
   "Val": "2",
   "Val": "2",
   "Identifier": false,
   "Identifier": false,
+  "AllowEscapes": false,
   "Lline": 1,
   "Lline": 1,
   "Lpos": 2
   "Lpos": 2
 }
 }
@@ -85,6 +87,7 @@ Identifier is different false vs true
   "Pos": 1,
   "Pos": 1,
   "Val": "1",
   "Val": "1",
   "Identifier": false,
   "Identifier": false,
+  "AllowEscapes": false,
   "Lline": 1,
   "Lline": 1,
   "Lpos": 2
   "Lpos": 2
 }
 }
@@ -94,6 +97,7 @@ vs
   "Pos": 1,
   "Pos": 1,
   "Val": "a",
   "Val": "a",
   "Identifier": true,
   "Identifier": true,
+  "AllowEscapes": false,
   "Lline": 1,
   "Lline": 1,
   "Lpos": 2
   "Lpos": 2
 }
 }
@@ -207,6 +211,7 @@ Lpos is different 1 vs 10
   "Pos": 0,
   "Pos": 0,
   "Val": "1",
   "Val": "1",
   "Identifier": false,
   "Identifier": false,
+  "AllowEscapes": false,
   "Lline": 1,
   "Lline": 1,
   "Lpos": 1
   "Lpos": 1
 }
 }
@@ -216,6 +221,7 @@ vs
   "Pos": 9,
   "Pos": 9,
   "Val": "1",
   "Val": "1",
   "Identifier": false,
   "Identifier": false,
+  "AllowEscapes": false,
   "Lline": 1,
   "Lline": 1,
   "Lpos": 10
   "Lpos": 10
 }
 }

+ 20 - 17
parser/lexer.go

@@ -28,12 +28,13 @@ var numberPattern = regexp.MustCompile("^[0-9].*$")
 LexToken represents a token which is returned by the lexer.
 LexToken represents a token which is returned by the lexer.
 */
 */
 type LexToken struct {
 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)
-	Lline      int        // Line in the input this token appears
-	Lpos       int        // Position in the input line this token appears
+	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
+	Lline        int        // Line in the input this token appears
+	Lpos         int        // Position in the input line this token appears
 }
 }
 
 
 /*
 /*
@@ -45,6 +46,7 @@ func NewLexTokenInstance(t LexToken) *LexToken {
 		t.Pos,
 		t.Pos,
 		t.Val,
 		t.Val,
 		t.Identifier,
 		t.Identifier,
+		t.AllowEscapes,
 		t.Lline,
 		t.Lline,
 		t.Lpos,
 		t.Lpos,
 	}
 	}
@@ -404,12 +406,12 @@ emitToken passes a token back to the client.
 */
 */
 func (l *lexer) emitToken(t LexTokenID) {
 func (l *lexer) emitToken(t LexTokenID) {
 	if t == TokenEOF {
 	if t == TokenEOF {
-		l.emitTokenAndValue(t, "", false)
+		l.emitTokenAndValue(t, "", false, false)
 		return
 		return
 	}
 	}
 
 
 	if l.tokens != nil {
 	if l.tokens != nil {
-		l.tokens <- LexToken{t, l.start, l.input[l.start:l.pos], false,
+		l.tokens <- LexToken{t, l.start, l.input[l.start:l.pos], false, false,
 			l.line + 1, l.start - l.lastnl + 1}
 			l.line + 1, l.start - l.lastnl + 1}
 	}
 	}
 }
 }
@@ -417,9 +419,9 @@ func (l *lexer) emitToken(t LexTokenID) {
 /*
 /*
 emitTokenAndValue passes a token with a given value back to the client.
 emitTokenAndValue passes a token with a given value back to the client.
 */
 */
-func (l *lexer) emitTokenAndValue(t LexTokenID, val string, identifier bool) {
+func (l *lexer) emitTokenAndValue(t LexTokenID, val string, identifier bool, allowEscapes bool) {
 	if l.tokens != nil {
 	if l.tokens != nil {
-		l.tokens <- LexToken{t, l.start, val, identifier, l.line + 1, l.start - l.lastnl + 1}
+		l.tokens <- LexToken{t, l.start, val, identifier, allowEscapes, l.line + 1, l.start - l.lastnl + 1}
 	}
 	}
 }
 }
 
 
@@ -428,7 +430,7 @@ emitError passes an error token back to the client.
 */
 */
 func (l *lexer) emitError(msg string) {
 func (l *lexer) emitError(msg string) {
 	if l.tokens != nil {
 	if l.tokens != nil {
-		l.tokens <- LexToken{TokenError, l.start, msg, false, l.line + 1, l.start - l.lastnl + 1}
+		l.tokens <- LexToken{TokenError, l.start, msg, false, false, l.line + 1, l.start - l.lastnl + 1}
 	}
 	}
 }
 }
 
 
@@ -580,7 +582,7 @@ func lexToken(l *lexer) lexFunc {
 		_, err := strconv.ParseFloat(keywordCandidate, 64)
 		_, err := strconv.ParseFloat(keywordCandidate, 64)
 
 
 		if err == nil {
 		if err == nil {
-			l.emitTokenAndValue(TokenNUMBER, keywordCandidate, false)
+			l.emitTokenAndValue(TokenNUMBER, keywordCandidate, false, false)
 			return lexToken
 			return lexToken
 		}
 		}
 	}
 	}
@@ -618,7 +620,7 @@ func lexToken(l *lexer) lexFunc {
 
 
 		// An identifier was found
 		// An identifier was found
 
 
-		l.emitTokenAndValue(TokenIDENTIFIER, identifierCandidate, true)
+		l.emitTokenAndValue(TokenIDENTIFIER, identifierCandidate, true, false)
 	}
 	}
 
 
 	return lexToken
 	return lexToken
@@ -693,10 +695,11 @@ func lexValue(l *lexer) lexFunc {
 			return nil
 			return nil
 		}
 		}
 
 
-		l.emitTokenAndValue(TokenSTRING, s, true)
+		l.emitTokenAndValue(TokenSTRING, s, true, true)
 
 
 	} else {
 	} else {
-		l.emitTokenAndValue(TokenSTRING, l.input[l.start+2:l.pos-1], true)
+
+		l.emitTokenAndValue(TokenSTRING, l.input[l.start+2:l.pos-1], true, false)
 	}
 	}
 
 
 	//  Set newline
 	//  Set newline
@@ -724,7 +727,7 @@ func lexComment(l *lexer) lexFunc {
 			r = l.next(0)
 			r = l.next(0)
 		}
 		}
 
 
-		l.emitTokenAndValue(TokenPOSTCOMMENT, l.input[l.start:l.pos], false)
+		l.emitTokenAndValue(TokenPOSTCOMMENT, l.input[l.start:l.pos], false, false)
 
 
 		if r == RuneEOF {
 		if r == RuneEOF {
 			return nil
 			return nil
@@ -757,7 +760,7 @@ func lexComment(l *lexer) lexFunc {
 			}
 			}
 		}
 		}
 
 
-		l.emitTokenAndValue(TokenPRECOMMENT, l.input[l.start:l.pos-1], false)
+		l.emitTokenAndValue(TokenPRECOMMENT, l.input[l.start:l.pos-1], false, false)
 
 
 		// Consume final /
 		// Consume final /
 
 

+ 16 - 2
parser/lexer_test.go

@@ -81,6 +81,7 @@ Lpos is different 1 vs 2
   "Pos": 0,
   "Pos": 0,
   "Val": "not",
   "Val": "not",
   "Identifier": false,
   "Identifier": false,
+  "AllowEscapes": false,
   "Lline": 1,
   "Lline": 1,
   "Lpos": 1
   "Lpos": 1
 }
 }
@@ -90,6 +91,7 @@ vs
   "Pos": 5,
   "Pos": 5,
   "Val": "test",
   "Val": "test",
   "Identifier": true,
   "Identifier": true,
+  "AllowEscapes": false,
   "Lline": 2,
   "Lline": 2,
   "Lpos": 2
   "Lpos": 2
 }` {
 }` {
@@ -237,18 +239,30 @@ func TestStringLexing(t *testing.T) {
 
 
 	input = `name r"te
 	input = `name r"te
 	st"  'bla'`
 	st"  'bla'`
-	if res := LexToList("mytest", input); fmt.Sprint(res) != `["name" "te\n\tst" "bla" EOF]` {
+	res := LexToList("mytest", input)
+	if fmt.Sprint(res) != `["name" "te\n\tst" "bla" EOF]` {
 		t.Error("Unexpected lexer result:", res)
 		t.Error("Unexpected lexer result:", res)
 		return
 		return
 	}
 	}
 
 
+	if res[1].AllowEscapes {
+		t.Error("String value should not allow escapes")
+		return
+	}
+
 	// Parsing with escape sequences
 	// Parsing with escape sequences
 
 
 	input = `"test\n\ttest"  '\nfoo\u0028bar' "test{foo}.5w3f"`
 	input = `"test\n\ttest"  '\nfoo\u0028bar' "test{foo}.5w3f"`
-	if res := LexToList("mytest", input); fmt.Sprint(res) != `["test\n\ttest" "\nfoo(bar" "test{foo}.5w3f" EOF]` {
+	res = LexToList("mytest", input)
+	if fmt.Sprint(res) != `["test\n\ttest" "\nfoo(bar" "test{foo}.5w3f" EOF]` {
 		t.Error("Unexpected lexer result:", res)
 		t.Error("Unexpected lexer result:", res)
 		return
 		return
 	}
 	}
+
+	if !res[0].AllowEscapes {
+		t.Error("String value should allow escapes")
+		return
+	}
 }
 }
 
 
 func TestCommentLexing(t *testing.T) {
 func TestCommentLexing(t *testing.T) {