jsonatapy 2.2.12__tar.gz → 2.2.13__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.
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/CHANGELOG.md +49 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/Cargo.lock +1 -1
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/Cargo.toml +1 -1
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/PKG-INFO +7 -7
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/README.md +4 -4
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/pyproject.toml +4 -4
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/capi.rs +1 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/evaluator.rs +99 -6
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/lib.rs +2 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/vm.rs +20 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/integration_test.rs +156 -1
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/.gitignore +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/.gitmodules +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/.serena/.gitignore +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/.serena/memories/architecture.md +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/.serena/memories/conventions.md +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/.serena/memories/core.md +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/.serena/memories/memory_maintenance.md +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/.serena/memories/suggested_commands.md +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/.serena/memories/task_completion.md +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/.serena/memories/tech_stack.md +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/CONTRIBUTORS.md +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/LICENSE +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/SECURITY.md +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/benches/evaluator_bench.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/bindings/c/README.md +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/bindings/c/examples/smoke.c +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/bindings/c/jsonata.h +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/examples/evaluator_demo.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/examples/host_functions.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/examples/parser_demo.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/examples/simd_json_bench.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/python/jsonatapy/__init__.py +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/python/jsonatapy/__main__.py +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/python/jsonatapy/_cli/__init__.py +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/python/jsonatapy/_cli/bindings.py +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/python/jsonatapy/_cli/error_format.py +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/python/jsonatapy/_cli/mcp_server.py +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/python/jsonatapy/_cli/resolve.py +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/python/jsonatapy/_cli/run.py +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/python/jsonatapy/py.typed +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/allocator.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/ast.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/ast_transform.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/bin/jsonata/bindings.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/bin/jsonata/error_format.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/bin/jsonata/main.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/bin/jsonata/resolve.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/builtins.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/compiler.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/datetime.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/expression.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/functions.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/lazy.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/parser.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/pystack.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/signature.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/src/value.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/study/cli_fixtures.json +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/study/cli_fixtures_testdata.json +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/study/cli_spec.md +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/array_flags_test.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/cli_fixtures_test.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/cli_test.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/common/mod.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/datetime_picture_suite.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/deep_value_drop_test.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/evaluator_reuse_test.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/fixtures/array_flags_differential.json +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/fixtures/builtin_arity.json +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/fixtures/builtin_differential.json +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/fixtures/builtin_signatures.json +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/fixtures/fastpath_differential.json +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/fixtures/fastpath_known_divergences.json +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/fixtures/path_semantics_combinations.json +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/fixtures/path_semantics_combinations_known.json +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/fixtures/path_semantics_differential.json +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/host_functions_test.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/lambda_closure_suite.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/parent_and_focus_binding_suite.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/path_semantics_test.rs +0 -0
- {jsonatapy-2.2.12 → jsonatapy-2.2.13}/tests/step_cache_test.rs +0 -0
|
@@ -19,6 +19,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
19
19
|
|
|
20
20
|
### Security
|
|
21
21
|
|
|
22
|
+
## [2.2.13] - 2026-10-24
|
|
23
|
+
|
|
24
|
+
**Upgrade notes**
|
|
25
|
+
|
|
26
|
+
- **Breaking for Rust code that builds `EvaluatorOptions` field by field.** `EvaluatorOptions`
|
|
27
|
+
has a new public `cancel` field (see Added), so a struct literal that lists every field
|
|
28
|
+
without `..Default::default()` no longer compiles. Add `cancel: None`, or end the literal with
|
|
29
|
+
`..Default::default()`. Strict semver would call this a major change. It ships in a patch
|
|
30
|
+
release because jsonata-core's version tracks the jsonata-js version it targets (2.2), and
|
|
31
|
+
code that uses `..Default::default()` is unaffected. The Python and C APIs are unchanged.
|
|
32
|
+
- **`jsonatapy[mcp]` now needs fastmcp 4.1.2 or later**, which brings `mcp` 2.x (see Changed).
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- **An evaluation can be cancelled from another thread.** `EvaluatorOptions` has a new
|
|
37
|
+
`cancel` field that takes a `CancelToken`. Calling `cancel()` on any clone of the token
|
|
38
|
+
stops the evaluation with `U1003: Evaluation cancelled` wherever `timeout_ms` is checked:
|
|
39
|
+
each node evaluated, each element a higher-order builtin hands its callback, each match
|
|
40
|
+
`$replace` hands its replacement function, and each pass of a tail-call loop. A single
|
|
41
|
+
builtin call over a large input finishes first, as it does under `timeout_ms`, and if it is
|
|
42
|
+
the last step the evaluation returns its result. jsonata-js has no cancellation, so the
|
|
43
|
+
code is in the `U` series. This is a Rust API only. See the upgrade notes above for the
|
|
44
|
+
`EvaluatorOptions` source break. (#264) Thanks to @jonking-ajar for the design, the
|
|
45
|
+
implementation and its tests.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- **The `mcp` extra now needs fastmcp 4.1.2 or later** (was 3.0). fastmcp 4 brings `mcp` 2.x
|
|
50
|
+
and replaces `httpx` with `httpx2`, so an environment that pins `mcp<2` or depends on
|
|
51
|
+
`httpx` alongside `jsonatapy[mcp]` may need updating. The `jsonatapy mcp` server itself is
|
|
52
|
+
unchanged. 4.1.2 is also the first 4.x release with the fix for GHSA-7xgj-8392-5q4r.
|
|
53
|
+
|
|
54
|
+
### Deprecated
|
|
55
|
+
|
|
56
|
+
### Removed
|
|
57
|
+
|
|
58
|
+
### Fixed
|
|
59
|
+
|
|
60
|
+
- **`timeout_ms` now stops `$map`, `$filter`, `$reduce`, `$single`, `$sift`, `$each` and
|
|
61
|
+
`$sort` when their callback is a builtin, host or partial function, and `$replace` when its
|
|
62
|
+
replacement function is one.** Their loops over such a function had no checkpoint, because
|
|
63
|
+
the call evaluates no node, so `$map(items, $string)` over a large array ran to the end past
|
|
64
|
+
its timeout. A lambda callback was already stopped, since its body is checked like any node.
|
|
65
|
+
An expression with `timeout_ms` set that used to finish late in one of these loops and
|
|
66
|
+
return a result now fails with D1012 instead. (#264) Thanks to @jonking-ajar, who found and
|
|
67
|
+
fixed this alongside the cancellation work.
|
|
68
|
+
|
|
69
|
+
### Security
|
|
70
|
+
|
|
22
71
|
## [2.2.12] "Gymkata" - 2026-10-20
|
|
23
72
|
|
|
24
73
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: jsonatapy
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.13
|
|
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>=
|
|
27
|
+
Requires-Dist: fastmcp>=4.1.2 ; 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>=
|
|
32
|
+
Requires-Dist: fastmcp>=4.1.2 ; extra == 'mcp'
|
|
33
33
|
Provides-Extra: bench
|
|
34
34
|
Provides-Extra: dev
|
|
35
35
|
Provides-Extra: docs
|
|
@@ -75,13 +75,13 @@ 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.
|
|
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."
|
|
79
79
|
src="https://raw.githubusercontent.com/txjmb/jsonata-core/main/docs/assets/realistic-workload-light.svg">
|
|
80
80
|
</picture>
|
|
81
81
|
</a>
|
|
82
82
|
|
|
83
83
|
*Click the chart for the full category-by-category tables, including jsonata-python
|
|
84
|
-
and jsonata-rs, which are left off the chart because at
|
|
84
|
+
and jsonata-rs, which are left off the chart because at 67 ms–9 s they would
|
|
85
85
|
flatten everything else into a sliver.*
|
|
86
86
|
<!-- END generated performance chart -->
|
|
87
87
|
|
|
@@ -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.
|
|
159
|
+
jsonata-core = "2.2.13" # pure Rust, no Python dependency
|
|
160
160
|
|
|
161
161
|
# Optional: disable SIMD for constrained targets
|
|
162
|
-
jsonata-core = { version = "2.2.
|
|
162
|
+
jsonata-core = { version = "2.2.13", default-features = false }
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
---
|
|
@@ -26,13 +26,13 @@
|
|
|
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.
|
|
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."
|
|
30
30
|
src="https://raw.githubusercontent.com/txjmb/jsonata-core/main/docs/assets/realistic-workload-light.svg">
|
|
31
31
|
</picture>
|
|
32
32
|
</a>
|
|
33
33
|
|
|
34
34
|
*Click the chart for the full category-by-category tables, including jsonata-python
|
|
35
|
-
and jsonata-rs, which are left off the chart because at
|
|
35
|
+
and jsonata-rs, which are left off the chart because at 67 ms–9 s they would
|
|
36
36
|
flatten everything else into a sliver.*
|
|
37
37
|
<!-- END generated performance chart -->
|
|
38
38
|
|
|
@@ -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.
|
|
110
|
+
jsonata-core = "2.2.13" # pure Rust, no Python dependency
|
|
111
111
|
|
|
112
112
|
# Optional: disable SIMD for constrained targets
|
|
113
|
-
jsonata-core = { version = "2.2.
|
|
113
|
+
jsonata-core = { version = "2.2.13", 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.
|
|
7
|
+
version = "2.2.13"
|
|
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,10 @@ dev = [
|
|
|
42
42
|
"ruff>=0.3.0", # Linting and formatting
|
|
43
43
|
"mypy>=1.0",
|
|
44
44
|
"maturin>=1.0",
|
|
45
|
-
"fastmcp>=
|
|
45
|
+
"fastmcp>=4.1.2", # So `dev` installs can run the full test suite (Task 9)
|
|
46
46
|
]
|
|
47
47
|
mcp = [
|
|
48
|
-
"fastmcp>=
|
|
48
|
+
"fastmcp>=4.1.2",
|
|
49
49
|
]
|
|
50
50
|
bench = [
|
|
51
51
|
"jsonata>=0.2.0", # jsonata-python for benchmarking
|
|
@@ -82,7 +82,7 @@ dev = [
|
|
|
82
82
|
"ruff>=0.3.0",
|
|
83
83
|
"mypy>=1.0",
|
|
84
84
|
"maturin>=1.0",
|
|
85
|
-
"fastmcp>=
|
|
85
|
+
"fastmcp>=4.1.2",
|
|
86
86
|
]
|
|
87
87
|
|
|
88
88
|
[tool.ruff]
|
|
@@ -491,6 +491,7 @@ pub unsafe extern "C" fn jsonata_set_limits(
|
|
|
491
491
|
timeout_ms: (timeout_ms > 0).then_some(timeout_ms),
|
|
492
492
|
max_stack_depth: (max_stack_depth > 0).then_some(max_stack_depth as usize),
|
|
493
493
|
max_sequence_length: (max_sequence_length > 0).then_some(max_sequence_length as usize),
|
|
494
|
+
cancel: None,
|
|
494
495
|
};
|
|
495
496
|
clear_error();
|
|
496
497
|
0
|
|
@@ -3691,12 +3691,76 @@ impl TupleKeyBindings {
|
|
|
3691
3691
|
}
|
|
3692
3692
|
}
|
|
3693
3693
|
|
|
3694
|
+
/// A handle that stops an evaluation from another thread.
|
|
3695
|
+
///
|
|
3696
|
+
/// Clones share one flag. Put a clone in [`EvaluatorOptions::cancel`] and call
|
|
3697
|
+
/// [`CancelToken::cancel`] on another, and the evaluation raises U1003 at its
|
|
3698
|
+
/// next checkpoint: each node it evaluates, each element a higher-order
|
|
3699
|
+
/// builtin (`$map`, `$filter`, `$reduce`, `$single`, `$sift`, `$each`,
|
|
3700
|
+
/// `$sort`) hands its callback, each match `$replace` hands its replacement
|
|
3701
|
+
/// function, and each pass of a tail-call loop. These are the places
|
|
3702
|
+
/// `timeout_ms` is checked.
|
|
3703
|
+
///
|
|
3704
|
+
/// A single builtin call over a large input, such as `$string` of a long
|
|
3705
|
+
/// array, finishes before the flag is seen. If that call is the last step,
|
|
3706
|
+
/// the evaluation returns its result even though the token was cancelled, so
|
|
3707
|
+
/// check [`CancelToken::is_cancelled`] after it returns when that matters.
|
|
3708
|
+
///
|
|
3709
|
+
/// An [`Evaluator`] keeps its options, so one built with a cancelled token
|
|
3710
|
+
/// fails every later evaluation. Pass a fresh token with each call, as below,
|
|
3711
|
+
/// or build a new `Evaluator`.
|
|
3712
|
+
///
|
|
3713
|
+
/// ```
|
|
3714
|
+
/// use jsonata_core::evaluator::{CancelToken, EvaluatorOptions};
|
|
3715
|
+
/// use jsonata_core::value::JValue;
|
|
3716
|
+
/// use jsonata_core::Expression;
|
|
3717
|
+
///
|
|
3718
|
+
/// let expr = Expression::compile("($inf := function(){ $inf() }; $inf())")?;
|
|
3719
|
+
///
|
|
3720
|
+
/// let token = CancelToken::new();
|
|
3721
|
+
/// let canceller = token.clone();
|
|
3722
|
+
/// std::thread::spawn(move || {
|
|
3723
|
+
/// std::thread::sleep(std::time::Duration::from_millis(10));
|
|
3724
|
+
/// canceller.cancel();
|
|
3725
|
+
/// });
|
|
3726
|
+
/// let options = EvaluatorOptions {
|
|
3727
|
+
/// cancel: Some(token),
|
|
3728
|
+
/// ..Default::default()
|
|
3729
|
+
/// };
|
|
3730
|
+
/// let err = expr.evaluate_with_options(&JValue::Null, options).unwrap_err();
|
|
3731
|
+
/// assert!(err.to_string().contains("U1003"));
|
|
3732
|
+
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
|
3733
|
+
/// ```
|
|
3734
|
+
#[derive(Clone, Debug, Default)]
|
|
3735
|
+
pub struct CancelToken(std::sync::Arc<std::sync::atomic::AtomicBool>);
|
|
3736
|
+
|
|
3737
|
+
impl CancelToken {
|
|
3738
|
+
/// A token that has not been cancelled.
|
|
3739
|
+
pub fn new() -> Self {
|
|
3740
|
+
Self::default()
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
/// Stop every evaluation that holds a clone of this token. It cannot be
|
|
3744
|
+
/// reset.
|
|
3745
|
+
pub fn cancel(&self) {
|
|
3746
|
+
self.0.store(true, std::sync::atomic::Ordering::Relaxed);
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
/// Whether [`CancelToken::cancel`] has been called on any clone.
|
|
3750
|
+
pub fn is_cancelled(&self) -> bool {
|
|
3751
|
+
self.0.load(std::sync::atomic::Ordering::Relaxed)
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3694
3755
|
/// Resource-limit guardrails, mirroring jsonata-js 2.2.1's `timeout`/`stack`/`sequence`
|
|
3695
3756
|
/// evaluator options. All fields default to `None` = unlimited (current behavior).
|
|
3696
3757
|
#[derive(Default, Clone, Debug)]
|
|
3697
3758
|
pub struct EvaluatorOptions {
|
|
3698
3759
|
/// Maximum wall-clock evaluation time in milliseconds. Exceeding it raises D1012.
|
|
3699
3760
|
pub timeout_ms: Option<u64>,
|
|
3761
|
+
/// Stops the evaluation with U1003 once the token is cancelled, from any
|
|
3762
|
+
/// thread. jsonata-js has no equivalent; see [`CancelToken`].
|
|
3763
|
+
pub cancel: Option<CancelToken>,
|
|
3700
3764
|
/// Maximum evaluation stack depth, like jsonata-js's `stack` option. Exceeding
|
|
3701
3765
|
/// it raises D1011. Without it, evaluation may nest up to the built-in
|
|
3702
3766
|
/// `MAX_EVAL_DEPTH` (30,000 levels, about 10,000 non-tail recursive calls),
|
|
@@ -4262,14 +4326,29 @@ pub(crate) fn check_sequence_length(
|
|
|
4262
4326
|
Ok(())
|
|
4263
4327
|
}
|
|
4264
4328
|
|
|
4265
|
-
///
|
|
4266
|
-
///
|
|
4267
|
-
|
|
4329
|
+
/// U1003 once the caller has cancelled the evaluation. jsonata-js has no
|
|
4330
|
+
/// cancellation and so no code for it; like U1001 and U1002 this one is ours.
|
|
4331
|
+
#[inline(always)]
|
|
4332
|
+
pub(crate) fn check_cancelled(options: &EvaluatorOptions) -> Result<(), EvaluatorError> {
|
|
4333
|
+
match &options.cancel {
|
|
4334
|
+
Some(token) if token.is_cancelled() => Err(EvaluatorError::EvaluationError(
|
|
4335
|
+
"U1003: Evaluation cancelled".to_string(),
|
|
4336
|
+
)),
|
|
4337
|
+
_ => Ok(()),
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
|
|
4341
|
+
/// Per-iteration D1012 and U1003 check for loops that don't pass through
|
|
4342
|
+
/// `evaluate_internal`'s per-node checkpoint and would otherwise run untimed:
|
|
4343
|
+
/// compiled map/filter/reduce element loops, FilterByBytecode, the tail-call
|
|
4344
|
+
/// trampoline, and the higher-order builtins' loops over a builtin, host or
|
|
4345
|
+
/// partial callback, whose call evaluates no node.
|
|
4268
4346
|
#[inline]
|
|
4269
4347
|
pub(crate) fn check_loop_timeout(
|
|
4270
4348
|
options: &EvaluatorOptions,
|
|
4271
4349
|
start_time: Option<Instant>,
|
|
4272
4350
|
) -> Result<(), EvaluatorError> {
|
|
4351
|
+
check_cancelled(options)?;
|
|
4273
4352
|
if let Some(timeout_ms) = options.timeout_ms {
|
|
4274
4353
|
if let Some(start) = start_time {
|
|
4275
4354
|
if start.elapsed().as_millis() as u64 > timeout_ms {
|
|
@@ -4915,6 +4994,7 @@ impl Evaluator {
|
|
|
4915
4994
|
} else {
|
|
4916
4995
|
// Unexpected param count - fall back to generic path
|
|
4917
4996
|
while i < left.len() && j < right.len() {
|
|
4997
|
+
check_loop_timeout(&self.options, self.start_time)?;
|
|
4918
4998
|
let cmp_result = self.apply_function(
|
|
4919
4999
|
comparator,
|
|
4920
5000
|
&[left[i].clone(), right[j].clone()],
|
|
@@ -4932,6 +5012,7 @@ impl Evaluator {
|
|
|
4932
5012
|
} else {
|
|
4933
5013
|
// Non-lambda comparator: use generic apply_function path
|
|
4934
5014
|
while i < left.len() && j < right.len() {
|
|
5015
|
+
check_loop_timeout(&self.options, self.start_time)?;
|
|
4935
5016
|
let cmp_result =
|
|
4936
5017
|
self.apply_function(comparator, &[left[i].clone(), right[j].clone()], data)?;
|
|
4937
5018
|
if js_truthy(&cmp_result) {
|
|
@@ -5111,9 +5192,9 @@ impl Evaluator {
|
|
|
5111
5192
|
self.with_checkpoint(|ev| ev.evaluate_internal_impl(node, data))
|
|
5112
5193
|
}
|
|
5113
5194
|
|
|
5114
|
-
/// Run one level of evaluation under the recursion-depth,
|
|
5115
|
-
/// native-stack checks every node evaluation makes. Inlined so
|
|
5116
|
-
/// node-evaluation path pays no extra call frame for it. The trampoline's
|
|
5195
|
+
/// Run one level of evaluation under the recursion-depth, cancellation,
|
|
5196
|
+
/// timeout and native-stack checks every node evaluation makes. Inlined so
|
|
5197
|
+
/// the hot node-evaluation path pays no extra call frame for it. The trampoline's
|
|
5117
5198
|
/// own recursion paths that bypass `evaluate_internal` (a tail call of a
|
|
5118
5199
|
/// higher-order builtin or of a composition) run under it too, so no
|
|
5119
5200
|
/// recursion cycle grows the native stack unchecked (#191).
|
|
@@ -5152,6 +5233,11 @@ impl Evaluator {
|
|
|
5152
5233
|
));
|
|
5153
5234
|
}
|
|
5154
5235
|
|
|
5236
|
+
if let Err(e) = check_cancelled(&self.options) {
|
|
5237
|
+
self.recursion_depth -= 1;
|
|
5238
|
+
return Err(e);
|
|
5239
|
+
}
|
|
5240
|
+
|
|
5155
5241
|
// Check evaluation timeout (D1012). `start_time` is only set (in
|
|
5156
5242
|
// `evaluate()`) when `options.timeout_ms` is configured, so this is a
|
|
5157
5243
|
// single `is_none()` branch of overhead when no timeout is set.
|
|
@@ -8347,6 +8433,7 @@ impl Evaluator {
|
|
|
8347
8433
|
let call_args =
|
|
8348
8434
|
hof_object_call_args(value, key, obj_value.as_ref(), param_count);
|
|
8349
8435
|
|
|
8436
|
+
check_loop_timeout(&evaluator.options, evaluator.start_time)?;
|
|
8350
8437
|
let pred_result =
|
|
8351
8438
|
evaluator.apply_function(func_node, &call_args, context_data)?;
|
|
8352
8439
|
if evaluator.is_truthy(&pred_result) {
|
|
@@ -8549,6 +8636,7 @@ impl Evaluator {
|
|
|
8549
8636
|
let call_args =
|
|
8550
8637
|
hof_array_call_args(item, index, arr_value.as_ref(), param_count);
|
|
8551
8638
|
|
|
8639
|
+
check_loop_timeout(&self.options, self.start_time)?;
|
|
8552
8640
|
let mapped = self.apply_function(&args[1], &call_args, data)?;
|
|
8553
8641
|
// Filter out undefined results but keep explicit null (JSONata map semantics)
|
|
8554
8642
|
// undefined comes from missing else clause, null is explicit
|
|
@@ -8686,6 +8774,7 @@ impl Evaluator {
|
|
|
8686
8774
|
let call_args =
|
|
8687
8775
|
hof_array_call_args(item, index, arr_value.as_ref(), param_count);
|
|
8688
8776
|
|
|
8777
|
+
check_loop_timeout(&self.options, self.start_time)?;
|
|
8689
8778
|
let predicate_result = self.apply_function(&args[1], &call_args, data)?;
|
|
8690
8779
|
if self.is_truthy(&predicate_result) {
|
|
8691
8780
|
result.push(item.clone());
|
|
@@ -8848,6 +8937,7 @@ impl Evaluator {
|
|
|
8848
8937
|
],
|
|
8849
8938
|
};
|
|
8850
8939
|
|
|
8940
|
+
check_loop_timeout(&self.options, self.start_time)?;
|
|
8851
8941
|
accumulator = self.apply_function(&args[1], &call_args, data)?;
|
|
8852
8942
|
}
|
|
8853
8943
|
|
|
@@ -8908,6 +8998,7 @@ impl Evaluator {
|
|
|
8908
8998
|
// hofFuncArgs shaping (see hof_array_call_args)
|
|
8909
8999
|
let call_args =
|
|
8910
9000
|
hof_array_call_args(item, index, arr_value.as_ref(), param_count);
|
|
9001
|
+
check_loop_timeout(&self.options, self.start_time)?;
|
|
8911
9002
|
let predicate_result = self.apply_function(&args[1], &call_args, data)?;
|
|
8912
9003
|
if self.is_truthy(&predicate_result) {
|
|
8913
9004
|
matches.push(item.clone());
|
|
@@ -8960,6 +9051,7 @@ impl Evaluator {
|
|
|
8960
9051
|
let call_args =
|
|
8961
9052
|
hof_object_call_args(value, key, obj_whole.as_ref(), param_count);
|
|
8962
9053
|
|
|
9054
|
+
check_loop_timeout(&self.options, self.start_time)?;
|
|
8963
9055
|
let fn_result = self.apply_function(func_arg, &call_args, data)?;
|
|
8964
9056
|
// Skip undefined results only. jsonata-js guards
|
|
8965
9057
|
// this push with `typeof val !== 'undefined'`, so an
|
|
@@ -10223,6 +10315,7 @@ impl Evaluator {
|
|
|
10223
10315
|
group,
|
|
10224
10316
|
);
|
|
10225
10317
|
}
|
|
10318
|
+
check_loop_timeout(&self.options, self.start_time)?;
|
|
10226
10319
|
match self.as_callback(|ev| ev.call_function_value(replacement, &[match_object()], data))? {
|
|
10227
10320
|
JValue::String(replaced) => {
|
|
10228
10321
|
out.push_str(&replaced);
|
|
@@ -343,6 +343,7 @@ impl JsonataExpression {
|
|
|
343
343
|
timeout_ms: timeout.or(self.default_options.timeout_ms),
|
|
344
344
|
max_stack_depth: max_stack_depth.or(self.default_options.max_stack_depth),
|
|
345
345
|
max_sequence_length: max_sequence_length.or(self.default_options.max_sequence_length),
|
|
346
|
+
cancel: self.default_options.cancel.clone(),
|
|
346
347
|
}
|
|
347
348
|
}
|
|
348
349
|
|
|
@@ -792,6 +793,7 @@ fn build_evaluator_options(
|
|
|
792
793
|
timeout_ms: timeout,
|
|
793
794
|
max_stack_depth,
|
|
794
795
|
max_sequence_length,
|
|
796
|
+
cancel: None,
|
|
795
797
|
}
|
|
796
798
|
}
|
|
797
799
|
|
|
@@ -199,6 +199,7 @@ impl<'prog> Vm<'prog> {
|
|
|
199
199
|
data: &JValue,
|
|
200
200
|
vars: Option<&HashMap<&str, &JValue>>,
|
|
201
201
|
) -> Result<JValue, EvaluatorError> {
|
|
202
|
+
crate::evaluator::check_cancelled(&self.options)?;
|
|
202
203
|
let data = &data.clone().into_outer_wrapper();
|
|
203
204
|
let start_time = if self.options.timeout_ms.is_some() {
|
|
204
205
|
Some(std::time::Instant::now())
|
|
@@ -830,4 +831,23 @@ mod tests {
|
|
|
830
831
|
.expect_err("expected D1012");
|
|
831
832
|
assert!(err.to_string().contains("D1012"), "got: {err}");
|
|
832
833
|
}
|
|
834
|
+
|
|
835
|
+
#[test]
|
|
836
|
+
fn test_filter_by_bytecode_raises_u1003_when_cancelled() {
|
|
837
|
+
let data: JValue = serde_json::json!({
|
|
838
|
+
"items": (0..1000).collect::<Vec<i32>>()
|
|
839
|
+
})
|
|
840
|
+
.into();
|
|
841
|
+
let prog = compile_and_assert_uses_filter_by_bytecode("items[$ >= 0]");
|
|
842
|
+
let token = crate::evaluator::CancelToken::new();
|
|
843
|
+
token.cancel();
|
|
844
|
+
let options = EvaluatorOptions {
|
|
845
|
+
cancel: Some(token),
|
|
846
|
+
..Default::default()
|
|
847
|
+
};
|
|
848
|
+
let err = Vm::with_options(&prog, options)
|
|
849
|
+
.run(&data, None)
|
|
850
|
+
.expect_err("expected U1003");
|
|
851
|
+
assert!(err.to_string().contains("U1003"), "got: {err}");
|
|
852
|
+
}
|
|
833
853
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// to process complete JSONata expressions.
|
|
5
5
|
|
|
6
6
|
use jsonata_core::{
|
|
7
|
-
evaluator::{Context, Evaluator, EvaluatorOptions},
|
|
7
|
+
evaluator::{CancelToken, Context, Evaluator, EvaluatorOptions},
|
|
8
8
|
parser::parse,
|
|
9
9
|
value::JValue,
|
|
10
10
|
};
|
|
@@ -1022,6 +1022,161 @@ fn test_no_timeout_configured_never_raises_d1012() {
|
|
|
1022
1022
|
assert!(result.is_ok(), "expected Ok, got: {result:?}");
|
|
1023
1023
|
}
|
|
1024
1024
|
|
|
1025
|
+
/// Options carrying `token`, with a 10 s timeout so a cancel that is never
|
|
1026
|
+
/// seen fails with D1012 instead of hanging the test run.
|
|
1027
|
+
fn cancellable(token: &CancelToken) -> EvaluatorOptions {
|
|
1028
|
+
EvaluatorOptions {
|
|
1029
|
+
cancel: Some(token.clone()),
|
|
1030
|
+
timeout_ms: Some(10_000),
|
|
1031
|
+
..Default::default()
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/// Evaluates `expr_str` with a host function `$tick` that cancels the
|
|
1036
|
+
/// evaluation on its `cancel_on`th call. It returns `"x"`, since a string suits
|
|
1037
|
+
/// every callback slot and a `$replace` replacement must return one. Returns
|
|
1038
|
+
/// the error and how many times `$tick` ran.
|
|
1039
|
+
fn evaluate_cancelled_on_call(
|
|
1040
|
+
expr_str: &str,
|
|
1041
|
+
cancel_on: usize,
|
|
1042
|
+
) -> (jsonata_core::evaluator::EvaluatorError, usize) {
|
|
1043
|
+
let ast = parse(expr_str).unwrap();
|
|
1044
|
+
let token = CancelToken::new();
|
|
1045
|
+
let calls = std::rc::Rc::new(std::cell::Cell::new(0));
|
|
1046
|
+
let mut evaluator = Evaluator::with_options(Context::new(), cancellable(&token));
|
|
1047
|
+
let (tick_token, tick_calls) = (token.clone(), calls.clone());
|
|
1048
|
+
evaluator
|
|
1049
|
+
.register_fn("tick", move |_args: &[JValue]| {
|
|
1050
|
+
tick_calls.set(tick_calls.get() + 1);
|
|
1051
|
+
if tick_calls.get() == cancel_on {
|
|
1052
|
+
tick_token.cancel();
|
|
1053
|
+
}
|
|
1054
|
+
Ok(JValue::string("x"))
|
|
1055
|
+
})
|
|
1056
|
+
.unwrap();
|
|
1057
|
+
let err = evaluator
|
|
1058
|
+
.evaluate(&ast, &JValue::Null)
|
|
1059
|
+
.expect_err("expected a U1003 cancellation");
|
|
1060
|
+
(err, calls.get())
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
/// A token cancelled before evaluation starts stops it at the first node.
|
|
1064
|
+
#[test]
|
|
1065
|
+
fn test_cancel_before_evaluation_raises_u1003() {
|
|
1066
|
+
let ast = parse("$sum([1..10])").unwrap();
|
|
1067
|
+
let token = CancelToken::new();
|
|
1068
|
+
token.cancel();
|
|
1069
|
+
let err = Evaluator::with_options(Context::new(), cancellable(&token))
|
|
1070
|
+
.evaluate(&ast, &JValue::Null)
|
|
1071
|
+
.expect_err("expected U1003");
|
|
1072
|
+
assert!(
|
|
1073
|
+
err.to_string().contains("U1003"),
|
|
1074
|
+
"expected U1003, got: {err}"
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
/// A token cancelled before the run stops a compiled `Expression` too, whose
|
|
1079
|
+
/// simple expressions run on the bytecode VM rather than the tree-walker.
|
|
1080
|
+
#[test]
|
|
1081
|
+
fn test_cancel_before_evaluation_raises_u1003_on_compiled_expression() {
|
|
1082
|
+
let token = CancelToken::new();
|
|
1083
|
+
token.cancel();
|
|
1084
|
+
let data = JValue::from(json!({"a": 1}));
|
|
1085
|
+
for expr_str in ["a + 1", "a", "$sum([a, 2])"] {
|
|
1086
|
+
let expr =
|
|
1087
|
+
jsonata_core::Expression::compile_with_options(expr_str, cancellable(&token)).unwrap();
|
|
1088
|
+
let err = expr.evaluate(&data).expect_err("expected U1003");
|
|
1089
|
+
assert!(
|
|
1090
|
+
err.to_string().contains("U1003"),
|
|
1091
|
+
"{expr_str}: expected U1003, got: {err}"
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/// A runaway tail-recursive loop, which has no iteration cap, stops once
|
|
1097
|
+
/// another thread cancels it. The trampoline checks on every pass.
|
|
1098
|
+
#[test]
|
|
1099
|
+
fn test_cancel_from_another_thread_stops_tail_recursion() {
|
|
1100
|
+
let ast = parse("($inf := function(){$inf()}; $inf())").unwrap();
|
|
1101
|
+
let token = CancelToken::new();
|
|
1102
|
+
let canceller = token.clone();
|
|
1103
|
+
let cancel_thread = std::thread::spawn(move || {
|
|
1104
|
+
std::thread::sleep(std::time::Duration::from_millis(20));
|
|
1105
|
+
canceller.cancel();
|
|
1106
|
+
});
|
|
1107
|
+
let err = Evaluator::with_options(Context::new(), cancellable(&token))
|
|
1108
|
+
.evaluate(&ast, &JValue::Null)
|
|
1109
|
+
.expect_err("expected U1003");
|
|
1110
|
+
cancel_thread.join().unwrap();
|
|
1111
|
+
assert!(
|
|
1112
|
+
err.to_string().contains("U1003"),
|
|
1113
|
+
"expected U1003, got: {err}"
|
|
1114
|
+
);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/// A cancel stops a higher-order builtin between elements, whether its
|
|
1118
|
+
/// callback is a lambda, whose body is checked like any node, or a host
|
|
1119
|
+
/// function, whose call evaluates no node and is checked by the loop.
|
|
1120
|
+
#[test]
|
|
1121
|
+
fn test_cancel_stops_higher_order_builtins_between_elements() {
|
|
1122
|
+
let cases = [
|
|
1123
|
+
"$map([1..2000], function($x){ $tick($x) })",
|
|
1124
|
+
"$map([1..2000], $tick)",
|
|
1125
|
+
"$filter([1..2000], $tick)",
|
|
1126
|
+
"$reduce([1..2000], $tick)",
|
|
1127
|
+
"$single([1..2000], $tick)",
|
|
1128
|
+
"$sift($merge([1..2000].{$string($): $}), $tick)",
|
|
1129
|
+
"$each($merge([1..2000].{$string($): $}), $tick)",
|
|
1130
|
+
"$sort([1..2000], $tick)",
|
|
1131
|
+
"$replace($pad('', 2000, 'a'), /a/, $tick)",
|
|
1132
|
+
];
|
|
1133
|
+
for expr_str in cases {
|
|
1134
|
+
let (err, calls) = evaluate_cancelled_on_call(expr_str, 100);
|
|
1135
|
+
assert!(
|
|
1136
|
+
err.to_string().contains("U1003"),
|
|
1137
|
+
"{expr_str}: expected U1003, got: {err}"
|
|
1138
|
+
);
|
|
1139
|
+
assert_eq!(calls, 100, "{expr_str}: $tick ran {calls} times");
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
/// `timeout_ms` stops `$map` over a host callback too. The loop had no
|
|
1144
|
+
/// check, so a slow callback ran past the deadline to the end of the array.
|
|
1145
|
+
#[test]
|
|
1146
|
+
fn test_timeout_stops_map_over_a_host_callback() {
|
|
1147
|
+
let ast = parse("$map([1..1000], $slow)").unwrap();
|
|
1148
|
+
let options = EvaluatorOptions {
|
|
1149
|
+
timeout_ms: Some(50),
|
|
1150
|
+
..Default::default()
|
|
1151
|
+
};
|
|
1152
|
+
let mut evaluator = Evaluator::with_options(Context::new(), options);
|
|
1153
|
+
evaluator
|
|
1154
|
+
.register_fn("slow", |args: &[JValue]| {
|
|
1155
|
+
std::thread::sleep(std::time::Duration::from_millis(1));
|
|
1156
|
+
Ok(args.first().cloned().unwrap_or(JValue::Null))
|
|
1157
|
+
})
|
|
1158
|
+
.unwrap();
|
|
1159
|
+
let err = evaluator
|
|
1160
|
+
.evaluate(&ast, &JValue::Null)
|
|
1161
|
+
.expect_err("expected D1012");
|
|
1162
|
+
assert!(
|
|
1163
|
+
err.to_string().contains("D1012"),
|
|
1164
|
+
"expected D1012, got: {err}"
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/// A token that is never cancelled changes no result.
|
|
1169
|
+
#[test]
|
|
1170
|
+
fn test_uncancelled_token_changes_nothing() {
|
|
1171
|
+
let ast = parse("$map([1..1000], function($x){$x*2})").unwrap();
|
|
1172
|
+
let token = CancelToken::new();
|
|
1173
|
+
let with_token = Evaluator::with_options(Context::new(), cancellable(&token))
|
|
1174
|
+
.evaluate(&ast, &JValue::Null)
|
|
1175
|
+
.unwrap();
|
|
1176
|
+
let without = Evaluator::new().evaluate(&ast, &JValue::Null).unwrap();
|
|
1177
|
+
assert_eq!(with_token, without);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1025
1180
|
/// jsonata-js's own canonical guardrails-documentation example: an infinite
|
|
1026
1181
|
/// tail-recursive call. `invoke_lambda_with_tco`'s trampoline loop previously
|
|
1027
1182
|
/// had no timeout check at all -- only a hardcoded 100,000-iteration cap,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|