detached b74c395

docs(expr): document the added limits and defer the environment cache

commit
b74c39583c523a16d30d8977ae7899f307432030
author
brice · · 2026-10-12 15:12 UTC
parent
9611ea2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +55 -2
M expr/PORTING.md +55 -2
@@ -7,12 +7,57 @@ reference answers for 33 575 expressions (tokens with positions, error
77 messages, result kinds, string coercions, `toJSON` output), and
88 `TestOracle` replays it. `gen/oracle.mjs` regenerates it, `gen/golden.mjs` the
99 `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).
10+casing tables (`go generate ./expr`; Node 25 / V8 14.1, Unicode 16). The
11+tables are relative to the Go toolchain's own Unicode tables
12+(`unicode.Version`); `TestCasingTablesMatchGoUnicodeVersion` fails with the
13+regeneration command when the toolchain changes. The oracle records the
14+npm integrity (sha512) of the reference tarball and the Node/V8/Unicode
15+versions in its `reference` field, and `TestOracle` logs them.
1116
1217 The C# runner (actions/runner, `DistributedTask/Expressions2`) is the
1318 implementation that runs on github.com. It was not available while this was
1419 written. Nothing below has been checked against it.
1520
21+## Limits added by this port (not in the JavaScript)
22+
23+The reference has no bound on what an evaluation may build. These are
24+additions, each reported as an `*EvalError` (or `*Error` for the parser) and
25+none changes the result of an evaluation that stays within them.
26+
27+- **Output budget** (`MaxResultBytes`, 1 MiB by default, `EvalContext.
28+ MaxResultBytes` to override, negative to disable). One budget per
29+ `Evaluate`, charged by `format` (every piece appended), `join` (items and
30+ separators), `toJSON` (the encoder stops as soon as the output would exceed
31+ what is left) and by every element of the arrays built by `.*` / `[*]`
32+ (16 bytes each). `format('{0}{0}', format('{0}{0}', ...))` chains and
33+ `a.*.*.*` fan-outs over shared arrays are cut off instead of exhausting
34+ memory. Strings that come out of `fromJSON` are not charged: their size is
35+ bounded by the input the caller provided. The cost per filtered element is a
36+ guess; revisit it if real workflows hit the limit.
37+- **Cancellation**: `Evaluate(ctx, ...)` checks `ctx` every 64 nodes and in the
38+ loops of filters, `join` and `contains`, and passes `ctx` to extension
39+ functions.
40+- **Evaluation depth** of 1000 for hand-built trees (parsed trees stay far
41+ below it), **JSON depth** `MaxJSONDepth` = 1000 for `FromJSON` and for the
42+ encoder (cyclic values built with `Array.Append` yield an error from
43+ `EncodeJSON`, `"null"` from `ToJSON`, an `*EvalError` from `toJSON()`).
44+ The JS recursion limit is engine-dependent, around 5 000 to 10 000.
45+- **Built-in arity is re-checked at evaluation**, so a hand-built AST gets an
46+ `*EvalError`, not a panic.
47+- **Input length** is checked before the input is converted, without
48+ allocating.
49+
50+Other limits that exist in the JS and are ported: expression length 21000
51+UTF-16 units and parser depth 50 (see below).
52+
53+## Performance notes (deferred)
54+
55+- GO-008: `Evaluate` rebuilds the case-folded context and function maps on
56+ every call, and `foldKey` lower-cases on each lookup. Fine for per-step
57+ evaluation; when a caller evaluates thousands of expressions against the
58+ same contexts, add a prepared `Environment` (fold once, reuse) and cache
59+ folded names in the AST.
60+
1661 ## Where the JavaScript is believed to differ from the C# runner
1762
1863 Each item is implemented as the JavaScript does. Check the runner's tests
@@ -125,7 +170,15 @@ a bug.
125170 evaluating e.g. `github.true` with an empty context: the reference does not
126171 handle a missing root context. Here it is null.
127172 - Recursion limits: JS recursion depth for `fromJSON` input is engine
128- dependent (about 5 000 to 10 000 levels). `FromJSON` accepts 10 000 levels.
173+ dependent (about 5 000 to 10 000 levels). `FromJSON` accepts `MaxJSONDepth`
174+ (1000) levels.
175+- Lone surrogates: JS strings are UTF-16 and can hold them; Go strings are
176+ UTF-8. They are kept as WTF-8 (`0xED 0xA0-0xBF xx`) so that `fromJSON` /
177+ `toJSON` round-trip, `==`/`<`/`contains`/`startsWith`/`endsWith` compare
178+ them like code units, case mapping copies them, and `format`/`join` rejoin a
179+ high surrogate followed by a low one into the character they form. This is
180+ covered by oracle cases. Two surrogate strings concatenated by other means
181+ (an extension function) are not rejoined.
129182
130183 ## Other things to check once the runner is available
131184
@@ -7,12 +7,57 @@ reference answers for 33 575 expressions (tokens with positions, error
7 messages, result kinds, string coercions, `toJSON` output), and7 messages, result kinds, string coercions, `toJSON` output), and
8 `TestOracle` replays it. `gen/oracle.mjs` regenerates it, `gen/golden.mjs` the8 `TestOracle` replays it. `gen/oracle.mjs` regenerates it, `gen/golden.mjs` the
9 `testdata/tojson` goldens, and `gen/casing_dump.mjs` + `gen/casing.go` the9 `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).10+casing tables (`go generate ./expr`; Node 25 / V8 14.1, Unicode 16). The
11+tables are relative to the Go toolchain's own Unicode tables
12+(`unicode.Version`); `TestCasingTablesMatchGoUnicodeVersion` fails with the
13+regeneration command when the toolchain changes. The oracle records the
14+npm integrity (sha512) of the reference tarball and the Node/V8/Unicode
15+versions in its `reference` field, and `TestOracle` logs them.
11 16
12 The C# runner (actions/runner, `DistributedTask/Expressions2`) is the17 The C# runner (actions/runner, `DistributedTask/Expressions2`) is the
13 implementation that runs on github.com. It was not available while this was18 implementation that runs on github.com. It was not available while this was
14 written. Nothing below has been checked against it.19 written. Nothing below has been checked against it.
15 20
21+## Limits added by this port (not in the JavaScript)
22+
23+The reference has no bound on what an evaluation may build. These are
24+additions, each reported as an `*EvalError` (or `*Error` for the parser) and
25+none changes the result of an evaluation that stays within them.
26+
27+- **Output budget** (`MaxResultBytes`, 1 MiB by default, `EvalContext.
28+ MaxResultBytes` to override, negative to disable). One budget per
29+ `Evaluate`, charged by `format` (every piece appended), `join` (items and
30+ separators), `toJSON` (the encoder stops as soon as the output would exceed
31+ what is left) and by every element of the arrays built by `.*` / `[*]`
32+ (16 bytes each). `format('{0}{0}', format('{0}{0}', ...))` chains and
33+ `a.*.*.*` fan-outs over shared arrays are cut off instead of exhausting
34+ memory. Strings that come out of `fromJSON` are not charged: their size is
35+ bounded by the input the caller provided. The cost per filtered element is a
36+ guess; revisit it if real workflows hit the limit.
37+- **Cancellation**: `Evaluate(ctx, ...)` checks `ctx` every 64 nodes and in the
38+ loops of filters, `join` and `contains`, and passes `ctx` to extension
39+ functions.
40+- **Evaluation depth** of 1000 for hand-built trees (parsed trees stay far
41+ below it), **JSON depth** `MaxJSONDepth` = 1000 for `FromJSON` and for the
42+ encoder (cyclic values built with `Array.Append` yield an error from
43+ `EncodeJSON`, `"null"` from `ToJSON`, an `*EvalError` from `toJSON()`).
44+ The JS recursion limit is engine-dependent, around 5 000 to 10 000.
45+- **Built-in arity is re-checked at evaluation**, so a hand-built AST gets an
46+ `*EvalError`, not a panic.
47+- **Input length** is checked before the input is converted, without
48+ allocating.
49+
50+Other limits that exist in the JS and are ported: expression length 21000
51+UTF-16 units and parser depth 50 (see below).
52+
53+## Performance notes (deferred)
54+
55+- GO-008: `Evaluate` rebuilds the case-folded context and function maps on
56+ every call, and `foldKey` lower-cases on each lookup. Fine for per-step
57+ evaluation; when a caller evaluates thousands of expressions against the
58+ same contexts, add a prepared `Environment` (fold once, reuse) and cache
59+ folded names in the AST.
60+
16 ## Where the JavaScript is believed to differ from the C# runner61 ## Where the JavaScript is believed to differ from the C# runner
17 62
18 Each item is implemented as the JavaScript does. Check the runner's tests63 Each item is implemented as the JavaScript does. Check the runner's tests
@@ -125,7 +170,15 @@ a bug.
125 evaluating e.g. `github.true` with an empty context: the reference does not170 evaluating e.g. `github.true` with an empty context: the reference does not
126 handle a missing root context. Here it is null.171 handle a missing root context. Here it is null.
127 - Recursion limits: JS recursion depth for `fromJSON` input is engine172 - Recursion limits: JS recursion depth for `fromJSON` input is engine
128- dependent (about 5 000 to 10 000 levels). `FromJSON` accepts 10 000 levels.173+ dependent (about 5 000 to 10 000 levels). `FromJSON` accepts `MaxJSONDepth`
174+ (1000) levels.
175+- Lone surrogates: JS strings are UTF-16 and can hold them; Go strings are
176+ UTF-8. They are kept as WTF-8 (`0xED 0xA0-0xBF xx`) so that `fromJSON` /
177+ `toJSON` round-trip, `==`/`<`/`contains`/`startsWith`/`endsWith` compare
178+ them like code units, case mapping copies them, and `format`/`join` rejoin a
179+ high surrogate followed by a low one into the character they form. This is
180+ covered by oracle cases. Two surrogate strings concatenated by other means
181+ (an extension function) are not rejoined.
129 182
130 ## Other things to check once the runner is available183 ## Other things to check once the runner is available
131 184

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close