jsonatapy 2.2.13__tar.gz → 2.2.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/CHANGELOG.md +45 -0
  2. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/Cargo.lock +6 -26
  3. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/Cargo.toml +4 -3
  4. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/PKG-INFO +6 -6
  5. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/README.md +3 -3
  6. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/pyproject.toml +6 -4
  7. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/python/jsonatapy/__main__.py +1 -1
  8. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/builtins.rs +63 -2
  9. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/evaluator.rs +15 -3
  10. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/functions.rs +448 -2
  11. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/value.rs +67 -3
  12. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/vm.rs +2 -1
  13. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/integration_test.rs +158 -0
  14. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/.gitignore +0 -0
  15. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/.gitmodules +0 -0
  16. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/.serena/.gitignore +0 -0
  17. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/.serena/memories/architecture.md +0 -0
  18. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/.serena/memories/conventions.md +0 -0
  19. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/.serena/memories/core.md +0 -0
  20. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/.serena/memories/memory_maintenance.md +0 -0
  21. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/.serena/memories/suggested_commands.md +0 -0
  22. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/.serena/memories/task_completion.md +0 -0
  23. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/.serena/memories/tech_stack.md +0 -0
  24. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/CONTRIBUTORS.md +0 -0
  25. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/LICENSE +0 -0
  26. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/SECURITY.md +0 -0
  27. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/benches/evaluator_bench.rs +0 -0
  28. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/bindings/c/README.md +0 -0
  29. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/bindings/c/examples/smoke.c +0 -0
  30. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/bindings/c/jsonata.h +0 -0
  31. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/examples/evaluator_demo.rs +0 -0
  32. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/examples/host_functions.rs +0 -0
  33. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/examples/parser_demo.rs +0 -0
  34. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/examples/simd_json_bench.rs +0 -0
  35. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/python/jsonatapy/__init__.py +0 -0
  36. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/python/jsonatapy/_cli/__init__.py +0 -0
  37. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/python/jsonatapy/_cli/bindings.py +0 -0
  38. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/python/jsonatapy/_cli/error_format.py +0 -0
  39. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/python/jsonatapy/_cli/mcp_server.py +0 -0
  40. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/python/jsonatapy/_cli/resolve.py +0 -0
  41. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/python/jsonatapy/_cli/run.py +0 -0
  42. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/python/jsonatapy/py.typed +0 -0
  43. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/allocator.rs +0 -0
  44. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/ast.rs +0 -0
  45. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/ast_transform.rs +0 -0
  46. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/bin/jsonata/bindings.rs +0 -0
  47. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/bin/jsonata/error_format.rs +0 -0
  48. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/bin/jsonata/main.rs +0 -0
  49. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/bin/jsonata/resolve.rs +0 -0
  50. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/capi.rs +0 -0
  51. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/compiler.rs +0 -0
  52. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/datetime.rs +0 -0
  53. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/expression.rs +0 -0
  54. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/lazy.rs +0 -0
  55. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/lib.rs +0 -0
  56. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/parser.rs +0 -0
  57. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/pystack.rs +0 -0
  58. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/src/signature.rs +0 -0
  59. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/study/cli_fixtures.json +0 -0
  60. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/study/cli_fixtures_testdata.json +0 -0
  61. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/study/cli_spec.md +0 -0
  62. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/array_flags_test.rs +0 -0
  63. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/cli_fixtures_test.rs +0 -0
  64. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/cli_test.rs +0 -0
  65. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/common/mod.rs +0 -0
  66. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/datetime_picture_suite.rs +0 -0
  67. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/deep_value_drop_test.rs +0 -0
  68. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/evaluator_reuse_test.rs +0 -0
  69. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/fixtures/array_flags_differential.json +0 -0
  70. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/fixtures/builtin_arity.json +0 -0
  71. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/fixtures/builtin_differential.json +0 -0
  72. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/fixtures/builtin_signatures.json +0 -0
  73. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/fixtures/fastpath_differential.json +0 -0
  74. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/fixtures/fastpath_known_divergences.json +0 -0
  75. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/fixtures/path_semantics_combinations.json +0 -0
  76. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/fixtures/path_semantics_combinations_known.json +0 -0
  77. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/fixtures/path_semantics_differential.json +0 -0
  78. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/host_functions_test.rs +0 -0
  79. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/lambda_closure_suite.rs +0 -0
  80. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/parent_and_focus_binding_suite.rs +0 -0
  81. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/path_semantics_test.rs +0 -0
  82. {jsonatapy-2.2.13 → jsonatapy-2.2.14}/tests/step_cache_test.rs +0 -0
