fix(expr): evaluate built-in arguments lazily and parse format strings like the runner
- commit
- 3b23706640897154fba558d888167abc634722be
- author
- brice · · 2026-10-12 18:05 UTC
- parent
- 900025e
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +332 -122
M expr/PORTING.md +38 -11
| @@ -72,17 +72,30 @@ whether to keep the JS behaviour or follow the runner. | |||
| 72 | 72 | a bug of the editor library. Tests: `TestEvalLiteralsAndOperators`; the | |
| 73 | 73 | oracle excuses the JS mismatch because `testdata/runner.json.gz` agrees | |
| 74 | 74 | with this package. | |
| 75 | - | - [ ] **`case()` availability.** The JS gates it behind the experimental flag | |
| 76 | - | `allowCaseFunction`, but only in completion; the parser and the | |
| 77 | - | evaluator always accept it. Here it is always on. Confirm whether the | |
| 78 | - | hosted service accepts `case` and with which arity rules (the JS: at | |
| 79 | - | least 3 arguments, an odd number, predicates must be booleans). | |
| 80 | - | - [ ] **`format()`**: the reference uses `parseInt` on the digits, so | |
| 81 | - | `{99999999999999999999}` is an Infinity-like index and reports "more | |
| 82 | - | arguments than were supplied". The `{0:x}` form silently ignores the | |
| 83 | - | specifier; C# may throw. The error texts are the JS ones. Check the C# | |
| 84 | - | `Format` node for the exact messages and for `{N}` with `N` > 255 or | |
| 85 | - | leading zeros. | |
| 75 | + | - [x] **`case()` availability.** Resolved: the runner has `case` always | |
| 76 | + | (`ExpressionConstants`: `AddFunction<Case>("case", 3, Byte.MaxValue)`), | |
| 77 | + | with an odd number of arguments enforced at parse time | |
| 78 | + | (`ExpressionParser.FlushTopEndParameters`: `EvenParameters`, reported on | |
| 79 | + | the `(` token) and at evaluation (`Sdk/Functions/Case.cs`), predicates | |
| 80 | + | must be booleans ("case predicate must evaluate to a boolean value"), and | |
| 81 | + | only the matching branch is evaluated. `ExpressionParserL0` has a | |
| 82 | + | regression test that `case(...)` must not turn on "allow unknown | |
| 83 | + | keywords" (ported in `runner_l0_test.go`). Here it is always on, with | |
| 84 | + | the maximum of 255 arguments (the JS has no maximum). | |
| 85 | + | - [x] **`format()`**: resolved, follows the C#. `Sdk/Functions/Format.cs`: the | |
| 86 | + | format string is scanned first (syntax and "more arguments than were | |
| 87 | + | supplied" are reported before any argument is evaluated), arguments are | |
| 88 | + | then evaluated lazily, once each. The index is a **byte** | |
| 89 | + | (`Byte.TryParse`): `{255}` is the last valid index, `{256}` and | |
| 90 | + | `{12345678901234567890123}` are "format string is invalid", not "more | |
| 91 | + | arguments". After the digits only `}` or `:specifiers}` are allowed | |
| 92 | + | (`{0 }`, `{0,5}` are invalid); **specifiers are an error** for every | |
| 93 | + | value: `The format specifiers 'x' are not valid for objects of type | |
| 94 | + | 'Number'` (kind names `Null Boolean Number String Array Object`). | |
| 95 | + | Messages are the same as the JS ones otherwise (`ExpressionResources.resx`: | |
| 96 | + | `InvalidFormatString`, `InvalidFormatArgIndex`, `InvalidFormatSpecifiers`). | |
| 97 | + | The runner has the same 255-argument cap as the JS (`Byte.MaxValue`, | |
| 98 | + | `ExpressionConstants`). | |
| 86 | 99 | - [x] **`contains()`, `startsWith`/`endsWith`, `==`, `<`, `>` and key lookup use | |
| 87 | 100 | `OrdinalIgnoreCase`.** Resolved: follows the C#. Every string comparison | |
| 88 | 101 | in the runner is `StringComparison.OrdinalIgnoreCase` | |
| @@ -177,6 +190,20 @@ whether to keep the JS behaviour or follow the runner. | |||
| 177 | 190 | `JsonConvert.SerializeObject`; the real one passes the runner's | |
| 178 | 191 | `VssJsonMediaTypeFormatter` settings, which do not change string | |
| 179 | 192 | escaping. | |
| 193 | + | - [x] **Lazy evaluation of built-in function arguments and of index | |
| 194 | + | expressions**: found by the differential run, not in the original list. | |
| 195 | + | In the runner a function evaluates only the arguments it uses | |
| 196 | + | (`Parameters[i].Evaluate(context)` inside `EvaluateCore`): `case` stops | |
| 197 | + | at the first true predicate, `contains`/`startsWith`/`endsWith` do not | |
| 198 | + | evaluate the second argument when the first is not primitive (or, for | |
| 199 | + | `contains`, not a non-empty array), `join` evaluates the separator only | |
| 200 | + | with two or more items, `format` evaluates only referenced arguments. And | |
| 201 | + | `a[b]` (`Index.cs`) evaluates `b` only if `a` is an array or a | |
| 202 | + | dictionary, and does not wrap its errors (the JS evaluates `b` first and | |
| 203 | + | wraps them in "could not evaluate index for index access"). So | |
| 204 | + | `join(success(), format('{2}'))` is `true` in the runner and an error in | |
| 205 | + | the JS. Built-ins now receive the call and evaluate what they need; | |
| 206 | + | extension functions still get evaluated arguments. | |
| 180 | 207 | - [ ] **Index access**: `a[b]` where `b` is a number uses `floor(b)` and | |
| 181 | 208 | rejects NaN, negatives and infinities; where `b` is a string it is a key | |
| 182 | 209 | or a decimal index (`'1'`, `' 1 '`, `'1.9'` all index 1 of an array). | |
| @@ -72,17 +72,30 @@ whether to keep the JS behaviour or follow the runner. | |||||
| 72 | a bug of the editor library. Tests: `TestEvalLiteralsAndOperators`; the | 72 | a bug of the editor library. Tests: `TestEvalLiteralsAndOperators`; the | ||
| 73 | oracle excuses the JS mismatch because `testdata/runner.json.gz` agrees | 73 | oracle excuses the JS mismatch because `testdata/runner.json.gz` agrees | ||
| 74 | with this package. | 74 | with this package. | ||
| 75 | - | - [ ] **`case()` availability.** The JS gates it behind the experimental flag | 75 | + | - [x] **`case()` availability.** Resolved: the runner has `case` always |
| 76 | - | `allowCaseFunction`, but only in completion; the parser and the | 76 | + | (`ExpressionConstants`: `AddFunction<Case>("case", 3, Byte.MaxValue)`), |
| 77 | - | evaluator always accept it. Here it is always on. Confirm whether the | 77 | + | with an odd number of arguments enforced at parse time |
| 78 | - | hosted service accepts `case` and with which arity rules (the JS: at | 78 | + | (`ExpressionParser.FlushTopEndParameters`: `EvenParameters`, reported on |
| 79 | - | least 3 arguments, an odd number, predicates must be booleans). | 79 | + | the `(` token) and at evaluation (`Sdk/Functions/Case.cs`), predicates |
| 80 | - | - [ ] **`format()`**: the reference uses `parseInt` on the digits, so | 80 | + | must be booleans ("case predicate must evaluate to a boolean value"), and |
| 81 | - | `{99999999999999999999}` is an Infinity-like index and reports "more | 81 | + | only the matching branch is evaluated. `ExpressionParserL0` has a |
| 82 | - | arguments than were supplied". The `{0:x}` form silently ignores the | 82 | + | regression test that `case(...)` must not turn on "allow unknown |
| 83 | - | specifier; C# may throw. The error texts are the JS ones. Check the C# | 83 | + | keywords" (ported in `runner_l0_test.go`). Here it is always on, with |
| 84 | - | `Format` node for the exact messages and for `{N}` with `N` > 255 or | 84 | + | the maximum of 255 arguments (the JS has no maximum). |
| 85 | - | leading zeros. | 85 | + | - [x] **`format()`**: resolved, follows the C#. `Sdk/Functions/Format.cs`: the |
| 86 | + | format string is scanned first (syntax and "more arguments than were | |||
| 87 | + | supplied" are reported before any argument is evaluated), arguments are | |||
| 88 | + | then evaluated lazily, once each. The index is a **byte** | |||
| 89 | + | (`Byte.TryParse`): `{255}` is the last valid index, `{256}` and | |||
| 90 | + | `{12345678901234567890123}` are "format string is invalid", not "more | |||
| 91 | + | arguments". After the digits only `}` or `:specifiers}` are allowed | |||
| 92 | + | (`{0 }`, `{0,5}` are invalid); **specifiers are an error** for every | |||
| 93 | + | value: `The format specifiers 'x' are not valid for objects of type | |||
| 94 | + | 'Number'` (kind names `Null Boolean Number String Array Object`). | |||
| 95 | + | Messages are the same as the JS ones otherwise (`ExpressionResources.resx`: | |||
| 96 | + | `InvalidFormatString`, `InvalidFormatArgIndex`, `InvalidFormatSpecifiers`). | |||
| 97 | + | The runner has the same 255-argument cap as the JS (`Byte.MaxValue`, | |||
| 98 | + | `ExpressionConstants`). | |||
| 86 | - [x] **`contains()`, `startsWith`/`endsWith`, `==`, `<`, `>` and key lookup use | 99 | - [x] **`contains()`, `startsWith`/`endsWith`, `==`, `<`, `>` and key lookup use | ||
| 87 | `OrdinalIgnoreCase`.** Resolved: follows the C#. Every string comparison | 100 | `OrdinalIgnoreCase`.** Resolved: follows the C#. Every string comparison | ||
| 88 | in the runner is `StringComparison.OrdinalIgnoreCase` | 101 | in the runner is `StringComparison.OrdinalIgnoreCase` | ||
| @@ -177,6 +190,20 @@ whether to keep the JS behaviour or follow the runner. | |||||
| 177 | `JsonConvert.SerializeObject`; the real one passes the runner's | 190 | `JsonConvert.SerializeObject`; the real one passes the runner's | ||
| 178 | `VssJsonMediaTypeFormatter` settings, which do not change string | 191 | `VssJsonMediaTypeFormatter` settings, which do not change string | ||
| 179 | escaping. | 192 | escaping. | ||
| 193 | + | - [x] **Lazy evaluation of built-in function arguments and of index | |||
| 194 | + | expressions**: found by the differential run, not in the original list. | |||
| 195 | + | In the runner a function evaluates only the arguments it uses | |||
| 196 | + | (`Parameters[i].Evaluate(context)` inside `EvaluateCore`): `case` stops | |||
| 197 | + | at the first true predicate, `contains`/`startsWith`/`endsWith` do not | |||
| 198 | + | evaluate the second argument when the first is not primitive (or, for | |||
| 199 | + | `contains`, not a non-empty array), `join` evaluates the separator only | |||
| 200 | + | with two or more items, `format` evaluates only referenced arguments. And | |||
| 201 | + | `a[b]` (`Index.cs`) evaluates `b` only if `a` is an array or a | |||
| 202 | + | dictionary, and does not wrap its errors (the JS evaluates `b` first and | |||
| 203 | + | wraps them in "could not evaluate index for index access"). So | |||
| 204 | + | `join(success(), format('{2}'))` is `true` in the runner and an error in | |||
| 205 | + | the JS. Built-ins now receive the call and evaluate what they need; | |||
| 206 | + | extension functions still get evaluated arguments. | |||
| 180 | - [ ] **Index access**: `a[b]` where `b` is a number uses `floor(b)` and | 207 | - [ ] **Index access**: `a[b]` where `b` is a number uses `floor(b)` and | ||
| 181 | rejects NaN, negatives and infinities; where `b` is a string it is a key | 208 | rejects NaN, negatives and infinities; where `b` is a string it is a key | ||
| 182 | or a decimal index (`'1'`, `' 1 '`, `'1.9'` all index 1 of an array). | 209 | or a decimal index (`'1'`, `' 1 '`, `'1.9'` all index 1 of an array). | ||
M expr/eval.go +30 -13
| @@ -249,6 +249,10 @@ func (e *evaluator) call(c *FunctionCall) (Value, error) { | |||
| 249 | 249 | } | |
| 250 | 250 | impl = builtinImpls[name] | |
| 251 | 251 | } | |
| 252 | + | if !isExt { | |
| 253 | + | // Built-ins evaluate their own arguments, lazily. | |
| 254 | + | return impl(e, c) | |
| 255 | + | } | |
| 252 | 256 | args := make([]Value, len(c.Args)) | |
| 253 | 257 | for i, a := range c.Args { | |
| 254 | 258 | v, err := e.eval(a) | |
| @@ -257,7 +261,7 @@ func (e *evaluator) call(c *FunctionCall) (Value, error) { | |||
| 257 | 261 | } | |
| 258 | 262 | args[i] = v | |
| 259 | 263 | } | |
| 260 | - | if isExt { | |
| 264 | + | { | |
| 261 | 265 | v, err := ext(e.ctx, args) | |
| 262 | 266 | if err != nil { | |
| 263 | 267 | var ee *EvalError | |
| @@ -268,7 +272,6 @@ func (e *evaluator) call(c *FunctionCall) (Value, error) { | |||
| 268 | 272 | } | |
| 269 | 273 | return v, nil | |
| 270 | 274 | } | |
| 271 | - | return impl(e, args) | |
| 272 | 275 | } | |
| 273 | 276 | ||
| 274 | 277 | // indexer is idxHelper of the reference: how an index value is read, as a | |
| @@ -309,36 +312,50 @@ func (ix indexer) pos(n int) (int, bool) { | |||
| 309 | 312 | return int(ix.intv), true | |
| 310 | 313 | } | |
| 311 | 314 | ||
| 315 | + | // indexAccess evaluates a[b] as Index.cs does: the object first, then, only if | |
| 316 | + | // it is an array or a dictionary, the index, whose errors are reported as they | |
| 317 | + | // are (the JavaScript reference evaluates the index first and wraps its errors | |
| 318 | + | // in "could not evaluate index for index access"). | |
| 312 | 319 | func (e *evaluator) indexAccess(ia *IndexAccess) (Value, error) { | |
| 313 | - | var ix indexer | |
| 314 | - | if _, isStar := ia.Index.(*Star); isStar { | |
| 315 | - | ix = newIndexer(true, nil) | |
| 316 | - | } else { | |
| 317 | - | v, err := e.eval(ia.Index) | |
| 318 | - | if err != nil { | |
| 319 | - | return nil, &EvalError{Message: "could not evaluate index for index access: " + err.Error(), Err: err} | |
| 320 | - | } | |
| 321 | - | ix = newIndexer(false, v) | |
| 322 | - | } | |
| 320 | + | _, isStar := ia.Index.(*Star) | |
| 323 | 321 | obj, err := e.eval(ia.Expr) | |
| 324 | 322 | if err != nil { | |
| 325 | 323 | return nil, err | |
| 326 | 324 | } | |
| 327 | 325 | switch o := normalize(obj).(type) { | |
| 328 | 326 | case *Array: | |
| 327 | + | ix, err := e.indexer(ia, isStar) | |
| 328 | + | if err != nil { | |
| 329 | + | return nil, err | |
| 330 | + | } | |
| 329 | 331 | if o.filtered { | |
| 330 | 332 | return e.filteredArrayAccess(o, ix) | |
| 331 | 333 | } | |
| 332 | 334 | return e.arrayAccess(o, ix) | |
| 333 | 335 | case *Dictionary: | |
| 336 | + | ix, err := e.indexer(ia, isStar) | |
| 337 | + | if err != nil { | |
| 338 | + | return nil, err | |
| 339 | + | } | |
| 334 | 340 | return e.objectAccess(o, ix) | |
| 335 | 341 | } | |
| 336 | - | if ix.star { | |
| 342 | + | if isStar { | |
| 337 | 343 | return &Array{filtered: true}, nil | |
| 338 | 344 | } | |
| 339 | 345 | return Null{}, nil | |
| 340 | 346 | } | |
| 341 | 347 | ||
| 348 | + | func (e *evaluator) indexer(ia *IndexAccess, isStar bool) (indexer, error) { | |
| 349 | + | if isStar { | |
| 350 | + | return newIndexer(true, nil), nil | |
| 351 | + | } | |
| 352 | + | v, err := e.eval(ia.Index) | |
| 353 | + | if err != nil { | |
| 354 | + | return indexer{}, err | |
| 355 | + | } | |
| 356 | + | return newIndexer(false, v), nil | |
| 357 | + | } | |
| 358 | + | ||
| 342 | 359 | // filteredArrayAccess applies an index to each element of a filtered array | |
| 343 | 360 | // and collects the hits: "a.*.b" is the b of every element that has one. | |
| 344 | 361 | func (e *evaluator) filteredArrayAccess(fa *Array, ix indexer) (Value, error) { | |
| @@ -249,6 +249,10 @@ func (e *evaluator) call(c *FunctionCall) (Value, error) { | |||||
| 249 | } | 249 | } | ||
| 250 | impl = builtinImpls[name] | 250 | impl = builtinImpls[name] | ||
| 251 | } | 251 | } | ||
| 252 | + | if !isExt { | |||
| 253 | + | // Built-ins evaluate their own arguments, lazily. | |||
| 254 | + | return impl(e, c) | |||
| 255 | + | } | |||
| 252 | args := make([]Value, len(c.Args)) | 256 | args := make([]Value, len(c.Args)) | ||
| 253 | for i, a := range c.Args { | 257 | for i, a := range c.Args { | ||
| 254 | v, err := e.eval(a) | 258 | v, err := e.eval(a) | ||
| @@ -257,7 +261,7 @@ func (e *evaluator) call(c *FunctionCall) (Value, error) { | |||||
| 257 | } | 261 | } | ||
| 258 | args[i] = v | 262 | args[i] = v | ||
| 259 | } | 263 | } | ||
| 260 | - | if isExt { | 264 | + | { |
| 261 | v, err := ext(e.ctx, args) | 265 | v, err := ext(e.ctx, args) | ||
| 262 | if err != nil { | 266 | if err != nil { | ||
| 263 | var ee *EvalError | 267 | var ee *EvalError | ||
| @@ -268,7 +272,6 @@ func (e *evaluator) call(c *FunctionCall) (Value, error) { | |||||
| 268 | } | 272 | } | ||
| 269 | return v, nil | 273 | return v, nil | ||
| 270 | } | 274 | } | ||
| 271 | - | return impl(e, args) | |||
| 272 | } | 275 | } | ||
| 273 | 276 | ||||
| 274 | // indexer is idxHelper of the reference: how an index value is read, as a | 277 | // indexer is idxHelper of the reference: how an index value is read, as a | ||
| @@ -309,36 +312,50 @@ func (ix indexer) pos(n int) (int, bool) { | |||||
| 309 | return int(ix.intv), true | 312 | return int(ix.intv), true | ||
| 310 | } | 313 | } | ||
| 311 | 314 | ||||
| 315 | + | // indexAccess evaluates a[b] as Index.cs does: the object first, then, only if | |||
| 316 | + | // it is an array or a dictionary, the index, whose errors are reported as they | |||
| 317 | + | // are (the JavaScript reference evaluates the index first and wraps its errors | |||
| 318 | + | // in "could not evaluate index for index access"). | |||
| 312 | func (e *evaluator) indexAccess(ia *IndexAccess) (Value, error) { | 319 | func (e *evaluator) indexAccess(ia *IndexAccess) (Value, error) { | ||
| 313 | - | var ix indexer | 320 | + | _, isStar := ia.Index.(*Star) |
| 314 | - | if _, isStar := ia.Index.(*Star); isStar { | |||
| 315 | - | ix = newIndexer(true, nil) | |||
| 316 | - | } else { | |||
| 317 | - | v, err := e.eval(ia.Index) | |||
| 318 | - | if err != nil { | |||
| 319 | - | return nil, &EvalError{Message: "could not evaluate index for index access: " + err.Error(), Err: err} | |||
| 320 | - | } | |||
| 321 | - | ix = newIndexer(false, v) | |||
| 322 | - | } | |||
| 323 | obj, err := e.eval(ia.Expr) | 321 | obj, err := e.eval(ia.Expr) | ||
| 324 | if err != nil { | 322 | if err != nil { | ||
| 325 | return nil, err | 323 | return nil, err | ||
| 326 | } | 324 | } | ||
| 327 | switch o := normalize(obj).(type) { | 325 | switch o := normalize(obj).(type) { | ||
| 328 | case *Array: | 326 | case *Array: | ||
| 327 | + | ix, err := e.indexer(ia, isStar) | |||
| 328 | + | if err != nil { | |||
| 329 | + | return nil, err | |||
| 330 | + | } | |||
| 329 | if o.filtered { | 331 | if o.filtered { | ||
| 330 | return e.filteredArrayAccess(o, ix) | 332 | return e.filteredArrayAccess(o, ix) | ||
| 331 | } | 333 | } | ||
| 332 | return e.arrayAccess(o, ix) | 334 | return e.arrayAccess(o, ix) | ||
| 333 | case *Dictionary: | 335 | case *Dictionary: | ||
| 336 | + | ix, err := e.indexer(ia, isStar) | |||
| 337 | + | if err != nil { | |||
| 338 | + | return nil, err | |||
| 339 | + | } | |||
| 334 | return e.objectAccess(o, ix) | 340 | return e.objectAccess(o, ix) | ||
| 335 | } | 341 | } | ||
| 336 | - | if ix.star { | 342 | + | if isStar { |
| 337 | return &Array{filtered: true}, nil | 343 | return &Array{filtered: true}, nil | ||
| 338 | } | 344 | } | ||
| 339 | return Null{}, nil | 345 | return Null{}, nil | ||
| 340 | } | 346 | } | ||
| 341 | 347 | ||||
| 348 | + | func (e *evaluator) indexer(ia *IndexAccess, isStar bool) (indexer, error) { | |||
| 349 | + | if isStar { | |||
| 350 | + | return newIndexer(true, nil), nil | |||
| 351 | + | } | |||
| 352 | + | v, err := e.eval(ia.Index) | |||
| 353 | + | if err != nil { | |||
| 354 | + | return indexer{}, err | |||
| 355 | + | } | |||
| 356 | + | return newIndexer(false, v), nil | |||
| 357 | + | } | |||
| 358 | + | ||||
| 342 | // filteredArrayAccess applies an index to each element of a filtered array | 359 | // filteredArrayAccess applies an index to each element of a filtered array | ||
| 343 | // and collects the hits: "a.*.b" is the b of every element that has one. | 360 | // and collects the hits: "a.*.b" is the b of every element that has one. | ||
| 344 | func (e *evaluator) filteredArrayAccess(fa *Array, ix indexer) (Value, error) { | 361 | func (e *evaluator) filteredArrayAccess(fa *Array, ix indexer) (Value, error) { | ||
M expr/eval_test.go +14 -3
| @@ -458,8 +458,18 @@ func TestEvalBuiltinFunctions(t *testing.T) { | |||
| 458 | 458 | {"format('{{0}}', 1)", "String:{0}"}, | |
| 459 | 459 | {"format('{{{0}}}', 1)", "String:{1}"}, | |
| 460 | 460 | {"format('{0}', 'a', 'b')", "String:a"}, // extra arguments are fine | |
| 461 | - | {"format('{0:x}', 1)", "String:1"}, // a format specifier is skipped | |
| 462 | - | {"format('{0 anything}', 1)", "String:1"}, | |
| 461 | + | // The runner rejects format specifiers (Format.cs) and anything but | |
| 462 | + | // digits between the index and the closing brace; the JavaScript | |
| 463 | + | // reference skips them. | |
| 464 | + | {"format('{0:x}', 1)", "ERR The format specifiers 'x' are not valid for objects of type 'Number'"}, | |
| 465 | + | {"format('{0:}', 1)", "String:1"}, | |
| 466 | + | {"format('{0:x}}y}', true)", "ERR The format specifiers 'x}y' are not valid for objects of type 'Boolean'"}, | |
| 467 | + | {"format('{0 anything}', 1)", "ERR The following format string is invalid: {0 anything}"}, | |
| 468 | + | {"format('{0 }', 1)", "ERR The following format string is invalid: {0 }"}, | |
| 469 | + | {"format('{0,5}', 1)", "ERR The following format string is invalid: {0,5}"}, | |
| 470 | + | {"format('{255}', 1)", "ERR The following format string references more arguments than were supplied: {255}"}, | |
| 471 | + | {"format('{256}', 1)", "ERR The following format string is invalid: {256}"}, | |
| 472 | + | {"format('{00}', 7)", "String:7"}, | |
| 463 | 473 | {"format('{00}', 1)", "String:1"}, | |
| 464 | 474 | {"format('{01}', 1, 2)", "String:2"}, | |
| 465 | 475 | {"format('{0}', null)", "String:"}, | |
| @@ -481,7 +491,8 @@ func TestEvalBuiltinFunctions(t *testing.T) { | |||
| 481 | 491 | {"format('0}', 1)", "ERR The following format string is invalid: 0}"}, | |
| 482 | 492 | {"format('{{0}', 1)", "ERR The following format string is invalid: {{0}"}, | |
| 483 | 493 | {"format('{0}}', 1)", "ERR The following format string is invalid: {0}}"}, | |
| 484 | - | {"format('{12345678901234567890123}', 1)", "ERR The following format string references more arguments than were supplied: {12345678901234567890123}"}, | |
| 494 | + | // The index is a byte: a longer number makes the string invalid. | |
| 495 | + | {"format('{12345678901234567890123}', 1)", "ERR The following format string is invalid: {12345678901234567890123}"}, | |
| 485 | 496 | {"format()", "PARSE Too few parameters supplied: 'format'"}, | |
| 486 | 497 | ||
| 487 | 498 | // case | |
| @@ -458,8 +458,18 @@ func TestEvalBuiltinFunctions(t *testing.T) { | |||||
| 458 | {"format('{{0}}', 1)", "String:{0}"}, | 458 | {"format('{{0}}', 1)", "String:{0}"}, | ||
| 459 | {"format('{{{0}}}', 1)", "String:{1}"}, | 459 | {"format('{{{0}}}', 1)", "String:{1}"}, | ||
| 460 | {"format('{0}', 'a', 'b')", "String:a"}, // extra arguments are fine | 460 | {"format('{0}', 'a', 'b')", "String:a"}, // extra arguments are fine | ||
| 461 | - | {"format('{0:x}', 1)", "String:1"}, // a format specifier is skipped | 461 | + | // The runner rejects format specifiers (Format.cs) and anything but |
| 462 | - | {"format('{0 anything}', 1)", "String:1"}, | 462 | + | // digits between the index and the closing brace; the JavaScript |
| 463 | + | // reference skips them. | |||
| 464 | + | {"format('{0:x}', 1)", "ERR The format specifiers 'x' are not valid for objects of type 'Number'"}, | |||
| 465 | + | {"format('{0:}', 1)", "String:1"}, | |||
| 466 | + | {"format('{0:x}}y}', true)", "ERR The format specifiers 'x}y' are not valid for objects of type 'Boolean'"}, | |||
| 467 | + | {"format('{0 anything}', 1)", "ERR The following format string is invalid: {0 anything}"}, | |||
| 468 | + | {"format('{0 }', 1)", "ERR The following format string is invalid: {0 }"}, | |||
| 469 | + | {"format('{0,5}', 1)", "ERR The following format string is invalid: {0,5}"}, | |||
| 470 | + | {"format('{255}', 1)", "ERR The following format string references more arguments than were supplied: {255}"}, | |||
| 471 | + | {"format('{256}', 1)", "ERR The following format string is invalid: {256}"}, | |||
| 472 | + | {"format('{00}', 7)", "String:7"}, | |||
| 463 | {"format('{00}', 1)", "String:1"}, | 473 | {"format('{00}', 1)", "String:1"}, | ||
| 464 | {"format('{01}', 1, 2)", "String:2"}, | 474 | {"format('{01}', 1, 2)", "String:2"}, | ||
| 465 | {"format('{0}', null)", "String:"}, | 475 | {"format('{0}', null)", "String:"}, | ||
| @@ -481,7 +491,8 @@ func TestEvalBuiltinFunctions(t *testing.T) { | |||||
| 481 | {"format('0}', 1)", "ERR The following format string is invalid: 0}"}, | 491 | {"format('0}', 1)", "ERR The following format string is invalid: 0}"}, | ||
| 482 | {"format('{{0}', 1)", "ERR The following format string is invalid: {{0}"}, | 492 | {"format('{{0}', 1)", "ERR The following format string is invalid: {{0}"}, | ||
| 483 | {"format('{0}}', 1)", "ERR The following format string is invalid: {0}}"}, | 493 | {"format('{0}}', 1)", "ERR The following format string is invalid: {0}}"}, | ||
| 484 | - | {"format('{12345678901234567890123}', 1)", "ERR The following format string references more arguments than were supplied: {12345678901234567890123}"}, | 494 | + | // The index is a byte: a longer number makes the string invalid. |
| 495 | + | {"format('{12345678901234567890123}', 1)", "ERR The following format string is invalid: {12345678901234567890123}"}, | |||
| 485 | {"format()", "PARSE Too few parameters supplied: 'format'"}, | 496 | {"format()", "PARSE Too few parameters supplied: 'format'"}, | ||
| 486 | 497 | ||||
| 487 | // case | 498 | // case | ||
M expr/funcs.go +250 -95
| @@ -4,58 +4,88 @@ package expr | |||
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 6 | "errors" | |
| 7 | - | "math" | |
| 8 | 7 | "strconv" | |
| 9 | 8 | "strings" | |
| 9 | + | "unicode" | |
| 10 | 10 | ) | |
| 11 | 11 | ||
| 12 | 12 | // builtinFunc implements a built-in function. It runs inside the evaluator so | |
| 13 | - | // that it can charge the output budget and watch the context. | |
| 14 | - | type builtinFunc func(e *evaluator, args []Value) (Value, error) | |
| 13 | + | // that it can charge the output budget and watch the context, and it receives | |
| 14 | + | // the call, not the evaluated arguments: like the runner's functions | |
| 15 | + | // (Sdk/Functions/*.cs) it evaluates only the arguments it needs, in its own | |
| 16 | + | // order, so an argument that is never reached cannot fail. | |
| 17 | + | type builtinFunc func(e *evaluator, c *FunctionCall) (Value, error) | |
| 15 | 18 | ||
| 16 | 19 | // builtinImpls are the implementations of the built-in functions, keyed by | |
| 17 | - | // lower-cased name. Arity is validated by the parser, so each may assume its | |
| 18 | - | // documented argument count. | |
| 19 | - | var builtinImpls = map[string]builtinFunc{ | |
| 20 | - | "CASE": funcCase, | |
| 21 | - | "CONTAINS": funcContains, | |
| 22 | - | "ENDSWITH": funcEndsWith, | |
| 23 | - | "FORMAT": funcFormat, | |
| 24 | - | "FROMJSON": funcFromJSON, | |
| 25 | - | "JOIN": funcJoin, | |
| 26 | - | "STARTSWITH": funcStartsWith, | |
| 27 | - | "TOJSON": funcToJSON, | |
| 20 | + | // upper-cased name (see foldKey). Arity is validated by the parser, so each | |
| 21 | + | // may assume its documented argument count. | |
| 22 | + | var builtinImpls map[string]builtinFunc | |
| 23 | + | ||
| 24 | + | // The map is filled in init: the implementations call back into the evaluator, | |
| 25 | + | // which would make a package-level initializer cyclic. | |
| 26 | + | func init() { | |
| 27 | + | builtinImpls = map[string]builtinFunc{ | |
| 28 | + | "CASE": funcCase, | |
| 29 | + | "CONTAINS": funcContains, | |
| 30 | + | "ENDSWITH": funcEndsWith, | |
| 31 | + | "FORMAT": funcFormat, | |
| 32 | + | "FROMJSON": funcFromJSON, | |
| 33 | + | "JOIN": funcJoin, | |
| 34 | + | "STARTSWITH": funcStartsWith, | |
| 35 | + | "TOJSON": funcToJSON, | |
| 36 | + | } | |
| 28 | 37 | } | |
| 29 | 38 | ||
| 30 | - | // funcCase is case(pred1, val1, pred2, val2, ..., default). | |
| 31 | - | // | |
| 32 | - | // The JavaScript reference ships it behind an experimental feature flag | |
| 33 | - | // (allowCaseFunction) that only affects editor completion; the parser always | |
| 34 | - | // accepts it, and so does this package. | |
| 35 | - | func funcCase(e *evaluator, args []Value) (Value, error) { | |
| 36 | - | for i := 0; i < len(args)-1; i += 2 { | |
| 37 | - | pred, ok := normalize(args[i]).(Boolean) | |
| 39 | + | // funcCase is case(pred1, val1, pred2, val2, ..., default): the predicates are | |
| 40 | + | // evaluated in order up to the first true one, then only its value (Case.cs). | |
| 41 | + | // A predicate that is not a boolean is an error. The JavaScript reference | |
| 42 | + | // ships case behind an experimental feature flag that only affects editor | |
| 43 | + | // completion; the parser always accepts it, and so does the runner. | |
| 44 | + | func funcCase(e *evaluator, c *FunctionCall) (Value, error) { | |
| 45 | + | n := len(c.Args) | |
| 46 | + | if n%2 == 0 { | |
| 47 | + | return nil, evalErrorf("case requires an odd number of arguments") | |
| 48 | + | } | |
| 49 | + | for i := 0; i < n-1; i += 2 { | |
| 50 | + | p, err := e.eval(c.Args[i]) | |
| 51 | + | if err != nil { | |
| 52 | + | return nil, err | |
| 53 | + | } | |
| 54 | + | pred, ok := normalize(p).(Boolean) | |
| 38 | 55 | if !ok { | |
| 39 | 56 | return nil, evalErrorf("case predicate must evaluate to a boolean value") | |
| 40 | 57 | } | |
| 41 | 58 | if pred { | |
| 42 | - | return args[i+1], nil | |
| 59 | + | return e.eval(c.Args[i+1]) | |
| 43 | 60 | } | |
| 44 | 61 | } | |
| 45 | - | return args[len(args)-1], nil | |
| 62 | + | return e.eval(c.Args[n-1]) | |
| 46 | 63 | } | |
| 47 | 64 | ||
| 48 | - | // funcContains is contains(search, item): substring test, case-insensitive | |
| 49 | - | // (OrdinalIgnoreCase), when both are primitive; membership by == in an array; | |
| 50 | - | // false otherwise. | |
| 51 | - | func funcContains(e *evaluator, args []Value) (Value, error) { | |
| 52 | - | left, right := normalize(args[0]), normalize(args[1]) | |
| 65 | + | // funcContains is contains(search, item) (Contains.cs): when search is a | |
| 66 | + | // primitive and so is item, a case-insensitive (OrdinalIgnoreCase) substring | |
| 67 | + | // test on their string forms; when search is a non-empty array, == against each | |
| 68 | + | // element; false otherwise. item is only evaluated when it can matter. | |
| 69 | + | func funcContains(e *evaluator, c *FunctionCall) (Value, error) { | |
| 70 | + | l, err := e.eval(c.Args[0]) | |
| 71 | + | if err != nil { | |
| 72 | + | return nil, err | |
| 73 | + | } | |
| 74 | + | left := normalize(l) | |
| 53 | 75 | switch { | |
| 54 | 76 | case isPrimitive(left): | |
| 55 | - | if isPrimitive(right) { | |
| 77 | + | r, err := e.eval(c.Args[1]) | |
| 78 | + | if err != nil { | |
| 79 | + | return nil, err | |
| 80 | + | } | |
| 81 | + | if right := normalize(r); isPrimitive(right) { | |
| 56 | 82 | return Boolean(jsIncludes(ordinalFold(CoerceString(left)), ordinalFold(CoerceString(right)))), nil | |
| 57 | 83 | } | |
| 58 | - | case left.Kind() == KindArray: | |
| 84 | + | case left.Kind() == KindArray && len(left.(*Array).items) > 0: | |
| 85 | + | right, err := e.eval(c.Args[1]) | |
| 86 | + | if err != nil { | |
| 87 | + | return nil, err | |
| 88 | + | } | |
| 59 | 89 | for _, v := range left.(*Array).items { | |
| 60 | 90 | if err := e.tick(); err != nil { | |
| 61 | 91 | return nil, err | |
| @@ -68,40 +98,57 @@ func funcContains(e *evaluator, args []Value) (Value, error) { | |||
| 68 | 98 | return Boolean(false), nil | |
| 69 | 99 | } | |
| 70 | 100 | ||
| 71 | - | func funcStartsWith(e *evaluator, args []Value) (Value, error) { | |
| 72 | - | ls, rs, ok := primitivePair(args) | |
| 73 | - | return Boolean(ok && jsHasPrefix(ordinalFold(ls), ordinalFold(rs))), nil | |
| 101 | + | func funcStartsWith(e *evaluator, c *FunctionCall) (Value, error) { | |
| 102 | + | ls, rs, ok, err := e.primitivePair(c) | |
| 103 | + | return Boolean(ok && jsHasPrefix(ordinalFold(ls), ordinalFold(rs))), err | |
| 74 | 104 | } | |
| 75 | 105 | ||
| 76 | - | func funcEndsWith(e *evaluator, args []Value) (Value, error) { | |
| 77 | - | ls, rs, ok := primitivePair(args) | |
| 78 | - | return Boolean(ok && jsHasSuffix(ordinalFold(ls), ordinalFold(rs))), nil | |
| 106 | + | func funcEndsWith(e *evaluator, c *FunctionCall) (Value, error) { | |
| 107 | + | ls, rs, ok, err := e.primitivePair(c) | |
| 108 | + | return Boolean(ok && jsHasSuffix(ordinalFold(ls), ordinalFold(rs))), err | |
| 79 | 109 | } | |
| 80 | 110 | ||
| 81 | - | // primitivePair returns the string forms of the two arguments, and false | |
| 82 | - | // when either is an array or a dictionary. | |
| 83 | - | func primitivePair(args []Value) (string, string, bool) { | |
| 84 | - | if !isPrimitive(args[0]) || !isPrimitive(args[1]) { | |
| 85 | - | return "", "", false | |
| 111 | + | // primitivePair evaluates the two arguments, the second only if the first is | |
| 112 | + | // primitive, and returns their string forms; ok is false when either is an | |
| 113 | + | // array or a dictionary. | |
| 114 | + | func (e *evaluator) primitivePair(c *FunctionCall) (ls, rs string, ok bool, err error) { | |
| 115 | + | l, err := e.eval(c.Args[0]) | |
| 116 | + | if err != nil || !isPrimitive(l) { | |
| 117 | + | return "", "", false, err | |
| 86 | 118 | } | |
| 87 | - | return CoerceString(args[0]), CoerceString(args[1]), true | |
| 119 | + | r, err := e.eval(c.Args[1]) | |
| 120 | + | if err != nil || !isPrimitive(r) { | |
| 121 | + | return "", "", false, err | |
| 122 | + | } | |
| 123 | + | return CoerceString(l), CoerceString(r), true, nil | |
| 88 | 124 | } | |
| 89 | 125 | ||
| 90 | - | // funcJoin is join(array, separator). A primitive first argument is just | |
| 91 | - | // converted to a string, ignoring the separator; a dictionary gives "". A | |
| 92 | - | // separator that is not primitive means ",". | |
| 93 | - | func funcJoin(e *evaluator, args []Value) (Value, error) { | |
| 94 | - | first := normalize(args[0]) | |
| 126 | + | // funcJoin is join(array, separator) (Join.cs). A primitive first argument is | |
| 127 | + | // just converted to a string, ignoring the separator; a dictionary or an empty | |
| 128 | + | // array gives "". The separator is evaluated only when there are at least two | |
| 129 | + | // items to separate, and a separator that is not primitive means ",". | |
| 130 | + | func funcJoin(e *evaluator, c *FunctionCall) (Value, error) { | |
| 131 | + | f, err := e.eval(c.Args[0]) | |
| 132 | + | if err != nil { | |
| 133 | + | return nil, err | |
| 134 | + | } | |
| 135 | + | first := normalize(f) | |
| 95 | 136 | if isPrimitive(first) { | |
| 96 | 137 | return String(CoerceString(first)), nil | |
| 97 | 138 | } | |
| 98 | 139 | arr, ok := first.(*Array) | |
| 99 | - | if !ok { | |
| 140 | + | if !ok || len(arr.items) == 0 { | |
| 100 | 141 | return String(""), nil | |
| 101 | 142 | } | |
| 102 | 143 | sep := "," | |
| 103 | - | if len(args) > 1 && isPrimitive(args[1]) { | |
| 104 | - | sep = CoerceString(args[1]) | |
| 144 | + | if len(arr.items) > 1 && len(c.Args) > 1 { | |
| 145 | + | sv, err := e.eval(c.Args[1]) | |
| 146 | + | if err != nil { | |
| 147 | + | return nil, err | |
| 148 | + | } | |
| 149 | + | if isPrimitive(sv) { | |
| 150 | + | sep = CoerceString(sv) | |
| 151 | + | } | |
| 105 | 152 | } | |
| 106 | 153 | var out strings.Builder | |
| 107 | 154 | for i, it := range arr.items { | |
| @@ -121,17 +168,21 @@ func funcJoin(e *evaluator, args []Value) (Value, error) { | |||
| 121 | 168 | } | |
| 122 | 169 | out.WriteString(part) | |
| 123 | 170 | } | |
| 124 | - | return String(normalizeWTF8(out.String())), nil | |
| 171 | + | return String(out.String()), nil | |
| 125 | 172 | } | |
| 126 | 173 | ||
| 127 | 174 | // funcToJSON is toJSON(value). The output counts against the budget and is | |
| 128 | 175 | // cut off as soon as it exceeds what is left. | |
| 129 | - | func funcToJSON(e *evaluator, args []Value) (Value, error) { | |
| 176 | + | func funcToJSON(e *evaluator, c *FunctionCall) (Value, error) { | |
| 177 | + | v, err := e.eval(c.Args[0]) | |
| 178 | + | if err != nil { | |
| 179 | + | return nil, err | |
| 180 | + | } | |
| 130 | 181 | max := -1 | |
| 131 | 182 | if e.limit >= 0 { | |
| 132 | 183 | max = e.limit - e.used | |
| 133 | 184 | } | |
| 134 | - | out, err := EncodeJSON(args[0], max) | |
| 185 | + | out, err := EncodeJSON(v, max) | |
| 135 | 186 | switch { | |
| 136 | 187 | case errors.Is(err, errJSONTooBig): | |
| 137 | 188 | return nil, e.budgetError() | |
| @@ -145,11 +196,15 @@ func funcToJSON(e *evaluator, args []Value) (Value, error) { | |||
| 145 | 196 | } | |
| 146 | 197 | ||
| 147 | 198 | // funcFromJSON is fromJSON(value). Blank input is "empty input"; anything | |
| 148 | - | // that is not valid JSON is "Error parsing JSON when evaluating fromJson" | |
| 149 | - | // wrapping the syntax error. | |
| 150 | - | func funcFromJSON(e *evaluator, args []Value) (Value, error) { | |
| 151 | - | in := CoerceString(args[0]) | |
| 152 | - | if jsTrim(in) == "" { | |
| 199 | + | // that is not readable by Json.NET is "Error parsing JSON when evaluating | |
| 200 | + | // fromJson" wrapping the reader's error. | |
| 201 | + | func funcFromJSON(e *evaluator, c *FunctionCall) (Value, error) { | |
| 202 | + | a, err := e.eval(c.Args[0]) | |
| 203 | + | if err != nil { | |
| 204 | + | return nil, err | |
| 205 | + | } | |
| 206 | + | in := CoerceString(a) | |
| 207 | + | if strings.TrimFunc(in, isNetSpace) == "" { | |
| 153 | 208 | return nil, evalErrorf("empty input") | |
| 154 | 209 | } | |
| 155 | 210 | v, err := FromJSON([]byte(in)) | |
| @@ -159,13 +214,39 @@ func funcFromJSON(e *evaluator, args []Value) (Value, error) { | |||
| 159 | 214 | return v, nil | |
| 160 | 215 | } | |
| 161 | 216 | ||
| 162 | - | // funcFormat is format(fmt, args...): "{N}" inserts argument N, "{{" and "}}" | |
| 163 | - | // are literal braces. Anything between the digits of "{N" and the closing | |
| 164 | - | // brace is ignored ("{0:x}" is "{0}"). Every other use of a brace is an | |
| 165 | - | // error, as is a reference to a missing argument. | |
| 166 | - | func funcFormat(e *evaluator, args []Value) (Value, error) { | |
| 167 | - | fs := CoerceString(args[0]) | |
| 168 | - | var out strings.Builder | |
| 217 | + | // formatPiece is a segment of the result of format(): literal text, or the | |
| 218 | + | // index of an argument (evaluated when the result is assembled) with the | |
| 219 | + | // format specifiers written after the colon. | |
| 220 | + | type formatPiece struct { | |
| 221 | + | text string | |
| 222 | + | arg int | |
| 223 | + | specs string | |
| 224 | + | isArg bool | |
| 225 | + | } | |
| 226 | + | ||
| 227 | + | // funcFormat is format(fmt, args...) (Format.cs): "{N}" inserts argument N, | |
| 228 | + | // "{{" and "}}" are literal braces. The format string is scanned completely | |
| 229 | + | // first (so a syntax error or a reference to a missing argument is reported | |
| 230 | + | // before any argument is evaluated), then the arguments are evaluated, once | |
| 231 | + | // each, in the order they appear. N is at most 255 (a byte). Format specifiers | |
| 232 | + | // ("{0:x}") are not supported for any value: they are an error. | |
| 233 | + | func funcFormat(e *evaluator, c *FunctionCall) (Value, error) { | |
| 234 | + | f, err := e.eval(c.Args[0]) | |
| 235 | + | if err != nil { | |
| 236 | + | return nil, err | |
| 237 | + | } | |
| 238 | + | fs := CoerceString(f) | |
| 239 | + | var pieces []formatPiece | |
| 240 | + | text := func(t string) error { | |
| 241 | + | if t == "" { | |
| 242 | + | return nil | |
| 243 | + | } | |
| 244 | + | if err := e.charge(utf16Len(t)); err != nil { | |
| 245 | + | return err | |
| 246 | + | } | |
| 247 | + | pieces = append(pieces, formatPiece{text: t}) | |
| 248 | + | return nil | |
| 249 | + | } | |
| 169 | 250 | index := 0 | |
| 170 | 251 | for index < len(fs) { | |
| 171 | 252 | lbrace := indexByteFrom(fs, '{', index) | |
| @@ -174,27 +255,26 @@ func funcFormat(e *evaluator, args []Value) (Value, error) { | |||
| 174 | 255 | case lbrace >= 0 && (rbrace < 0 || rbrace > lbrace): | |
| 175 | 256 | // Escaped left brace. | |
| 176 | 257 | if charAt(fs, lbrace+1) == '{' { | |
| 177 | - | if err := e.charge(lbrace + 1 - index); err != nil { | |
| 258 | + | if err := text(fs[index : lbrace+1]); err != nil { | |
| 178 | 259 | return nil, err | |
| 179 | 260 | } | |
| 180 | - | out.WriteString(fs[index : lbrace+1]) | |
| 181 | 261 | index = lbrace + 2 | |
| 182 | 262 | continue | |
| 183 | 263 | } | |
| 184 | 264 | // Left brace, number, optional format specifiers, right brace. | |
| 185 | 265 | if rbrace > lbrace+1 { | |
| 186 | - | if argIndex, ok := readArgIndex(fs, lbrace+1); ok { | |
| 187 | - | if 1+argIndex > float64(len(args)-1) { | |
| 188 | - | return nil, evalErrorf("The following format string references more arguments than were supplied: %s", fs) | |
| 266 | + | if argIndex, end, ok := readArgIndex(fs, lbrace+1); ok { | |
| 267 | + | if specs, closing, ok := readFormatSpecifiers(fs, end+1); ok { | |
| 268 | + | if argIndex > len(c.Args)-2 { | |
| 269 | + | return nil, evalErrorf("The following format string references more arguments than were supplied: %s", fs) | |
| 270 | + | } | |
| 271 | + | if err := text(fs[index:lbrace]); err != nil { | |
| 272 | + | return nil, err | |
| 273 | + | } | |
| 274 | + | pieces = append(pieces, formatPiece{arg: argIndex, specs: specs, isArg: true}) | |
| 275 | + | index = closing + 1 | |
| 276 | + | continue | |
| 189 | 277 | } | |
| 190 | - | arg := CoerceString(args[1+int(argIndex)]) | |
| 191 | - | if err := e.charge(lbrace - index + len(arg)); err != nil { | |
| 192 | - | return nil, err | |
| 193 | - | } | |
| 194 | - | out.WriteString(fs[index:lbrace]) | |
| 195 | - | out.WriteString(arg) | |
| 196 | - | index = rbrace + 1 | |
| 197 | - | continue | |
| 198 | 278 | } | |
| 199 | 279 | } | |
| 200 | 280 | return nil, evalErrorf("The following format string is invalid: %s", fs) | |
| @@ -203,20 +283,50 @@ func funcFormat(e *evaluator, args []Value) (Value, error) { | |||
| 203 | 283 | if charAt(fs, rbrace+1) != '}' { | |
| 204 | 284 | return nil, evalErrorf("The following format string is invalid: %s", fs) | |
| 205 | 285 | } | |
| 206 | - | if err := e.charge(rbrace + 1 - index); err != nil { | |
| 286 | + | if err := text(fs[index : rbrace+1]); err != nil { | |
| 207 | 287 | return nil, err | |
| 208 | 288 | } | |
| 209 | - | out.WriteString(fs[index : rbrace+1]) | |
| 210 | 289 | index = rbrace + 2 | |
| 211 | 290 | default: | |
| 212 | - | if err := e.charge(len(fs) - index); err != nil { | |
| 291 | + | if err := text(fs[index:]); err != nil { | |
| 213 | 292 | return nil, err | |
| 214 | 293 | } | |
| 215 | - | out.WriteString(fs[index:]) | |
| 216 | 294 | index = len(fs) | |
| 217 | 295 | } | |
| 218 | 296 | } | |
| 219 | - | return String(normalizeWTF8(out.String())), nil | |
| 297 | + | // Assemble: arguments are evaluated lazily, once each, in the order of | |
| 298 | + | // their first appearance. | |
| 299 | + | type argValue struct { | |
| 300 | + | str string | |
| 301 | + | kind Kind | |
| 302 | + | done bool | |
| 303 | + | } | |
| 304 | + | cache := make([]argValue, len(c.Args)-1) | |
| 305 | + | var out strings.Builder | |
| 306 | + | for _, p := range pieces { | |
| 307 | + | if !p.isArg { | |
| 308 | + | out.WriteString(p.text) | |
| 309 | + | continue | |
| 310 | + | } | |
| 311 | + | av := &cache[p.arg] | |
| 312 | + | if !av.done { | |
| 313 | + | v, err := e.eval(c.Args[1+p.arg]) | |
| 314 | + | if err != nil { | |
| 315 | + | return nil, err | |
| 316 | + | } | |
| 317 | + | *av = argValue{str: CoerceString(v), kind: normalize(v).Kind(), done: true} | |
| 318 | + | } | |
| 319 | + | if p.specs != "" { | |
| 320 | + | return nil, evalErrorf("The format specifiers '%s' are not valid for objects of type '%s'", p.specs, av.kind) | |
| 321 | + | } | |
| 322 | + | if av.str != "" { | |
| 323 | + | if err := e.charge(utf16Len(av.str)); err != nil { | |
| 324 | + | return nil, err | |
| 325 | + | } | |
| 326 | + | } | |
| 327 | + | out.WriteString(av.str) | |
| 328 | + | } | |
| 329 | + | return String(out.String()), nil | |
| 220 | 330 | } | |
| 221 | 331 | ||
| 222 | 332 | func indexByteFrom(s string, c byte, from int) int { | |
| @@ -238,20 +348,65 @@ func charAt(s string, i int) byte { | |||
| 238 | 348 | return 0 | |
| 239 | 349 | } | |
| 240 | 350 | ||
| 241 | - | // readArgIndex reads the decimal digits at s[start:]. The index is a float | |
| 242 | - | // because parseInt in the reference yields Infinity for absurdly long digit | |
| 243 | - | // runs, which then fails the argument-count check. | |
| 244 | - | func readArgIndex(s string, start int) (float64, bool) { | |
| 351 | + | // readArgIndex reads the digits at s[start:] as the index of a format | |
| 352 | + | // argument: a byte, so at most 255 (Byte.TryParse). It returns the index of the | |
| 353 | + | // last digit. Digits are Unicode decimal digits for the scan but only ASCII | |
| 354 | + | // ones parse, so any other digit makes the format string invalid. | |
| 355 | + | func readArgIndex(s string, start int) (value, end int, ok bool) { | |
| 245 | 356 | n := 0 | |
| 246 | - | for charAt(s, start+n) >= '0' && charAt(s, start+n) <= '9' { | |
| 247 | - | n++ | |
| 357 | + | for i := start; i < len(s); { | |
| 358 | + | r, w := decodeRune(s[i:]) | |
| 359 | + | if !unicode.IsDigit(r) { | |
| 360 | + | break | |
| 361 | + | } | |
| 362 | + | i += w | |
| 363 | + | n = i - start | |
| 248 | 364 | } | |
| 249 | 365 | if n == 0 { | |
| 250 | - | return 0, false | |
| 366 | + | return 0, 0, false | |
| 367 | + | } | |
| 368 | + | v, err := strconv.ParseUint(s[start:start+n], 10, 8) | |
| 369 | + | if err != nil || !isASCII(s[start:start+n]) { | |
| 370 | + | return 0, 0, false | |
| 371 | + | } | |
| 372 | + | return int(v), start + n - 1, true | |
| 373 | + | } | |
| 374 | + | ||
| 375 | + | // readFormatSpecifiers reads what follows the digits of "{N": either the | |
| 376 | + | // closing brace, or ":" and specifiers up to a closing brace, where "}}" is a | |
| 377 | + | // literal brace inside the specifiers. It returns the specifiers and the index | |
| 378 | + | // of the closing brace. | |
| 379 | + | func readFormatSpecifiers(s string, start int) (specs string, closing int, ok bool) { | |
| 380 | + | switch charAt(s, start) { | |
| 381 | + | case '}': | |
| 382 | + | return "", start, true | |
| 383 | + | case ':': | |
| 384 | + | default: | |
| 385 | + | return "", 0, false | |
| 251 | 386 | } | |
| 252 | - | f, _ := strconv.ParseFloat(s[start:start+n], 64) | |
| 253 | - | if math.IsNaN(f) { | |
| 254 | - | return 0, false | |
| 387 | + | var b strings.Builder | |
| 388 | + | for i := start + 1; i < len(s); { | |
| 389 | + | if s[i] != '}' { | |
| 390 | + | b.WriteByte(s[i]) | |
| 391 | + | i++ | |
| 392 | + | } else if charAt(s, i+1) == '}' { | |
| 393 | + | b.WriteByte('}') | |
| 394 | + | i += 2 | |
| 395 | + | } else { | |
| 396 | + | return b.String(), i, true | |
| 397 | + | } | |
| 398 | + | } | |
| 399 | + | return "", 0, false | |
| 400 | + | } | |
| 401 | + | ||
| 402 | + | // utf16Len is the length of s in UTF-16 code units, the length of a .NET string. | |
| 403 | + | func utf16Len(s string) int { | |
| 404 | + | n := 0 | |
| 405 | + | for _, r := range s { | |
| 406 | + | n++ | |
| 407 | + | if r >= 0x10000 { | |
| 408 | + | n++ | |
| 409 | + | } | |
| 255 | 410 | } | |
| 256 | - | return f, true | |
| 411 | + | return n | |
| 257 | 412 | } | |
| @@ -4,58 +4,88 @@ package expr | |||||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | "errors" | 6 | "errors" | ||
| 7 | - | "math" | |||
| 8 | "strconv" | 7 | "strconv" | ||
| 9 | "strings" | 8 | "strings" | ||
| 9 | + | "unicode" | |||
| 10 | ) | 10 | ) | ||
| 11 | 11 | ||||
| 12 | // builtinFunc implements a built-in function. It runs inside the evaluator so | 12 | // builtinFunc implements a built-in function. It runs inside the evaluator so | ||
| 13 | - | // that it can charge the output budget and watch the context. | 13 | + | // that it can charge the output budget and watch the context, and it receives |
| 14 | - | type builtinFunc func(e *evaluator, args []Value) (Value, error) | 14 | + | // the call, not the evaluated arguments: like the runner's functions |
| 15 | + | // (Sdk/Functions/*.cs) it evaluates only the arguments it needs, in its own | |||
| 16 | + | // order, so an argument that is never reached cannot fail. | |||
| 17 | + | type builtinFunc func(e *evaluator, c *FunctionCall) (Value, error) | |||
| 15 | 18 | ||||
| 16 | // builtinImpls are the implementations of the built-in functions, keyed by | 19 | // builtinImpls are the implementations of the built-in functions, keyed by | ||
| 17 | - | // lower-cased name. Arity is validated by the parser, so each may assume its | 20 | + | // upper-cased name (see foldKey). Arity is validated by the parser, so each |
| 18 | - | // documented argument count. | 21 | + | // may assume its documented argument count. |
| 19 | - | var builtinImpls = map[string]builtinFunc{ | 22 | + | var builtinImpls map[string]builtinFunc |
| 20 | - | "CASE": funcCase, | 23 | + | |
| 21 | - | "CONTAINS": funcContains, | 24 | + | // The map is filled in init: the implementations call back into the evaluator, |
| 22 | - | "ENDSWITH": funcEndsWith, | 25 | + | // which would make a package-level initializer cyclic. |
| 23 | - | "FORMAT": funcFormat, | 26 | + | func init() { |
| 24 | - | "FROMJSON": funcFromJSON, | 27 | + | builtinImpls = map[string]builtinFunc{ |
| 25 | - | "JOIN": funcJoin, | 28 | + | "CASE": funcCase, |
| 26 | - | "STARTSWITH": funcStartsWith, | 29 | + | "CONTAINS": funcContains, |
| 27 | - | "TOJSON": funcToJSON, | 30 | + | "ENDSWITH": funcEndsWith, |
| 31 | + | "FORMAT": funcFormat, | |||
| 32 | + | "FROMJSON": funcFromJSON, | |||
| 33 | + | "JOIN": funcJoin, | |||
| 34 | + | "STARTSWITH": funcStartsWith, | |||
| 35 | + | "TOJSON": funcToJSON, | |||
| 36 | + | } | |||
| 28 | } | 37 | } | ||
| 29 | 38 | ||||
| 30 | - | // funcCase is case(pred1, val1, pred2, val2, ..., default). | 39 | + | // funcCase is case(pred1, val1, pred2, val2, ..., default): the predicates are |
| 31 | - | // | 40 | + | // evaluated in order up to the first true one, then only its value (Case.cs). |
| 32 | - | // The JavaScript reference ships it behind an experimental feature flag | 41 | + | // A predicate that is not a boolean is an error. The JavaScript reference |
| 33 | - | // (allowCaseFunction) that only affects editor completion; the parser always | 42 | + | // ships case behind an experimental feature flag that only affects editor |
| 34 | - | // accepts it, and so does this package. | 43 | + | // completion; the parser always accepts it, and so does the runner. |
| 35 | - | func funcCase(e *evaluator, args []Value) (Value, error) { | 44 | + | func funcCase(e *evaluator, c *FunctionCall) (Value, error) { |
| 36 | - | for i := 0; i < len(args)-1; i += 2 { | 45 | + | n := len(c.Args) |
| 37 | - | pred, ok := normalize(args[i]).(Boolean) | 46 | + | if n%2 == 0 { |
| 47 | + | return nil, evalErrorf("case requires an odd number of arguments") | |||
| 48 | + | } | |||
| 49 | + | for i := 0; i < n-1; i += 2 { | |||
| 50 | + | p, err := e.eval(c.Args[i]) | |||
| 51 | + | if err != nil { | |||
| 52 | + | return nil, err | |||
| 53 | + | } | |||
| 54 | + | pred, ok := normalize(p).(Boolean) | |||
| 38 | if !ok { | 55 | if !ok { | ||
| 39 | return nil, evalErrorf("case predicate must evaluate to a boolean value") | 56 | return nil, evalErrorf("case predicate must evaluate to a boolean value") | ||
| 40 | } | 57 | } | ||
| 41 | if pred { | 58 | if pred { | ||
| 42 | - | return args[i+1], nil | 59 | + | return e.eval(c.Args[i+1]) |
| 43 | } | 60 | } | ||
| 44 | } | 61 | } | ||
| 45 | - | return args[len(args)-1], nil | 62 | + | return e.eval(c.Args[n-1]) |
| 46 | } | 63 | } | ||
| 47 | 64 | ||||
| 48 | - | // funcContains is contains(search, item): substring test, case-insensitive | 65 | + | // funcContains is contains(search, item) (Contains.cs): when search is a |
| 49 | - | // (OrdinalIgnoreCase), when both are primitive; membership by == in an array; | 66 | + | // primitive and so is item, a case-insensitive (OrdinalIgnoreCase) substring |
| 50 | - | // false otherwise. | 67 | + | // test on their string forms; when search is a non-empty array, == against each |
| 51 | - | func funcContains(e *evaluator, args []Value) (Value, error) { | 68 | + | // element; false otherwise. item is only evaluated when it can matter. |
| 52 | - | left, right := normalize(args[0]), normalize(args[1]) | 69 | + | func funcContains(e *evaluator, c *FunctionCall) (Value, error) { |
| 70 | + | l, err := e.eval(c.Args[0]) | |||
| 71 | + | if err != nil { | |||
| 72 | + | return nil, err | |||
| 73 | + | } | |||
| 74 | + | left := normalize(l) | |||
| 53 | switch { | 75 | switch { | ||
| 54 | case isPrimitive(left): | 76 | case isPrimitive(left): | ||
| 55 | - | if isPrimitive(right) { | 77 | + | r, err := e.eval(c.Args[1]) |
| 78 | + | if err != nil { | |||
| 79 | + | return nil, err | |||
| 80 | + | } | |||
| 81 | + | if right := normalize(r); isPrimitive(right) { | |||
| 56 | return Boolean(jsIncludes(ordinalFold(CoerceString(left)), ordinalFold(CoerceString(right)))), nil | 82 | return Boolean(jsIncludes(ordinalFold(CoerceString(left)), ordinalFold(CoerceString(right)))), nil | ||
| 57 | } | 83 | } | ||
| 58 | - | case left.Kind() == KindArray: | 84 | + | case left.Kind() == KindArray && len(left.(*Array).items) > 0: |
| 85 | + | right, err := e.eval(c.Args[1]) | |||
| 86 | + | if err != nil { | |||
| 87 | + | return nil, err | |||
| 88 | + | } | |||
| 59 | for _, v := range left.(*Array).items { | 89 | for _, v := range left.(*Array).items { | ||
| 60 | if err := e.tick(); err != nil { | 90 | if err := e.tick(); err != nil { | ||
| 61 | return nil, err | 91 | return nil, err | ||
| @@ -68,40 +98,57 @@ func funcContains(e *evaluator, args []Value) (Value, error) { | |||||
| 68 | return Boolean(false), nil | 98 | return Boolean(false), nil | ||
| 69 | } | 99 | } | ||
| 70 | 100 | ||||
| 71 | - | func funcStartsWith(e *evaluator, args []Value) (Value, error) { | 101 | + | func funcStartsWith(e *evaluator, c *FunctionCall) (Value, error) { |
| 72 | - | ls, rs, ok := primitivePair(args) | 102 | + | ls, rs, ok, err := e.primitivePair(c) |
| 73 | - | return Boolean(ok && jsHasPrefix(ordinalFold(ls), ordinalFold(rs))), nil | 103 | + | return Boolean(ok && jsHasPrefix(ordinalFold(ls), ordinalFold(rs))), err |
| 74 | } | 104 | } | ||
| 75 | 105 | ||||
| 76 | - | func funcEndsWith(e *evaluator, args []Value) (Value, error) { | 106 | + | func funcEndsWith(e *evaluator, c *FunctionCall) (Value, error) { |
| 77 | - | ls, rs, ok := primitivePair(args) | 107 | + | ls, rs, ok, err := e.primitivePair(c) |
| 78 | - | return Boolean(ok && jsHasSuffix(ordinalFold(ls), ordinalFold(rs))), nil | 108 | + | return Boolean(ok && jsHasSuffix(ordinalFold(ls), ordinalFold(rs))), err |
| 79 | } | 109 | } | ||
| 80 | 110 | ||||
| 81 | - | // primitivePair returns the string forms of the two arguments, and false | 111 | + | // primitivePair evaluates the two arguments, the second only if the first is |
| 82 | - | // when either is an array or a dictionary. | 112 | + | // primitive, and returns their string forms; ok is false when either is an |
| 83 | - | func primitivePair(args []Value) (string, string, bool) { | 113 | + | // array or a dictionary. |
| 84 | - | if !isPrimitive(args[0]) || !isPrimitive(args[1]) { | 114 | + | func (e *evaluator) primitivePair(c *FunctionCall) (ls, rs string, ok bool, err error) { |
| 85 | - | return "", "", false | 115 | + | l, err := e.eval(c.Args[0]) |
| 116 | + | if err != nil || !isPrimitive(l) { | |||
| 117 | + | return "", "", false, err | |||
| 86 | } | 118 | } | ||
| 87 | - | return CoerceString(args[0]), CoerceString(args[1]), true | 119 | + | r, err := e.eval(c.Args[1]) |
| 120 | + | if err != nil || !isPrimitive(r) { | |||
| 121 | + | return "", "", false, err | |||
| 122 | + | } | |||
| 123 | + | return CoerceString(l), CoerceString(r), true, nil | |||
| 88 | } | 124 | } | ||
| 89 | 125 | ||||
| 90 | - | // funcJoin is join(array, separator). A primitive first argument is just | 126 | + | // funcJoin is join(array, separator) (Join.cs). A primitive first argument is |
| 91 | - | // converted to a string, ignoring the separator; a dictionary gives "". A | 127 | + | // just converted to a string, ignoring the separator; a dictionary or an empty |
| 92 | - | // separator that is not primitive means ",". | 128 | + | // array gives "". The separator is evaluated only when there are at least two |
| 93 | - | func funcJoin(e *evaluator, args []Value) (Value, error) { | 129 | + | // items to separate, and a separator that is not primitive means ",". |
| 94 | - | first := normalize(args[0]) | 130 | + | func funcJoin(e *evaluator, c *FunctionCall) (Value, error) { |
| 131 | + | f, err := e.eval(c.Args[0]) | |||
| 132 | + | if err != nil { | |||
| 133 | + | return nil, err | |||
| 134 | + | } | |||
| 135 | + | first := normalize(f) | |||
| 95 | if isPrimitive(first) { | 136 | if isPrimitive(first) { | ||
| 96 | return String(CoerceString(first)), nil | 137 | return String(CoerceString(first)), nil | ||
| 97 | } | 138 | } | ||
| 98 | arr, ok := first.(*Array) | 139 | arr, ok := first.(*Array) | ||
| 99 | - | if !ok { | 140 | + | if !ok || len(arr.items) == 0 { |
| 100 | return String(""), nil | 141 | return String(""), nil | ||
| 101 | } | 142 | } | ||
| 102 | sep := "," | 143 | sep := "," | ||
| 103 | - | if len(args) > 1 && isPrimitive(args[1]) { | 144 | + | if len(arr.items) > 1 && len(c.Args) > 1 { |
| 104 | - | sep = CoerceString(args[1]) | 145 | + | sv, err := e.eval(c.Args[1]) |
| 146 | + | if err != nil { | |||
| 147 | + | return nil, err | |||
| 148 | + | } | |||
| 149 | + | if isPrimitive(sv) { | |||
| 150 | + | sep = CoerceString(sv) | |||
| 151 | + | } | |||
| 105 | } | 152 | } | ||
| 106 | var out strings.Builder | 153 | var out strings.Builder | ||
| 107 | for i, it := range arr.items { | 154 | for i, it := range arr.items { | ||
| @@ -121,17 +168,21 @@ func funcJoin(e *evaluator, args []Value) (Value, error) { | |||||
| 121 | } | 168 | } | ||
| 122 | out.WriteString(part) | 169 | out.WriteString(part) | ||
| 123 | } | 170 | } | ||
| 124 | - | return String(normalizeWTF8(out.String())), nil | 171 | + | return String(out.String()), nil |
| 125 | } | 172 | } | ||
| 126 | 173 | ||||
| 127 | // funcToJSON is toJSON(value). The output counts against the budget and is | 174 | // funcToJSON is toJSON(value). The output counts against the budget and is | ||
| 128 | // cut off as soon as it exceeds what is left. | 175 | // cut off as soon as it exceeds what is left. | ||
| 129 | - | func funcToJSON(e *evaluator, args []Value) (Value, error) { | 176 | + | func funcToJSON(e *evaluator, c *FunctionCall) (Value, error) { |
| 177 | + | v, err := e.eval(c.Args[0]) | |||
| 178 | + | if err != nil { | |||
| 179 | + | return nil, err | |||
| 180 | + | } | |||
| 130 | max := -1 | 181 | max := -1 | ||
| 131 | if e.limit >= 0 { | 182 | if e.limit >= 0 { | ||
| 132 | max = e.limit - e.used | 183 | max = e.limit - e.used | ||
| 133 | } | 184 | } | ||
| 134 | - | out, err := EncodeJSON(args[0], max) | 185 | + | out, err := EncodeJSON(v, max) |
| 135 | switch { | 186 | switch { | ||
| 136 | case errors.Is(err, errJSONTooBig): | 187 | case errors.Is(err, errJSONTooBig): | ||
| 137 | return nil, e.budgetError() | 188 | return nil, e.budgetError() | ||
| @@ -145,11 +196,15 @@ func funcToJSON(e *evaluator, args []Value) (Value, error) { | |||||
| 145 | } | 196 | } | ||
| 146 | 197 | ||||
| 147 | // funcFromJSON is fromJSON(value). Blank input is "empty input"; anything | 198 | // funcFromJSON is fromJSON(value). Blank input is "empty input"; anything | ||
| 148 | - | // that is not valid JSON is "Error parsing JSON when evaluating fromJson" | 199 | + | // that is not readable by Json.NET is "Error parsing JSON when evaluating |
| 149 | - | // wrapping the syntax error. | 200 | + | // fromJson" wrapping the reader's error. |
| 150 | - | func funcFromJSON(e *evaluator, args []Value) (Value, error) { | 201 | + | func funcFromJSON(e *evaluator, c *FunctionCall) (Value, error) { |
| 151 | - | in := CoerceString(args[0]) | 202 | + | a, err := e.eval(c.Args[0]) |
| 152 | - | if jsTrim(in) == "" { | 203 | + | if err != nil { |
| 204 | + | return nil, err | |||
| 205 | + | } | |||
| 206 | + | in := CoerceString(a) | |||
| 207 | + | if strings.TrimFunc(in, isNetSpace) == "" { | |||
| 153 | return nil, evalErrorf("empty input") | 208 | return nil, evalErrorf("empty input") | ||
| 154 | } | 209 | } | ||
| 155 | v, err := FromJSON([]byte(in)) | 210 | v, err := FromJSON([]byte(in)) | ||
| @@ -159,13 +214,39 @@ func funcFromJSON(e *evaluator, args []Value) (Value, error) { | |||||
| 159 | return v, nil | 214 | return v, nil | ||
| 160 | } | 215 | } | ||
| 161 | 216 | ||||
| 162 | - | // funcFormat is format(fmt, args...): "{N}" inserts argument N, "{{" and "}}" | 217 | + | // formatPiece is a segment of the result of format(): literal text, or the |
| 163 | - | // are literal braces. Anything between the digits of "{N" and the closing | 218 | + | // index of an argument (evaluated when the result is assembled) with the |
| 164 | - | // brace is ignored ("{0:x}" is "{0}"). Every other use of a brace is an | 219 | + | // format specifiers written after the colon. |
| 165 | - | // error, as is a reference to a missing argument. | 220 | + | type formatPiece struct { |
| 166 | - | func funcFormat(e *evaluator, args []Value) (Value, error) { | 221 | + | text string |
| 167 | - | fs := CoerceString(args[0]) | 222 | + | arg int |
| 168 | - | var out strings.Builder | 223 | + | specs string |
| 224 | + | isArg bool | |||
| 225 | + | } | |||
| 226 | + | ||||
| 227 | + | // funcFormat is format(fmt, args...) (Format.cs): "{N}" inserts argument N, | |||
| 228 | + | // "{{" and "}}" are literal braces. The format string is scanned completely | |||
| 229 | + | // first (so a syntax error or a reference to a missing argument is reported | |||
| 230 | + | // before any argument is evaluated), then the arguments are evaluated, once | |||
| 231 | + | // each, in the order they appear. N is at most 255 (a byte). Format specifiers | |||
| 232 | + | // ("{0:x}") are not supported for any value: they are an error. | |||
| 233 | + | func funcFormat(e *evaluator, c *FunctionCall) (Value, error) { | |||
| 234 | + | f, err := e.eval(c.Args[0]) | |||
| 235 | + | if err != nil { | |||
| 236 | + | return nil, err | |||
| 237 | + | } | |||
| 238 | + | fs := CoerceString(f) | |||
| 239 | + | var pieces []formatPiece | |||
| 240 | + | text := func(t string) error { | |||
| 241 | + | if t == "" { | |||
| 242 | + | return nil | |||
| 243 | + | } | |||
| 244 | + | if err := e.charge(utf16Len(t)); err != nil { | |||
| 245 | + | return err | |||
| 246 | + | } | |||
| 247 | + | pieces = append(pieces, formatPiece{text: t}) | |||
| 248 | + | return nil | |||
| 249 | + | } | |||
| 169 | index := 0 | 250 | index := 0 | ||
| 170 | for index < len(fs) { | 251 | for index < len(fs) { | ||
| 171 | lbrace := indexByteFrom(fs, '{', index) | 252 | lbrace := indexByteFrom(fs, '{', index) | ||
| @@ -174,27 +255,26 @@ func funcFormat(e *evaluator, args []Value) (Value, error) { | |||||
| 174 | case lbrace >= 0 && (rbrace < 0 || rbrace > lbrace): | 255 | case lbrace >= 0 && (rbrace < 0 || rbrace > lbrace): | ||
| 175 | // Escaped left brace. | 256 | // Escaped left brace. | ||
| 176 | if charAt(fs, lbrace+1) == '{' { | 257 | if charAt(fs, lbrace+1) == '{' { | ||
| 177 | - | if err := e.charge(lbrace + 1 - index); err != nil { | 258 | + | if err := text(fs[index : lbrace+1]); err != nil { |
| 178 | return nil, err | 259 | return nil, err | ||
| 179 | } | 260 | } | ||
| 180 | - | out.WriteString(fs[index : lbrace+1]) | |||
| 181 | index = lbrace + 2 | 261 | index = lbrace + 2 | ||
| 182 | continue | 262 | continue | ||
| 183 | } | 263 | } | ||
| 184 | // Left brace, number, optional format specifiers, right brace. | 264 | // Left brace, number, optional format specifiers, right brace. | ||
| 185 | if rbrace > lbrace+1 { | 265 | if rbrace > lbrace+1 { | ||
| 186 | - | if argIndex, ok := readArgIndex(fs, lbrace+1); ok { | 266 | + | if argIndex, end, ok := readArgIndex(fs, lbrace+1); ok { |
| 187 | - | if 1+argIndex > float64(len(args)-1) { | 267 | + | if specs, closing, ok := readFormatSpecifiers(fs, end+1); ok { |
| 188 | - | return nil, evalErrorf("The following format string references more arguments than were supplied: %s", fs) | 268 | + | if argIndex > len(c.Args)-2 { |
| 269 | + | return nil, evalErrorf("The following format string references more arguments than were supplied: %s", fs) | |||
| 270 | + | } | |||
| 271 | + | if err := text(fs[index:lbrace]); err != nil { | |||
| 272 | + | return nil, err | |||
| 273 | + | } | |||
| 274 | + | pieces = append(pieces, formatPiece{arg: argIndex, specs: specs, isArg: true}) | |||
| 275 | + | index = closing + 1 | |||
| 276 | + | continue | |||
| 189 | } | 277 | } | ||
| 190 | - | arg := CoerceString(args[1+int(argIndex)]) | |||
| 191 | - | if err := e.charge(lbrace - index + len(arg)); err != nil { | |||
| 192 | - | return nil, err | |||
| 193 | - | } | |||
| 194 | - | out.WriteString(fs[index:lbrace]) | |||
| 195 | - | out.WriteString(arg) | |||
| 196 | - | index = rbrace + 1 | |||
| 197 | - | continue | |||
| 198 | } | 278 | } | ||
| 199 | } | 279 | } | ||
| 200 | return nil, evalErrorf("The following format string is invalid: %s", fs) | 280 | return nil, evalErrorf("The following format string is invalid: %s", fs) | ||
| @@ -203,20 +283,50 @@ func funcFormat(e *evaluator, args []Value) (Value, error) { | |||||
| 203 | if charAt(fs, rbrace+1) != '}' { | 283 | if charAt(fs, rbrace+1) != '}' { | ||
| 204 | return nil, evalErrorf("The following format string is invalid: %s", fs) | 284 | return nil, evalErrorf("The following format string is invalid: %s", fs) | ||
| 205 | } | 285 | } | ||
| 206 | - | if err := e.charge(rbrace + 1 - index); err != nil { | 286 | + | if err := text(fs[index : rbrace+1]); err != nil { |
| 207 | return nil, err | 287 | return nil, err | ||
| 208 | } | 288 | } | ||
| 209 | - | out.WriteString(fs[index : rbrace+1]) | |||
| 210 | index = rbrace + 2 | 289 | index = rbrace + 2 | ||
| 211 | default: | 290 | default: | ||
| 212 | - | if err := e.charge(len(fs) - index); err != nil { | 291 | + | if err := text(fs[index:]); err != nil { |
| 213 | return nil, err | 292 | return nil, err | ||
| 214 | } | 293 | } | ||
| 215 | - | out.WriteString(fs[index:]) | |||
| 216 | index = len(fs) | 294 | index = len(fs) | ||
| 217 | } | 295 | } | ||
| 218 | } | 296 | } | ||
| 219 | - | return String(normalizeWTF8(out.String())), nil | 297 | + | // Assemble: arguments are evaluated lazily, once each, in the order of |
| 298 | + | // their first appearance. | |||
| 299 | + | type argValue struct { | |||
| 300 | + | str string | |||
| 301 | + | kind Kind | |||
| 302 | + | done bool | |||
| 303 | + | } | |||
| 304 | + | cache := make([]argValue, len(c.Args)-1) | |||
| 305 | + | var out strings.Builder | |||
| 306 | + | for _, p := range pieces { | |||
| 307 | + | if !p.isArg { | |||
| 308 | + | out.WriteString(p.text) | |||
| 309 | + | continue | |||
| 310 | + | } | |||
| 311 | + | av := &cache[p.arg] | |||
| 312 | + | if !av.done { | |||
| 313 | + | v, err := e.eval(c.Args[1+p.arg]) | |||
| 314 | + | if err != nil { | |||
| 315 | + | return nil, err | |||
| 316 | + | } | |||
| 317 | + | *av = argValue{str: CoerceString(v), kind: normalize(v).Kind(), done: true} | |||
| 318 | + | } | |||
| 319 | + | if p.specs != "" { | |||
| 320 | + | return nil, evalErrorf("The format specifiers '%s' are not valid for objects of type '%s'", p.specs, av.kind) | |||
| 321 | + | } | |||
| 322 | + | if av.str != "" { | |||
| 323 | + | if err := e.charge(utf16Len(av.str)); err != nil { | |||
| 324 | + | return nil, err | |||
| 325 | + | } | |||
| 326 | + | } | |||
| 327 | + | out.WriteString(av.str) | |||
| 328 | + | } | |||
| 329 | + | return String(out.String()), nil | |||
| 220 | } | 330 | } | ||
| 221 | 331 | ||||
| 222 | func indexByteFrom(s string, c byte, from int) int { | 332 | func indexByteFrom(s string, c byte, from int) int { | ||
| @@ -238,20 +348,65 @@ func charAt(s string, i int) byte { | |||||
| 238 | return 0 | 348 | return 0 | ||
| 239 | } | 349 | } | ||
| 240 | 350 | ||||
| 241 | - | // readArgIndex reads the decimal digits at s[start:]. The index is a float | 351 | + | // readArgIndex reads the digits at s[start:] as the index of a format |
| 242 | - | // because parseInt in the reference yields Infinity for absurdly long digit | 352 | + | // argument: a byte, so at most 255 (Byte.TryParse). It returns the index of the |
| 243 | - | // runs, which then fails the argument-count check. | 353 | + | // last digit. Digits are Unicode decimal digits for the scan but only ASCII |
| 244 | - | func readArgIndex(s string, start int) (float64, bool) { | 354 | + | // ones parse, so any other digit makes the format string invalid. |
| 355 | + | func readArgIndex(s string, start int) (value, end int, ok bool) { | |||
| 245 | n := 0 | 356 | n := 0 | ||
| 246 | - | for charAt(s, start+n) >= '0' && charAt(s, start+n) <= '9' { | 357 | + | for i := start; i < len(s); { |
| 247 | - | n++ | 358 | + | r, w := decodeRune(s[i:]) |
| 359 | + | if !unicode.IsDigit(r) { | |||
| 360 | + | break | |||
| 361 | + | } | |||
| 362 | + | i += w | |||
| 363 | + | n = i - start | |||
| 248 | } | 364 | } | ||
| 249 | if n == 0 { | 365 | if n == 0 { | ||
| 250 | - | return 0, false | 366 | + | return 0, 0, false |
| 367 | + | } | |||
| 368 | + | v, err := strconv.ParseUint(s[start:start+n], 10, 8) | |||
| 369 | + | if err != nil || !isASCII(s[start:start+n]) { | |||
| 370 | + | return 0, 0, false | |||
| 371 | + | } | |||
| 372 | + | return int(v), start + n - 1, true | |||
| 373 | + | } | |||
| 374 | + | ||||
| 375 | + | // readFormatSpecifiers reads what follows the digits of "{N": either the | |||
| 376 | + | // closing brace, or ":" and specifiers up to a closing brace, where "}}" is a | |||
| 377 | + | // literal brace inside the specifiers. It returns the specifiers and the index | |||
| 378 | + | // of the closing brace. | |||
| 379 | + | func readFormatSpecifiers(s string, start int) (specs string, closing int, ok bool) { | |||
| 380 | + | switch charAt(s, start) { | |||
| 381 | + | case '}': | |||
| 382 | + | return "", start, true | |||
| 383 | + | case ':': | |||
| 384 | + | default: | |||
| 385 | + | return "", 0, false | |||
| 251 | } | 386 | } | ||
| 252 | - | f, _ := strconv.ParseFloat(s[start:start+n], 64) | 387 | + | var b strings.Builder |
| 253 | - | if math.IsNaN(f) { | 388 | + | for i := start + 1; i < len(s); { |
| 254 | - | return 0, false | 389 | + | if s[i] != '}' { |
| 390 | + | b.WriteByte(s[i]) | |||
| 391 | + | i++ | |||
| 392 | + | } else if charAt(s, i+1) == '}' { | |||
| 393 | + | b.WriteByte('}') | |||
| 394 | + | i += 2 | |||
| 395 | + | } else { | |||
| 396 | + | return b.String(), i, true | |||
| 397 | + | } | |||
| 398 | + | } | |||
| 399 | + | return "", 0, false | |||
| 400 | + | } | |||
| 401 | + | ||||
| 402 | + | // utf16Len is the length of s in UTF-16 code units, the length of a .NET string. | |||
| 403 | + | func utf16Len(s string) int { | |||
| 404 | + | n := 0 | |||
| 405 | + | for _, r := range s { | |||
| 406 | + | n++ | |||
| 407 | + | if r >= 0x10000 { | |||
| 408 | + | n++ | |||
| 409 | + | } | |||
| 255 | } | 410 | } | ||
| 256 | - | return f, true | 411 | + | return n |
| 257 | } | 412 | } | ||