feat(expr): add the parser
- commit
- a3227f448517f66ea4a1b4d1b1dcfc2c9368090a
- author
- brice · · 2026-10-12 14:46 UTC
- parent
- 1facd52
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +441 -0
A expr/ast.go +73 -0
| @@ -0,0 +1,73 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package expr | |
| 4 | + | ||
| 5 | + | // Node is a node of a parsed expression. The concrete types are [*Literal], | |
| 6 | + | // [*Unary], [*FunctionCall], [*Binary], [*Logical], [*Grouping], | |
| 7 | + | // [*ContextAccess], [*IndexAccess] and [*Star]. | |
| 8 | + | type Node interface { | |
| 9 | + | isNode() | |
| 10 | + | } | |
| 11 | + | ||
| 12 | + | // Literal is a null, boolean, number or string constant. | |
| 13 | + | type Literal struct { | |
| 14 | + | Value Value | |
| 15 | + | Token Token | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | // Unary is a negation: "!x". | |
| 19 | + | type Unary struct { | |
| 20 | + | Operator Token | |
| 21 | + | Expr Node | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | // FunctionCall is a call such as "contains(a, b)". Name is the identifier | |
| 25 | + | // token as written; function names are case-insensitive. | |
| 26 | + | type FunctionCall struct { | |
| 27 | + | Name Token | |
| 28 | + | Args []Node | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | // Binary is a comparison: ==, !=, <, <=, > or >=. | |
| 32 | + | type Binary struct { | |
| 33 | + | Left Node | |
| 34 | + | Operator Token | |
| 35 | + | Right Node | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | // Logical is a chain of && or || operands. The chain is flat: "a || b || c" | |
| 39 | + | // is one Logical with three Args. Operator is the first operator token. | |
| 40 | + | type Logical struct { | |
| 41 | + | Operator Token | |
| 42 | + | Args []Node | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // Grouping is a parenthesized expression. | |
| 46 | + | type Grouping struct { | |
| 47 | + | Group Node | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // ContextAccess is the name of a context, such as "github". | |
| 51 | + | type ContextAccess struct { | |
| 52 | + | Name Token | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // IndexAccess is "expr[index]" or "expr.name" (Index is then a string | |
| 56 | + | // [Literal]) or "expr.*" (Index is a [Star]). | |
| 57 | + | type IndexAccess struct { | |
| 58 | + | Expr Node | |
| 59 | + | Index Node | |
| 60 | + | } | |
| 61 | + | ||
| 62 | + | // Star is the "*" of "expr.*" and "expr[*]". | |
| 63 | + | type Star struct{} | |
| 64 | + | ||
| 65 | + | func (*Literal) isNode() {} | |
| 66 | + | func (*Unary) isNode() {} | |
| 67 | + | func (*FunctionCall) isNode() {} | |
| 68 | + | func (*Binary) isNode() {} | |
| 69 | + | func (*Logical) isNode() {} | |
| 70 | + | func (*Grouping) isNode() {} | |
| 71 | + | func (*ContextAccess) isNode() {} | |
| 72 | + | func (*IndexAccess) isNode() {} | |
| 73 | + | func (*Star) isNode() {} | |
| @@ -0,0 +1,73 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package expr | |||
| 4 | + | ||||
| 5 | + | // Node is a node of a parsed expression. The concrete types are [*Literal], | |||
| 6 | + | // [*Unary], [*FunctionCall], [*Binary], [*Logical], [*Grouping], | |||
| 7 | + | // [*ContextAccess], [*IndexAccess] and [*Star]. | |||
| 8 | + | type Node interface { | |||
| 9 | + | isNode() | |||
| 10 | + | } | |||
| 11 | + | ||||
| 12 | + | // Literal is a null, boolean, number or string constant. | |||
| 13 | + | type Literal struct { | |||
| 14 | + | Value Value | |||
| 15 | + | Token Token | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | // Unary is a negation: "!x". | |||
| 19 | + | type Unary struct { | |||
| 20 | + | Operator Token | |||
| 21 | + | Expr Node | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | // FunctionCall is a call such as "contains(a, b)". Name is the identifier | |||
| 25 | + | // token as written; function names are case-insensitive. | |||
| 26 | + | type FunctionCall struct { | |||
| 27 | + | Name Token | |||
| 28 | + | Args []Node | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | // Binary is a comparison: ==, !=, <, <=, > or >=. | |||
| 32 | + | type Binary struct { | |||
| 33 | + | Left Node | |||
| 34 | + | Operator Token | |||
| 35 | + | Right Node | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | // Logical is a chain of && or || operands. The chain is flat: "a || b || c" | |||
| 39 | + | // is one Logical with three Args. Operator is the first operator token. | |||
| 40 | + | type Logical struct { | |||
| 41 | + | Operator Token | |||
| 42 | + | Args []Node | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // Grouping is a parenthesized expression. | |||
| 46 | + | type Grouping struct { | |||
| 47 | + | Group Node | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // ContextAccess is the name of a context, such as "github". | |||
| 51 | + | type ContextAccess struct { | |||
| 52 | + | Name Token | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // IndexAccess is "expr[index]" or "expr.name" (Index is then a string | |||
| 56 | + | // [Literal]) or "expr.*" (Index is a [Star]). | |||
| 57 | + | type IndexAccess struct { | |||
| 58 | + | Expr Node | |||
| 59 | + | Index Node | |||
| 60 | + | } | |||
| 61 | + | ||||
| 62 | + | // Star is the "*" of "expr.*" and "expr[*]". | |||
| 63 | + | type Star struct{} | |||
| 64 | + | ||||
| 65 | + | func (*Literal) isNode() {} | |||
| 66 | + | func (*Unary) isNode() {} | |||
| 67 | + | func (*FunctionCall) isNode() {} | |||
| 68 | + | func (*Binary) isNode() {} | |||
| 69 | + | func (*Logical) isNode() {} | |||
| 70 | + | func (*Grouping) isNode() {} | |||
| 71 | + | func (*ContextAccess) isNode() {} | |||
| 72 | + | func (*IndexAccess) isNode() {} | |||
| 73 | + | func (*Star) isNode() {} | |||
A expr/parser.go +368 -0
| @@ -0,0 +1,368 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package expr | |
| 4 | + | ||
| 5 | + | import "math" | |
| 6 | + | ||
| 7 | + | // FuncInfo describes a function that expressions may call besides the | |
| 8 | + | // built-in ones (case, contains, endsWith, format, fromJSON, join, | |
| 9 | + | // startsWith, toJSON). The parser checks names and argument counts against | |
| 10 | + | // it; the implementation is supplied at evaluation time in | |
| 11 | + | // [EvalContext.Funcs]. | |
| 12 | + | type FuncInfo struct { | |
| 13 | + | // Name is matched case-insensitively. | |
| 14 | + | Name string | |
| 15 | + | // MinArgs and MaxArgs bound the number of arguments. A negative MaxArgs | |
| 16 | + | // means no upper bound. | |
| 17 | + | MinArgs int | |
| 18 | + | MaxArgs int | |
| 19 | + | } | |
| 20 | + | ||
| 21 | + | // builtinFuncs are the functions of the reference's wellKnownFunctions, keyed | |
| 22 | + | // by lower-cased name. They take precedence over caller-supplied FuncInfo for | |
| 23 | + | // validation. | |
| 24 | + | var builtinFuncs = map[string]FuncInfo{ | |
| 25 | + | "case": {"case", 3, math.MaxInt}, | |
| 26 | + | "contains": {"contains", 2, 2}, | |
| 27 | + | "endswith": {"endsWith", 2, 2}, | |
| 28 | + | "format": {"format", 1, 255}, | |
| 29 | + | "fromjson": {"fromJson", 1, 1}, | |
| 30 | + | "join": {"join", 1, 2}, | |
| 31 | + | "startswith": {"startsWith", 2, 2}, | |
| 32 | + | "tojson": {"toJson", 1, 1}, | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // Parse parses the text of one expression (the inside of ${{ }}). | |
| 36 | + | // | |
| 37 | + | // contexts lists the context names the expression may reference, such as | |
| 38 | + | // "github" or "matrix"; funcs lists the extra functions it may call. Names | |
| 39 | + | // are case-insensitive. A reference to any other context or function is an | |
| 40 | + | // error, as is a call with a wrong number of arguments. | |
| 41 | + | // | |
| 42 | + | // The returned error is an [*Error]. An expression with no tokens, such as | |
| 43 | + | // "" or " ", is not an error: the reference returns nothing for it, and Parse | |
| 44 | + | // returns a nil [Node], which [Evaluate] rejects. | |
| 45 | + | func Parse(expr string, contexts []string, funcs []FuncInfo) (Node, error) { | |
| 46 | + | tokens, err := Lex(expr) | |
| 47 | + | if err != nil { | |
| 48 | + | return nil, err | |
| 49 | + | } | |
| 50 | + | p := &parser{ | |
| 51 | + | tokens: tokens, | |
| 52 | + | contexts: make(map[string]bool, len(contexts)), | |
| 53 | + | funcs: make(map[string]FuncInfo, len(funcs)), | |
| 54 | + | } | |
| 55 | + | for _, c := range contexts { | |
| 56 | + | p.contexts[foldKey(c)] = true | |
| 57 | + | } | |
| 58 | + | for _, f := range funcs { | |
| 59 | + | p.funcs[foldKey(f.Name)] = f | |
| 60 | + | } | |
| 61 | + | return p.parse() | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // parser is a port of Parser in parser.js: recursive descent, with the same | |
| 65 | + | // precedence (lowest first): ||, &&, == !=, < <= > >=, !, then index and | |
| 66 | + | // call. | |
| 67 | + | type parser struct { | |
| 68 | + | tokens []Token | |
| 69 | + | offset int | |
| 70 | + | depth int | |
| 71 | + | contexts map[string]bool | |
| 72 | + | funcs map[string]FuncInfo | |
| 73 | + | } | |
| 74 | + | ||
| 75 | + | func (p *parser) parse() (Node, error) { | |
| 76 | + | if p.atEnd() { | |
| 77 | + | return nil, nil | |
| 78 | + | } | |
| 79 | + | n, err := p.expression() | |
| 80 | + | if err != nil { | |
| 81 | + | return nil, err | |
| 82 | + | } | |
| 83 | + | if !p.atEnd() { | |
| 84 | + | return nil, newTokenError(ErrorUnexpectedSymbol, p.peek()) | |
| 85 | + | } | |
| 86 | + | return n, nil | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | func (p *parser) expression() (Node, error) { | |
| 90 | + | if err := p.incrDepth(); err != nil { | |
| 91 | + | return nil, err | |
| 92 | + | } | |
| 93 | + | n, err := p.logicalOr() | |
| 94 | + | p.depth-- | |
| 95 | + | return n, err | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | func (p *parser) logicalOr() (Node, error) { | |
| 99 | + | n, err := p.logicalAnd() | |
| 100 | + | if err != nil || !p.check(TokenOr) { | |
| 101 | + | return n, err | |
| 102 | + | } | |
| 103 | + | return p.logicalChain(TokenOr, n, p.logicalAnd) | |
| 104 | + | } | |
| 105 | + | ||
| 106 | + | func (p *parser) logicalAnd() (Node, error) { | |
| 107 | + | n, err := p.equality() | |
| 108 | + | if err != nil || !p.check(TokenAnd) { | |
| 109 | + | return n, err | |
| 110 | + | } | |
| 111 | + | return p.logicalChain(TokenAnd, n, p.equality) | |
| 112 | + | } | |
| 113 | + | ||
| 114 | + | // logicalChain parses the rest of a flat chain of operator operands, the | |
| 115 | + | // first of which is already parsed. | |
| 116 | + | func (p *parser) logicalChain(op TokenType, first Node, operand func() (Node, error)) (Node, error) { | |
| 117 | + | if err := p.incrDepth(); err != nil { | |
| 118 | + | return nil, err | |
| 119 | + | } | |
| 120 | + | logical := &Logical{Operator: p.peek(), Args: []Node{first}} | |
| 121 | + | for p.match(op) { | |
| 122 | + | right, err := operand() | |
| 123 | + | if err != nil { | |
| 124 | + | return nil, err | |
| 125 | + | } | |
| 126 | + | logical.Args = append(logical.Args, right) | |
| 127 | + | } | |
| 128 | + | p.depth-- | |
| 129 | + | return logical, nil | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | func (p *parser) equality() (Node, error) { | |
| 133 | + | n, err := p.comparison() | |
| 134 | + | if err != nil { | |
| 135 | + | return nil, err | |
| 136 | + | } | |
| 137 | + | for p.match(TokenBangEqual, TokenEqualEqual) { | |
| 138 | + | op := p.previous() | |
| 139 | + | right, err := p.comparison() | |
| 140 | + | if err != nil { | |
| 141 | + | return nil, err | |
| 142 | + | } | |
| 143 | + | n = &Binary{Left: n, Operator: op, Right: right} | |
| 144 | + | } | |
| 145 | + | return n, nil | |
| 146 | + | } | |
| 147 | + | ||
| 148 | + | func (p *parser) comparison() (Node, error) { | |
| 149 | + | n, err := p.unary() | |
| 150 | + | if err != nil { | |
| 151 | + | return nil, err | |
| 152 | + | } | |
| 153 | + | for p.match(TokenGreater, TokenGreaterEqual, TokenLess, TokenLessEqual) { | |
| 154 | + | op := p.previous() | |
| 155 | + | right, err := p.unary() | |
| 156 | + | if err != nil { | |
| 157 | + | return nil, err | |
| 158 | + | } | |
| 159 | + | n = &Binary{Left: n, Operator: op, Right: right} | |
| 160 | + | } | |
| 161 | + | return n, nil | |
| 162 | + | } | |
| 163 | + | ||
| 164 | + | func (p *parser) unary() (Node, error) { | |
| 165 | + | if p.match(TokenBang) { | |
| 166 | + | if err := p.incrDepth(); err != nil { | |
| 167 | + | return nil, err | |
| 168 | + | } | |
| 169 | + | op := p.previous() | |
| 170 | + | operand, err := p.unary() | |
| 171 | + | if err != nil { | |
| 172 | + | return nil, err | |
| 173 | + | } | |
| 174 | + | p.depth-- | |
| 175 | + | return &Unary{Operator: op, Expr: operand}, nil | |
| 176 | + | } | |
| 177 | + | return p.index() | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | func (p *parser) index() (Node, error) { | |
| 181 | + | n, err := p.call() | |
| 182 | + | if err != nil { | |
| 183 | + | return nil, err | |
| 184 | + | } | |
| 185 | + | depthIncreased := 0 | |
| 186 | + | switch n.(type) { | |
| 187 | + | case *Grouping, *FunctionCall, *ContextAccess: | |
| 188 | + | loop: | |
| 189 | + | for { | |
| 190 | + | switch { | |
| 191 | + | case p.match(TokenLeftBracket): | |
| 192 | + | var idx Node | |
| 193 | + | if p.match(TokenStar) { | |
| 194 | + | idx = &Star{} | |
| 195 | + | } else if idx, err = p.expression(); err != nil { | |
| 196 | + | return nil, err | |
| 197 | + | } | |
| 198 | + | if err := p.consume(TokenRightBracket, ErrorUnexpectedSymbol); err != nil { | |
| 199 | + | return nil, err | |
| 200 | + | } | |
| 201 | + | if err := p.incrDepth(); err != nil { | |
| 202 | + | return nil, err | |
| 203 | + | } | |
| 204 | + | depthIncreased++ | |
| 205 | + | n = &IndexAccess{Expr: n, Index: idx} | |
| 206 | + | case p.match(TokenDot): | |
| 207 | + | if err := p.incrDepth(); err != nil { | |
| 208 | + | return nil, err | |
| 209 | + | } | |
| 210 | + | depthIncreased++ | |
| 211 | + | switch { | |
| 212 | + | case p.match(TokenIdentifier): | |
| 213 | + | prop := p.previous() | |
| 214 | + | n = &IndexAccess{Expr: n, Index: &Literal{Value: String(prop.Lexeme), Token: prop}} | |
| 215 | + | case p.match(TokenStar): | |
| 216 | + | n = &IndexAccess{Expr: n, Index: &Star{}} | |
| 217 | + | default: | |
| 218 | + | return nil, newTokenError(ErrorUnexpectedSymbol, p.peek()) | |
| 219 | + | } | |
| 220 | + | default: | |
| 221 | + | break loop | |
| 222 | + | } | |
| 223 | + | } | |
| 224 | + | } | |
| 225 | + | p.depth -= depthIncreased | |
| 226 | + | return n, nil | |
| 227 | + | } | |
| 228 | + | ||
| 229 | + | func (p *parser) call() (Node, error) { | |
| 230 | + | if !p.check(TokenIdentifier) { | |
| 231 | + | return p.primary() | |
| 232 | + | } | |
| 233 | + | ident := p.next() | |
| 234 | + | if !p.match(TokenLeftParen) { | |
| 235 | + | if !p.contexts[foldKey(ident.Lexeme)] { | |
| 236 | + | return nil, newTokenError(ErrorUnrecognizedContext, ident) | |
| 237 | + | } | |
| 238 | + | return &ContextAccess{Name: ident}, nil | |
| 239 | + | } | |
| 240 | + | var args []Node | |
| 241 | + | for !p.match(TokenRightParen) { | |
| 242 | + | arg, err := p.expression() | |
| 243 | + | if err != nil { | |
| 244 | + | return nil, err | |
| 245 | + | } | |
| 246 | + | args = append(args, arg) | |
| 247 | + | if !p.check(TokenRightParen) { | |
| 248 | + | // A trailing comma before ")" is accepted, as in the reference. | |
| 249 | + | if err := p.consume(TokenComma, ErrorUnexpectedSymbol); err != nil { | |
| 250 | + | return nil, err | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | } | |
| 254 | + | if err := p.validateFunction(ident, len(args)); err != nil { | |
| 255 | + | return nil, err | |
| 256 | + | } | |
| 257 | + | return &FunctionCall{Name: ident, Args: args}, nil | |
| 258 | + | } | |
| 259 | + | ||
| 260 | + | // validateFunction checks that the function exists and accepts argCount | |
| 261 | + | // arguments (funcs.js validateFunction). | |
| 262 | + | func (p *parser) validateFunction(ident Token, argCount int) error { | |
| 263 | + | name := foldKey(ident.Lexeme) | |
| 264 | + | f, ok := builtinFuncs[name] | |
| 265 | + | if !ok { | |
| 266 | + | f, ok = p.funcs[name] | |
| 267 | + | if !ok { | |
| 268 | + | return newTokenError(ErrorUnrecognizedFunction, ident) | |
| 269 | + | } | |
| 270 | + | } | |
| 271 | + | if argCount < f.MinArgs { | |
| 272 | + | return newTokenError(ErrorTooFewParameters, ident) | |
| 273 | + | } | |
| 274 | + | if f.MaxArgs >= 0 && argCount > f.MaxArgs { | |
| 275 | + | return newTokenError(ErrorTooManyParameters, ident) | |
| 276 | + | } | |
| 277 | + | if name == "case" && argCount%2 == 0 { | |
| 278 | + | return newTokenError(ErrorEvenParameters, ident) | |
| 279 | + | } | |
| 280 | + | return nil | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | func (p *parser) primary() (Node, error) { | |
| 284 | + | switch { | |
| 285 | + | case p.match(TokenFalse): | |
| 286 | + | return &Literal{Value: Boolean(false), Token: p.previous()}, nil | |
| 287 | + | case p.match(TokenTrue): | |
| 288 | + | return &Literal{Value: Boolean(true), Token: p.previous()}, nil | |
| 289 | + | case p.match(TokenNull): | |
| 290 | + | return &Literal{Value: Null{}, Token: p.previous()}, nil | |
| 291 | + | case p.match(TokenNumber): | |
| 292 | + | return &Literal{Value: p.previous().Value, Token: p.previous()}, nil | |
| 293 | + | case p.match(TokenString): | |
| 294 | + | return &Literal{Value: p.previous().Value, Token: p.previous()}, nil | |
| 295 | + | case p.match(TokenLeftParen): | |
| 296 | + | n, err := p.expression() | |
| 297 | + | if err != nil { | |
| 298 | + | return nil, err | |
| 299 | + | } | |
| 300 | + | if p.atEnd() { | |
| 301 | + | // Back up to the last token before the EOF. | |
| 302 | + | return nil, newTokenError(ErrorUnexpectedEndOfExpression, p.previous()) | |
| 303 | + | } | |
| 304 | + | if err := p.consume(TokenRightParen, ErrorUnexpectedSymbol); err != nil { | |
| 305 | + | return nil, err | |
| 306 | + | } | |
| 307 | + | return &Grouping{Group: n}, nil | |
| 308 | + | case p.atEnd(): | |
| 309 | + | return nil, newTokenError(ErrorUnexpectedEndOfExpression, p.previous()) | |
| 310 | + | } | |
| 311 | + | return nil, newTokenError(ErrorUnexpectedSymbol, p.peek()) | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | // match consumes the next token if it has one of the given types. | |
| 315 | + | func (p *parser) match(types ...TokenType) bool { | |
| 316 | + | for _, t := range types { | |
| 317 | + | if p.check(t) { | |
| 318 | + | p.next() | |
| 319 | + | return true | |
| 320 | + | } | |
| 321 | + | } | |
| 322 | + | return false | |
| 323 | + | } | |
| 324 | + | ||
| 325 | + | func (p *parser) check(t TokenType) bool { | |
| 326 | + | if p.atEnd() { | |
| 327 | + | return false | |
| 328 | + | } | |
| 329 | + | return p.peek().Type == t | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | func (p *parser) atEnd() bool { return p.peek().Type == TokenEOF } | |
| 333 | + | ||
| 334 | + | func (p *parser) next() Token { | |
| 335 | + | if !p.atEnd() { | |
| 336 | + | p.offset++ | |
| 337 | + | } | |
| 338 | + | return p.previous() | |
| 339 | + | } | |
| 340 | + | ||
| 341 | + | func (p *parser) peek() Token { return p.tokens[p.offset] } | |
| 342 | + | ||
| 343 | + | // previous returns the token before the current one. At offset 0 the | |
| 344 | + | // reference reads tokens[-1], which is undefined and crashes; here it | |
| 345 | + | // returns the zero Token. That is unreachable: previous is only used after | |
| 346 | + | // a token was consumed. | |
| 347 | + | func (p *parser) previous() Token { | |
| 348 | + | if p.offset == 0 { | |
| 349 | + | return Token{} | |
| 350 | + | } | |
| 351 | + | return p.tokens[p.offset-1] | |
| 352 | + | } | |
| 353 | + | ||
| 354 | + | func (p *parser) consume(t TokenType, errType ErrorType) error { | |
| 355 | + | if p.check(t) { | |
| 356 | + | p.next() | |
| 357 | + | return nil | |
| 358 | + | } | |
| 359 | + | return newTokenError(errType, p.peek()) | |
| 360 | + | } | |
| 361 | + | ||
| 362 | + | func (p *parser) incrDepth() error { | |
| 363 | + | p.depth++ | |
| 364 | + | if p.depth > MaxParserDepth { | |
| 365 | + | return newTokenError(ErrorExceededMaxDepth, p.peek()) | |
| 366 | + | } | |
| 367 | + | return nil | |
| 368 | + | } | |
| @@ -0,0 +1,368 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package expr | |||
| 4 | + | ||||
| 5 | + | import "math" | |||
| 6 | + | ||||
| 7 | + | // FuncInfo describes a function that expressions may call besides the | |||
| 8 | + | // built-in ones (case, contains, endsWith, format, fromJSON, join, | |||
| 9 | + | // startsWith, toJSON). The parser checks names and argument counts against | |||
| 10 | + | // it; the implementation is supplied at evaluation time in | |||
| 11 | + | // [EvalContext.Funcs]. | |||
| 12 | + | type FuncInfo struct { | |||
| 13 | + | // Name is matched case-insensitively. | |||
| 14 | + | Name string | |||
| 15 | + | // MinArgs and MaxArgs bound the number of arguments. A negative MaxArgs | |||
| 16 | + | // means no upper bound. | |||
| 17 | + | MinArgs int | |||
| 18 | + | MaxArgs int | |||
| 19 | + | } | |||
| 20 | + | ||||
| 21 | + | // builtinFuncs are the functions of the reference's wellKnownFunctions, keyed | |||
| 22 | + | // by lower-cased name. They take precedence over caller-supplied FuncInfo for | |||
| 23 | + | // validation. | |||
| 24 | + | var builtinFuncs = map[string]FuncInfo{ | |||
| 25 | + | "case": {"case", 3, math.MaxInt}, | |||
| 26 | + | "contains": {"contains", 2, 2}, | |||
| 27 | + | "endswith": {"endsWith", 2, 2}, | |||
| 28 | + | "format": {"format", 1, 255}, | |||
| 29 | + | "fromjson": {"fromJson", 1, 1}, | |||
| 30 | + | "join": {"join", 1, 2}, | |||
| 31 | + | "startswith": {"startsWith", 2, 2}, | |||
| 32 | + | "tojson": {"toJson", 1, 1}, | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // Parse parses the text of one expression (the inside of ${{ }}). | |||
| 36 | + | // | |||
| 37 | + | // contexts lists the context names the expression may reference, such as | |||
| 38 | + | // "github" or "matrix"; funcs lists the extra functions it may call. Names | |||
| 39 | + | // are case-insensitive. A reference to any other context or function is an | |||
| 40 | + | // error, as is a call with a wrong number of arguments. | |||
| 41 | + | // | |||
| 42 | + | // The returned error is an [*Error]. An expression with no tokens, such as | |||
| 43 | + | // "" or " ", is not an error: the reference returns nothing for it, and Parse | |||
| 44 | + | // returns a nil [Node], which [Evaluate] rejects. | |||
| 45 | + | func Parse(expr string, contexts []string, funcs []FuncInfo) (Node, error) { | |||
| 46 | + | tokens, err := Lex(expr) | |||
| 47 | + | if err != nil { | |||
| 48 | + | return nil, err | |||
| 49 | + | } | |||
| 50 | + | p := &parser{ | |||
| 51 | + | tokens: tokens, | |||
| 52 | + | contexts: make(map[string]bool, len(contexts)), | |||
| 53 | + | funcs: make(map[string]FuncInfo, len(funcs)), | |||
| 54 | + | } | |||
| 55 | + | for _, c := range contexts { | |||
| 56 | + | p.contexts[foldKey(c)] = true | |||
| 57 | + | } | |||
| 58 | + | for _, f := range funcs { | |||
| 59 | + | p.funcs[foldKey(f.Name)] = f | |||
| 60 | + | } | |||
| 61 | + | return p.parse() | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // parser is a port of Parser in parser.js: recursive descent, with the same | |||
| 65 | + | // precedence (lowest first): ||, &&, == !=, < <= > >=, !, then index and | |||
| 66 | + | // call. | |||
| 67 | + | type parser struct { | |||
| 68 | + | tokens []Token | |||
| 69 | + | offset int | |||
| 70 | + | depth int | |||
| 71 | + | contexts map[string]bool | |||
| 72 | + | funcs map[string]FuncInfo | |||
| 73 | + | } | |||
| 74 | + | ||||
| 75 | + | func (p *parser) parse() (Node, error) { | |||
| 76 | + | if p.atEnd() { | |||
| 77 | + | return nil, nil | |||
| 78 | + | } | |||
| 79 | + | n, err := p.expression() | |||
| 80 | + | if err != nil { | |||
| 81 | + | return nil, err | |||
| 82 | + | } | |||
| 83 | + | if !p.atEnd() { | |||
| 84 | + | return nil, newTokenError(ErrorUnexpectedSymbol, p.peek()) | |||
| 85 | + | } | |||
| 86 | + | return n, nil | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | func (p *parser) expression() (Node, error) { | |||
| 90 | + | if err := p.incrDepth(); err != nil { | |||
| 91 | + | return nil, err | |||
| 92 | + | } | |||
| 93 | + | n, err := p.logicalOr() | |||
| 94 | + | p.depth-- | |||
| 95 | + | return n, err | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | func (p *parser) logicalOr() (Node, error) { | |||
| 99 | + | n, err := p.logicalAnd() | |||
| 100 | + | if err != nil || !p.check(TokenOr) { | |||
| 101 | + | return n, err | |||
| 102 | + | } | |||
| 103 | + | return p.logicalChain(TokenOr, n, p.logicalAnd) | |||
| 104 | + | } | |||
| 105 | + | ||||
| 106 | + | func (p *parser) logicalAnd() (Node, error) { | |||
| 107 | + | n, err := p.equality() | |||
| 108 | + | if err != nil || !p.check(TokenAnd) { | |||
| 109 | + | return n, err | |||
| 110 | + | } | |||
| 111 | + | return p.logicalChain(TokenAnd, n, p.equality) | |||
| 112 | + | } | |||
| 113 | + | ||||
| 114 | + | // logicalChain parses the rest of a flat chain of operator operands, the | |||
| 115 | + | // first of which is already parsed. | |||
| 116 | + | func (p *parser) logicalChain(op TokenType, first Node, operand func() (Node, error)) (Node, error) { | |||
| 117 | + | if err := p.incrDepth(); err != nil { | |||
| 118 | + | return nil, err | |||
| 119 | + | } | |||
| 120 | + | logical := &Logical{Operator: p.peek(), Args: []Node{first}} | |||
| 121 | + | for p.match(op) { | |||
| 122 | + | right, err := operand() | |||
| 123 | + | if err != nil { | |||
| 124 | + | return nil, err | |||
| 125 | + | } | |||
| 126 | + | logical.Args = append(logical.Args, right) | |||
| 127 | + | } | |||
| 128 | + | p.depth-- | |||
| 129 | + | return logical, nil | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | func (p *parser) equality() (Node, error) { | |||
| 133 | + | n, err := p.comparison() | |||
| 134 | + | if err != nil { | |||
| 135 | + | return nil, err | |||
| 136 | + | } | |||
| 137 | + | for p.match(TokenBangEqual, TokenEqualEqual) { | |||
| 138 | + | op := p.previous() | |||
| 139 | + | right, err := p.comparison() | |||
| 140 | + | if err != nil { | |||
| 141 | + | return nil, err | |||
| 142 | + | } | |||
| 143 | + | n = &Binary{Left: n, Operator: op, Right: right} | |||
| 144 | + | } | |||
| 145 | + | return n, nil | |||
| 146 | + | } | |||
| 147 | + | ||||
| 148 | + | func (p *parser) comparison() (Node, error) { | |||
| 149 | + | n, err := p.unary() | |||
| 150 | + | if err != nil { | |||
| 151 | + | return nil, err | |||
| 152 | + | } | |||
| 153 | + | for p.match(TokenGreater, TokenGreaterEqual, TokenLess, TokenLessEqual) { | |||
| 154 | + | op := p.previous() | |||
| 155 | + | right, err := p.unary() | |||
| 156 | + | if err != nil { | |||
| 157 | + | return nil, err | |||
| 158 | + | } | |||
| 159 | + | n = &Binary{Left: n, Operator: op, Right: right} | |||
| 160 | + | } | |||
| 161 | + | return n, nil | |||
| 162 | + | } | |||
| 163 | + | ||||
| 164 | + | func (p *parser) unary() (Node, error) { | |||
| 165 | + | if p.match(TokenBang) { | |||
| 166 | + | if err := p.incrDepth(); err != nil { | |||
| 167 | + | return nil, err | |||
| 168 | + | } | |||
| 169 | + | op := p.previous() | |||
| 170 | + | operand, err := p.unary() | |||
| 171 | + | if err != nil { | |||
| 172 | + | return nil, err | |||
| 173 | + | } | |||
| 174 | + | p.depth-- | |||
| 175 | + | return &Unary{Operator: op, Expr: operand}, nil | |||
| 176 | + | } | |||
| 177 | + | return p.index() | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | func (p *parser) index() (Node, error) { | |||
| 181 | + | n, err := p.call() | |||
| 182 | + | if err != nil { | |||
| 183 | + | return nil, err | |||
| 184 | + | } | |||
| 185 | + | depthIncreased := 0 | |||
| 186 | + | switch n.(type) { | |||
| 187 | + | case *Grouping, *FunctionCall, *ContextAccess: | |||
| 188 | + | loop: | |||
| 189 | + | for { | |||
| 190 | + | switch { | |||
| 191 | + | case p.match(TokenLeftBracket): | |||
| 192 | + | var idx Node | |||
| 193 | + | if p.match(TokenStar) { | |||
| 194 | + | idx = &Star{} | |||
| 195 | + | } else if idx, err = p.expression(); err != nil { | |||
| 196 | + | return nil, err | |||
| 197 | + | } | |||
| 198 | + | if err := p.consume(TokenRightBracket, ErrorUnexpectedSymbol); err != nil { | |||
| 199 | + | return nil, err | |||
| 200 | + | } | |||
| 201 | + | if err := p.incrDepth(); err != nil { | |||
| 202 | + | return nil, err | |||
| 203 | + | } | |||
| 204 | + | depthIncreased++ | |||
| 205 | + | n = &IndexAccess{Expr: n, Index: idx} | |||
| 206 | + | case p.match(TokenDot): | |||
| 207 | + | if err := p.incrDepth(); err != nil { | |||
| 208 | + | return nil, err | |||
| 209 | + | } | |||
| 210 | + | depthIncreased++ | |||
| 211 | + | switch { | |||
| 212 | + | case p.match(TokenIdentifier): | |||
| 213 | + | prop := p.previous() | |||
| 214 | + | n = &IndexAccess{Expr: n, Index: &Literal{Value: String(prop.Lexeme), Token: prop}} | |||
| 215 | + | case p.match(TokenStar): | |||
| 216 | + | n = &IndexAccess{Expr: n, Index: &Star{}} | |||
| 217 | + | default: | |||
| 218 | + | return nil, newTokenError(ErrorUnexpectedSymbol, p.peek()) | |||
| 219 | + | } | |||
| 220 | + | default: | |||
| 221 | + | break loop | |||
| 222 | + | } | |||
| 223 | + | } | |||
| 224 | + | } | |||
| 225 | + | p.depth -= depthIncreased | |||
| 226 | + | return n, nil | |||
| 227 | + | } | |||
| 228 | + | ||||
| 229 | + | func (p *parser) call() (Node, error) { | |||
| 230 | + | if !p.check(TokenIdentifier) { | |||
| 231 | + | return p.primary() | |||
| 232 | + | } | |||
| 233 | + | ident := p.next() | |||
| 234 | + | if !p.match(TokenLeftParen) { | |||
| 235 | + | if !p.contexts[foldKey(ident.Lexeme)] { | |||
| 236 | + | return nil, newTokenError(ErrorUnrecognizedContext, ident) | |||
| 237 | + | } | |||
| 238 | + | return &ContextAccess{Name: ident}, nil | |||
| 239 | + | } | |||
| 240 | + | var args []Node | |||
| 241 | + | for !p.match(TokenRightParen) { | |||
| 242 | + | arg, err := p.expression() | |||
| 243 | + | if err != nil { | |||
| 244 | + | return nil, err | |||
| 245 | + | } | |||
| 246 | + | args = append(args, arg) | |||
| 247 | + | if !p.check(TokenRightParen) { | |||
| 248 | + | // A trailing comma before ")" is accepted, as in the reference. | |||
| 249 | + | if err := p.consume(TokenComma, ErrorUnexpectedSymbol); err != nil { | |||
| 250 | + | return nil, err | |||
| 251 | + | } | |||
| 252 | + | } | |||
| 253 | + | } | |||
| 254 | + | if err := p.validateFunction(ident, len(args)); err != nil { | |||
| 255 | + | return nil, err | |||
| 256 | + | } | |||
| 257 | + | return &FunctionCall{Name: ident, Args: args}, nil | |||
| 258 | + | } | |||
| 259 | + | ||||
| 260 | + | // validateFunction checks that the function exists and accepts argCount | |||
| 261 | + | // arguments (funcs.js validateFunction). | |||
| 262 | + | func (p *parser) validateFunction(ident Token, argCount int) error { | |||
| 263 | + | name := foldKey(ident.Lexeme) | |||
| 264 | + | f, ok := builtinFuncs[name] | |||
| 265 | + | if !ok { | |||
| 266 | + | f, ok = p.funcs[name] | |||
| 267 | + | if !ok { | |||
| 268 | + | return newTokenError(ErrorUnrecognizedFunction, ident) | |||
| 269 | + | } | |||
| 270 | + | } | |||
| 271 | + | if argCount < f.MinArgs { | |||
| 272 | + | return newTokenError(ErrorTooFewParameters, ident) | |||
| 273 | + | } | |||
| 274 | + | if f.MaxArgs >= 0 && argCount > f.MaxArgs { | |||
| 275 | + | return newTokenError(ErrorTooManyParameters, ident) | |||
| 276 | + | } | |||
| 277 | + | if name == "case" && argCount%2 == 0 { | |||
| 278 | + | return newTokenError(ErrorEvenParameters, ident) | |||
| 279 | + | } | |||
| 280 | + | return nil | |||
| 281 | + | } | |||
| 282 | + | ||||
| 283 | + | func (p *parser) primary() (Node, error) { | |||
| 284 | + | switch { | |||
| 285 | + | case p.match(TokenFalse): | |||
| 286 | + | return &Literal{Value: Boolean(false), Token: p.previous()}, nil | |||
| 287 | + | case p.match(TokenTrue): | |||
| 288 | + | return &Literal{Value: Boolean(true), Token: p.previous()}, nil | |||
| 289 | + | case p.match(TokenNull): | |||
| 290 | + | return &Literal{Value: Null{}, Token: p.previous()}, nil | |||
| 291 | + | case p.match(TokenNumber): | |||
| 292 | + | return &Literal{Value: p.previous().Value, Token: p.previous()}, nil | |||
| 293 | + | case p.match(TokenString): | |||
| 294 | + | return &Literal{Value: p.previous().Value, Token: p.previous()}, nil | |||
| 295 | + | case p.match(TokenLeftParen): | |||
| 296 | + | n, err := p.expression() | |||
| 297 | + | if err != nil { | |||
| 298 | + | return nil, err | |||
| 299 | + | } | |||
| 300 | + | if p.atEnd() { | |||
| 301 | + | // Back up to the last token before the EOF. | |||
| 302 | + | return nil, newTokenError(ErrorUnexpectedEndOfExpression, p.previous()) | |||
| 303 | + | } | |||
| 304 | + | if err := p.consume(TokenRightParen, ErrorUnexpectedSymbol); err != nil { | |||
| 305 | + | return nil, err | |||
| 306 | + | } | |||
| 307 | + | return &Grouping{Group: n}, nil | |||
| 308 | + | case p.atEnd(): | |||
| 309 | + | return nil, newTokenError(ErrorUnexpectedEndOfExpression, p.previous()) | |||
| 310 | + | } | |||
| 311 | + | return nil, newTokenError(ErrorUnexpectedSymbol, p.peek()) | |||
| 312 | + | } | |||
| 313 | + | ||||
| 314 | + | // match consumes the next token if it has one of the given types. | |||
| 315 | + | func (p *parser) match(types ...TokenType) bool { | |||
| 316 | + | for _, t := range types { | |||
| 317 | + | if p.check(t) { | |||
| 318 | + | p.next() | |||
| 319 | + | return true | |||
| 320 | + | } | |||
| 321 | + | } | |||
| 322 | + | return false | |||
| 323 | + | } | |||
| 324 | + | ||||
| 325 | + | func (p *parser) check(t TokenType) bool { | |||
| 326 | + | if p.atEnd() { | |||
| 327 | + | return false | |||
| 328 | + | } | |||
| 329 | + | return p.peek().Type == t | |||
| 330 | + | } | |||
| 331 | + | ||||
| 332 | + | func (p *parser) atEnd() bool { return p.peek().Type == TokenEOF } | |||
| 333 | + | ||||
| 334 | + | func (p *parser) next() Token { | |||
| 335 | + | if !p.atEnd() { | |||
| 336 | + | p.offset++ | |||
| 337 | + | } | |||
| 338 | + | return p.previous() | |||
| 339 | + | } | |||
| 340 | + | ||||
| 341 | + | func (p *parser) peek() Token { return p.tokens[p.offset] } | |||
| 342 | + | ||||
| 343 | + | // previous returns the token before the current one. At offset 0 the | |||
| 344 | + | // reference reads tokens[-1], which is undefined and crashes; here it | |||
| 345 | + | // returns the zero Token. That is unreachable: previous is only used after | |||
| 346 | + | // a token was consumed. | |||
| 347 | + | func (p *parser) previous() Token { | |||
| 348 | + | if p.offset == 0 { | |||
| 349 | + | return Token{} | |||
| 350 | + | } | |||
| 351 | + | return p.tokens[p.offset-1] | |||
| 352 | + | } | |||
| 353 | + | ||||
| 354 | + | func (p *parser) consume(t TokenType, errType ErrorType) error { | |||
| 355 | + | if p.check(t) { | |||
| 356 | + | p.next() | |||
| 357 | + | return nil | |||
| 358 | + | } | |||
| 359 | + | return newTokenError(errType, p.peek()) | |||
| 360 | + | } | |||
| 361 | + | ||||
| 362 | + | func (p *parser) incrDepth() error { | |||
| 363 | + | p.depth++ | |||
| 364 | + | if p.depth > MaxParserDepth { | |||
| 365 | + | return newTokenError(ErrorExceededMaxDepth, p.peek()) | |||
| 366 | + | } | |||
| 367 | + | return nil | |||
| 368 | + | } | |||