docs(expr): list what remains to check against the C# runner
- commit
- 879809200fe4e7a5f2e644616ff27058ce7b4f17
- author
- brice · · 2026-10-12 14:46 UTC
- parent
- c82ace7
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +153 -0
A expr/PORTING.md +153 -0
| @@ -0,0 +1,153 @@ | |||
| 1 | + | # Porting notes for package expr | |
| 2 | + | ||
| 3 | + | The package is a port of `@actions/expressions` 0.3.61 (MIT, GitHub, | |
| 4 | + | actions/languageservices). The JavaScript is the specification wherever this | |
| 5 | + | package could be checked against it: `testdata/oracle.json.gz` holds what the | |
| 6 | + | reference answers for 33 575 expressions (tokens with positions, error | |
| 7 | + | messages, result kinds, string coercions, `toJSON` output), and | |
| 8 | + | `TestOracle` replays it. `gen/oracle.mjs` regenerates it, `gen/golden.mjs` the | |
| 9 | + | `testdata/tojson` goldens, and `gen/casing_dump.mjs` + `gen/casing.go` the | |
| 10 | + | casing tables (Node 25 / V8 14.1, Unicode 16; Go 1.27 ships Unicode 15). | |
| 11 | + | ||
| 12 | + | The C# runner (actions/runner, `DistributedTask/Expressions2`) is the | |
| 13 | + | implementation that runs on github.com. It was not available while this was | |
| 14 | + | written. Nothing below has been checked against it. | |
| 15 | + | ||
| 16 | + | ## Where the JavaScript is believed to differ from the C# runner | |
| 17 | + | ||
| 18 | + | Each item is implemented as the JavaScript does. Check the runner's tests | |
| 19 | + | (`src/Test/L0/Expressions*`, `Sdk/Expressions2/...`) and decide for each one | |
| 20 | + | whether to keep the JS behaviour or follow the runner. | |
| 21 | + | ||
| 22 | + | - [ ] **String escapes: only the first `''` is unescaped.** The lexer does | |
| 23 | + | `value.replace("''", "'")` with a string pattern, which replaces one | |
| 24 | + | occurrence: `'a''b''c'` is `a'b''c`. The C# lexer should unescape every | |
| 25 | + | pair (`StringBuilder`/`Replace`). Probable JS bug. (`lexer.go`, | |
| 26 | + | `consumeString`; tests: `TestEvalLiteralsAndOperators`.) | |
| 27 | + | - [ ] **`case()` availability.** The JS gates it behind the experimental flag | |
| 28 | + | `allowCaseFunction`, but only in completion; the parser and the | |
| 29 | + | evaluator always accept it. Here it is always on. Confirm whether the | |
| 30 | + | hosted service accepts `case` and with which arity rules (the JS: at | |
| 31 | + | least 3 arguments, an odd number, predicates must be booleans). | |
| 32 | + | - [ ] **`format()`**: the reference uses `parseInt` on the digits, so | |
| 33 | + | `{99999999999999999999}` is an Infinity-like index and reports "more | |
| 34 | + | arguments than were supplied". The `{0:x}` form silently ignores the | |
| 35 | + | specifier; C# may throw. The error texts are the JS ones. Check the C# | |
| 36 | + | `Format` node for the exact messages and for `{N}` with `N` > 255 or | |
| 37 | + | leading zeros. | |
| 38 | + | - [ ] **`contains()` lower-cases (`toLowerCase`), `startsWith`/`endsWith` | |
| 39 | + | upper-case (`toUpperSpecial`).** The C# uses `OrdinalIgnoreCase` | |
| 40 | + | comparisons, which differ for `ß`, `İ`, `ı`, the Kelvin sign and final | |
| 41 | + | sigma. `contains('ß', 'ss')` is false in JS; `startsWith('ß', 'ss')` is | |
| 42 | + | true. Check what the runner does. | |
| 43 | + | - [ ] **`==` on strings uses `toUpperSpecial`** (upper-case with the dotless | |
| 44 | + | `ı` left alone, full Unicode mapping so `ß == 'SS'`), and `<`/`>` compare | |
| 45 | + | the upper-cased UTF-16 code units. C# `OrdinalIgnoreCase` upper-cases | |
| 46 | + | per UTF-16 code unit with simple mappings only: `ß == 'SS'` should be | |
| 47 | + | false there. | |
| 48 | + | - [ ] **Number to string (`CoerceString`)**: `(+x.toFixed(15)).toString()`. | |
| 49 | + | C# formats a `double` with `"G15"`-like rules. They agree for ordinary | |
| 50 | + | values; check very small and very large numbers (`1e-7` here is `1e-7`, | |
| 51 | + | `1e21` is `1e+21`), `-0`, and 16-17 significant digit values. | |
| 52 | + | - [ ] **String to number**: `Number()` semantics (hex, octal, binary, | |
| 53 | + | `Infinity`, whitespace trimmed, empty is 0). C# `double.TryParse` with | |
| 54 | + | `NumberStyles` differs (no octal/binary; check `0x` and `Infinity`). | |
| 55 | + | The lexer applies the same conversion to number literals, so `0x10`, | |
| 56 | + | `0b11`, `1e3`, `.5`, `5.` and `+1` are all literals here. | |
| 57 | + | - [ ] **Number literals after `-` / `+`**: `-1` is one literal token; `a -1` | |
| 58 | + | is therefore a parse error, as in the JS. | |
| 59 | + | - [ ] **Object key order from `fromJSON`**: array-index keys first, ascending | |
| 60 | + | (JavaScript `Object.keys`), then insertion order. `Json.NET` keeps | |
| 61 | + | document order. The same ordering is applied by `toJSON` (the JS | |
| 62 | + | replacer builds a plain object). Check against the runner for | |
| 63 | + | `toJSON(fromJSON('{"b":1,"1":2}'))`. | |
| 64 | + | - [ ] **Duplicate JSON keys**: last value wins at the first position (JS); | |
| 65 | + | keys that differ only by case collapse to the first. | |
| 66 | + | - [ ] **`fromJSON` errors**: the JS message is | |
| 67 | + | `Error parsing JSON when evaluating fromJson` with the V8 `SyntaxError` | |
| 68 | + | as cause; here the cause is a Go error with similar but not identical | |
| 69 | + | wording (only the outer message is stable). `empty input` for blank | |
| 70 | + | input. C# messages differ; check. | |
| 71 | + | - [ ] **`toJSON`**: `JSON.stringify(v, replacer, " ")`: two-space indent, | |
| 72 | + | `[]` and `{}` for empties, `NaN`/`Infinity` as `null`, `-0` as `0`, | |
| 73 | + | no escaping of `<`, `>`, `&`, U+2028/U+2029; lone surrogates as | |
| 74 | + | `\udXXX`. Json.NET differs on some of these (`Infinity`, `-0`, escapes). | |
| 75 | + | - [ ] **Index access**: `a[b]` where `b` is a number uses `floor(b)` and | |
| 76 | + | rejects NaN, negatives and infinities; where `b` is a string it is a key | |
| 77 | + | or a decimal index (`'1'`, `' 1 '`, `'1.9'` all index 1 of an array). | |
| 78 | + | Booleans and null index as `1`/`0` on arrays and as `"true"`/`""` keys on | |
| 79 | + | dictionaries. | |
| 80 | + | - [ ] **Filtered arrays** (`a.*`): a dictionary filtered by `*` yields its | |
| 81 | + | values; `.*.b` skips elements without `b`; `.*.*` flattens one level; | |
| 82 | + | arrays produced by `*` are fresh, so `a.* == a.*` is false. | |
| 83 | + | - [ ] **Comparison**: `null` is only ordered against numbers/booleans (it | |
| 84 | + | becomes 0); arrays and dictionaries are never ordered and `==` is | |
| 85 | + | identity; `NaN` is unordered and unequal to itself; `true > false`. | |
| 86 | + | - [ ] **`&&`/`||` return an operand**, not a boolean (`'' || 'x'` is `'x'`). | |
| 87 | + | - [ ] **Limits**: max expression length 21000 (UTF-16 units; the JS throws a | |
| 88 | + | bare `Error("ErrorExceededMaxLength")`), max parse depth 50. The depth | |
| 89 | + | counting (including `index()` and the flat logical chains) is ported | |
| 90 | + | literally; C# `MaxDepth` is 50 with its own counting. Cross-check with | |
| 91 | + | deep-nesting tests. There is no node-count or evaluation-memory limit | |
| 92 | + | here; the C# runner has `MaxMemory` (1 MB) and a result-size limit. | |
| 93 | + | - [ ] **Trailing comma in calls**: `contains('a', 'b',)` parses (the JS loop | |
| 94 | + | only requires a comma when the next token is not `)`). `f(,)` does not. | |
| 95 | + | - [ ] **Whitespace**: only space, `\t`, `\r`, `\n` separate tokens. U+00A0 and | |
| 96 | + | other Unicode spaces end a word but are not skipped, so `1 \u00a0 2` | |
| 97 | + | reports an unexpected symbol. `\v`, `\f` likewise. | |
| 98 | + | - [ ] **Empty expression**: the JS parser returns `undefined` for no tokens. | |
| 99 | + | `Parse` returns a nil Node and no error; `Evaluate(nil)` errors. | |
| 100 | + | - [ ] **Unknown contexts at evaluation time** are null (the JS `Dictionary.get` | |
| 101 | + | returns `undefined` and `visitContextAccess` then crashes later); here | |
| 102 | + | declared-but-absent contexts are null, never a crash. | |
| 103 | + | - [ ] **Error positions**: the JS EOF token's start column is negative after a | |
| 104 | + | trailing newline (stale `start`); ported as is | |
| 105 | + | (`TestEOFTokenPosition`). | |
| 106 | + | - [ ] **Function names and context names are case-insensitive**; the JS | |
| 107 | + | lower-cases with `toLowerCase`, as does this port (`foldKey`). Extension | |
| 108 | + | functions override built-ins at evaluation (`functions?.get(...) || | |
| 109 | + | wellKnownFunctions[...]`) but built-ins win at validation time (arity). | |
| 110 | + | ||
| 111 | + | ## JavaScript behaviours deliberately NOT ported | |
| 112 | + | ||
| 113 | + | These are accidents of JavaScript objects used as maps; porting them would be | |
| 114 | + | a bug. | |
| 115 | + | ||
| 116 | + | - `Dictionary.add` tests `key.toLowerCase() in this.indexMap` on a plain | |
| 117 | + | object, so keys that collide with `Object.prototype` properties | |
| 118 | + | (`constructor`, `__proto__`, `valueOf`, ...) are silently dropped or hidden. | |
| 119 | + | Here every key is stored. | |
| 120 | + | - `wellKnownFunctions[name]` is a plain-object lookup, so `constructor(1)`, | |
| 121 | + | `toString(1)` and `hasOwnProperty(1)` pass validation or crash with a | |
| 122 | + | `TypeError` instead of "Unrecognized function". Here they are "Unrecognized | |
| 123 | + | function". | |
| 124 | + | - A `TypeError` in the JS (`Cannot read properties of undefined`) when | |
| 125 | + | evaluating e.g. `github.true` with an empty context: the reference does not | |
| 126 | + | handle a missing root context. Here it is null. | |
| 127 | + | - Recursion limits: JS recursion depth for `fromJSON` input is engine | |
| 128 | + | dependent (about 5 000 to 10 000 levels). `FromJSON` accepts 10 000 levels. | |
| 129 | + | ||
| 130 | + | ## Other things to check once the runner is available | |
| 131 | + | ||
| 132 | + | - [ ] Run the runner's expression test vectors (`ExpressionParserL0`, | |
| 133 | + | `ExpressionTests`, `ExpressionValuesL0`, `ExpressionTestUtil`) through a | |
| 134 | + | table adapter; list every disagreement here before deciding. | |
| 135 | + | - [ ] Decide whether `EvalContext` needs a hook for the functions the runner | |
| 136 | + | implements natively with special semantics: `success()`, `failure()`, | |
| 137 | + | `always()`, `cancelled()` (take no arguments; read job status) and | |
| 138 | + | `hashFiles()` (variadic glob patterns, string result). The API supports | |
| 139 | + | them through `FuncInfo` + `Func` without changes; arity is declared by | |
| 140 | + | the caller. | |
| 141 | + | - [ ] Context names the runner allows per location (`github`, `env`, `vars`, | |
| 142 | + | `job`, `jobs`, `steps`, `runner`, `secrets`, `strategy`, `matrix`, | |
| 143 | + | `needs`, `inputs`) and which functions each location may call | |
| 144 | + | (`success`... only in `if`/`jobs.*.if`...). This belongs to the template | |
| 145 | + | layer, not to package expr. | |
| 146 | + | - [ ] `format()` argument limit is 255 in the JS (`MAX_ARGUMENTS`); the C# | |
| 147 | + | limit is `ExpressionConstants.MaxArguments` (check). | |
| 148 | + | - [ ] Case-insensitive comparison and `contains`: if C# `OrdinalIgnoreCase` | |
| 149 | + | is the truth, replace `toUpperSpecial`/`jsToLower` by per-UTF-16-unit | |
| 150 | + | simple upper-casing and regenerate the oracle tests accordingly. | |
| 151 | + | - [ ] Secret masking and the `${{ }}` splitting of a larger string are not | |
| 152 | + | part of this package (they belong to the template layer): `Parse` | |
| 153 | + | takes the text between the braces only. | |
| @@ -0,0 +1,153 @@ | |||||
| 1 | + | # Porting notes for package expr | |||
| 2 | + | ||||
| 3 | + | The package is a port of `@actions/expressions` 0.3.61 (MIT, GitHub, | |||
| 4 | + | actions/languageservices). The JavaScript is the specification wherever this | |||
| 5 | + | package could be checked against it: `testdata/oracle.json.gz` holds what the | |||
| 6 | + | reference answers for 33 575 expressions (tokens with positions, error | |||
| 7 | + | messages, result kinds, string coercions, `toJSON` output), and | |||
| 8 | + | `TestOracle` replays it. `gen/oracle.mjs` regenerates it, `gen/golden.mjs` the | |||
| 9 | + | `testdata/tojson` goldens, and `gen/casing_dump.mjs` + `gen/casing.go` the | |||
| 10 | + | casing tables (Node 25 / V8 14.1, Unicode 16; Go 1.27 ships Unicode 15). | |||
| 11 | + | ||||
| 12 | + | The C# runner (actions/runner, `DistributedTask/Expressions2`) is the | |||
| 13 | + | implementation that runs on github.com. It was not available while this was | |||
| 14 | + | written. Nothing below has been checked against it. | |||
| 15 | + | ||||
| 16 | + | ## Where the JavaScript is believed to differ from the C# runner | |||
| 17 | + | ||||
| 18 | + | Each item is implemented as the JavaScript does. Check the runner's tests | |||
| 19 | + | (`src/Test/L0/Expressions*`, `Sdk/Expressions2/...`) and decide for each one | |||
| 20 | + | whether to keep the JS behaviour or follow the runner. | |||
| 21 | + | ||||
| 22 | + | - [ ] **String escapes: only the first `''` is unescaped.** The lexer does | |||
| 23 | + | `value.replace("''", "'")` with a string pattern, which replaces one | |||
| 24 | + | occurrence: `'a''b''c'` is `a'b''c`. The C# lexer should unescape every | |||
| 25 | + | pair (`StringBuilder`/`Replace`). Probable JS bug. (`lexer.go`, | |||
| 26 | + | `consumeString`; tests: `TestEvalLiteralsAndOperators`.) | |||
| 27 | + | - [ ] **`case()` availability.** The JS gates it behind the experimental flag | |||
| 28 | + | `allowCaseFunction`, but only in completion; the parser and the | |||
| 29 | + | evaluator always accept it. Here it is always on. Confirm whether the | |||
| 30 | + | hosted service accepts `case` and with which arity rules (the JS: at | |||
| 31 | + | least 3 arguments, an odd number, predicates must be booleans). | |||
| 32 | + | - [ ] **`format()`**: the reference uses `parseInt` on the digits, so | |||
| 33 | + | `{99999999999999999999}` is an Infinity-like index and reports "more | |||
| 34 | + | arguments than were supplied". The `{0:x}` form silently ignores the | |||
| 35 | + | specifier; C# may throw. The error texts are the JS ones. Check the C# | |||
| 36 | + | `Format` node for the exact messages and for `{N}` with `N` > 255 or | |||
| 37 | + | leading zeros. | |||
| 38 | + | - [ ] **`contains()` lower-cases (`toLowerCase`), `startsWith`/`endsWith` | |||
| 39 | + | upper-case (`toUpperSpecial`).** The C# uses `OrdinalIgnoreCase` | |||
| 40 | + | comparisons, which differ for `ß`, `İ`, `ı`, the Kelvin sign and final | |||
| 41 | + | sigma. `contains('ß', 'ss')` is false in JS; `startsWith('ß', 'ss')` is | |||
| 42 | + | true. Check what the runner does. | |||
| 43 | + | - [ ] **`==` on strings uses `toUpperSpecial`** (upper-case with the dotless | |||
| 44 | + | `ı` left alone, full Unicode mapping so `ß == 'SS'`), and `<`/`>` compare | |||
| 45 | + | the upper-cased UTF-16 code units. C# `OrdinalIgnoreCase` upper-cases | |||
| 46 | + | per UTF-16 code unit with simple mappings only: `ß == 'SS'` should be | |||
| 47 | + | false there. | |||
| 48 | + | - [ ] **Number to string (`CoerceString`)**: `(+x.toFixed(15)).toString()`. | |||
| 49 | + | C# formats a `double` with `"G15"`-like rules. They agree for ordinary | |||
| 50 | + | values; check very small and very large numbers (`1e-7` here is `1e-7`, | |||
| 51 | + | `1e21` is `1e+21`), `-0`, and 16-17 significant digit values. | |||
| 52 | + | - [ ] **String to number**: `Number()` semantics (hex, octal, binary, | |||
| 53 | + | `Infinity`, whitespace trimmed, empty is 0). C# `double.TryParse` with | |||
| 54 | + | `NumberStyles` differs (no octal/binary; check `0x` and `Infinity`). | |||
| 55 | + | The lexer applies the same conversion to number literals, so `0x10`, | |||
| 56 | + | `0b11`, `1e3`, `.5`, `5.` and `+1` are all literals here. | |||
| 57 | + | - [ ] **Number literals after `-` / `+`**: `-1` is one literal token; `a -1` | |||
| 58 | + | is therefore a parse error, as in the JS. | |||
| 59 | + | - [ ] **Object key order from `fromJSON`**: array-index keys first, ascending | |||
| 60 | + | (JavaScript `Object.keys`), then insertion order. `Json.NET` keeps | |||
| 61 | + | document order. The same ordering is applied by `toJSON` (the JS | |||
| 62 | + | replacer builds a plain object). Check against the runner for | |||
| 63 | + | `toJSON(fromJSON('{"b":1,"1":2}'))`. | |||
| 64 | + | - [ ] **Duplicate JSON keys**: last value wins at the first position (JS); | |||
| 65 | + | keys that differ only by case collapse to the first. | |||
| 66 | + | - [ ] **`fromJSON` errors**: the JS message is | |||
| 67 | + | `Error parsing JSON when evaluating fromJson` with the V8 `SyntaxError` | |||
| 68 | + | as cause; here the cause is a Go error with similar but not identical | |||
| 69 | + | wording (only the outer message is stable). `empty input` for blank | |||
| 70 | + | input. C# messages differ; check. | |||
| 71 | + | - [ ] **`toJSON`**: `JSON.stringify(v, replacer, " ")`: two-space indent, | |||
| 72 | + | `[]` and `{}` for empties, `NaN`/`Infinity` as `null`, `-0` as `0`, | |||
| 73 | + | no escaping of `<`, `>`, `&`, U+2028/U+2029; lone surrogates as | |||
| 74 | + | `\udXXX`. Json.NET differs on some of these (`Infinity`, `-0`, escapes). | |||
| 75 | + | - [ ] **Index access**: `a[b]` where `b` is a number uses `floor(b)` and | |||
| 76 | + | rejects NaN, negatives and infinities; where `b` is a string it is a key | |||
| 77 | + | or a decimal index (`'1'`, `' 1 '`, `'1.9'` all index 1 of an array). | |||
| 78 | + | Booleans and null index as `1`/`0` on arrays and as `"true"`/`""` keys on | |||
| 79 | + | dictionaries. | |||
| 80 | + | - [ ] **Filtered arrays** (`a.*`): a dictionary filtered by `*` yields its | |||
| 81 | + | values; `.*.b` skips elements without `b`; `.*.*` flattens one level; | |||
| 82 | + | arrays produced by `*` are fresh, so `a.* == a.*` is false. | |||
| 83 | + | - [ ] **Comparison**: `null` is only ordered against numbers/booleans (it | |||
| 84 | + | becomes 0); arrays and dictionaries are never ordered and `==` is | |||
| 85 | + | identity; `NaN` is unordered and unequal to itself; `true > false`. | |||
| 86 | + | - [ ] **`&&`/`||` return an operand**, not a boolean (`'' || 'x'` is `'x'`). | |||
| 87 | + | - [ ] **Limits**: max expression length 21000 (UTF-16 units; the JS throws a | |||
| 88 | + | bare `Error("ErrorExceededMaxLength")`), max parse depth 50. The depth | |||
| 89 | + | counting (including `index()` and the flat logical chains) is ported | |||
| 90 | + | literally; C# `MaxDepth` is 50 with its own counting. Cross-check with | |||
| 91 | + | deep-nesting tests. There is no node-count or evaluation-memory limit | |||
| 92 | + | here; the C# runner has `MaxMemory` (1 MB) and a result-size limit. | |||
| 93 | + | - [ ] **Trailing comma in calls**: `contains('a', 'b',)` parses (the JS loop | |||
| 94 | + | only requires a comma when the next token is not `)`). `f(,)` does not. | |||
| 95 | + | - [ ] **Whitespace**: only space, `\t`, `\r`, `\n` separate tokens. U+00A0 and | |||
| 96 | + | other Unicode spaces end a word but are not skipped, so `1 \u00a0 2` | |||
| 97 | + | reports an unexpected symbol. `\v`, `\f` likewise. | |||
| 98 | + | - [ ] **Empty expression**: the JS parser returns `undefined` for no tokens. | |||
| 99 | + | `Parse` returns a nil Node and no error; `Evaluate(nil)` errors. | |||
| 100 | + | - [ ] **Unknown contexts at evaluation time** are null (the JS `Dictionary.get` | |||
| 101 | + | returns `undefined` and `visitContextAccess` then crashes later); here | |||
| 102 | + | declared-but-absent contexts are null, never a crash. | |||
| 103 | + | - [ ] **Error positions**: the JS EOF token's start column is negative after a | |||
| 104 | + | trailing newline (stale `start`); ported as is | |||
| 105 | + | (`TestEOFTokenPosition`). | |||
| 106 | + | - [ ] **Function names and context names are case-insensitive**; the JS | |||
| 107 | + | lower-cases with `toLowerCase`, as does this port (`foldKey`). Extension | |||
| 108 | + | functions override built-ins at evaluation (`functions?.get(...) || | |||
| 109 | + | wellKnownFunctions[...]`) but built-ins win at validation time (arity). | |||
| 110 | + | ||||
| 111 | + | ## JavaScript behaviours deliberately NOT ported | |||
| 112 | + | ||||
| 113 | + | These are accidents of JavaScript objects used as maps; porting them would be | |||
| 114 | + | a bug. | |||
| 115 | + | ||||
| 116 | + | - `Dictionary.add` tests `key.toLowerCase() in this.indexMap` on a plain | |||
| 117 | + | object, so keys that collide with `Object.prototype` properties | |||
| 118 | + | (`constructor`, `__proto__`, `valueOf`, ...) are silently dropped or hidden. | |||
| 119 | + | Here every key is stored. | |||
| 120 | + | - `wellKnownFunctions[name]` is a plain-object lookup, so `constructor(1)`, | |||
| 121 | + | `toString(1)` and `hasOwnProperty(1)` pass validation or crash with a | |||
| 122 | + | `TypeError` instead of "Unrecognized function". Here they are "Unrecognized | |||
| 123 | + | function". | |||
| 124 | + | - A `TypeError` in the JS (`Cannot read properties of undefined`) when | |||
| 125 | + | evaluating e.g. `github.true` with an empty context: the reference does not | |||
| 126 | + | handle a missing root context. Here it is null. | |||
| 127 | + | - Recursion limits: JS recursion depth for `fromJSON` input is engine | |||
| 128 | + | dependent (about 5 000 to 10 000 levels). `FromJSON` accepts 10 000 levels. | |||
| 129 | + | ||||
| 130 | + | ## Other things to check once the runner is available | |||
| 131 | + | ||||
| 132 | + | - [ ] Run the runner's expression test vectors (`ExpressionParserL0`, | |||
| 133 | + | `ExpressionTests`, `ExpressionValuesL0`, `ExpressionTestUtil`) through a | |||
| 134 | + | table adapter; list every disagreement here before deciding. | |||
| 135 | + | - [ ] Decide whether `EvalContext` needs a hook for the functions the runner | |||
| 136 | + | implements natively with special semantics: `success()`, `failure()`, | |||
| 137 | + | `always()`, `cancelled()` (take no arguments; read job status) and | |||
| 138 | + | `hashFiles()` (variadic glob patterns, string result). The API supports | |||
| 139 | + | them through `FuncInfo` + `Func` without changes; arity is declared by | |||
| 140 | + | the caller. | |||
| 141 | + | - [ ] Context names the runner allows per location (`github`, `env`, `vars`, | |||
| 142 | + | `job`, `jobs`, `steps`, `runner`, `secrets`, `strategy`, `matrix`, | |||
| 143 | + | `needs`, `inputs`) and which functions each location may call | |||
| 144 | + | (`success`... only in `if`/`jobs.*.if`...). This belongs to the template | |||
| 145 | + | layer, not to package expr. | |||
| 146 | + | - [ ] `format()` argument limit is 255 in the JS (`MAX_ARGUMENTS`); the C# | |||
| 147 | + | limit is `ExpressionConstants.MaxArguments` (check). | |||
| 148 | + | - [ ] Case-insensitive comparison and `contains`: if C# `OrdinalIgnoreCase` | |||
| 149 | + | is the truth, replace `toUpperSpecial`/`jsToLower` by per-UTF-16-unit | |||
| 150 | + | simple upper-casing and regenerate the oracle tests accordingly. | |||
| 151 | + | - [ ] Secret masking and the `${{ }}` splitting of a larger string are not | |||
| 152 | + | part of this package (they belong to the template layer): `Parse` | |||
| 153 | + | takes the text between the braces only. | |||