mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-03-22 06:45:03 +01:00
Replace expressions engine (#133)
This commit is contained in:
306
internal/expr/expression_parser.go
Normal file
306
internal/expr/expression_parser.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Node represents a node in the expression tree.
|
||||
// It is intentionally minimal – only the fields needed for the parser.
|
||||
// Users can extend it with more information if required.
|
||||
|
||||
type Node interface {
|
||||
String() string
|
||||
}
|
||||
|
||||
// ValueNode represents a literal value (number, string, boolean, null) or a named value.
|
||||
// The Kind field indicates the type.
|
||||
// For named values the Value is nil.
|
||||
|
||||
type ValueNode struct {
|
||||
Kind TokenKind
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
// FunctionNode represents a function call with arguments.
|
||||
|
||||
type FunctionNode struct {
|
||||
Name string
|
||||
Args []Node
|
||||
}
|
||||
|
||||
// BinaryNode represents a binary operator.
|
||||
|
||||
type BinaryNode struct {
|
||||
Op string
|
||||
Left Node
|
||||
Right Node
|
||||
}
|
||||
|
||||
// UnaryNode represents a unary operator.
|
||||
|
||||
type UnaryNode struct {
|
||||
Op string
|
||||
Operand Node
|
||||
}
|
||||
|
||||
// Parser holds the lexer and the stacks used by the shunting‑yard algorithm.
|
||||
|
||||
type Parser struct {
|
||||
lexer *Lexer
|
||||
tokens []Token
|
||||
pos int
|
||||
ops []OpToken
|
||||
vals []Node
|
||||
}
|
||||
|
||||
type OpToken struct {
|
||||
Token
|
||||
StartPos int
|
||||
}
|
||||
|
||||
func precedence(tkn Token) int {
|
||||
switch tkn.Kind {
|
||||
case TokenKindStartGroup:
|
||||
return 20
|
||||
case TokenKindStartIndex, TokenKindStartParameters, TokenKindDereference:
|
||||
return 19
|
||||
case TokenKindLogicalOperator:
|
||||
switch tkn.Raw {
|
||||
case "!":
|
||||
return 16
|
||||
case ">", ">=", "<", "<=":
|
||||
return 11
|
||||
case "==", "!=":
|
||||
return 10
|
||||
case "&&":
|
||||
return 6
|
||||
case "||":
|
||||
return 5
|
||||
}
|
||||
case TokenKindEndGroup, TokenKindEndIndex, TokenKindEndParameters, TokenKindSeparator:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Parse parses the expression and returns the root node.
|
||||
func Parse(expression string) (Node, error) {
|
||||
lexer := NewLexer(expression, 0)
|
||||
p := &Parser{}
|
||||
// Tokenise all tokens
|
||||
if err := p.initWithLexer(lexer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p.parse()
|
||||
}
|
||||
|
||||
func (p *Parser) parse() (Node, error) {
|
||||
// Shunting‑yard algorithm
|
||||
for p.pos < len(p.tokens) {
|
||||
tok := p.tokens[p.pos]
|
||||
p.pos++
|
||||
switch tok.Kind {
|
||||
case TokenKindNumber, TokenKindString, TokenKindBoolean, TokenKindNull:
|
||||
p.pushValue(&ValueNode{Kind: tok.Kind, Value: tok.Value})
|
||||
case TokenKindNamedValue, TokenKindPropertyName, TokenKindWildcard:
|
||||
p.pushValue(&ValueNode{Kind: tok.Kind, Value: tok.Raw})
|
||||
case TokenKindFunction:
|
||||
p.pushFunc(tok, len(p.vals))
|
||||
case TokenKindStartParameters, TokenKindStartGroup, TokenKindStartIndex, TokenKindLogicalOperator, TokenKindDereference:
|
||||
if err := p.pushOp(tok); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case TokenKindSeparator:
|
||||
if err := p.popGroup(TokenKindStartParameters); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case TokenKindEndParameters:
|
||||
if err := p.pushFuncValue(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case TokenKindEndGroup:
|
||||
if err := p.popGroup(TokenKindStartGroup); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
p.ops = p.ops[:len(p.ops)-1]
|
||||
case TokenKindEndIndex:
|
||||
if err := p.popGroup(TokenKindStartIndex); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// pop the start parameters
|
||||
p.ops = p.ops[:len(p.ops)-1]
|
||||
right := p.vals[len(p.vals)-1]
|
||||
p.vals = p.vals[:len(p.vals)-1]
|
||||
left := p.vals[len(p.vals)-1]
|
||||
p.vals = p.vals[:len(p.vals)-1]
|
||||
p.vals = append(p.vals, &BinaryNode{Op: "[", Left: left, Right: right})
|
||||
}
|
||||
}
|
||||
for len(p.ops) > 0 {
|
||||
if err := p.popOp(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(p.vals) != 1 {
|
||||
return nil, errors.New("invalid expression")
|
||||
}
|
||||
return p.vals[0], nil
|
||||
}
|
||||
|
||||
func (p *Parser) pushFuncValue() error {
|
||||
if err := p.popGroup(TokenKindStartParameters); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// pop the start parameters
|
||||
p.ops = p.ops[:len(p.ops)-1]
|
||||
// create function node
|
||||
fnTok := p.ops[len(p.ops)-1]
|
||||
if fnTok.Kind != TokenKindFunction {
|
||||
return errors.New("expected function token")
|
||||
}
|
||||
p.ops = p.ops[:len(p.ops)-1]
|
||||
// collect arguments
|
||||
args := []Node{}
|
||||
for len(p.vals) > fnTok.StartPos {
|
||||
args = append([]Node{p.vals[len(p.vals)-1]}, args...)
|
||||
p.vals = p.vals[:len(p.vals)-1]
|
||||
}
|
||||
p.pushValue(&FunctionNode{Name: fnTok.Raw, Args: args})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) initWithLexer(lexer *Lexer) error {
|
||||
p.lexer = lexer
|
||||
for {
|
||||
tok := lexer.Next()
|
||||
if tok == nil {
|
||||
break
|
||||
}
|
||||
if tok.Kind == TokenKindUnexpected {
|
||||
return fmt.Errorf("unexpected token %s at position %d", tok.Raw, tok.Index)
|
||||
}
|
||||
p.tokens = append(p.tokens, *tok)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) popGroup(kind TokenKind) error {
|
||||
for len(p.ops) > 0 && p.ops[len(p.ops)-1].Kind != kind {
|
||||
if err := p.popOp(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(p.ops) == 0 {
|
||||
return errors.New("mismatched parentheses")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) pushValue(v Node) {
|
||||
p.vals = append(p.vals, v)
|
||||
}
|
||||
|
||||
func (p *Parser) pushOp(t Token) error {
|
||||
for len(p.ops) > 0 {
|
||||
top := p.ops[len(p.ops)-1]
|
||||
if precedence(top.Token) >= precedence(t) &&
|
||||
top.Kind != TokenKindStartGroup &&
|
||||
top.Kind != TokenKindStartIndex &&
|
||||
top.Kind != TokenKindStartParameters &&
|
||||
top.Kind != TokenKindSeparator {
|
||||
if err := p.popOp(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
p.ops = append(p.ops, OpToken{Token: t})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) pushFunc(t Token, start int) {
|
||||
p.ops = append(p.ops, OpToken{Token: t, StartPos: start})
|
||||
}
|
||||
|
||||
func (p *Parser) popOp() error {
|
||||
if len(p.ops) == 0 {
|
||||
return nil
|
||||
}
|
||||
op := p.ops[len(p.ops)-1]
|
||||
p.ops = p.ops[:len(p.ops)-1]
|
||||
switch op.Kind {
|
||||
case TokenKindLogicalOperator:
|
||||
if op.Raw == "!" {
|
||||
if len(p.vals) < 1 {
|
||||
return errors.New("insufficient operands")
|
||||
}
|
||||
right := p.vals[len(p.vals)-1]
|
||||
p.vals = p.vals[:len(p.vals)-1]
|
||||
p.vals = append(p.vals, &UnaryNode{Op: op.Raw, Operand: right})
|
||||
} else {
|
||||
if len(p.vals) < 2 {
|
||||
return errors.New("insufficient operands")
|
||||
}
|
||||
right := p.vals[len(p.vals)-1]
|
||||
left := p.vals[len(p.vals)-2]
|
||||
p.vals = p.vals[:len(p.vals)-2]
|
||||
p.vals = append(p.vals, &BinaryNode{Op: op.Raw, Left: left, Right: right})
|
||||
}
|
||||
case TokenKindStartParameters:
|
||||
// unary operator '!' handled elsewhere
|
||||
case TokenKindDereference:
|
||||
if len(p.vals) < 2 {
|
||||
return errors.New("insufficient operands")
|
||||
}
|
||||
right := p.vals[len(p.vals)-1]
|
||||
left := p.vals[len(p.vals)-2]
|
||||
p.vals = p.vals[:len(p.vals)-2]
|
||||
p.vals = append(p.vals, &BinaryNode{Op: ".", Left: left, Right: right})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// String returns a string representation of the node.
|
||||
func (n *ValueNode) String() string { return fmt.Sprintf("%v", n.Value) }
|
||||
|
||||
// String returns a string representation of the node.
|
||||
func (n *FunctionNode) String() string {
|
||||
return fmt.Sprintf("%s(%s)", n.Name, strings.Join(funcArgs(n.Args), ", "))
|
||||
}
|
||||
|
||||
func funcArgs(args []Node) []string {
|
||||
res := []string{}
|
||||
for _, a := range args {
|
||||
res = append(res, a.String())
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// String returns a string representation of the node.
|
||||
func (n *BinaryNode) String() string {
|
||||
return fmt.Sprintf("(%s %s %s)", n.Left.String(), n.Op, n.Right.String())
|
||||
}
|
||||
|
||||
// String returns a string representation of the node.
|
||||
func (n *UnaryNode) String() string { return fmt.Sprintf("(%s%s)", n.Op, n.Operand.String()) }
|
||||
|
||||
func VisitNode(exprNode Node, callback func(node Node)) {
|
||||
callback(exprNode)
|
||||
switch node := exprNode.(type) {
|
||||
case *FunctionNode:
|
||||
for _, arg := range node.Args {
|
||||
VisitNode(arg, callback)
|
||||
}
|
||||
case *UnaryNode:
|
||||
VisitNode(node.Operand, callback)
|
||||
case *BinaryNode:
|
||||
VisitNode(node.Left, callback)
|
||||
VisitNode(node.Right, callback)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user