@@ -19,6 +19,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
19
19
 
20
20
  ### Security
21
21
 
22
+ ## [2.2.14] - 2026-11-04
23
+
24
+ ### Added
25
+
26
+ ### Changed
27
+
28
+ - **Compiled regexes are reused.** The same regex literal evaluated again, such as once per element
29
+ under `$map`, no longer compiles again; each thread keeps up to 256.
30
+ - Updated Rust dependencies: `simd-json` 0.18.1 → 0.19.0, used by the default `simd` feature. (#274)
31
+ - **The `mcp` extra now needs Python 3.11 or later.** On Python 3.10, `pip install
32
+ 'jsonatapy[mcp]'` installs no `fastmcp`, and `jsonatapy mcp` says the extra needs 3.11. Its
33
+ `dnspython` dependency (through `email-validator`) has no release for 3.10 with the fix for
34
+ GHSA-45c3-73f7-pv4m. The rest of `jsonatapy` still supports 3.10.
35
+
36
+ ### Deprecated
37
+
38
+ ### Removed
39
+
40
+ ### Fixed
41
+
42
+ - **Object keys are in JavaScript's order**: integer-like keys first in ascending order, then the
43
+ rest in insertion order, so `$keys({"b":1,"2":2})` is `["2","b"]` as in jsonata-js. Python
44
+ callers see it too: a dict an expression builds comes back in that order, while a dict passed
45
+ through untouched is the caller's own object and keeps its order. (#273) Thanks to
46
+ @Sawmonabo for finding this and for the fix.
47
+ - **A cancel or a timeout now stops `$distinct` and `$keys` part way.** Both compare each element
48
+ with every one kept so far, and over a large input that ran to the end before U1003 or D1012 was
49
+ seen. (#272) Thanks to @Sawmonabo for the cancel check; the timeout check followed from it.
50
+ - **`\d`, `\w`, `\s` and `\b` in a regex now mean what they mean in jsonata-js**: ASCII digits and
51
+ word characters and ECMAScript's whitespace, so `$contains("٣", /\d/)` is `false`. (#271)
52
+ Thanks to @Sawmonabo for finding and fixing this.
53
+ - **A regex with the `i` flag folds case as jsonata-js does**, by upper case rather than Unicode
54
+ case folding. The Kelvin sign (U+212A) and the long s (U+017F) match only themselves, so
55
+ `$contains("\u212A", /k/i)` and `/[\w]/i` against it are `false`; `ß` and `ẞ` no longer match,
56
+ nor the Ohm and Angstrom signs with `ω` and `å`; and characters outside the BMP no longer fold.
57
+
58
+ ### Security
59
+
60
+ - `uv.lock`: `authlib` 1.7.2 → 1.9.1 (seven advisories), `joserfc` 1.7.3 → 1.7.7
61
+ (GHSA-jpxq-7h8p-2q76, GHSA-9pg5-8c7x-5hfx), and `dnspython` 2.8.0 → 2.9.0
62
+ (GHSA-45c3-73f7-pv4m), which no longer resolves for Python 3.10 now that the `mcp` extra needs
63
+ 3.11 (see Changed). All three are transitive dependencies of the optional `mcp` extra, not
64
+ of the published crate or of the wheel, which has no runtime dependencies. Users of the `mcp`
65
+ extra resolve their own versions and should upgrade these. (#269, #270)
66
+
22
67
  ## [2.2.13] - 2026-10-24
23
68
 
24
69
  **Upgrade notes**
@@ -620,7 +620,7 @@ dependencies = [
620
620
 
621
621
  [[package]]
622
622
  name = "jsonata-core"
623
- version = "2.2.13"
623
+ version = "2.2.14"
624
624
  dependencies = [
625
625
  "assert_cmd",
626
626
  "base64",
@@ -637,6 +637,7 @@ dependencies = [
637
637
  "pyo3",
638
638
  "rand",
639
639
  "regex",
640
+ "regex-syntax",
640
641
  "serde",
641
642
  "serde_json",
642
643
  "simd-json",
@@ -986,26 +987,6 @@ dependencies = [
986
987
  "crossbeam-utils",
987
988
  ]
988
989
 
989
- [[package]]
990
- name = "ref-cast"
991
- version = "1.0.25"
992
- source = "registry+https://github.com/rust-lang/crates.io-index"
993
- checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
994
- dependencies = [
995
- "ref-cast-impl",
996
- ]
997
-
998
- [[package]]
999
- name = "ref-cast-impl"
1000
- version = "1.0.25"
1001
- source = "registry+https://github.com/rust-lang/crates.io-index"
1002
- checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
1003
- dependencies = [
1004
- "proc-macro2",
1005
- "quote",
1006
- "syn 2.0.118",
1007
- ]
1008
-
1009
990
  [[package]]
1010
991
  name = "regex"
1011
992
  version = "1.13.1"
@@ -1134,12 +1115,11 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
1134
1115
 
1135
1116
  [[package]]
1136
1117
  name = "simd-json"
1137
- version = "0.18.1"
1118
+ version = "0.19.0"
1138
1119
  source = "registry+https://github.com/rust-lang/crates.io-index"
1139
- checksum = "6ebe141eb4a23ecc4d5de93fa5b139ac65dc07c38037a2fe9a8fb7c9d36bd832"
1120
+ checksum = "e8f2e4d523646f7da6dde3983bd18ede0babc21c258ab708b3dd93ea0259719d"
1140
1121
  dependencies = [
1141
1122
  "halfbrown",
1142
- "ref-cast",
1143
1123
  "serde",
1144
1124
  "serde_json",
1145
1125
  "simdutf8",
@@ -1304,9 +1284,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1304
1284
 
1305
1285
  [[package]]
1306
1286
  name = "value-trait"
1307
- version = "0.12.1"
1287
+ version = "0.12.3"
1308
1288
  source = "registry+https://github.com/rust-lang/crates.io-index"
1309
- checksum = "8e80f0c733af0720a501b3905d22e2f97662d8eacfe082a75ed7ffb5ab08cb59"
1289
+ checksum = "545a3d7089f11f1741d12963c057adf9443fc24325110a6a6daf582e825fd51b"
1310
1290
  dependencies = [
1311
1291
  "float-cmp",
1312
1292
  "halfbrown",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "jsonata-core"
3
- version = "2.2.13"
3
+ version = "2.2.14"
4
4
  edition = "2021"
5
5
  authors = ["txjmb <txjmb@users.noreply.github.com>"]
6
6
  description = "High-performance Rust implementation of JSONata query and transformation language"
@@ -46,14 +46,15 @@ serde = { version = "1.0", features = ["derive"] }
46
46
  serde_json = { version = "1.0", features = ["preserve_order"] }
47
47
  thiserror = "2.0"
48
48
  regex = "1.12"
49
+ regex-syntax = "0.8"
49
50
  chrono = "0.4"
50
51
  num-traits = "0.2"
51
52
  base64 = "0.23"
52
- indexmap = "2.2"
53
+ indexmap = "2.2.3"
53
54
  percent-encoding = "2.3"
54
55
  rand = "0.10"
55
56
  stacker = "0.1"
56
- simd-json = { version = "0.18", optional = true }
57
+ simd-json = { version = "0.19", optional = true }
57
58
  clap = { version = "4.6", features = ["derive"], optional = true }
58
59
  mimalloc = { version = "0.1.52", optional = true }
59
60
  libmimalloc-sys = { version = "0.1.49", optional = true }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonatapy
3
- Version: 2.2.13
3
+ Version: 2.2.14
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -24,12 +24,12 @@ Requires-Dist: pytest-asyncio>=0.24 ; extra == 'dev'
24
24
  Requires-Dist: ruff>=0.3.0 ; extra == 'dev'
25
25
  Requires-Dist: mypy>=1.0 ; extra == 'dev'
26
26
  Requires-Dist: maturin>=1.0 ; extra == 'dev'
27
- Requires-Dist: fastmcp>=4.1.2 ; extra == 'dev'
27
+ Requires-Dist: fastmcp>=4.1.2 ; python_full_version >= '3.11' and extra == 'dev'
28
28
  Requires-Dist: mkdocs>=1.5 ; extra == 'docs'
29
29
  Requires-Dist: mkdocs-material>=9.0 ; extra == 'docs'
30
30
  Requires-Dist: mkdocstrings[python]>=0.24 ; extra == 'docs'
31
31
  Requires-Dist: pymdown-extensions>=10.0 ; extra == 'docs'
32
- Requires-Dist: fastmcp>=4.1.2 ; extra == 'mcp'
32
+ Requires-Dist: fastmcp>=4.1.2 ; python_full_version >= '3.11' and extra == 'mcp'
33
33
  Provides-Extra: bench
34
34
  Provides-Extra: dev
35
35
  Provides-Extra: docs
@@ -75,7 +75,7 @@ Project-URL: Repository, https://github.com/txjmb/jsonata-core
75
75
  <source media="(prefers-color-scheme: dark)"
76
76
  srcset="https://raw.githubusercontent.com/txjmb/jsonata-core/main/docs/assets/realistic-workload-dark.svg">
77
77
  <img width="880"
78
- alt="Realistic-workload benchmark on a 100-product dataset, lower is better: jsonatapy is 8.1x faster than the jsonata-js reference on the geometric mean of 5 e-commerce queries, and the pure-Rust jsonata-core engine is faster still."
78
+ alt="Realistic-workload benchmark on a 100-product dataset, lower is better: jsonatapy is 8.2x faster than the jsonata-js reference on the geometric mean of 5 e-commerce queries, and the pure-Rust jsonata-core engine is faster still."
79
79
  src="https://raw.githubusercontent.com/txjmb/jsonata-core/main/docs/assets/realistic-workload-light.svg">
80
80
  </picture>
81
81
  </a>
@@ -156,10 +156,10 @@ let result = Evaluator::new().evaluate(&ast, &data)?;
156
156
  ```toml
157
157
  # Cargo.toml
158
158
  [dependencies]
159
- jsonata-core = "2.2.13" # pure Rust, no Python dependency
159
+ jsonata-core = "2.2.14" # pure Rust, no Python dependency
160
160
 
161
161
  # Optional: disable SIMD for constrained targets
162
- jsonata-core = { version = "2.2.13", default-features = false }
162
+ jsonata-core = { version = "2.2.14", default-features = false }
163
163
  ```
164
164
 
165
165
  ---
@@ -26,7 +26,7 @@
26
26
  <source media="(prefers-color-scheme: dark)"
27
27
  srcset="https://raw.githubusercontent.com/txjmb/jsonata-core/main/docs/assets/realistic-workload-dark.svg">
28
28
  <img width="880"
29
- alt="Realistic-workload benchmark on a 100-product dataset, lower is better: jsonatapy is 8.1x faster than the jsonata-js reference on the geometric mean of 5 e-commerce queries, and the pure-Rust jsonata-core engine is faster still."
29
+ alt="Realistic-workload benchmark on a 100-product dataset, lower is better: jsonatapy is 8.2x faster than the jsonata-js reference on the geometric mean of 5 e-commerce queries, and the pure-Rust jsonata-core engine is faster still."
30
30
  src="https://raw.githubusercontent.com/txjmb/jsonata-core/main/docs/assets/realistic-workload-light.svg">
31
31
  </picture>
32
32
  </a>
@@ -107,10 +107,10 @@ let result = Evaluator::new().evaluate(&ast, &data)?;
107
107
  ```toml
108
108
  # Cargo.toml
109
109
  [dependencies]
110
- jsonata-core = "2.2.13" # pure Rust, no Python dependency
110
+ jsonata-core = "2.2.14" # pure Rust, no Python dependency
111
111
 
112
112
  # Optional: disable SIMD for constrained targets
113
- jsonata-core = { version = "2.2.13", default-features = false }
113
+ jsonata-core = { version = "2.2.14", default-features = false }
114
114
  ```
115
115
 
116
116
  ---
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "jsonatapy"
7
- version = "2.2.13"
7
+ version = "2.2.14"
8
8
  description = "High-performance Python/Rust implementation of JSONata query and transformation language"
9
9
  authors = [
10
10
  {name = "txjmb", email = "txjmb@users.noreply.github.com"}
@@ -42,10 +42,12 @@ dev = [
42
42
  "ruff>=0.3.0", # Linting and formatting
43
43
  "mypy>=1.0",
44
44
  "maturin>=1.0",
45
- "fastmcp>=4.1.2", # So `dev` installs can run the full test suite (Task 9)
45
+ "fastmcp>=4.1.2; python_version >= '3.11'", # So `dev` installs can run the full test suite
46
46
  ]
47
47
  mcp = [
48
- "fastmcp>=4.1.2",
48
+ # 3.11+ only: fastmcp pulls in dnspython through email-validator, and dnspython 2.9.0,
49
+ # the first release with the fix for GHSA-45c3-73f7-pv4m, dropped 3.10.
50
+ "fastmcp>=4.1.2; python_version >= '3.11'",
49
51
  ]
50
52
  bench = [
51
53
  "jsonata>=0.2.0", # jsonata-python for benchmarking
@@ -82,7 +84,7 @@ dev = [
82
84
  "ruff>=0.3.0",
83
85
  "mypy>=1.0",
84
86
  "maturin>=1.0",
85
- "fastmcp>=4.1.2",
87
+ "fastmcp>=4.1.2; python_version >= '3.11'",
86
88
  ]
87
89
 
88
90
  [tool.ruff]
@@ -24,7 +24,7 @@ def _run_mcp(argv: list[str]) -> int:
24
24
  from ._cli.mcp_server import serve
25
25
  except ImportError:
26
26
  print(
27
- "error: the 'mcp' extra is not installed. Run:\n"
27
+ "error: the 'mcp' extra is not installed (it needs Python 3.11 or later). Run:\n"
28
28
  ' uvx --from "jsonatapy[mcp]" jsonatapy mcp\n'
29
29
  "or: pip install jsonatapy[mcp]",
30
30
  file=sys.stderr,
@@ -241,11 +241,17 @@ fn describe_operand(v: &JValue) -> String {
241
241
  /// boundary collapses it, so a builtin passed as a callback (`$map(arr,
242
242
  /// $keys)`) is unwrapped once, by the higher-order function's own result,
243
243
  /// exactly as in jsonata-js.
244
+ ///
245
+ /// `start_time` is when the evaluation started, if it has a timeout; the
246
+ /// builtins whose walk is quadratic (`$distinct`, `$keys` over an array)
247
+ /// check it with the cancel token per element, so neither runs on past
248
+ /// D1012 or U1003.
244
249
  pub(crate) fn dispatch_pure(
245
250
  name: &str,
246
251
  args: &[JValue],
247
252
  context: &JValue,
248
253
  options: &EvaluatorOptions,
254
+ start_time: Option<std::time::Instant>,
249
255
  ) -> Result<JValue, EvaluatorError> {
250
256
  // 1. Implicit context insertion. The union of what the two paths used to
251
257
  // do separately: only the tree-walker had `fromMillis` here and
@@ -916,7 +922,9 @@ pub(crate) fn dispatch_pure(
916
922
  // genuinely absent argument) moves to Undefined.
917
923
  None | Some(JValue::Undefined) => Ok(JValue::Undefined),
918
924
  Some(JValue::Array(arr)) if arr.len() > 1 => {
919
- let mut result = functions::array::distinct(arr)?;
925
+ let mut result = functions::array::distinct_checked(arr, || {
926
+ crate::evaluator::check_loop_timeout(options, start_time)
927
+ })?;
920
928
  // jsonata-js builds a sequence only when the input was one.
921
929
  if arr.sequence {
922
930
  result.mark_sequence();
@@ -1035,6 +1043,7 @@ pub(crate) fn dispatch_pure(
1035
1043
  let normalized_item = crate::evaluator::normalize_lazy(item)?;
1036
1044
  if let JValue::Object(obj) = &normalized_item {
1037
1045
  for key in obj.keys() {
1046
+ crate::evaluator::check_loop_timeout(options, start_time)?;
1038
1047
  let k = JValue::string(key.clone());
1039
1048
  if !all_keys.contains(&k) {
1040
1049
  all_keys.push(k);
@@ -1608,6 +1617,7 @@ mod tests {
1608
1617
  &[JValue::Undefined],
1609
1618
  &JValue::Number(5.0),
1610
1619
  &opts,
1620
+ None,
1611
1621
  )
1612
1622
  .expect_err("a numeric context cannot satisfy substring's first parameter");
1613
1623
  assert!(
@@ -1625,11 +1635,62 @@ mod tests {
1625
1635
  // assert on the member only one list had.
1626
1636
  let opts = EvaluatorOptions::default();
1627
1637
  // $fromMillis() with a numeric context formats that context.
1628
- let got = dispatch_pure("fromMillis", &[], &JValue::Number(0.0), &opts)
1638
+ let got = dispatch_pure("fromMillis", &[], &JValue::Number(0.0), &opts, None)
1629
1639
  .expect("fromMillis should read the context");
1630
1640
  assert!(
1631
1641
  matches!(&got, JValue::String(s) if s.starts_with("1970-01-01")),
1632
1642
  "expected an epoch timestamp, got {got:?}"
1633
1643
  );
1634
1644
  }
1645
+
1646
+ #[test]
1647
+ fn distinct_and_keys_stop_on_cancel() {
1648
+ // Both walk their input once per element against everything kept so
1649
+ // far, so a cancel has to be seen inside the walk, not only after it.
1650
+ let token = crate::evaluator::CancelToken::new();
1651
+ token.cancel();
1652
+ let opts = EvaluatorOptions {
1653
+ cancel: Some(token),
1654
+ ..Default::default()
1655
+ };
1656
+ for (name, arg) in [
1657
+ ("distinct", serde_json::json!([1, 2, 1])),
1658
+ ("keys", serde_json::json!([{"a": 1}, {"b": 2}])),
1659
+ ] {
1660
+ let err = dispatch_pure(name, &[JValue::from(arg)], &JValue::Undefined, &opts, None)
1661
+ .expect_err("a cancelled evaluation must not finish the walk");
1662
+ assert!(
1663
+ err.to_string().contains("U1003"),
1664
+ "{name}: expected U1003, got {err}"
1665
+ );
1666
+ }
1667
+ }
1668
+
1669
+ #[test]
1670
+ fn distinct_and_keys_stop_on_timeout() {
1671
+ // The same walks honour the evaluation timeout, not only a cancel.
1672
+ let opts = EvaluatorOptions {
1673
+ timeout_ms: Some(1),
1674
+ ..Default::default()
1675
+ };
1676
+ let started = std::time::Instant::now();
1677
+ std::thread::sleep(std::time::Duration::from_millis(5));
1678
+ for (name, arg) in [
1679
+ ("distinct", serde_json::json!([1, 2, 1])),
1680
+ ("keys", serde_json::json!([{"a": 1}, {"b": 2}])),
1681
+ ] {
1682
+ let err = dispatch_pure(
1683
+ name,
1684
+ &[JValue::from(arg)],
1685
+ &JValue::Undefined,
1686
+ &opts,
1687
+ Some(started),
1688
+ )
1689
+ .expect_err("a timed-out evaluation must not finish the walk");
1690
+ assert!(
1691
+ err.to_string().contains("D1012"),
1692
+ "{name}: expected D1012, got {err}"
1693
+ );
1694
+ }
1695
+ }
1635
1696
  }
@@ -1396,7 +1396,7 @@ fn eval_compiled_node(
1396
1396
  arg, data, vars, ctx, shape, options, start_time,
1397
1397
  )?);
1398
1398
  }
1399
- crate::builtins::dispatch_pure(name, &evaled_args, data, options)
1399
+ crate::builtins::dispatch_pure(name, &evaled_args, data, options, start_time)
1400
1400
  }
1401
1401
 
1402
1402
  // Block: evaluate each expression in sequence, return the last value.
@@ -8249,7 +8249,13 @@ impl Evaluator {
8249
8249
  // per-function logic. Only the builtins that call back into evaluation
8250
8250
  // are still handled below.
8251
8251
  if crate::builtins::is_pure_builtin(name) {
8252
- return crate::builtins::dispatch_pure(name, &evaluated_args, data, &self.options);
8252
+ return crate::builtins::dispatch_pure(
8253
+ name,
8254
+ &evaluated_args,
8255
+ data,
8256
+ &self.options,
8257
+ self.start_time,
8258
+ );
8253
8259
  }
8254
8260
 
8255
8261
  // JSONata feature: when a function is called with one fewer argument than
@@ -10757,7 +10763,13 @@ impl Evaluator {
10757
10763
  // "at least 1 argument" first would wrongly reject `$zip`/`$millis`
10758
10764
  // (arity 0) reached by reference.
10759
10765
  if crate::builtins::is_pure_builtin(name) {
10760
- return crate::builtins::dispatch_pure(name, values, context, &self.options);
10766
+ return crate::builtins::dispatch_pure(
10767
+ name,
10768
+ values,
10769
+ context,
10770
+ &self.options,
10771
+ self.start_time,
10772
+ );
10761
10773
  }
10762
10774
 
10763
10775
  // `$eval` is evaluator-dependent but its arguments are ordinary