From eb08b3bb37f42c00144b2261e5aab398234d6659 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 21 Jun 2025 20:18:27 +0200 Subject: [PATCH] Added expression package --- src/expression/Expression.go | 94 +++++++++++++++ src/expression/Expression_test.go | 182 ++++++++++++++++++++++++++++++ src/expression/New.go | 6 + src/expression/NewLeaf.go | 8 ++ src/expression/NewList.go | 17 +++ src/expression/Parse.go | 173 ++++++++++++++++++++++++++++ src/expression/bench_test.go | 17 +++ src/expression/isComplete.go | 27 +++++ src/expression/operator.go | 67 +++++++++++ src/expression/write.go | 33 ++++++ 10 files changed, 624 insertions(+) create mode 100644 src/expression/Expression.go create mode 100644 src/expression/Expression_test.go create mode 100644 src/expression/New.go create mode 100644 src/expression/NewLeaf.go create mode 100644 src/expression/NewList.go create mode 100644 src/expression/Parse.go create mode 100644 src/expression/bench_test.go create mode 100644 src/expression/isComplete.go create mode 100644 src/expression/operator.go create mode 100644 src/expression/write.go diff --git a/src/expression/Expression.go b/src/expression/Expression.go new file mode 100644 index 0000000..3450120 --- /dev/null +++ b/src/expression/Expression.go @@ -0,0 +1,94 @@ +package expression + +import ( + "strings" + + "git.urbach.dev/cli/q/src/token" +) + +// Expression is a tree that can represent a mathematical expression with precedence levels. +type Expression struct { + Parent *Expression + Children []*Expression + Token token.Token + precedence int8 +} + +// AddChild adds a child to the expression. +func (expr *Expression) AddChild(child *Expression) { + if expr.Children == nil { + expr.Children = make([]*Expression, 0, 2) + } + + expr.Children = append(expr.Children, child) + child.Parent = expr +} + +// Reset resets all values to the default. +func (expr *Expression) Reset() { + expr.Parent = nil + + if expr.Children != nil { + expr.Children = expr.Children[:0] + } + + expr.Token.Reset() + expr.precedence = 0 +} + +// EachLeaf iterates through all leaves in the tree. +func (expr *Expression) EachLeaf(call func(*Expression) error) error { + if expr.IsLeaf() { + return call(expr) + } + + for _, child := range expr.Children { + err := child.EachLeaf(call) + + if err != nil { + return err + } + } + + return nil +} + +// RemoveChild removes a child from the expression. +func (expr *Expression) RemoveChild(child *Expression) { + for i, c := range expr.Children { + if c == child { + expr.Children = append(expr.Children[:i], expr.Children[i+1:]...) + child.Parent = nil + return + } + } +} + +// InsertAbove replaces this expression in its parent's children with the given new parent, +// and attaches this expression as a child of the new parent. Effectively, it promotes the +// given tree above the current node. It assumes that the caller is the last child. +func (expr *Expression) InsertAbove(tree *Expression) { + if expr.Parent != nil { + expr.Parent.Children[len(expr.Parent.Children)-1] = tree + tree.Parent = expr.Parent + } + + tree.AddChild(expr) +} + +// IsLeaf returns true if the expression has no children. +func (expr *Expression) IsLeaf() bool { + return len(expr.Children) == 0 +} + +// LastChild returns the last child. +func (expr *Expression) LastChild() *Expression { + return expr.Children[len(expr.Children)-1] +} + +// String generates a textual representation of the expression. +func (expr *Expression) String(source []byte) string { + builder := strings.Builder{} + expr.write(&builder, source) + return builder.String() +} \ No newline at end of file diff --git a/src/expression/Expression_test.go b/src/expression/Expression_test.go new file mode 100644 index 0000000..1303305 --- /dev/null +++ b/src/expression/Expression_test.go @@ -0,0 +1,182 @@ +package expression_test + +import ( + "errors" + "testing" + + "git.urbach.dev/cli/q/src/expression" + "git.urbach.dev/cli/q/src/token" + "git.urbach.dev/go/assert" +) + +func TestParse(t *testing.T) { + tests := []struct { + Name string + Expression string + Result string + }{ + {"Identity", "1", "1"}, + {"Basic calculation", "1+2", "(+ 1 2)"}, + + {"Same operator", "1+2+3", "(+ (+ 1 2) 3)"}, + {"Same operator 2", "1+2+3+4", "(+ (+ (+ 1 2) 3) 4)"}, + + {"Different operator", "1+2-3", "(- (+ 1 2) 3)"}, + {"Different operator 2", "1+2-3+4", "(+ (- (+ 1 2) 3) 4)"}, + {"Different operator 3", "1+2-3+4-5", "(- (+ (- (+ 1 2) 3) 4) 5)"}, + + {"Grouped identity", "(1)", "1"}, + {"Grouped identity 2", "((1))", "1"}, + {"Grouped identity 3", "(((1)))", "1"}, + + {"Adding identity", "(1)+(2)", "(+ 1 2)"}, + {"Adding identity 2", "(1)+(2)+(3)", "(+ (+ 1 2) 3)"}, + {"Adding identity 3", "(1)+(2)+(3)+(4)", "(+ (+ (+ 1 2) 3) 4)"}, + + {"Grouping", "(1+2)", "(+ 1 2)"}, + {"Grouping 2", "(1+2+3)", "(+ (+ 1 2) 3)"}, + {"Grouping 3", "((1)+(2)+(3))", "(+ (+ 1 2) 3)"}, + {"Grouping left", "(1+2)*3", "(* (+ 1 2) 3)"}, + {"Grouping right", "1*(2+3)", "(* 1 (+ 2 3))"}, + {"Grouping same operator", "1+(2+3)", "(+ 1 (+ 2 3))"}, + {"Grouping same operator 2", "1+(2+3)+(4+5)", "(+ (+ 1 (+ 2 3)) (+ 4 5))"}, + + {"Two groups", "(1+2)*(3+4)", "(* (+ 1 2) (+ 3 4))"}, + {"Two groups 2", "(1+2-3)*(3+4-5)", "(* (- (+ 1 2) 3) (- (+ 3 4) 5))"}, + {"Two groups 3", "(1+2)*(3+4-5)", "(* (+ 1 2) (- (+ 3 4) 5))"}, + + {"Operator priority", "1+2*3", "(+ 1 (* 2 3))"}, + {"Operator priority 2", "1*2+3", "(+ (* 1 2) 3)"}, + {"Operator priority 3", "1+2*3+4", "(+ (+ 1 (* 2 3)) 4)"}, + {"Operator priority 4", "1+2*(3+4)+5", "(+ (+ 1 (* 2 (+ 3 4))) 5)"}, + {"Operator priority 5", "1+2*3*4", "(+ 1 (* (* 2 3) 4))"}, + {"Operator priority 6", "1+2*3+4*5", "(+ (+ 1 (* 2 3)) (* 4 5))"}, + {"Operator priority 7", "1+2*3*4*5*6", "(+ 1 (* (* (* (* 2 3) 4) 5) 6))"}, + {"Operator priority 8", "1*2*3+4*5*6", "(+ (* (* 1 2) 3) (* (* 4 5) 6))"}, + + {"Complex", "(1+2-3*4)*(5+6-7*8)", "(* (- (+ 1 2) (* 3 4)) (- (+ 5 6) (* 7 8)))"}, + {"Complex 2", "(1+2*3-4)*(5+6*7-8)", "(* (- (+ 1 (* 2 3)) 4) (- (+ 5 (* 6 7)) 8))"}, + {"Complex 3", "(1+2*3-4)*(5+6*7-8)+9-10*11", "(- (+ (* (- (+ 1 (* 2 3)) 4) (- (+ 5 (* 6 7)) 8)) 9) (* 10 11))"}, + + {"Unary not", "!", "!"}, + {"Unary not 2", "!a", "(! a)"}, + {"Unary not 3", "!(!a)", "(! (! a))"}, + {"Unary not 4", "!(a||b)", "(! (|| a b))"}, + {"Unary not 5", "a || !b", "(|| a (! b))"}, + + {"Unary minus", "-", "-"}, + {"Unary minus 2", "-a", "(- a)"}, + {"Unary minus 3", "-(-a)", "(- (- a))"}, + {"Unary minus 4", "-a+b", "(+ (- a) b)"}, + {"Unary minus 5", "-(a+b)", "(- (+ a b))"}, + {"Unary minus 6", "a + -b", "(+ a (- b))"}, + {"Unary minus 7", "-a + -b", "(+ (- a) (- b))"}, + + {"Assign bitwise operation", "a|=b", "(|= a b)"}, + {"Assign bitwise operation 2", "a|=b< cursor.precedence { + cursor.LastChild().InsertAbove(node) + } else { + if cursor == root { + root = node + } + + cursor.InsertAbove(node) + } + + for _, param := range parameters { + node.AddChild(param) + } + + cursor = node + continue + } + + group := Parse(tokens[groupPosition:i]) + + if group == nil { + continue + } + + group.precedence = math.MaxInt8 + + if cursor == nil { + if t.Kind == token.ArrayEnd { + cursor = New() + cursor.Token.Position = tokens[groupPosition].Position + cursor.Token.Kind = token.Array + cursor.precedence = precedence(token.Array) + cursor.AddChild(group) + root = cursor + } else { + cursor = group + root = group + } + } else { + cursor.AddChild(group) + } + + continue + } + + if groupLevel > 0 { + continue + } + + if t.Kind == token.Identifier || t.Kind == token.Number || t.Kind == token.String || t.Kind == token.Rune { + if cursor != nil { + node := NewLeaf(t) + cursor.AddChild(node) + } else { + cursor = NewLeaf(t) + root = cursor + } + + continue + } + + if !t.IsOperator() { + continue + } + + if cursor == nil { + cursor = NewLeaf(t) + cursor.precedence = precedence(t.Kind) + root = cursor + continue + } + + node := NewLeaf(t) + node.precedence = precedence(t.Kind) + + if cursor.Token.IsOperator() { + oldPrecedence := cursor.precedence + newPrecedence := node.precedence + + if newPrecedence > oldPrecedence { + if len(cursor.Children) == numOperands(cursor.Token.Kind) { + cursor.LastChild().InsertAbove(node) + } else { + cursor.AddChild(node) + } + } else { + start := cursor + + for start != nil { + precedence := start.precedence + + if precedence < newPrecedence { + start.LastChild().InsertAbove(node) + break + } + + if precedence == newPrecedence { + if start == root { + root = node + } + + start.InsertAbove(node) + break + } + + start = start.Parent + } + + if start == nil { + root.InsertAbove(node) + root = node + } + } + } else { + node.AddChild(cursor) + root = node + } + + cursor = node + } + + return root +} \ No newline at end of file diff --git a/src/expression/bench_test.go b/src/expression/bench_test.go new file mode 100644 index 0000000..db1cdb4 --- /dev/null +++ b/src/expression/bench_test.go @@ -0,0 +1,17 @@ +package expression_test + +import ( + "testing" + + "git.urbach.dev/cli/q/src/expression" + "git.urbach.dev/cli/q/src/token" +) + +func BenchmarkExpression(b *testing.B) { + src := []byte("(1+2-3*4)+(5*6-7+8)") + tokens := token.Tokenize(src) + + for b.Loop() { + expression.Parse(tokens) + } +} \ No newline at end of file diff --git a/src/expression/isComplete.go b/src/expression/isComplete.go new file mode 100644 index 0000000..16f9667 --- /dev/null +++ b/src/expression/isComplete.go @@ -0,0 +1,27 @@ +package expression + +import "git.urbach.dev/cli/q/src/token" + +// isComplete returns true if the expression is complete (a binary operation with a single operand is incomplete). +func isComplete(expr *Expression) bool { + if expr == nil { + return false + } + + switch expr.Token.Kind { + case token.Identifier, token.Number, token.String: + // These aren't operators but they always count as complete expressions. + return true + case token.Call: + // Even though token.Call is an operator and could be handled by the upcoming branch, + // the number of operands is variable. + // Therefore we consider every single call expression as complete. + return true + } + + if expr.Token.IsOperator() && len(expr.Children) == numOperands(expr.Token.Kind) { + return true + } + + return false +} \ No newline at end of file diff --git a/src/expression/operator.go b/src/expression/operator.go new file mode 100644 index 0000000..d94eb56 --- /dev/null +++ b/src/expression/operator.go @@ -0,0 +1,67 @@ +package expression + +import ( + "math" + + "git.urbach.dev/cli/q/src/token" +) + +// operator represents an operator for mathematical expressions. +type operator struct { + Symbol string + Precedence int8 + Operands int8 +} + +// operators defines the operators used in the language. +// The number corresponds to the operator priority and can not be zero. +var operators = [64]operator{ + token.Dot: {".", 13, 2}, + token.Call: {"λ", 12, 1}, + token.Array: {"@", 12, 2}, + token.Negate: {"-", 11, 1}, + token.Not: {"!", 11, 1}, + token.Mul: {"*", 10, 2}, + token.Div: {"/", 10, 2}, + token.Mod: {"%", 10, 2}, + token.Add: {"+", 9, 2}, + token.Sub: {"-", 9, 2}, + token.Shr: {">>", 8, 2}, + token.Shl: {"<<", 8, 2}, + token.And: {"&", 7, 2}, + token.Xor: {"^", 6, 2}, + token.Or: {"|", 5, 2}, + + token.Greater: {">", 4, 2}, + token.Less: {"<", 4, 2}, + token.GreaterEqual: {">=", 4, 2}, + token.LessEqual: {"<=", 4, 2}, + token.Equal: {"==", 3, 2}, + token.NotEqual: {"!=", 3, 2}, + token.LogicalAnd: {"&&", 2, 2}, + token.LogicalOr: {"||", 1, 2}, + + token.Range: {"..", 0, 2}, + token.Separator: {",", 0, 2}, + + token.Assign: {"=", math.MinInt8, 2}, + token.Define: {":=", math.MinInt8, 2}, + token.AddAssign: {"+=", math.MinInt8, 2}, + token.SubAssign: {"-=", math.MinInt8, 2}, + token.MulAssign: {"*=", math.MinInt8, 2}, + token.DivAssign: {"/=", math.MinInt8, 2}, + token.ModAssign: {"%=", math.MinInt8, 2}, + token.AndAssign: {"&=", math.MinInt8, 2}, + token.OrAssign: {"|=", math.MinInt8, 2}, + token.XorAssign: {"^=", math.MinInt8, 2}, + token.ShrAssign: {">>=", math.MinInt8, 2}, + token.ShlAssign: {"<<=", math.MinInt8, 2}, +} + +func numOperands(symbol token.Kind) int { + return int(operators[symbol].Operands) +} + +func precedence(symbol token.Kind) int8 { + return operators[symbol].Precedence +} \ No newline at end of file diff --git a/src/expression/write.go b/src/expression/write.go new file mode 100644 index 0000000..5db5f14 --- /dev/null +++ b/src/expression/write.go @@ -0,0 +1,33 @@ +package expression + +import ( + "strings" + + "git.urbach.dev/cli/q/src/token" +) + +// write generates a textual representation of the expression. +func (expr *Expression) write(builder *strings.Builder, source []byte) { + if expr.IsLeaf() { + builder.WriteString(expr.Token.String(source)) + return + } + + builder.WriteByte('(') + + switch expr.Token.Kind { + case token.Call: + builder.WriteString(operators[token.Call].Symbol) + case token.Array: + builder.WriteString(operators[token.Array].Symbol) + default: + builder.WriteString(expr.Token.String(source)) + } + + for _, child := range expr.Children { + builder.WriteByte(' ') + child.write(builder, source) + } + + builder.WriteByte(')') +} \ No newline at end of file