Add String() to nodes

This commit is contained in:
Anton Medvedev
2023-08-17 17:23:18 +02:00
parent 33a21bd624
commit c8b485f6a8
9 changed files with 422 additions and 134 deletions
+59
View File
@@ -0,0 +1,59 @@
package ast
import (
"fmt"
"reflect"
"regexp"
)
func Dump(node Node) string {
return dump(reflect.ValueOf(node), "")
}
func dump(v reflect.Value, ident string) string {
if !v.IsValid() {
return "nil"
}
t := v.Type()
switch t.Kind() {
case reflect.Struct:
out := t.Name() + "{\n"
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if isPrivate(f.Name) {
continue
}
s := v.Field(i)
out += fmt.Sprintf("%v%v: %v,\n", ident+"\t", f.Name, dump(s, ident+"\t"))
}
return out + ident + "}"
case reflect.Slice:
if v.Len() == 0 {
return t.String() + "{}"
}
out := t.String() + "{\n"
for i := 0; i < v.Len(); i++ {
s := v.Index(i)
out += fmt.Sprintf("%v%v,", ident+"\t", dump(s, ident+"\t"))
if i+1 < v.Len() {
out += "\n"
}
}
return out + "\n" + ident + "}"
case reflect.Ptr:
return dump(v.Elem(), ident)
case reflect.Interface:
return dump(reflect.ValueOf(v.Interface()), ident)
case reflect.String:
return fmt.Sprintf("%q", v)
default:
return fmt.Sprintf("%v", v)
}
}
var isCapital = regexp.MustCompile("^[A-Z]")
func isPrivate(s string) bool {
return !isCapital.Match([]byte(s))
}
+1
View File
@@ -14,6 +14,7 @@ type Node interface {
SetLocation(file.Location)
Type() reflect.Type
SetType(reflect.Type)
String() string
}
func Patch(node *Node, newNode Node) {
+147 -44
View File
@@ -1,59 +1,162 @@
package ast
import (
"encoding/json"
"fmt"
"reflect"
"regexp"
"strings"
"github.com/antonmedv/expr/parser/operator"
"github.com/antonmedv/expr/parser/utils"
)
func Dump(node Node) string {
return dump(reflect.ValueOf(node), "")
func (n *NilNode) String() string {
return "nil"
}
func dump(v reflect.Value, ident string) string {
if !v.IsValid() {
func (n *IdentifierNode) String() string {
return n.Value
}
func (n *IntegerNode) String() string {
return fmt.Sprintf("%d", n.Value)
}
func (n *FloatNode) String() string {
return fmt.Sprintf("%v", n.Value)
}
func (n *BoolNode) String() string {
return fmt.Sprintf("%t", n.Value)
}
func (n *StringNode) String() string {
return fmt.Sprintf("%q", n.Value)
}
func (n *ConstantNode) String() string {
if n.Value == nil {
return "nil"
}
t := v.Type()
switch t.Kind() {
case reflect.Struct:
out := t.Name() + "{\n"
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if isPrivate(f.Name) {
continue
}
s := v.Field(i)
out += fmt.Sprintf("%v%v: %v,\n", ident+"\t", f.Name, dump(s, ident+"\t"))
}
return out + ident + "}"
case reflect.Slice:
if v.Len() == 0 {
return t.String() + "{}"
}
out := t.String() + "{\n"
for i := 0; i < v.Len(); i++ {
s := v.Index(i)
out += fmt.Sprintf("%v%v,", ident+"\t", dump(s, ident+"\t"))
if i+1 < v.Len() {
out += "\n"
}
}
return out + "\n" + ident + "}"
case reflect.Ptr:
return dump(v.Elem(), ident)
case reflect.Interface:
return dump(reflect.ValueOf(v.Interface()), ident)
case reflect.String:
return fmt.Sprintf("%q", v)
default:
return fmt.Sprintf("%v", v)
b, err := json.Marshal(n.Value)
if err != nil {
panic(err)
}
return string(b)
}
var isCapital = regexp.MustCompile("^[A-Z]")
func (n *UnaryNode) String() string {
op := ""
if n.Operator == "not" {
op = fmt.Sprintf("%s ", n.Operator)
} else {
op = fmt.Sprintf("%s", n.Operator)
}
if _, ok := n.Node.(*BinaryNode); ok {
return fmt.Sprintf("%s(%s)", op, n.Node.String())
}
return fmt.Sprintf("%s%s", op, n.Node.String())
}
func isPrivate(s string) bool {
return !isCapital.Match([]byte(s))
func (n *BinaryNode) String() string {
var left, right string
if b, ok := n.Left.(*BinaryNode); ok && operator.Less(b.Operator, n.Operator) {
left = fmt.Sprintf("(%s)", n.Left.String())
} else {
left = n.Left.String()
}
if b, ok := n.Right.(*BinaryNode); ok && operator.Less(b.Operator, n.Operator) {
right = fmt.Sprintf("(%s)", n.Right.String())
} else {
right = n.Right.String()
}
return fmt.Sprintf("%s %s %s", left, n.Operator, right)
}
func (n *ChainNode) String() string {
return n.Node.String()
}
func (n *MemberNode) String() string {
if n.Optional {
if str, ok := n.Property.(*StringNode); ok && utils.IsValidIdentifier(str.Value) {
return fmt.Sprintf("%s?.%s", n.Node.String(), str.Value)
} else {
return fmt.Sprintf("get(%s, %s)", n.Node.String(), n.Property.String())
}
}
if str, ok := n.Property.(*StringNode); ok && utils.IsValidIdentifier(str.Value) {
if _, ok := n.Node.(*PointerNode); ok {
return fmt.Sprintf(".%s", str.Value)
}
return fmt.Sprintf("%s.%s", n.Node.String(), str.Value)
}
return fmt.Sprintf("%s[%s]", n.Node.String(), n.Property.String())
}
func (n *SliceNode) String() string {
return fmt.Sprintf("%s[%s:%s]", n.Node.String(), n.From.String(), n.To.String())
}
func (n *CallNode) String() string {
arguments := make([]string, len(n.Arguments))
for i, arg := range n.Arguments {
arguments[i] = arg.String()
}
return fmt.Sprintf("%s(%s)", n.Callee.String(), strings.Join(arguments, ", "))
}
func (n *BuiltinNode) String() string {
arguments := make([]string, len(n.Arguments))
for i, arg := range n.Arguments {
arguments[i] = arg.String()
}
return fmt.Sprintf("%s(%s)", n.Name, strings.Join(arguments, ", "))
}
func (n *ClosureNode) String() string {
return n.Node.String()
}
func (n *PointerNode) String() string {
return "#"
}
func (n *ConditionalNode) String() string {
var cond, exp1, exp2 string
if _, ok := n.Cond.(*ConditionalNode); ok {
cond = fmt.Sprintf("(%s)", n.Cond.String())
} else {
cond = n.Cond.String()
}
if _, ok := n.Exp1.(*ConditionalNode); ok {
exp1 = fmt.Sprintf("(%s)", n.Exp1.String())
} else {
exp1 = n.Exp1.String()
}
if _, ok := n.Exp2.(*ConditionalNode); ok {
exp2 = fmt.Sprintf("(%s)", n.Exp2.String())
} else {
exp2 = n.Exp2.String()
}
return fmt.Sprintf("%s ? %s : %s", cond, exp1, exp2)
}
func (n *ArrayNode) String() string {
nodes := make([]string, len(n.Nodes))
for i, node := range n.Nodes {
nodes[i] = node.String()
}
return fmt.Sprintf("[%s]", strings.Join(nodes, ", "))
}
func (n *MapNode) String() string {
pairs := make([]string, len(n.Pairs))
for i, pair := range n.Pairs {
pairs[i] = pair.String()
}
return fmt.Sprintf("{%s}", strings.Join(pairs, ", "))
}
func (n *PairNode) String() string {
return fmt.Sprintf("%s: %s", n.Key.String(), n.Value.String())
}
+112
View File
@@ -0,0 +1,112 @@
package ast_test
import (
"testing"
"github.com/antonmedv/expr/ast"
"github.com/antonmedv/expr/parser"
"github.com/stretchr/testify/require"
)
func TestPrint(t *testing.T) {
tests := []struct {
input string
want string
}{
{`nil`, `nil`},
{`true`, `true`},
{`false`, `false`},
{`1`, `1`},
{`1.1`, `1.1`},
{`"a"`, `"a"`},
{`'a'`, `"a"`},
{`a`, `a`},
{`a.b`, `a.b`},
{`a[0]`, `a[0]`},
{`a["the b"]`, `a["the b"]`},
{`a.b[0]`, `a.b[0]`},
{`a?.b`, `a?.b`},
{`-a`, `-a`},
{`!a`, `!a`},
{`not a`, `not a`},
{`a + b`, `a + b`},
{`a + b * c`, `a + b * c`},
{`(a + b) * c`, `(a + b) * c`},
{`a * (b + c)`, `a * (b + c)`},
{`-(a + b) * c`, `-(a + b) * c`},
{`a == b`, `a == b`},
{`a matches b`, `a matches b`},
{`a in b`, `a in b`},
{`a not in b`, `not (a in b)`},
{`a and b`, `a and b`},
{`a or b`, `a or b`},
{`a or b and c`, `a or b and c`},
{`a or (b and c)`, `a or b and c`},
{`(a or b) and c`, `(a or b) and c`},
{`a ? b : c`, `a ? b : c`},
{`a ? b : c ? d : e`, `a ? b : (c ? d : e)`},
{`(a ? b : c) ? d : e`, `(a ? b : c) ? d : e`},
{`a ? (b ? c : d) : e`, `a ? (b ? c : d) : e`},
{`func()`, `func()`},
{`func(a)`, `func(a)`},
{`func(a, b)`, `func(a, b)`},
{`{}`, `{}`},
{`{a: b}`, `{"a": b}`},
{`{a: b, c: d}`, `{"a": b, "c": d}`},
{`[]`, `[]`},
{`[a]`, `[a]`},
{`[a, b]`, `[a, b]`},
{`len(a)`, `len(a)`},
{`map(a, # > 0)`, `map(a, # > 0)`},
{`map(a, {# > 0})`, `map(a, # > 0)`},
{`map(a, .b)`, `map(a, .b)`},
{`a.b()`, `a.b()`},
{`a.b(c)`, `a.b(c)`},
{`a[1:-1]`, `a[1:-1]`},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
tree, err := parser.Parse(tt.input)
require.NoError(t, err)
require.Equal(t, tt.want, tree.Node.String())
})
}
}
func TestPrint_MemberNode(t *testing.T) {
node := &ast.MemberNode{
Node: &ast.IdentifierNode{
Value: "a",
},
Property: &ast.StringNode{Value: "b c"},
Optional: true,
}
require.Equal(t, `get(a, "b c")`, node.String())
}
func TestPrint_ConstantNode(t *testing.T) {
tests := []struct {
input interface{}
want string
}{
{nil, `nil`},
{true, `true`},
{false, `false`},
{1, `1`},
{1.1, `1.1`},
{"a", `"a"`},
{[]int{1, 2, 3}, `[1,2,3]`},
{map[string]int{"a": 1}, `{"a":1}`},
}
for _, tt := range tests {
t.Run(tt.want, func(t *testing.T) {
node := &ast.ConstantNode{
Value: tt.input,
}
require.Equal(t, tt.want, node.String())
})
}
}
+7 -5
View File
@@ -2,6 +2,8 @@ package lexer
import (
"strings"
"github.com/antonmedv/expr/parser/utils"
)
type stateFn func(*lexer) stateFn
@@ -11,7 +13,7 @@ func root(l *lexer) stateFn {
case r == eof:
l.emitEOF()
return nil
case IsSpace(r):
case utils.IsSpace(r):
l.ignore()
return root
case r == '\'' || r == '"':
@@ -43,7 +45,7 @@ func root(l *lexer) stateFn {
case r == '.':
l.backup()
return dot
case IsAlphaNumeric(r):
case utils.IsAlphaNumeric(r):
l.backup()
return identifier
default:
@@ -91,7 +93,7 @@ func (l *lexer) scanNumber() bool {
l.acceptRun(digits)
}
// Next thing mustn't be alphanumeric.
if IsAlphaNumeric(l.peek()) {
if utils.IsAlphaNumeric(l.peek()) {
l.next()
return false
}
@@ -113,7 +115,7 @@ func identifier(l *lexer) stateFn {
loop:
for {
switch r := l.next(); {
case IsAlphaNumeric(r):
case utils.IsAlphaNumeric(r):
// absorb
default:
l.backup()
@@ -141,7 +143,7 @@ func not(l *lexer) stateFn {
// Get the next word.
for {
r := l.next()
if IsAlphaNumeric(r) {
if utils.IsAlphaNumeric(r) {
// absorb
} else {
l.backup()
-13
View File
@@ -3,22 +3,9 @@ package lexer
import (
"fmt"
"strings"
"unicode"
"unicode/utf8"
)
func IsSpace(r rune) bool {
return unicode.IsSpace(r)
}
func IsAlphaNumeric(r rune) bool {
return IsAlphabetic(r) || unicode.IsDigit(r)
}
func IsAlphabetic(r rune) bool {
return r == '_' || r == '$' || unicode.IsLetter(r)
}
var (
newlineNormalizer = strings.NewReplacer("\r\n", "\n", "\r", "\n")
)
+52
View File
@@ -0,0 +1,52 @@
package operator
type Associativity int
const (
Left Associativity = iota + 1
Right
)
type Operator struct {
Precedence int
Associativity Associativity
}
func Less(a, b string) bool {
return Binary[a].Precedence < Binary[b].Precedence
}
var Unary = map[string]Operator{
"not": {50, Left},
"!": {50, Left},
"-": {90, Left},
"+": {90, Left},
}
var Binary = map[string]Operator{
"|": {0, Left},
"or": {10, Left},
"||": {10, Left},
"and": {15, Left},
"&&": {15, Left},
"==": {20, Left},
"!=": {20, Left},
"<": {20, Left},
">": {20, Left},
">=": {20, Left},
"<=": {20, Left},
"in": {20, Left},
"matches": {20, Left},
"contains": {20, Left},
"startsWith": {20, Left},
"endsWith": {20, Left},
"..": {25, Left},
"+": {30, Left},
"-": {30, Left},
"*": {60, Left},
"/": {60, Left},
"%": {60, Left},
"**": {100, Right},
"^": {100, Right},
"??": {500, Left},
}
+10 -72
View File
@@ -4,61 +4,15 @@ import (
"fmt"
"strconv"
"strings"
"unicode/utf8"
. "github.com/antonmedv/expr/ast"
"github.com/antonmedv/expr/builtin"
"github.com/antonmedv/expr/file"
. "github.com/antonmedv/expr/parser/lexer"
"github.com/antonmedv/expr/parser/operator"
"github.com/antonmedv/expr/parser/utils"
)
type associativity int
const (
left associativity = iota + 1
right
)
type operator struct {
precedence int
associativity associativity
}
var unaryOperators = map[string]operator{
"not": {50, left},
"!": {50, left},
"-": {90, left},
"+": {90, left},
}
var binaryOperators = map[string]operator{
"|": {0, left},
"or": {10, left},
"||": {10, left},
"and": {15, left},
"&&": {15, left},
"==": {20, left},
"!=": {20, left},
"<": {20, left},
">": {20, left},
">=": {20, left},
"<=": {20, left},
"in": {20, left},
"matches": {20, left},
"contains": {20, left},
"startsWith": {20, left},
"endsWith": {20, left},
"..": {25, left},
"+": {30, left},
"-": {30, left},
"*": {60, left},
"/": {60, left},
"%": {60, left},
"**": {100, right},
"^": {100, right},
"??": {500, left},
}
var predicates = map[string]struct {
arity int
}{
@@ -172,8 +126,8 @@ func (p *parser) parseExpression(precedence int) Node {
opToken = p.current
}
if op, ok := binaryOperators[opToken.Value]; ok {
if op.precedence >= precedence {
if op, ok := operator.Binary[opToken.Value]; ok {
if op.Precedence >= precedence {
p.next()
if opToken.Value == "|" {
@@ -187,10 +141,10 @@ func (p *parser) parseExpression(precedence int) Node {
}
var nodeRight Node
if op.associativity == left {
nodeRight = p.parseExpression(op.precedence + 1)
if op.Associativity == operator.Left {
nodeRight = p.parseExpression(op.Precedence + 1)
} else {
nodeRight = p.parseExpression(op.precedence)
nodeRight = p.parseExpression(op.Precedence)
}
nodeLeft = &BinaryNode{
@@ -253,9 +207,9 @@ func (p *parser) parsePrimary() Node {
token := p.current
if token.Is(Operator) {
if op, ok := unaryOperators[token.Value]; ok {
if op, ok := operator.Unary[token.Value]; ok {
p.next()
expr := p.parseExpression(op.precedence)
expr := p.parseExpression(op.Precedence)
node := &UnaryNode{
Operator: token.Value,
Node: expr,
@@ -510,7 +464,7 @@ func (p *parser) parsePostfixExpression(node Node) Node {
if propertyToken.Kind != Identifier &&
// Operators like "not" and "matches" are valid methods or property names.
(propertyToken.Kind != Operator || !isValidIdentifier(propertyToken.Value)) {
(propertyToken.Kind != Operator || !utils.IsValidIdentifier(propertyToken.Value)) {
p.error("expected name")
}
@@ -662,19 +616,3 @@ func (p *parser) parseArguments() []Node {
return nodes
}
func isValidIdentifier(str string) bool {
if len(str) == 0 {
return false
}
h, w := utf8.DecodeRuneInString(str)
if !IsAlphabetic(h) {
return false
}
for _, r := range str[w:] {
if !IsAlphaNumeric(r) {
return false
}
}
return true
}
+34
View File
@@ -0,0 +1,34 @@
package utils
import (
"unicode"
"unicode/utf8"
)
func IsValidIdentifier(str string) bool {
if len(str) == 0 {
return false
}
h, w := utf8.DecodeRuneInString(str)
if !IsAlphabetic(h) {
return false
}
for _, r := range str[w:] {
if !IsAlphaNumeric(r) {
return false
}
}
return true
}
func IsSpace(r rune) bool {
return unicode.IsSpace(r)
}
func IsAlphaNumeric(r rune) bool {
return IsAlphabetic(r) || unicode.IsDigit(r)
}
func IsAlphabetic(r rune) bool {
return r == '_' || r == '$' || unicode.IsLetter(r)
}