feat(expr): add the evaluator and built-in functions
- commit
- 7f4d0a46f7f2cbad330e2aac1af521606a0d0530
- author
- brice · · 2026-10-12 14:46 UTC
- parent
- a3227f4
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +613 -0
A expr/doc.go +27 -0
| @@ -0,0 +1,27 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | // Package expr implements GitHub Actions expressions: the language found | |
| 4 | + | // between the braces of ${{ }} in workflow files. | |
| 5 | + | // | |
| 6 | + | // The behaviour is a faithful port of GitHub's own implementation published | |
| 7 | + | // as the npm package @actions/expressions (MIT, actions/languageservices): | |
| 8 | + | // same tokens, precedence, error messages, coercions and built-in functions. | |
| 9 | + | // Where that implementation is believed to differ from the hosted service | |
| 10 | + | // (the C# runner in actions/runner), the Go code follows the JavaScript and | |
| 11 | + | // says so in a comment; PORTING.md lists what remains to be checked. | |
| 12 | + | // | |
| 13 | + | // The entry points are [Parse], which turns the text of one expression into a | |
| 14 | + | // [Node], and [Evaluate], which computes its [Value] against the named | |
| 15 | + | // contexts and functions of an [EvalContext]: | |
| 16 | + | // | |
| 17 | + | // node, err := expr.Parse("github.ref == 'refs/heads/main'", []string{"github"}, nil) | |
| 18 | + | // if err != nil { /* *expr.Error */ } | |
| 19 | + | // v, err := expr.Evaluate(node, expr.EvalContext{Contexts: map[string]expr.Value{"github": github}}) | |
| 20 | + | // if err != nil { /* *expr.EvalError */ } | |
| 21 | + | // if expr.Truthy(v) { /* ... */ } | |
| 22 | + | // | |
| 23 | + | // Values are the JSON-like [Null], [Boolean], [Number], [String], [*Array] | |
| 24 | + | // and [*Dictionary]. Dictionaries keep insertion order and look keys up | |
| 25 | + | // case-insensitively. [FromJSON] and [ToJSON] convert to and from JSON text | |
| 26 | + | // exactly as the fromJSON and toJSON functions do. | |
| 27 | + | package expr | |
| @@ -0,0 +1,27 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | // Package expr implements GitHub Actions expressions: the language found | |||
| 4 | + | // between the braces of ${{ }} in workflow files. | |||
| 5 | + | // | |||
| 6 | + | // The behaviour is a faithful port of GitHub's own implementation published | |||
| 7 | + | // as the npm package @actions/expressions (MIT, actions/languageservices): | |||
| 8 | + | // same tokens, precedence, error messages, coercions and built-in functions. | |||
| 9 | + | // Where that implementation is believed to differ from the hosted service | |||
| 10 | + | // (the C# runner in actions/runner), the Go code follows the JavaScript and | |||
| 11 | + | // says so in a comment; PORTING.md lists what remains to be checked. | |||
| 12 | + | // | |||
| 13 | + | // The entry points are [Parse], which turns the text of one expression into a | |||
| 14 | + | // [Node], and [Evaluate], which computes its [Value] against the named | |||
| 15 | + | // contexts and functions of an [EvalContext]: | |||
| 16 | + | // | |||
| 17 | + | // node, err := expr.Parse("github.ref == 'refs/heads/main'", []string{"github"}, nil) | |||
| 18 | + | // if err != nil { /* *expr.Error */ } | |||
| 19 | + | // v, err := expr.Evaluate(node, expr.EvalContext{Contexts: map[string]expr.Value{"github": github}}) | |||
| 20 | + | // if err != nil { /* *expr.EvalError */ } | |||
| 21 | + | // if expr.Truthy(v) { /* ... */ } | |||
| 22 | + | // | |||
| 23 | + | // Values are the JSON-like [Null], [Boolean], [Number], [String], [*Array] | |||
| 24 | + | // and [*Dictionary]. Dictionaries keep insertion order and look keys up | |||
| 25 | + | // case-insensitively. [FromJSON] and [ToJSON] convert to and from JSON text | |||
| 26 | + | // exactly as the fromJSON and toJSON functions do. | |||
| 27 | + | package expr | |||
A expr/eval.go +380 -0
| @@ -0,0 +1,380 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package expr | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "math" | |
| 7 | + | "sort" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // Func implements a function callable from expressions. It receives the | |
| 11 | + | // evaluated arguments in order and returns the result or an error, which | |
| 12 | + | // [Evaluate] returns as is. Arity is not checked at call time: declare it in | |
| 13 | + | // a [FuncInfo] given to [Parse]. | |
| 14 | + | type Func func(args []Value) (Value, error) | |
| 15 | + | ||
| 16 | + | // EvalContext is the environment an expression is evaluated in. | |
| 17 | + | type EvalContext struct { | |
| 18 | + | // Contexts maps context names ("github", "env", "matrix"...) to their | |
| 19 | + | // values. Names are matched case-insensitively; when several keys differ | |
| 20 | + | // only by case, the first in sorted order wins. A name that is absent | |
| 21 | + | // evaluates to null. | |
| 22 | + | Contexts map[string]Value | |
| 23 | + | // Funcs implements the extra functions declared to [Parse], such as | |
| 24 | + | // hashFiles, success, always, cancelled and failure. Names are matched | |
| 25 | + | // case-insensitively. As in the reference implementation these take | |
| 26 | + | // precedence over the built-in functions. | |
| 27 | + | Funcs map[string]Func | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // Evaluate computes the value of a parsed expression. | |
| 31 | + | // | |
| 32 | + | // Values of the context are never modified, and results may share them with | |
| 33 | + | // the context: treat the result as read-only. The error is an [*EvalError]. | |
| 34 | + | func Evaluate(node Node, ctx EvalContext) (Value, error) { | |
| 35 | + | if node == nil { | |
| 36 | + | return nil, &EvalError{Message: "cannot evaluate an empty expression"} | |
| 37 | + | } | |
| 38 | + | ev := &evaluator{ | |
| 39 | + | contexts: make(map[string]Value, len(ctx.Contexts)), | |
| 40 | + | funcs: make(map[string]Func, len(ctx.Funcs)), | |
| 41 | + | } | |
| 42 | + | names := make([]string, 0, len(ctx.Contexts)) | |
| 43 | + | for k := range ctx.Contexts { | |
| 44 | + | names = append(names, k) | |
| 45 | + | } | |
| 46 | + | sort.Strings(names) | |
| 47 | + | for _, k := range names { | |
| 48 | + | if _, dup := ev.contexts[foldKey(k)]; !dup { | |
| 49 | + | ev.contexts[foldKey(k)] = ctx.Contexts[k] | |
| 50 | + | } | |
| 51 | + | } | |
| 52 | + | names = names[:0] | |
| 53 | + | for k := range ctx.Funcs { | |
| 54 | + | names = append(names, k) | |
| 55 | + | } | |
| 56 | + | sort.Strings(names) | |
| 57 | + | for _, k := range names { | |
| 58 | + | if _, dup := ev.funcs[foldKey(k)]; !dup { | |
| 59 | + | ev.funcs[foldKey(k)] = ctx.Funcs[k] | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | v, err := ev.eval(node) | |
| 63 | + | if err != nil { | |
| 64 | + | return nil, err | |
| 65 | + | } | |
| 66 | + | return normalize(v), nil | |
| 67 | + | } | |
| 68 | + | ||
| 69 | + | type evaluator struct { | |
| 70 | + | contexts map[string]Value | |
| 71 | + | funcs map[string]Func | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | func (e *evaluator) eval(n Node) (Value, error) { | |
| 75 | + | switch n := n.(type) { | |
| 76 | + | case *Literal: | |
| 77 | + | return n.Value, nil | |
| 78 | + | case *Unary: | |
| 79 | + | r, err := e.eval(n.Expr) | |
| 80 | + | if err != nil { | |
| 81 | + | return nil, err | |
| 82 | + | } | |
| 83 | + | return Boolean(!Truthy(r)), nil | |
| 84 | + | case *Binary: | |
| 85 | + | return e.binary(n) | |
| 86 | + | case *Logical: | |
| 87 | + | return e.logical(n) | |
| 88 | + | case *Grouping: | |
| 89 | + | return e.eval(n.Group) | |
| 90 | + | case *ContextAccess: | |
| 91 | + | if v, ok := e.contexts[foldKey(n.Name.Lexeme)]; ok { | |
| 92 | + | return normalize(v), nil | |
| 93 | + | } | |
| 94 | + | return Null{}, nil | |
| 95 | + | case *IndexAccess: | |
| 96 | + | return e.indexAccess(n) | |
| 97 | + | case *FunctionCall: | |
| 98 | + | return e.call(n) | |
| 99 | + | } | |
| 100 | + | // *Star is only valid as an index and never evaluated on its own; the | |
| 101 | + | // reference throws "Method not implemented." for it. | |
| 102 | + | return nil, &EvalError{Message: "Method not implemented."} | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | func (e *evaluator) binary(b *Binary) (Value, error) { | |
| 106 | + | // Both operands are always evaluated: comparisons do not short-circuit. | |
| 107 | + | left, err := e.eval(b.Left) | |
| 108 | + | if err != nil { | |
| 109 | + | return nil, err | |
| 110 | + | } | |
| 111 | + | right, err := e.eval(b.Right) | |
| 112 | + | if err != nil { | |
| 113 | + | return nil, err | |
| 114 | + | } | |
| 115 | + | switch b.Operator.Type { | |
| 116 | + | case TokenEqualEqual: | |
| 117 | + | return Boolean(equals(left, right)), nil | |
| 118 | + | case TokenBangEqual: | |
| 119 | + | return Boolean(!equals(left, right)), nil | |
| 120 | + | case TokenGreater: | |
| 121 | + | return Boolean(greaterThan(left, right)), nil | |
| 122 | + | case TokenGreaterEqual: | |
| 123 | + | return Boolean(equals(left, right) || greaterThan(left, right)), nil | |
| 124 | + | case TokenLess: | |
| 125 | + | return Boolean(lessThan(left, right)), nil | |
| 126 | + | case TokenLessEqual: | |
| 127 | + | return Boolean(equals(left, right) || lessThan(left, right)), nil | |
| 128 | + | } | |
| 129 | + | return nil, evalErrorf("unknown binary operator: %s", b.Operator.Lexeme) | |
| 130 | + | } | |
| 131 | + | ||
| 132 | + | // logical returns the operand that decided the chain, not a boolean: | |
| 133 | + | // "a || b" is a when a is truthy, else b; "a && b" is a when a is falsy, else b. | |
| 134 | + | func (e *evaluator) logical(l *Logical) (Value, error) { | |
| 135 | + | var result Value | |
| 136 | + | for _, arg := range l.Args { | |
| 137 | + | r, err := e.eval(arg) | |
| 138 | + | if err != nil { | |
| 139 | + | return nil, err | |
| 140 | + | } | |
| 141 | + | result = r | |
| 142 | + | if (l.Operator.Type == TokenAnd && !Truthy(r)) || (l.Operator.Type == TokenOr && Truthy(r)) { | |
| 143 | + | break | |
| 144 | + | } | |
| 145 | + | } | |
| 146 | + | return result, nil | |
| 147 | + | } | |
| 148 | + | ||
| 149 | + | func (e *evaluator) call(c *FunctionCall) (Value, error) { | |
| 150 | + | args := make([]Value, len(c.Args)) | |
| 151 | + | for i, a := range c.Args { | |
| 152 | + | v, err := e.eval(a) | |
| 153 | + | if err != nil { | |
| 154 | + | return nil, err | |
| 155 | + | } | |
| 156 | + | args[i] = v | |
| 157 | + | } | |
| 158 | + | name := foldKey(c.Name.Lexeme) | |
| 159 | + | if f, ok := e.funcs[name]; ok && f != nil { | |
| 160 | + | return f(args) | |
| 161 | + | } | |
| 162 | + | if f, ok := builtinImpls[name]; ok { | |
| 163 | + | return f(args) | |
| 164 | + | } | |
| 165 | + | return nil, evalErrorf("function %s is not implemented", c.Name.Lexeme) | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | // indexer is idxHelper of the reference: how an index value is read, as a | |
| 169 | + | // star, as a string key and as an array position. | |
| 170 | + | type indexer struct { | |
| 171 | + | star bool | |
| 172 | + | hasStr bool | |
| 173 | + | str string | |
| 174 | + | hasInt bool | |
| 175 | + | // intv is only meaningful when hasInt; it is a float because positions | |
| 176 | + | // beyond the int range must simply compare as out of bounds. | |
| 177 | + | intv float64 | |
| 178 | + | } | |
| 179 | + | ||
| 180 | + | func newIndexer(star bool, v Value) indexer { | |
| 181 | + | ix := indexer{star: star} | |
| 182 | + | if star || v == nil { | |
| 183 | + | return ix | |
| 184 | + | } | |
| 185 | + | v = normalize(v) | |
| 186 | + | if isPrimitive(v) { | |
| 187 | + | ix.hasStr = true | |
| 188 | + | ix.str = CoerceString(v) | |
| 189 | + | } | |
| 190 | + | f := toNumber(v) | |
| 191 | + | if !math.IsNaN(f) && !math.IsInf(f, 0) && f >= 0 { | |
| 192 | + | ix.hasInt = true | |
| 193 | + | ix.intv = math.Floor(f) | |
| 194 | + | } | |
| 195 | + | return ix | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | // pos returns the position as an int when it is within [0, n). | |
| 199 | + | func (ix indexer) pos(n int) (int, bool) { | |
| 200 | + | if !ix.hasInt || ix.intv >= float64(n) { | |
| 201 | + | return 0, false | |
| 202 | + | } | |
| 203 | + | return int(ix.intv), true | |
| 204 | + | } | |
| 205 | + | ||
| 206 | + | func (e *evaluator) indexAccess(ia *IndexAccess) (Value, error) { | |
| 207 | + | var ix indexer | |
| 208 | + | if _, isStar := ia.Index.(*Star); isStar { | |
| 209 | + | ix = newIndexer(true, nil) | |
| 210 | + | } else { | |
| 211 | + | v, err := e.eval(ia.Index) | |
| 212 | + | if err != nil { | |
| 213 | + | return nil, &EvalError{Message: "could not evaluate index for index access: " + err.Error(), Err: err} | |
| 214 | + | } | |
| 215 | + | ix = newIndexer(false, v) | |
| 216 | + | } | |
| 217 | + | obj, err := e.eval(ia.Expr) | |
| 218 | + | if err != nil { | |
| 219 | + | return nil, err | |
| 220 | + | } | |
| 221 | + | switch o := normalize(obj).(type) { | |
| 222 | + | case *Array: | |
| 223 | + | if o.filtered { | |
| 224 | + | return filteredArrayAccess(o, ix), nil | |
| 225 | + | } | |
| 226 | + | return arrayAccess(o, ix), nil | |
| 227 | + | case *Dictionary: | |
| 228 | + | return objectAccess(o, ix), nil | |
| 229 | + | } | |
| 230 | + | if ix.star { | |
| 231 | + | return &Array{filtered: true}, nil | |
| 232 | + | } | |
| 233 | + | return Null{}, nil | |
| 234 | + | } | |
| 235 | + | ||
| 236 | + | // filteredArrayAccess applies an index to each element of a filtered array | |
| 237 | + | // and collects the hits: "a.*.b" is the b of every element that has one. | |
| 238 | + | func filteredArrayAccess(fa *Array, ix indexer) *Array { | |
| 239 | + | result := &Array{filtered: true} | |
| 240 | + | for _, item := range fa.items { | |
| 241 | + | switch it := normalize(item).(type) { | |
| 242 | + | case *Dictionary: | |
| 243 | + | if ix.star { | |
| 244 | + | result.items = append(result.items, it.vals...) | |
| 245 | + | } else if ix.hasStr { | |
| 246 | + | if v, ok := it.Get(ix.str); ok { | |
| 247 | + | result.items = append(result.items, v) | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | case *Array: | |
| 251 | + | if ix.star { | |
| 252 | + | result.items = append(result.items, it.items...) | |
| 253 | + | } else if i, ok := ix.pos(len(it.items)); ok { | |
| 254 | + | result.items = append(result.items, it.items[i]) | |
| 255 | + | } | |
| 256 | + | } | |
| 257 | + | } | |
| 258 | + | return result | |
| 259 | + | } | |
| 260 | + | ||
| 261 | + | func arrayAccess(a *Array, ix indexer) Value { | |
| 262 | + | if ix.star { | |
| 263 | + | return &Array{items: append([]Value(nil), a.items...), filtered: true} | |
| 264 | + | } | |
| 265 | + | if i, ok := ix.pos(len(a.items)); ok { | |
| 266 | + | return normalize(a.items[i]) | |
| 267 | + | } | |
| 268 | + | return Null{} | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | func objectAccess(d *Dictionary, ix indexer) Value { | |
| 272 | + | if ix.star { | |
| 273 | + | return &Array{items: append([]Value(nil), d.vals...), filtered: true} | |
| 274 | + | } | |
| 275 | + | if ix.hasStr { | |
| 276 | + | if v, ok := d.Get(ix.str); ok { | |
| 277 | + | return v | |
| 278 | + | } | |
| 279 | + | } | |
| 280 | + | return Null{} | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | // coerceTypes converts two values of different kinds to a common kind the | |
| 284 | + | // way the reference does, which is like the JavaScript abstract equality | |
| 285 | + | // algorithm except that arrays and dictionaries are never converted: null | |
| 286 | + | // and booleans become numbers, and a string compared with a number becomes | |
| 287 | + | // a number. | |
| 288 | + | func coerceTypes(l, r Value) (Value, Value) { | |
| 289 | + | l, r = normalize(l), normalize(r) | |
| 290 | + | if l.Kind() == r.Kind() { | |
| 291 | + | return l, r | |
| 292 | + | } | |
| 293 | + | switch l.Kind() { | |
| 294 | + | case KindNumber: | |
| 295 | + | if r.Kind() == KindString { | |
| 296 | + | return l, Number(toNumber(r)) | |
| 297 | + | } | |
| 298 | + | case KindString: | |
| 299 | + | if r.Kind() == KindNumber { | |
| 300 | + | return Number(toNumber(l)), r | |
| 301 | + | } | |
| 302 | + | case KindNull, KindBoolean: | |
| 303 | + | return coerceTypes(Number(toNumber(l)), r) | |
| 304 | + | } | |
| 305 | + | switch r.Kind() { | |
| 306 | + | case KindNull, KindBoolean: | |
| 307 | + | return coerceTypes(l, Number(toNumber(r))) | |
| 308 | + | } | |
| 309 | + | return l, r | |
| 310 | + | } | |
| 311 | + | ||
| 312 | + | // equals is the == operator: loose equality, case-insensitive for strings | |
| 313 | + | // (see toUpperSpecial), false for NaN, and by identity for arrays and | |
| 314 | + | // dictionaries. | |
| 315 | + | func equals(lhs, rhs Value) bool { | |
| 316 | + | l, r := coerceTypes(lhs, rhs) | |
| 317 | + | if l.Kind() != r.Kind() { | |
| 318 | + | return false | |
| 319 | + | } | |
| 320 | + | switch lv := l.(type) { | |
| 321 | + | case Null: | |
| 322 | + | return true | |
| 323 | + | case Number: | |
| 324 | + | return float64(lv) == float64(r.(Number)) // false when either is NaN | |
| 325 | + | case String: | |
| 326 | + | return toUpperSpecial(string(lv)) == toUpperSpecial(string(r.(String))) | |
| 327 | + | case Boolean: | |
| 328 | + | return lv == r.(Boolean) | |
| 329 | + | case *Array: | |
| 330 | + | return lv == r.(*Array) | |
| 331 | + | case *Dictionary: | |
| 332 | + | return lv == r.(*Dictionary) | |
| 333 | + | } | |
| 334 | + | return false | |
| 335 | + | } | |
| 336 | + | ||
| 337 | + | // compare implements < and >: it returns -1, 0 or 1, and ok false when the | |
| 338 | + | // values are not ordered (different kinds after coercion, NaN, null, arrays, | |
| 339 | + | // dictionaries). | |
| 340 | + | func compare(lhs, rhs Value) (c int, ok bool) { | |
| 341 | + | l, r := coerceTypes(lhs, rhs) | |
| 342 | + | if l.Kind() != r.Kind() { | |
| 343 | + | return 0, false | |
| 344 | + | } | |
| 345 | + | switch lv := l.(type) { | |
| 346 | + | case Number: | |
| 347 | + | lf, rf := float64(lv), float64(r.(Number)) | |
| 348 | + | switch { | |
| 349 | + | case math.IsNaN(lf) || math.IsNaN(rf): | |
| 350 | + | return 0, false | |
| 351 | + | case lf < rf: | |
| 352 | + | return -1, true | |
| 353 | + | case lf > rf: | |
| 354 | + | return 1, true | |
| 355 | + | } | |
| 356 | + | return 0, true | |
| 357 | + | case String: | |
| 358 | + | return compareUTF16(toUpperSpecial(string(lv)), toUpperSpecial(string(r.(String)))), true | |
| 359 | + | case Boolean: | |
| 360 | + | // false < true | |
| 361 | + | switch rb := r.(Boolean); { | |
| 362 | + | case lv == rb: | |
| 363 | + | return 0, true | |
| 364 | + | case !bool(lv): | |
| 365 | + | return -1, true | |
| 366 | + | } | |
| 367 | + | return 1, true | |
| 368 | + | } | |
| 369 | + | return 0, false | |
| 370 | + | } | |
| 371 | + | ||
| 372 | + | func greaterThan(l, r Value) bool { | |
| 373 | + | c, ok := compare(l, r) | |
| 374 | + | return ok && c > 0 | |
| 375 | + | } | |
| 376 | + | ||
| 377 | + | func lessThan(l, r Value) bool { | |
| 378 | + | c, ok := compare(l, r) | |
| 379 | + | return ok && c < 0 | |
| 380 | + | } | |
| @@ -0,0 +1,380 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package expr | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "math" | |||
| 7 | + | "sort" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // Func implements a function callable from expressions. It receives the | |||
| 11 | + | // evaluated arguments in order and returns the result or an error, which | |||
| 12 | + | // [Evaluate] returns as is. Arity is not checked at call time: declare it in | |||
| 13 | + | // a [FuncInfo] given to [Parse]. | |||
| 14 | + | type Func func(args []Value) (Value, error) | |||
| 15 | + | ||||
| 16 | + | // EvalContext is the environment an expression is evaluated in. | |||
| 17 | + | type EvalContext struct { | |||
| 18 | + | // Contexts maps context names ("github", "env", "matrix"...) to their | |||
| 19 | + | // values. Names are matched case-insensitively; when several keys differ | |||
| 20 | + | // only by case, the first in sorted order wins. A name that is absent | |||
| 21 | + | // evaluates to null. | |||
| 22 | + | Contexts map[string]Value | |||
| 23 | + | // Funcs implements the extra functions declared to [Parse], such as | |||
| 24 | + | // hashFiles, success, always, cancelled and failure. Names are matched | |||
| 25 | + | // case-insensitively. As in the reference implementation these take | |||
| 26 | + | // precedence over the built-in functions. | |||
| 27 | + | Funcs map[string]Func | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // Evaluate computes the value of a parsed expression. | |||
| 31 | + | // | |||
| 32 | + | // Values of the context are never modified, and results may share them with | |||
| 33 | + | // the context: treat the result as read-only. The error is an [*EvalError]. | |||
| 34 | + | func Evaluate(node Node, ctx EvalContext) (Value, error) { | |||
| 35 | + | if node == nil { | |||
| 36 | + | return nil, &EvalError{Message: "cannot evaluate an empty expression"} | |||
| 37 | + | } | |||
| 38 | + | ev := &evaluator{ | |||
| 39 | + | contexts: make(map[string]Value, len(ctx.Contexts)), | |||
| 40 | + | funcs: make(map[string]Func, len(ctx.Funcs)), | |||
| 41 | + | } | |||
| 42 | + | names := make([]string, 0, len(ctx.Contexts)) | |||
| 43 | + | for k := range ctx.Contexts { | |||
| 44 | + | names = append(names, k) | |||
| 45 | + | } | |||
| 46 | + | sort.Strings(names) | |||
| 47 | + | for _, k := range names { | |||
| 48 | + | if _, dup := ev.contexts[foldKey(k)]; !dup { | |||
| 49 | + | ev.contexts[foldKey(k)] = ctx.Contexts[k] | |||
| 50 | + | } | |||
| 51 | + | } | |||
| 52 | + | names = names[:0] | |||
| 53 | + | for k := range ctx.Funcs { | |||
| 54 | + | names = append(names, k) | |||
| 55 | + | } | |||
| 56 | + | sort.Strings(names) | |||
| 57 | + | for _, k := range names { | |||
| 58 | + | if _, dup := ev.funcs[foldKey(k)]; !dup { | |||
| 59 | + | ev.funcs[foldKey(k)] = ctx.Funcs[k] | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | v, err := ev.eval(node) | |||
| 63 | + | if err != nil { | |||
| 64 | + | return nil, err | |||
| 65 | + | } | |||
| 66 | + | return normalize(v), nil | |||
| 67 | + | } | |||
| 68 | + | ||||
| 69 | + | type evaluator struct { | |||
| 70 | + | contexts map[string]Value | |||
| 71 | + | funcs map[string]Func | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | func (e *evaluator) eval(n Node) (Value, error) { | |||
| 75 | + | switch n := n.(type) { | |||
| 76 | + | case *Literal: | |||
| 77 | + | return n.Value, nil | |||
| 78 | + | case *Unary: | |||
| 79 | + | r, err := e.eval(n.Expr) | |||
| 80 | + | if err != nil { | |||
| 81 | + | return nil, err | |||
| 82 | + | } | |||
| 83 | + | return Boolean(!Truthy(r)), nil | |||
| 84 | + | case *Binary: | |||
| 85 | + | return e.binary(n) | |||
| 86 | + | case *Logical: | |||
| 87 | + | return e.logical(n) | |||
| 88 | + | case *Grouping: | |||
| 89 | + | return e.eval(n.Group) | |||
| 90 | + | case *ContextAccess: | |||
| 91 | + | if v, ok := e.contexts[foldKey(n.Name.Lexeme)]; ok { | |||
| 92 | + | return normalize(v), nil | |||
| 93 | + | } | |||
| 94 | + | return Null{}, nil | |||
| 95 | + | case *IndexAccess: | |||
| 96 | + | return e.indexAccess(n) | |||
| 97 | + | case *FunctionCall: | |||
| 98 | + | return e.call(n) | |||
| 99 | + | } | |||
| 100 | + | // *Star is only valid as an index and never evaluated on its own; the | |||
| 101 | + | // reference throws "Method not implemented." for it. | |||
| 102 | + | return nil, &EvalError{Message: "Method not implemented."} | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | func (e *evaluator) binary(b *Binary) (Value, error) { | |||
| 106 | + | // Both operands are always evaluated: comparisons do not short-circuit. | |||
| 107 | + | left, err := e.eval(b.Left) | |||
| 108 | + | if err != nil { | |||
| 109 | + | return nil, err | |||
| 110 | + | } | |||
| 111 | + | right, err := e.eval(b.Right) | |||
| 112 | + | if err != nil { | |||
| 113 | + | return nil, err | |||
| 114 | + | } | |||
| 115 | + | switch b.Operator.Type { | |||
| 116 | + | case TokenEqualEqual: | |||
| 117 | + | return Boolean(equals(left, right)), nil | |||
| 118 | + | case TokenBangEqual: | |||
| 119 | + | return Boolean(!equals(left, right)), nil | |||
| 120 | + | case TokenGreater: | |||
| 121 | + | return Boolean(greaterThan(left, right)), nil | |||
| 122 | + | case TokenGreaterEqual: | |||
| 123 | + | return Boolean(equals(left, right) || greaterThan(left, right)), nil | |||
| 124 | + | case TokenLess: | |||
| 125 | + | return Boolean(lessThan(left, right)), nil | |||
| 126 | + | case TokenLessEqual: | |||
| 127 | + | return Boolean(equals(left, right) || lessThan(left, right)), nil | |||
| 128 | + | } | |||
| 129 | + | return nil, evalErrorf("unknown binary operator: %s", b.Operator.Lexeme) | |||
| 130 | + | } | |||
| 131 | + | ||||
| 132 | + | // logical returns the operand that decided the chain, not a boolean: | |||
| 133 | + | // "a || b" is a when a is truthy, else b; "a && b" is a when a is falsy, else b. | |||
| 134 | + | func (e *evaluator) logical(l *Logical) (Value, error) { | |||
| 135 | + | var result Value | |||
| 136 | + | for _, arg := range l.Args { | |||
| 137 | + | r, err := e.eval(arg) | |||
| 138 | + | if err != nil { | |||
| 139 | + | return nil, err | |||
| 140 | + | } | |||
| 141 | + | result = r | |||
| 142 | + | if (l.Operator.Type == TokenAnd && !Truthy(r)) || (l.Operator.Type == TokenOr && Truthy(r)) { | |||
| 143 | + | break | |||
| 144 | + | } | |||
| 145 | + | } | |||
| 146 | + | return result, nil | |||
| 147 | + | } | |||
| 148 | + | ||||
| 149 | + | func (e *evaluator) call(c *FunctionCall) (Value, error) { | |||
| 150 | + | args := make([]Value, len(c.Args)) | |||
| 151 | + | for i, a := range c.Args { | |||
| 152 | + | v, err := e.eval(a) | |||
| 153 | + | if err != nil { | |||
| 154 | + | return nil, err | |||
| 155 | + | } | |||
| 156 | + | args[i] = v | |||
| 157 | + | } | |||
| 158 | + | name := foldKey(c.Name.Lexeme) | |||
| 159 | + | if f, ok := e.funcs[name]; ok && f != nil { | |||
| 160 | + | return f(args) | |||
| 161 | + | } | |||
| 162 | + | if f, ok := builtinImpls[name]; ok { | |||
| 163 | + | return f(args) | |||
| 164 | + | } | |||
| 165 | + | return nil, evalErrorf("function %s is not implemented", c.Name.Lexeme) | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | // indexer is idxHelper of the reference: how an index value is read, as a | |||
| 169 | + | // star, as a string key and as an array position. | |||
| 170 | + | type indexer struct { | |||
| 171 | + | star bool | |||
| 172 | + | hasStr bool | |||
| 173 | + | str string | |||
| 174 | + | hasInt bool | |||
| 175 | + | // intv is only meaningful when hasInt; it is a float because positions | |||
| 176 | + | // beyond the int range must simply compare as out of bounds. | |||
| 177 | + | intv float64 | |||
| 178 | + | } | |||
| 179 | + | ||||
| 180 | + | func newIndexer(star bool, v Value) indexer { | |||
| 181 | + | ix := indexer{star: star} | |||
| 182 | + | if star || v == nil { | |||
| 183 | + | return ix | |||
| 184 | + | } | |||
| 185 | + | v = normalize(v) | |||
| 186 | + | if isPrimitive(v) { | |||
| 187 | + | ix.hasStr = true | |||
| 188 | + | ix.str = CoerceString(v) | |||
| 189 | + | } | |||
| 190 | + | f := toNumber(v) | |||
| 191 | + | if !math.IsNaN(f) && !math.IsInf(f, 0) && f >= 0 { | |||
| 192 | + | ix.hasInt = true | |||
| 193 | + | ix.intv = math.Floor(f) | |||
| 194 | + | } | |||
| 195 | + | return ix | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | // pos returns the position as an int when it is within [0, n). | |||
| 199 | + | func (ix indexer) pos(n int) (int, bool) { | |||
| 200 | + | if !ix.hasInt || ix.intv >= float64(n) { | |||
| 201 | + | return 0, false | |||
| 202 | + | } | |||
| 203 | + | return int(ix.intv), true | |||
| 204 | + | } | |||
| 205 | + | ||||
| 206 | + | func (e *evaluator) indexAccess(ia *IndexAccess) (Value, error) { | |||
| 207 | + | var ix indexer | |||
| 208 | + | if _, isStar := ia.Index.(*Star); isStar { | |||
| 209 | + | ix = newIndexer(true, nil) | |||
| 210 | + | } else { | |||
| 211 | + | v, err := e.eval(ia.Index) | |||
| 212 | + | if err != nil { | |||
| 213 | + | return nil, &EvalError{Message: "could not evaluate index for index access: " + err.Error(), Err: err} | |||
| 214 | + | } | |||
| 215 | + | ix = newIndexer(false, v) | |||
| 216 | + | } | |||
| 217 | + | obj, err := e.eval(ia.Expr) | |||
| 218 | + | if err != nil { | |||
| 219 | + | return nil, err | |||
| 220 | + | } | |||
| 221 | + | switch o := normalize(obj).(type) { | |||
| 222 | + | case *Array: | |||
| 223 | + | if o.filtered { | |||
| 224 | + | return filteredArrayAccess(o, ix), nil | |||
| 225 | + | } | |||
| 226 | + | return arrayAccess(o, ix), nil | |||
| 227 | + | case *Dictionary: | |||
| 228 | + | return objectAccess(o, ix), nil | |||
| 229 | + | } | |||
| 230 | + | if ix.star { | |||
| 231 | + | return &Array{filtered: true}, nil | |||
| 232 | + | } | |||
| 233 | + | return Null{}, nil | |||
| 234 | + | } | |||
| 235 | + | ||||
| 236 | + | // filteredArrayAccess applies an index to each element of a filtered array | |||
| 237 | + | // and collects the hits: "a.*.b" is the b of every element that has one. | |||
| 238 | + | func filteredArrayAccess(fa *Array, ix indexer) *Array { | |||
| 239 | + | result := &Array{filtered: true} | |||
| 240 | + | for _, item := range fa.items { | |||
| 241 | + | switch it := normalize(item).(type) { | |||
| 242 | + | case *Dictionary: | |||
| 243 | + | if ix.star { | |||
| 244 | + | result.items = append(result.items, it.vals...) | |||
| 245 | + | } else if ix.hasStr { | |||
| 246 | + | if v, ok := it.Get(ix.str); ok { | |||
| 247 | + | result.items = append(result.items, v) | |||
| 248 | + | } | |||
| 249 | + | } | |||
| 250 | + | case *Array: | |||
| 251 | + | if ix.star { | |||
| 252 | + | result.items = append(result.items, it.items...) | |||
| 253 | + | } else if i, ok := ix.pos(len(it.items)); ok { | |||
| 254 | + | result.items = append(result.items, it.items[i]) | |||
| 255 | + | } | |||
| 256 | + | } | |||
| 257 | + | } | |||
| 258 | + | return result | |||
| 259 | + | } | |||
| 260 | + | ||||
| 261 | + | func arrayAccess(a *Array, ix indexer) Value { | |||
| 262 | + | if ix.star { | |||
| 263 | + | return &Array{items: append([]Value(nil), a.items...), filtered: true} | |||
| 264 | + | } | |||
| 265 | + | if i, ok := ix.pos(len(a.items)); ok { | |||
| 266 | + | return normalize(a.items[i]) | |||
| 267 | + | } | |||
| 268 | + | return Null{} | |||
| 269 | + | } | |||
| 270 | + | ||||
| 271 | + | func objectAccess(d *Dictionary, ix indexer) Value { | |||
| 272 | + | if ix.star { | |||
| 273 | + | return &Array{items: append([]Value(nil), d.vals...), filtered: true} | |||
| 274 | + | } | |||
| 275 | + | if ix.hasStr { | |||
| 276 | + | if v, ok := d.Get(ix.str); ok { | |||
| 277 | + | return v | |||
| 278 | + | } | |||
| 279 | + | } | |||
| 280 | + | return Null{} | |||
| 281 | + | } | |||
| 282 | + | ||||
| 283 | + | // coerceTypes converts two values of different kinds to a common kind the | |||
| 284 | + | // way the reference does, which is like the JavaScript abstract equality | |||
| 285 | + | // algorithm except that arrays and dictionaries are never converted: null | |||
| 286 | + | // and booleans become numbers, and a string compared with a number becomes | |||
| 287 | + | // a number. | |||
| 288 | + | func coerceTypes(l, r Value) (Value, Value) { | |||
| 289 | + | l, r = normalize(l), normalize(r) | |||
| 290 | + | if l.Kind() == r.Kind() { | |||
| 291 | + | return l, r | |||
| 292 | + | } | |||
| 293 | + | switch l.Kind() { | |||
| 294 | + | case KindNumber: | |||
| 295 | + | if r.Kind() == KindString { | |||
| 296 | + | return l, Number(toNumber(r)) | |||
| 297 | + | } | |||
| 298 | + | case KindString: | |||
| 299 | + | if r.Kind() == KindNumber { | |||
| 300 | + | return Number(toNumber(l)), r | |||
| 301 | + | } | |||
| 302 | + | case KindNull, KindBoolean: | |||
| 303 | + | return coerceTypes(Number(toNumber(l)), r) | |||
| 304 | + | } | |||
| 305 | + | switch r.Kind() { | |||
| 306 | + | case KindNull, KindBoolean: | |||
| 307 | + | return coerceTypes(l, Number(toNumber(r))) | |||
| 308 | + | } | |||
| 309 | + | return l, r | |||
| 310 | + | } | |||
| 311 | + | ||||
| 312 | + | // equals is the == operator: loose equality, case-insensitive for strings | |||
| 313 | + | // (see toUpperSpecial), false for NaN, and by identity for arrays and | |||
| 314 | + | // dictionaries. | |||
| 315 | + | func equals(lhs, rhs Value) bool { | |||
| 316 | + | l, r := coerceTypes(lhs, rhs) | |||
| 317 | + | if l.Kind() != r.Kind() { | |||
| 318 | + | return false | |||
| 319 | + | } | |||
| 320 | + | switch lv := l.(type) { | |||
| 321 | + | case Null: | |||
| 322 | + | return true | |||
| 323 | + | case Number: | |||
| 324 | + | return float64(lv) == float64(r.(Number)) // false when either is NaN | |||
| 325 | + | case String: | |||
| 326 | + | return toUpperSpecial(string(lv)) == toUpperSpecial(string(r.(String))) | |||
| 327 | + | case Boolean: | |||
| 328 | + | return lv == r.(Boolean) | |||
| 329 | + | case *Array: | |||
| 330 | + | return lv == r.(*Array) | |||
| 331 | + | case *Dictionary: | |||
| 332 | + | return lv == r.(*Dictionary) | |||
| 333 | + | } | |||
| 334 | + | return false | |||
| 335 | + | } | |||
| 336 | + | ||||
| 337 | + | // compare implements < and >: it returns -1, 0 or 1, and ok false when the | |||
| 338 | + | // values are not ordered (different kinds after coercion, NaN, null, arrays, | |||
| 339 | + | // dictionaries). | |||
| 340 | + | func compare(lhs, rhs Value) (c int, ok bool) { | |||
| 341 | + | l, r := coerceTypes(lhs, rhs) | |||
| 342 | + | if l.Kind() != r.Kind() { | |||
| 343 | + | return 0, false | |||
| 344 | + | } | |||
| 345 | + | switch lv := l.(type) { | |||
| 346 | + | case Number: | |||
| 347 | + | lf, rf := float64(lv), float64(r.(Number)) | |||
| 348 | + | switch { | |||
| 349 | + | case math.IsNaN(lf) || math.IsNaN(rf): | |||
| 350 | + | return 0, false | |||
| 351 | + | case lf < rf: | |||
| 352 | + | return -1, true | |||
| 353 | + | case lf > rf: | |||
| 354 | + | return 1, true | |||
| 355 | + | } | |||
| 356 | + | return 0, true | |||
| 357 | + | case String: | |||
| 358 | + | return compareUTF16(toUpperSpecial(string(lv)), toUpperSpecial(string(r.(String)))), true | |||
| 359 | + | case Boolean: | |||
| 360 | + | // false < true | |||
| 361 | + | switch rb := r.(Boolean); { | |||
| 362 | + | case lv == rb: | |||
| 363 | + | return 0, true | |||
| 364 | + | case !bool(lv): | |||
| 365 | + | return -1, true | |||
| 366 | + | } | |||
| 367 | + | return 1, true | |||
| 368 | + | } | |||
| 369 | + | return 0, false | |||
| 370 | + | } | |||
| 371 | + | ||||
| 372 | + | func greaterThan(l, r Value) bool { | |||
| 373 | + | c, ok := compare(l, r) | |||
| 374 | + | return ok && c > 0 | |||
| 375 | + | } | |||
| 376 | + | ||||
| 377 | + | func lessThan(l, r Value) bool { | |||
| 378 | + | c, ok := compare(l, r) | |||
| 379 | + | return ok && c < 0 | |||
| 380 | + | } | |||
A expr/funcs.go +206 -0
| @@ -0,0 +1,206 @@ | |||
| 1 | + | // SPDX-License-Identifier: MIT | |
| 2 | + | ||
| 3 | + | package expr | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "math" | |
| 7 | + | "strconv" | |
| 8 | + | "strings" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // builtinImpls are the implementations of the built-in functions, keyed by | |
| 12 | + | // lower-cased name. Arity is validated by the parser, so each may assume its | |
| 13 | + | // documented argument count. | |
| 14 | + | var builtinImpls = map[string]Func{ | |
| 15 | + | "case": funcCase, | |
| 16 | + | "contains": funcContains, | |
| 17 | + | "endswith": funcEndsWith, | |
| 18 | + | "format": funcFormat, | |
| 19 | + | "fromjson": funcFromJSON, | |
| 20 | + | "join": funcJoin, | |
| 21 | + | "startswith": funcStartsWith, | |
| 22 | + | "tojson": funcToJSON, | |
| 23 | + | } | |
| 24 | + | ||
| 25 | + | // funcCase is case(pred1, val1, pred2, val2, ..., default). | |
| 26 | + | // | |
| 27 | + | // The JavaScript reference ships it behind an experimental feature flag | |
| 28 | + | // (allowCaseFunction) that only affects editor completion; the parser always | |
| 29 | + | // accepts it, and so does this package. | |
| 30 | + | func funcCase(args []Value) (Value, error) { | |
| 31 | + | for i := 0; i < len(args)-1; i += 2 { | |
| 32 | + | pred, ok := normalize(args[i]).(Boolean) | |
| 33 | + | if !ok { | |
| 34 | + | return nil, evalErrorf("case predicate must evaluate to a boolean value") | |
| 35 | + | } | |
| 36 | + | if pred { | |
| 37 | + | return args[i+1], nil | |
| 38 | + | } | |
| 39 | + | } | |
| 40 | + | return args[len(args)-1], nil | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | // funcContains is contains(search, item): substring test, case-insensitive | |
| 44 | + | // via toLowerCase, when both are primitive; membership by == in an array; | |
| 45 | + | // false otherwise. | |
| 46 | + | func funcContains(args []Value) (Value, error) { | |
| 47 | + | left, right := normalize(args[0]), normalize(args[1]) | |
| 48 | + | switch { | |
| 49 | + | case isPrimitive(left): | |
| 50 | + | if isPrimitive(right) { | |
| 51 | + | return Boolean(strings.Contains(jsToLower(CoerceString(left)), jsToLower(CoerceString(right)))), nil | |
| 52 | + | } | |
| 53 | + | case left.Kind() == KindArray: | |
| 54 | + | for _, v := range left.(*Array).items { | |
| 55 | + | if equals(right, v) { | |
| 56 | + | return Boolean(true), nil | |
| 57 | + | } | |
| 58 | + | } | |
| 59 | + | } | |
| 60 | + | return Boolean(false), nil | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | func funcStartsWith(args []Value) (Value, error) { | |
| 64 | + | ls, rs, ok := primitivePair(args) | |
| 65 | + | return Boolean(ok && strings.HasPrefix(toUpperSpecial(ls), toUpperSpecial(rs))), nil | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | func funcEndsWith(args []Value) (Value, error) { | |
| 69 | + | ls, rs, ok := primitivePair(args) | |
| 70 | + | return Boolean(ok && strings.HasSuffix(toUpperSpecial(ls), toUpperSpecial(rs))), nil | |
| 71 | + | } | |
| 72 | + | ||
| 73 | + | // primitivePair returns the string forms of the two arguments, and false | |
| 74 | + | // when either is an array or a dictionary. | |
| 75 | + | func primitivePair(args []Value) (string, string, bool) { | |
| 76 | + | if !isPrimitive(args[0]) || !isPrimitive(args[1]) { | |
| 77 | + | return "", "", false | |
| 78 | + | } | |
| 79 | + | return CoerceString(args[0]), CoerceString(args[1]), true | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | // funcJoin is join(array, separator). A primitive first argument is just | |
| 83 | + | // converted to a string, ignoring the separator; a dictionary gives "". A | |
| 84 | + | // separator that is not primitive means ",". | |
| 85 | + | func funcJoin(args []Value) (Value, error) { | |
| 86 | + | first := normalize(args[0]) | |
| 87 | + | if isPrimitive(first) { | |
| 88 | + | return String(CoerceString(first)), nil | |
| 89 | + | } | |
| 90 | + | arr, ok := first.(*Array) | |
| 91 | + | if !ok { | |
| 92 | + | return String(""), nil | |
| 93 | + | } | |
| 94 | + | sep := "," | |
| 95 | + | if len(args) > 1 && isPrimitive(args[1]) { | |
| 96 | + | sep = CoerceString(args[1]) | |
| 97 | + | } | |
| 98 | + | parts := make([]string, len(arr.items)) | |
| 99 | + | for i, it := range arr.items { | |
| 100 | + | parts[i] = CoerceString(it) | |
| 101 | + | } | |
| 102 | + | return String(strings.Join(parts, sep)), nil | |
| 103 | + | } | |
| 104 | + | ||
| 105 | + | func funcToJSON(args []Value) (Value, error) { | |
| 106 | + | return String(ToJSON(args[0])), nil | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | // funcFromJSON is fromJSON(value). Blank input is "empty input"; anything | |
| 110 | + | // that is not valid JSON is "Error parsing JSON when evaluating fromJson" | |
| 111 | + | // wrapping the syntax error. | |
| 112 | + | func funcFromJSON(args []Value) (Value, error) { | |
| 113 | + | in := CoerceString(args[0]) | |
| 114 | + | if jsTrim(in) == "" { | |
| 115 | + | return nil, evalErrorf("empty input") | |
| 116 | + | } | |
| 117 | + | v, err := FromJSON([]byte(in)) | |
| 118 | + | if err != nil { | |
| 119 | + | return nil, &EvalError{Message: "Error parsing JSON when evaluating fromJson", Err: err} | |
| 120 | + | } | |
| 121 | + | return v, nil | |
| 122 | + | } | |
| 123 | + | ||
| 124 | + | // funcFormat is format(fmt, args...): "{N}" inserts argument N, "{{" and "}}" | |
| 125 | + | // are literal braces. Anything between the digits of "{N" and the closing | |
| 126 | + | // brace is ignored ("{0:x}" is "{0}"). Every other use of a brace is an | |
| 127 | + | // error, as is a reference to a missing argument. | |
| 128 | + | func funcFormat(args []Value) (Value, error) { | |
| 129 | + | fs := CoerceString(args[0]) | |
| 130 | + | var out strings.Builder | |
| 131 | + | index := 0 | |
| 132 | + | for index < len(fs) { | |
| 133 | + | lbrace := indexByteFrom(fs, '{', index) | |
| 134 | + | rbrace := indexByteFrom(fs, '}', index) | |
| 135 | + | switch { | |
| 136 | + | case lbrace >= 0 && (rbrace < 0 || rbrace > lbrace): | |
| 137 | + | // Escaped left brace. | |
| 138 | + | if charAt(fs, lbrace+1) == '{' { | |
| 139 | + | out.WriteString(fs[index : lbrace+1]) | |
| 140 | + | index = lbrace + 2 | |
| 141 | + | continue | |
| 142 | + | } | |
| 143 | + | // Left brace, number, optional format specifiers, right brace. | |
| 144 | + | if rbrace > lbrace+1 { | |
| 145 | + | if argIndex, ok := readArgIndex(fs, lbrace+1); ok { | |
| 146 | + | if 1+argIndex > float64(len(args)-1) { | |
| 147 | + | return nil, evalErrorf("The following format string references more arguments than were supplied: %s", fs) | |
| 148 | + | } | |
| 149 | + | out.WriteString(fs[index:lbrace]) | |
| 150 | + | out.WriteString(CoerceString(args[1+int(argIndex)])) | |
| 151 | + | index = rbrace + 1 | |
| 152 | + | continue | |
| 153 | + | } | |
| 154 | + | } | |
| 155 | + | return nil, evalErrorf("The following format string is invalid: %s", fs) | |
| 156 | + | case rbrace >= 0: | |
| 157 | + | // Escaped right brace. | |
| 158 | + | if charAt(fs, rbrace+1) != '}' { | |
| 159 | + | return nil, evalErrorf("The following format string is invalid: %s", fs) | |
| 160 | + | } | |
| 161 | + | out.WriteString(fs[index : rbrace+1]) | |
| 162 | + | index = rbrace + 2 | |
| 163 | + | default: | |
| 164 | + | out.WriteString(fs[index:]) | |
| 165 | + | index = len(fs) | |
| 166 | + | } | |
| 167 | + | } | |
| 168 | + | return String(out.String()), nil | |
| 169 | + | } | |
| 170 | + | ||
| 171 | + | func indexByteFrom(s string, c byte, from int) int { | |
| 172 | + | if from >= len(s) { | |
| 173 | + | return -1 | |
| 174 | + | } | |
| 175 | + | i := strings.IndexByte(s[from:], c) | |
| 176 | + | if i < 0 { | |
| 177 | + | return -1 | |
| 178 | + | } | |
| 179 | + | return from + i | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | // charAt returns the byte at i, or 0 past the end (safeCharAt). | |
| 183 | + | func charAt(s string, i int) byte { | |
| 184 | + | if i < len(s) { | |
| 185 | + | return s[i] | |
| 186 | + | } | |
| 187 | + | return 0 | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // readArgIndex reads the decimal digits at s[start:]. The index is a float | |
| 191 | + | // because parseInt in the reference yields Infinity for absurdly long digit | |
| 192 | + | // runs, which then fails the argument-count check. | |
| 193 | + | func readArgIndex(s string, start int) (float64, bool) { | |
| 194 | + | n := 0 | |
| 195 | + | for charAt(s, start+n) >= '0' && charAt(s, start+n) <= '9' { | |
| 196 | + | n++ | |
| 197 | + | } | |
| 198 | + | if n == 0 { | |
| 199 | + | return 0, false | |
| 200 | + | } | |
| 201 | + | f, _ := strconv.ParseFloat(s[start:start+n], 64) | |
| 202 | + | if math.IsNaN(f) { | |
| 203 | + | return 0, false | |
| 204 | + | } | |
| 205 | + | return f, true | |
| 206 | + | } | |
| @@ -0,0 +1,206 @@ | |||||
| 1 | + | // SPDX-License-Identifier: MIT | |||
| 2 | + | ||||
| 3 | + | package expr | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "math" | |||
| 7 | + | "strconv" | |||
| 8 | + | "strings" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // builtinImpls are the implementations of the built-in functions, keyed by | |||
| 12 | + | // lower-cased name. Arity is validated by the parser, so each may assume its | |||
| 13 | + | // documented argument count. | |||
| 14 | + | var builtinImpls = map[string]Func{ | |||
| 15 | + | "case": funcCase, | |||
| 16 | + | "contains": funcContains, | |||
| 17 | + | "endswith": funcEndsWith, | |||
| 18 | + | "format": funcFormat, | |||
| 19 | + | "fromjson": funcFromJSON, | |||
| 20 | + | "join": funcJoin, | |||
| 21 | + | "startswith": funcStartsWith, | |||
| 22 | + | "tojson": funcToJSON, | |||
| 23 | + | } | |||
| 24 | + | ||||
| 25 | + | // funcCase is case(pred1, val1, pred2, val2, ..., default). | |||
| 26 | + | // | |||
| 27 | + | // The JavaScript reference ships it behind an experimental feature flag | |||
| 28 | + | // (allowCaseFunction) that only affects editor completion; the parser always | |||
| 29 | + | // accepts it, and so does this package. | |||
| 30 | + | func funcCase(args []Value) (Value, error) { | |||
| 31 | + | for i := 0; i < len(args)-1; i += 2 { | |||
| 32 | + | pred, ok := normalize(args[i]).(Boolean) | |||
| 33 | + | if !ok { | |||
| 34 | + | return nil, evalErrorf("case predicate must evaluate to a boolean value") | |||
| 35 | + | } | |||
| 36 | + | if pred { | |||
| 37 | + | return args[i+1], nil | |||
| 38 | + | } | |||
| 39 | + | } | |||
| 40 | + | return args[len(args)-1], nil | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | // funcContains is contains(search, item): substring test, case-insensitive | |||
| 44 | + | // via toLowerCase, when both are primitive; membership by == in an array; | |||
| 45 | + | // false otherwise. | |||
| 46 | + | func funcContains(args []Value) (Value, error) { | |||
| 47 | + | left, right := normalize(args[0]), normalize(args[1]) | |||
| 48 | + | switch { | |||
| 49 | + | case isPrimitive(left): | |||
| 50 | + | if isPrimitive(right) { | |||
| 51 | + | return Boolean(strings.Contains(jsToLower(CoerceString(left)), jsToLower(CoerceString(right)))), nil | |||
| 52 | + | } | |||
| 53 | + | case left.Kind() == KindArray: | |||
| 54 | + | for _, v := range left.(*Array).items { | |||
| 55 | + | if equals(right, v) { | |||
| 56 | + | return Boolean(true), nil | |||
| 57 | + | } | |||
| 58 | + | } | |||
| 59 | + | } | |||
| 60 | + | return Boolean(false), nil | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | func funcStartsWith(args []Value) (Value, error) { | |||
| 64 | + | ls, rs, ok := primitivePair(args) | |||
| 65 | + | return Boolean(ok && strings.HasPrefix(toUpperSpecial(ls), toUpperSpecial(rs))), nil | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | func funcEndsWith(args []Value) (Value, error) { | |||
| 69 | + | ls, rs, ok := primitivePair(args) | |||
| 70 | + | return Boolean(ok && strings.HasSuffix(toUpperSpecial(ls), toUpperSpecial(rs))), nil | |||
| 71 | + | } | |||
| 72 | + | ||||
| 73 | + | // primitivePair returns the string forms of the two arguments, and false | |||
| 74 | + | // when either is an array or a dictionary. | |||
| 75 | + | func primitivePair(args []Value) (string, string, bool) { | |||
| 76 | + | if !isPrimitive(args[0]) || !isPrimitive(args[1]) { | |||
| 77 | + | return "", "", false | |||
| 78 | + | } | |||
| 79 | + | return CoerceString(args[0]), CoerceString(args[1]), true | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | // funcJoin is join(array, separator). A primitive first argument is just | |||
| 83 | + | // converted to a string, ignoring the separator; a dictionary gives "". A | |||
| 84 | + | // separator that is not primitive means ",". | |||
| 85 | + | func funcJoin(args []Value) (Value, error) { | |||
| 86 | + | first := normalize(args[0]) | |||
| 87 | + | if isPrimitive(first) { | |||
| 88 | + | return String(CoerceString(first)), nil | |||
| 89 | + | } | |||
| 90 | + | arr, ok := first.(*Array) | |||
| 91 | + | if !ok { | |||
| 92 | + | return String(""), nil | |||
| 93 | + | } | |||
| 94 | + | sep := "," | |||
| 95 | + | if len(args) > 1 && isPrimitive(args[1]) { | |||
| 96 | + | sep = CoerceString(args[1]) | |||
| 97 | + | } | |||
| 98 | + | parts := make([]string, len(arr.items)) | |||
| 99 | + | for i, it := range arr.items { | |||
| 100 | + | parts[i] = CoerceString(it) | |||
| 101 | + | } | |||
| 102 | + | return String(strings.Join(parts, sep)), nil | |||
| 103 | + | } | |||
| 104 | + | ||||
| 105 | + | func funcToJSON(args []Value) (Value, error) { | |||
| 106 | + | return String(ToJSON(args[0])), nil | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | // funcFromJSON is fromJSON(value). Blank input is "empty input"; anything | |||
| 110 | + | // that is not valid JSON is "Error parsing JSON when evaluating fromJson" | |||
| 111 | + | // wrapping the syntax error. | |||
| 112 | + | func funcFromJSON(args []Value) (Value, error) { | |||
| 113 | + | in := CoerceString(args[0]) | |||
| 114 | + | if jsTrim(in) == "" { | |||
| 115 | + | return nil, evalErrorf("empty input") | |||
| 116 | + | } | |||
| 117 | + | v, err := FromJSON([]byte(in)) | |||
| 118 | + | if err != nil { | |||
| 119 | + | return nil, &EvalError{Message: "Error parsing JSON when evaluating fromJson", Err: err} | |||
| 120 | + | } | |||
| 121 | + | return v, nil | |||
| 122 | + | } | |||
| 123 | + | ||||
| 124 | + | // funcFormat is format(fmt, args...): "{N}" inserts argument N, "{{" and "}}" | |||
| 125 | + | // are literal braces. Anything between the digits of "{N" and the closing | |||
| 126 | + | // brace is ignored ("{0:x}" is "{0}"). Every other use of a brace is an | |||
| 127 | + | // error, as is a reference to a missing argument. | |||
| 128 | + | func funcFormat(args []Value) (Value, error) { | |||
| 129 | + | fs := CoerceString(args[0]) | |||
| 130 | + | var out strings.Builder | |||
| 131 | + | index := 0 | |||
| 132 | + | for index < len(fs) { | |||
| 133 | + | lbrace := indexByteFrom(fs, '{', index) | |||
| 134 | + | rbrace := indexByteFrom(fs, '}', index) | |||
| 135 | + | switch { | |||
| 136 | + | case lbrace >= 0 && (rbrace < 0 || rbrace > lbrace): | |||
| 137 | + | // Escaped left brace. | |||
| 138 | + | if charAt(fs, lbrace+1) == '{' { | |||
| 139 | + | out.WriteString(fs[index : lbrace+1]) | |||
| 140 | + | index = lbrace + 2 | |||
| 141 | + | continue | |||
| 142 | + | } | |||
| 143 | + | // Left brace, number, optional format specifiers, right brace. | |||
| 144 | + | if rbrace > lbrace+1 { | |||
| 145 | + | if argIndex, ok := readArgIndex(fs, lbrace+1); ok { | |||
| 146 | + | if 1+argIndex > float64(len(args)-1) { | |||
| 147 | + | return nil, evalErrorf("The following format string references more arguments than were supplied: %s", fs) | |||
| 148 | + | } | |||
| 149 | + | out.WriteString(fs[index:lbrace]) | |||
| 150 | + | out.WriteString(CoerceString(args[1+int(argIndex)])) | |||
| 151 | + | index = rbrace + 1 | |||
| 152 | + | continue | |||
| 153 | + | } | |||
| 154 | + | } | |||
| 155 | + | return nil, evalErrorf("The following format string is invalid: %s", fs) | |||
| 156 | + | case rbrace >= 0: | |||
| 157 | + | // Escaped right brace. | |||
| 158 | + | if charAt(fs, rbrace+1) != '}' { | |||
| 159 | + | return nil, evalErrorf("The following format string is invalid: %s", fs) | |||
| 160 | + | } | |||
| 161 | + | out.WriteString(fs[index : rbrace+1]) | |||
| 162 | + | index = rbrace + 2 | |||
| 163 | + | default: | |||
| 164 | + | out.WriteString(fs[index:]) | |||
| 165 | + | index = len(fs) | |||
| 166 | + | } | |||
| 167 | + | } | |||
| 168 | + | return String(out.String()), nil | |||
| 169 | + | } | |||
| 170 | + | ||||
| 171 | + | func indexByteFrom(s string, c byte, from int) int { | |||
| 172 | + | if from >= len(s) { | |||
| 173 | + | return -1 | |||
| 174 | + | } | |||
| 175 | + | i := strings.IndexByte(s[from:], c) | |||
| 176 | + | if i < 0 { | |||
| 177 | + | return -1 | |||
| 178 | + | } | |||
| 179 | + | return from + i | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | // charAt returns the byte at i, or 0 past the end (safeCharAt). | |||
| 183 | + | func charAt(s string, i int) byte { | |||
| 184 | + | if i < len(s) { | |||
| 185 | + | return s[i] | |||
| 186 | + | } | |||
| 187 | + | return 0 | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // readArgIndex reads the decimal digits at s[start:]. The index is a float | |||
| 191 | + | // because parseInt in the reference yields Infinity for absurdly long digit | |||
| 192 | + | // runs, which then fails the argument-count check. | |||
| 193 | + | func readArgIndex(s string, start int) (float64, bool) { | |||
| 194 | + | n := 0 | |||
| 195 | + | for charAt(s, start+n) >= '0' && charAt(s, start+n) <= '9' { | |||
| 196 | + | n++ | |||
| 197 | + | } | |||
| 198 | + | if n == 0 { | |||
| 199 | + | return 0, false | |||
| 200 | + | } | |||
| 201 | + | f, _ := strconv.ParseFloat(s[start:start+n], 64) | |||
| 202 | + | if math.IsNaN(f) { | |||
| 203 | + | return 0, false | |||
| 204 | + | } | |||
| 205 | + | return f, true | |||
| 206 | + | } | |||