jsonatapy 2.2.8__tar.gz → 2.2.9__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.8 → jsonatapy-2.2.9}/.gitignore +1 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/.serena/memories/architecture.md +6 -3
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/CHANGELOG.md +192 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/Cargo.lock +20 -1
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/Cargo.toml +3 -2
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/PKG-INFO +1 -1
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/bindings/c/README.md +7 -1
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/bindings/c/examples/smoke.c +21 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/bindings/c/jsonata.h +13 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/pyproject.toml +2 -2
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/ast_transform.rs +41 -144
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/bin/jsonata/error_format.rs +1 -9
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/bin/jsonata/main.rs +14 -6
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/builtins.rs +188 -256
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/capi.rs +77 -37
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/compiler.rs +12 -17
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/datetime.rs +2 -20
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/evaluator.rs +663 -1287
- jsonatapy-2.2.9/src/expression.rs +180 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/functions.rs +128 -290
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/lazy.rs +93 -28
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/lib.rs +56 -85
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/signature.rs +45 -136
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/value.rs +42 -29
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/vm.rs +33 -71
- jsonatapy-2.2.9/tests/lambda_closure_suite.rs +586 -0
- jsonatapy-2.2.8/src/parser/README.md +0 -321
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/.gitmodules +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/.serena/.gitignore +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/.serena/memories/conventions.md +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/.serena/memories/core.md +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/.serena/memories/memory_maintenance.md +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/.serena/memories/suggested_commands.md +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/.serena/memories/task_completion.md +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/.serena/memories/tech_stack.md +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/.serena/project.yml +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/LICENSE +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/README.md +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/benches/evaluator_bench.rs +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/examples/evaluator_demo.rs +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/examples/host_functions.rs +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/examples/parser_demo.rs +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/examples/simd_json_bench.rs +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/python/jsonatapy/__init__.py +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/python/jsonatapy/__main__.py +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/python/jsonatapy/_cli/__init__.py +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/python/jsonatapy/_cli/bindings.py +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/python/jsonatapy/_cli/error_format.py +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/python/jsonatapy/_cli/mcp_server.py +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/python/jsonatapy/_cli/resolve.py +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/python/jsonatapy/_cli/run.py +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/python/jsonatapy/py.typed +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/ast.rs +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/bin/jsonata/bindings.rs +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/bin/jsonata/resolve.rs +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/src/parser.rs +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/study/cli_fixtures.json +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/study/cli_fixtures_testdata.json +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/study/cli_spec.md +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/tests/cli_fixtures_test.rs +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/tests/cli_test.rs +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/tests/datetime_picture_suite.rs +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/tests/fixtures/builtin_arity.json +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/tests/fixtures/builtin_differential.json +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/tests/fixtures/builtin_signatures.json +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/tests/fixtures/fastpath_differential.json +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/tests/fixtures/fastpath_known_divergences.json +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/tests/host_functions_test.rs +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/tests/integration_test.rs +0 -0
- {jsonatapy-2.2.8 → jsonatapy-2.2.9}/tests/parent_and_focus_binding_suite.rs +0 -0
|
@@ -45,9 +45,12 @@ site (grep `unwrap_or(JValue::Null)` / `JValue::Null =>` near field-access code)
|
|
|
45
45
|
### Compilation pipeline (bytecode fast path)
|
|
46
46
|
`evaluator::try_compile_expr(ast)` → `CompiledExpr` (IR) → `compiler::BytecodeCompiler::compile`
|
|
47
47
|
→ `BytecodeProgram` (flat `Vec<Instr>`) → `peephole()` folds (`PushData+GetField` →
|
|
48
|
-
`GetDataField`, `GetVar+GetField` → `GetVarField`, elides `Not+Not`) →
|
|
49
|
-
`
|
|
50
|
-
|
|
48
|
+
`GetDataField`, `GetVar+GetField` → `GetVarField`, elides `Not+Not`) →
|
|
49
|
+
`vm::Vm::with_options(bc, options).run(...)`.
|
|
50
|
+
`JsonataExpression` (lib.rs) caches `bytecode: OnceCell<Option<BytecodeProgram>>`; all 5 evaluate
|
|
51
|
+
entrypoints funnel through `run_eval`, which tries the VM first and falls back to the tree-walker.
|
|
52
|
+
The `vm`/`compiler` modules exist only under the `python`, `capi`, or `bench` features (cfg-gated
|
|
53
|
+
in lib.rs) — a default-feature build, including the Rust CLI, always uses the tree-walker.
|
|
51
54
|
|
|
52
55
|
Key correctness rules baked into the compiler/VM (violate these and specific test-suite groups
|
|
53
56
|
regress):
|
|
@@ -19,6 +19,198 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
19
19
|
|
|
20
20
|
### Security
|
|
21
21
|
|
|
22
|
+
## [2.2.9] "Perform-ata" - 2026-08-31
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Lambda values now carry their closure directly — `JValue::Lambda(Rc<StoredLambda>)`
|
|
26
|
+
instead of a `lambda_id` name tag resolved through a per-scope side table (#157).
|
|
27
|
+
The `Rc` provides the closure's lifetime, so the side table (`Scope.lambdas`),
|
|
28
|
+
the escape-analysis GC (`extract_lambda_ids`/`collect_lambda_ids` walking every
|
|
29
|
+
block/lambda result, `pop_scope_preserving_lambdas`) and the dual-map handling in
|
|
30
|
+
`unbind`/`clear_current_scope` are gone, along with the O(result size) walk on
|
|
31
|
+
every scope pop. Recursion is late-bound letrec: a closure defined as
|
|
32
|
+
`$f := function ...` records its own name and binds it to itself at each
|
|
33
|
+
invocation, so no `Rc` cycles exist and closures cannot leak (guarded by a
|
|
34
|
+
live-count test). Free-variable capture remains a by-value snapshot at
|
|
35
|
+
definition time, now applied uniformly to lambda-valued variables too.
|
|
36
|
+
Behavioral fixes (all matching jsonata-js, pinned in
|
|
37
|
+
`tests/lambda_closure_suite.rs`): a transform bound to a variable is callable
|
|
38
|
+
and pipeable (`$t := |...|...|; $t(x)`, `x ~> $t`); a partial application
|
|
39
|
+
escaping the scope that defined its target function works; an alias
|
|
40
|
+
(`$g := $f`) survives `$f` being rebound; calling a name rebound to a
|
|
41
|
+
non-function raises T1006 instead of silently invoking the stale lambda; a
|
|
42
|
+
closure captured inside another escaping closure's environment no longer
|
|
43
|
+
dangles. Function equality is now closure identity rather than id-string
|
|
44
|
+
equality. Remaining known divergences from jsonata-js's live-frame capture
|
|
45
|
+
are documented in the same suite's `divergent_from_reference` module.
|
|
46
|
+
- Partial application is a typed representation instead of a stringly-typed
|
|
47
|
+
protocol: `StoredLambda` carries an optional `PartialApplication { target,
|
|
48
|
+
bound_args, placeholder_positions, total_args }`, replacing the
|
|
49
|
+
`"__partial_call:name:bool:n"` marker-string body that was parsed at every
|
|
50
|
+
invocation and the `__bound_arg_N` / `__placeholder_positions` /
|
|
51
|
+
`__total_args` / `__partial_target` values smuggled through `captured_env`.
|
|
52
|
+
A user-defined target is a captured closure (`PartialTarget::Lambda`);
|
|
53
|
+
builtins/host functions stay name-resolved at call time
|
|
54
|
+
(`PartialTarget::Named`), preserving call-time shadowing. Partials no longer
|
|
55
|
+
snapshot the entire environment at creation (the old
|
|
56
|
+
`capture_current_environment()` call was only ever a smuggling container),
|
|
57
|
+
and one behavior corner now matches jsonata-js: rebinding a user function
|
|
58
|
+
after partially applying it no longer changes what the partial calls
|
|
59
|
+
(pinned in `tests/lambda_closure_suite.rs` with eight other partial pins).
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
- `jsonata_set_limits` on the C ABI: wall-clock timeout (D1012), max AST recursion
|
|
63
|
+
depth (D1011), and max sequence length (D2015) — the same three guardrails the
|
|
64
|
+
Python bindings expose. C embedders previously had no way to bound a runaway
|
|
65
|
+
expression at all (the cleanup review's finding); 0 = unlimited, resettable per
|
|
66
|
+
handle. Covered by the C/C++ smoke tests and a Rust-side capi test.
|
|
67
|
+
- `jsonata_core::Expression` — a compile-once API for Rust callers that runs
|
|
68
|
+
compilable expressions on the bytecode VM, exactly like the Python and C
|
|
69
|
+
bindings always have. Until now the VM was unreachable from the pure-Rust
|
|
70
|
+
surface: `Evaluator` always tree-walks, so Rust (and CLI) callers silently
|
|
71
|
+
got the slower path. `Expression::compile(src)?.evaluate(&data)` lowers to
|
|
72
|
+
bytecode lazily and falls back to the tree-walker for non-compilable
|
|
73
|
+
expressions; bindings and host functions still go through `Evaluator`. The
|
|
74
|
+
dispatch now lives in one place (`expression::run_compiled`), shared by the
|
|
75
|
+
Rust API, the Python bindings' `run_eval`, and the C ABI (which previously
|
|
76
|
+
carried a hand-copied duplicate of it), and the CLI uses it when no `--arg`
|
|
77
|
+
bindings are given. The `vm`/`compiler` modules are unconditionally compiled
|
|
78
|
+
again (the cfg gates added earlier in this cycle are gone — the default
|
|
79
|
+
build now has a real consumer).
|
|
80
|
+
|
|
81
|
+
### Changed
|
|
82
|
+
- Python→Rust data conversion — the cost that dominates `evaluate(dict)` on array-heavy
|
|
83
|
+
inputs — is faster on two fronts. `lazy::convert` now dispatches on the exact type object
|
|
84
|
+
and iterates lists via borrowed references instead of pyo3's instance-check chain
|
|
85
|
+
(subclasses still take the original chain, so semantics, error types included, are
|
|
86
|
+
unchanged; pinned by `tests/python/test_convert_fastpath.py`). And the published wheels
|
|
87
|
+
now use mimalloc as the global allocator (new opt-in `mimalloc` cargo feature, enabled in
|
|
88
|
+
`pyproject.toml`), roughly halving the per-list/per-object allocation cost that JValue's
|
|
89
|
+
Rc-per-container representation makes the conversion floor. Measured together on the
|
|
90
|
+
benchmark suite's shapes: "Nested Array Access" (`data[1][1][1][1]`, the one row where
|
|
91
|
+
jsonata-js was still ahead) drops ~35% (12.0µs → 7.8µs on the dev container), the four
|
|
92
|
+
`lazy_check.py` gate rows drop 10–17%, and string-heavy conversion roughly halves. The
|
|
93
|
+
structural fix for the nested-array row — lazy list views — remains deferred (see the
|
|
94
|
+
2026-07-12 lazy-views spec's Limitations).
|
|
95
|
+
- The `vm` and `compiler` modules (the bytecode pipeline) are now cfg-gated on the features
|
|
96
|
+
that actually consume them (`python`, `capi`, `bench`) — they were compiled but unreachable
|
|
97
|
+
in a default-feature build, producing 12 permanent dead-code warnings that CI's
|
|
98
|
+
`--all-features` clippy never saw. A default `cargo build` is now warning-clean, and both
|
|
99
|
+
modules carry a header note naming their consumers. List and dict *subclasses* now take the
|
|
100
|
+
boundary converter's fast path too (`PyList_Check`/`PyDict_Check`; iteration was already
|
|
101
|
+
C-level for them, so behavior is unchanged), which let the duplicate slow-path container
|
|
102
|
+
loops be deleted.
|
|
103
|
+
|
|
104
|
+
### Changed (evaluator internals)
|
|
105
|
+
- The vestigial `*_is_explicit_null` plumbing is gone: since the null/undefined split
|
|
106
|
+
(#32) a runtime `JValue::Null` is always an explicit null, yet the flags were still
|
|
107
|
+
computed and threaded through ~20 signatures — nine arithmetic/comparison wrapper
|
|
108
|
+
methods, the `CompiledExpr::ExplicitNull` variant (now `Literal(Null)`), a
|
|
109
|
+
`PushExplicitNull` instruction, and per-instruction bool pairs on the VM's
|
|
110
|
+
arithmetic/comparison opcodes — with both terminal consumers ignoring them (~200
|
|
111
|
+
lines removed). Arithmetic and ordered comparison now have exactly one
|
|
112
|
+
implementation each, shared by the tree-walker, the compiled path, and the VM; as
|
|
113
|
+
part of that, the compiled/VM paths' T2010/T2009 error texts now match the
|
|
114
|
+
tree-walker's richer messages ("Cannot compare X and Y", operator symbol in T2009)
|
|
115
|
+
instead of the generic "Type mismatch in comparison" — same codes, better text, and
|
|
116
|
+
the two engines no longer disagree. `Evaluator::is_truthy` likewise now delegates to
|
|
117
|
+
the shared `compiled_is_truthy` instead of maintaining a twin (#111 had to be fixed
|
|
118
|
+
in both).
|
|
119
|
+
- More single-definition consolidation: the two verbatim 45-line signature-error
|
|
120
|
+
translation blocks (direct vs TCO lambda invocation) are one `coerce_lambda_args`
|
|
121
|
+
helper; the five copies of jsonata-js's `hofFuncArgs` argument shaping are two
|
|
122
|
+
helpers (`hof_array_call_args` for $map/$filter/$single, `hof_object_call_args` for
|
|
123
|
+
$sift/$each); the six copy-pasted encoding builtin arms share `unary_string_encoding`.
|
|
124
|
+
`signature.rs` dropped its dead `return_type` field and test-only constructors, and
|
|
125
|
+
a builtin signature that fails to parse now panics at cache build instead of leaving
|
|
126
|
+
that builtin silently unvalidated. `ast_transform.rs`'s 157-line module header — a
|
|
127
|
+
stale task journal whose ~20 line references were all wrong — is a 30-line statement
|
|
128
|
+
of the actual recursion/drop invariants, and `push_ast_node_children`'s leaf arm
|
|
129
|
+
lists every variant explicitly instead of `_ => {}`, so a future `AstNode` variant
|
|
130
|
+
cannot silently opt out of the stack-overflow-on-drop guard.
|
|
131
|
+
- Five hand-maintained parallel lists of builtin names (the pure set, the arity table,
|
|
132
|
+
the compilable set, the signature table, and a test's own literal copy) are one
|
|
133
|
+
sorted `builtins::BUILTINS` spec table; `is_pure_builtin`, `is_compilable_builtin`,
|
|
134
|
+
`builtin_arity`, and `signature::builtin_signature` all derive from it, and the
|
|
135
|
+
jsonata-js fixture drift tests still police the data. Adding a builtin is now one
|
|
136
|
+
row plus its dispatch arm. Membership and values are unchanged (the table was
|
|
137
|
+
generated mechanically from the lists it replaces).
|
|
138
|
+
- The bare-`AstNode::Name` arm of `evaluate_internal_impl` — a private field-access
|
|
139
|
+
copy that had drifted (kept nulls, never flattened, no tuple or lazy-dict handling)
|
|
140
|
+
and that instrumentation shows no expression in any suite reaches — now delegates
|
|
141
|
+
to `compiled_field_step`, the semantics owner for a single field step.
|
|
142
|
+
|
|
143
|
+
### Deprecated
|
|
144
|
+
|
|
145
|
+
### Removed
|
|
146
|
+
- Repository hygiene: the checked-in `rustup-init.exe` (12.9 MB Windows installer that
|
|
147
|
+
dominated clone size and permanently tripped the file-size lint), the orphaned root
|
|
148
|
+
`node_modules/jsonata/` copy (~830 KB; nothing referenced it — the benchmark harness
|
|
149
|
+
installs its own under `benchmarks/javascript/`, and the reference implementation is the
|
|
150
|
+
`tests/jsonata-js` submodule), and the stale `src/parser/README.md` (every structural
|
|
151
|
+
claim in it — line counts, ranges, even the crate name in its example — was wrong).
|
|
152
|
+
- Dead code that survived the 2.2.8 builtins consolidation: `functions::numeric::{max,
|
|
153
|
+
min, average}` and `functions::object::{keys, lookup}` had no callers and encoded
|
|
154
|
+
pre-#109 semantics the engine no longer has (`Null` instead of `Undefined` on empty
|
|
155
|
+
input, no nested-array recursion) — a trap for anyone fixing a `$max`/`$lookup` bug in
|
|
156
|
+
the wrong place. Their tests, which pinned the wrong behavior, went with them. Also
|
|
157
|
+
removed the unused `datetime::parse_iso8601`. Technically these were `pub` items of the
|
|
158
|
+
crate, so strict semver would call this a breaking change; they were never used by the
|
|
159
|
+
engine itself.
|
|
160
|
+
|
|
161
|
+
### Fixed
|
|
162
|
+
- Two families of tree-walker drift found by unifying the remaining field-extraction
|
|
163
|
+
loops onto `compiled_field_step` (each pinned on BOTH engines in
|
|
164
|
+
`tests/python/test_field_step_and_arraygroup.py` with jsonata-js reference outputs):
|
|
165
|
+
the single-step-Name fast path skipped null-valued fields and returned Null on empty
|
|
166
|
+
(`p` over `[{"p":null},{"p":2}]` was `2` on the tree-walker, `[null,2]` on the VM and
|
|
167
|
+
in the reference — the engines disagreed); and the `.[...]` array-group constructor
|
|
168
|
+
kept undefined elements as null (`foo.blah.[baz]` gave `[[..],[null],[null]]` instead
|
|
169
|
+
of `[[..],[],[]]`, masked downstream by that null-skip), while its empty results
|
|
170
|
+
conflated "constructed empty array over a single value" (kept: `{"a":1}.[b]` is `[]`)
|
|
171
|
+
with "mapped over an empty array" (undefined: `emptyarr.[b]`). Both tree-walker
|
|
172
|
+
Name-step loops (the fast path and the general step loop's inner loop) now delegate
|
|
173
|
+
to `compiled_field_step`, deleting ~100 lines of drifted copies; the reference
|
|
174
|
+
suite's `array-constructor/case013`/`case014` now pass by the correct route. The
|
|
175
|
+
fast path's tuple branch (a third copy, with the same null-skip drift) is deleted
|
|
176
|
+
outright — tuple streams fall through to the general step loop's single tuple
|
|
177
|
+
implementation, and `$#$i.p` over `[{"p":null},{"p":2}]` is now `[null,2]` on both
|
|
178
|
+
engines, matching the reference.
|
|
179
|
+
- Errors carry their JSONata spec code at the front of the message again: the
|
|
180
|
+
`FunctionError` wrapping used to bury codes behind prose prefixes ("Runtime
|
|
181
|
+
error: D3030: Cannot convert 'x' to number"), so the C ABI, the Rust CLI, and
|
|
182
|
+
the Python CLI each grew a different classifier and disagreed on whether the
|
|
183
|
+
same error was coded. The inner message now passes through unwrapped
|
|
184
|
+
("D3030: ..."), `EvaluatorError::code()` / `evaluator::error_code_prefix` is
|
|
185
|
+
the one definition of "coded error" (prefix-anchored), and the C ABI and both
|
|
186
|
+
CLIs classify with it — the same failure now presents identically in every
|
|
187
|
+
binary. The C ABI's mid-string code scan is retired (it existed only to see
|
|
188
|
+
through the now-removed prose prefixes).
|
|
189
|
+
- Number stringification now matches jsonata-js exactly (verified against the pinned
|
|
190
|
+
reference with a 307-case randomized differential over scalars, containers,
|
|
191
|
+
prettified output, and `&` concatenation). Three divergences fixed: non-integers
|
|
192
|
+
were truncated to 14 significant digits instead of rounded to 15 (`$string(1/3)`
|
|
193
|
+
was `0.33333333333333`, now `0.333333333333333` — the old formatter counted the
|
|
194
|
+
`0` of a leading `0.` as significant); integer-valued floats above 2^53 printed
|
|
195
|
+
their exact i64 digits instead of the float's shortest round-trip decimal; and
|
|
196
|
+
numbers nested in containers went through serde with different rules again.
|
|
197
|
+
`$string` now stringifies through its own writer implementing the reference's
|
|
198
|
+
replacer (functions → `""`, non-integers → `toPrecision(15)`), and one
|
|
199
|
+
`value::js_number_to_string` defines JS number printing (plain digits in
|
|
200
|
+
[1e-6, 1e21), exponential with `+` outside, `-0` as `0`) for `$string`, concat,
|
|
201
|
+
`Display`, and `$join`. Pinned by `tests/python/test_number_stringification.py`.
|
|
202
|
+
- `$var.field` over an array containing nested-array elements now recurses into
|
|
203
|
+
them like jsonata-js's `lookup`: `($v := [[{"p":1}],{"p":2}]; $v.p)` is `[1,2]`
|
|
204
|
+
(previously `2` — the tree-walker's two-step fast path hand-rolled its mapping
|
|
205
|
+
loop and silently skipped non-object elements; it now delegates to the shared
|
|
206
|
+
field step). Found while consolidating the seven divergent field-extraction
|
|
207
|
+
loops the cleanup review flagged; pinned by `tests/python/test_var_field_nested.py`.
|
|
208
|
+
- Regex literals now honor the `m` (multiline) flag in `$match` and the `~>`
|
|
209
|
+
chain-pipe, matching `$split`/`$replace` and jsonata-js — the two paths
|
|
210
|
+
previously translated only `i` (see `tests/python/test_regex_flags.py`).
|
|
211
|
+
|
|
212
|
+
### Security
|
|
213
|
+
|
|
22
214
|
## [2.2.8] "Conform-ata" - 2026-08-25
|
|
23
215
|
|
|
24
216
|
Primarily a conformance release, with documentation work on the Rust crate alongside it. Most
|
|
@@ -586,7 +586,7 @@ dependencies = [
|
|
|
586
586
|
|
|
587
587
|
[[package]]
|
|
588
588
|
name = "jsonata-core"
|
|
589
|
-
version = "2.2.
|
|
589
|
+
version = "2.2.9"
|
|
590
590
|
dependencies = [
|
|
591
591
|
"assert_cmd",
|
|
592
592
|
"base64",
|
|
@@ -594,6 +594,7 @@ dependencies = [
|
|
|
594
594
|
"clap",
|
|
595
595
|
"criterion",
|
|
596
596
|
"indexmap",
|
|
597
|
+
"mimalloc",
|
|
597
598
|
"num-traits",
|
|
598
599
|
"percent-encoding",
|
|
599
600
|
"predicates",
|
|
@@ -614,6 +615,15 @@ version = "0.2.186"
|
|
|
614
615
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
615
616
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
|
616
617
|
|
|
618
|
+
[[package]]
|
|
619
|
+
name = "libmimalloc-sys"
|
|
620
|
+
version = "0.1.49"
|
|
621
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
622
|
+
checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9"
|
|
623
|
+
dependencies = [
|
|
624
|
+
"cc",
|
|
625
|
+
]
|
|
626
|
+
|
|
617
627
|
[[package]]
|
|
618
628
|
name = "linux-raw-sys"
|
|
619
629
|
version = "0.12.1"
|
|
@@ -632,6 +642,15 @@ version = "2.8.3"
|
|
|
632
642
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
633
643
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
|
634
644
|
|
|
645
|
+
[[package]]
|
|
646
|
+
name = "mimalloc"
|
|
647
|
+
version = "0.1.52"
|
|
648
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
649
|
+
checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862"
|
|
650
|
+
dependencies = [
|
|
651
|
+
"libmimalloc-sys",
|
|
652
|
+
]
|
|
653
|
+
|
|
635
654
|
[[package]]
|
|
636
655
|
name = "normalize-line-endings"
|
|
637
656
|
version = "0.3.0"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "jsonata-core"
|
|
3
|
-
version = "2.2.
|
|
3
|
+
version = "2.2.9"
|
|
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"
|
|
@@ -29,7 +29,6 @@ exclude = [
|
|
|
29
29
|
"CLAUDE.MD",
|
|
30
30
|
"pyproject.toml",
|
|
31
31
|
"uv.lock",
|
|
32
|
-
"rustup-init.exe",
|
|
33
32
|
]
|
|
34
33
|
|
|
35
34
|
[lib]
|
|
@@ -56,6 +55,7 @@ rand = "0.10"
|
|
|
56
55
|
stacker = "0.1"
|
|
57
56
|
simd-json = { version = "0.18", optional = true }
|
|
58
57
|
clap = { version = "4.6", features = ["derive"], optional = true }
|
|
58
|
+
mimalloc = { version = "0.1.52", optional = true }
|
|
59
59
|
|
|
60
60
|
[features]
|
|
61
61
|
default = ["simd"]
|
|
@@ -64,6 +64,7 @@ python = ["dep:pyo3"]
|
|
|
64
64
|
cli = ["dep:clap"]
|
|
65
65
|
bench = [] # exposes _bench facade for Criterion benchmarks
|
|
66
66
|
capi = []
|
|
67
|
+
mimalloc = ["dep:mimalloc"]
|
|
67
68
|
|
|
68
69
|
[dev-dependencies]
|
|
69
70
|
criterion = "0.8"
|
|
@@ -141,7 +141,13 @@ Rust library rebuilds with your project.)
|
|
|
141
141
|
deliberately (e.g. a frozen `$now`, or a disabled `$eval`). `user_data`
|
|
142
142
|
must outlive the handle. Registering a host function routes evaluation
|
|
143
143
|
through the tree-walker, like variable bindings.
|
|
144
|
-
8. **
|
|
144
|
+
8. **Guardrails:** `jsonata_set_limits(expr, timeout_ms, max_stack_depth,
|
|
145
|
+
max_sequence_length)` bounds wall-clock time (D1012), AST recursion
|
|
146
|
+
depth (D1011), and query-result sequence length (D2015) for all
|
|
147
|
+
subsequent evaluations on that handle — the same three limits the
|
|
148
|
+
Python bindings expose. 0 means "unlimited" (the default); call again
|
|
149
|
+
to change or reset.
|
|
150
|
+
9. **Panics:** internal engine panics are caught at the boundary and
|
|
145
151
|
surface as errors prefixed `internal error:` — they will not abort
|
|
146
152
|
your process.
|
|
147
153
|
|
|
@@ -112,6 +112,27 @@ int main(void) {
|
|
|
112
112
|
jsonata_free_expr(e);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/* evaluation guardrails */
|
|
116
|
+
{
|
|
117
|
+
JsonataExpr *e = jsonata_compile("$map([1..100000], function($x) { $x })");
|
|
118
|
+
CHECK(jsonata_set_limits(NULL, 0, 0, 0) == -1, "set_limits NULL handle -> -1");
|
|
119
|
+
CHECK(jsonata_set_limits(e, 0, 0, 10) == 0, "set_limits succeeds");
|
|
120
|
+
char *r = jsonata_evaluate(e, "{}");
|
|
121
|
+
char *err = take_error();
|
|
122
|
+
char *code = jsonata_last_error_code();
|
|
123
|
+
CHECK(r == NULL && err != NULL, "sequence limit -> NULL + message");
|
|
124
|
+
CHECK(code != NULL && strcmp(code, "D2015") == 0,
|
|
125
|
+
"sequence limit -> spec code D2015");
|
|
126
|
+
jsonata_free_string(err);
|
|
127
|
+
jsonata_free_string(code);
|
|
128
|
+
/* lifting the limit makes the same handle work again */
|
|
129
|
+
CHECK(jsonata_set_limits(e, 0, 0, 0) == 0, "set_limits reset succeeds");
|
|
130
|
+
char *r2 = jsonata_evaluate(e, "{}");
|
|
131
|
+
CHECK(r2 != NULL, "evaluate succeeds after limit reset");
|
|
132
|
+
jsonata_free_string(r2);
|
|
133
|
+
jsonata_free_expr(e);
|
|
134
|
+
}
|
|
135
|
+
|
|
115
136
|
/* variable binding */
|
|
116
137
|
{
|
|
117
138
|
JsonataExpr *e = jsonata_compile("$sum($xs) + n");
|
|
@@ -110,6 +110,19 @@ int jsonata_register_fn(JsonataExpr *expr, const char *name,
|
|
|
110
110
|
int jsonata_register_fn_override(JsonataExpr *expr, const char *name,
|
|
111
111
|
jsonata_host_fn fn, void *user_data);
|
|
112
112
|
|
|
113
|
+
/*
|
|
114
|
+
* Set evaluation guardrails, applied to every subsequent jsonata_evaluate()
|
|
115
|
+
* on this handle. Each limit uses 0 for "unlimited" (the default):
|
|
116
|
+
* - timeout_ms bounds wall-clock evaluation time (D1012 on breach)
|
|
117
|
+
* - max_stack_depth bounds AST recursion depth (D1011)
|
|
118
|
+
* - max_sequence_length bounds query-result sequences (D2015)
|
|
119
|
+
* These are the same three guardrails the Python bindings expose.
|
|
120
|
+
* Returns 0 on success, -1 on a NULL handle (error slot set).
|
|
121
|
+
*/
|
|
122
|
+
int jsonata_set_limits(JsonataExpr *expr, unsigned long long timeout_ms,
|
|
123
|
+
unsigned long long max_stack_depth,
|
|
124
|
+
unsigned long long max_sequence_length);
|
|
125
|
+
|
|
113
126
|
/* Free a handle returned by jsonata_compile(). NULL is a no-op. */
|
|
114
127
|
void jsonata_free_expr(JsonataExpr *expr);
|
|
115
128
|
|
|
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "jsonatapy"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.9"
|
|
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"}
|
|
@@ -68,7 +68,7 @@ Repository = "https://github.com/txjmb/jsonata-core"
|
|
|
68
68
|
[tool.maturin]
|
|
69
69
|
python-source = "python"
|
|
70
70
|
module-name = "jsonatapy._jsonatapy"
|
|
71
|
-
features = ["pyo3/extension-module", "python"]
|
|
71
|
+
features = ["pyo3/extension-module", "python", "mimalloc"]
|
|
72
72
|
|
|
73
73
|
[dependency-groups]
|
|
74
74
|
dev = [
|
|
@@ -3,150 +3,31 @@
|
|
|
3
3
|
// (tests/jsonata-js/src/parser.js ~L937-1235), adapted to Rust's ownership
|
|
4
4
|
// model: instead of mutating tree nodes in place, this consumes the raw
|
|
5
5
|
// tree and rebuilds an enriched one with ancestor/tuple metadata resolved.
|
|
6
|
-
|
|
7
|
-
// Recursion-depth safety (added: see docs/superpowers/plans/2026-07-07-parser-depth-and-u16-truncation-fixes-plan.md):
|
|
8
|
-
// `parser::parse()` (src/parser.rs:1730) unconditionally pipes every parse
|
|
9
|
-
// through `resolve_ancestry` below, so a deeply-nested input expression can
|
|
10
|
-
// overflow the native stack here even though the raw Pratt parse itself
|
|
11
|
-
// completed successfully -- confirmed empirically: a 200,000-term
|
|
12
|
-
// left-nested arithmetic chain (`1+1+1+...`) SIGABRTs ("stack overflow")
|
|
13
|
-
// via the full `parser::parse()` entry point, in this file, not the parser.
|
|
14
|
-
//
|
|
15
|
-
// There are THREE recursive pieces in this file, but only TWO independent
|
|
16
|
-
// stack budgets: data flows one-way from resolve_ancestry into
|
|
17
|
-
// transform_node/transform_children/transform_path_steps/
|
|
18
|
-
// migrate_binding_markers ("cycle 1"), and separately into substitute_labels
|
|
19
|
-
// ("cycle 2", a second full-tree walk that only starts after cycle 1 has
|
|
20
|
-
// fully unwound -- see resolve_ancestry). walk_backward/seek_parent_step/
|
|
21
|
-
// seek_parent_wrapped ("cycle 3") is reached FROM cycle 1 (transform_path_
|
|
22
|
-
// steps's predicate/own-pending resolution, and transform_children's Sort
|
|
23
|
-
// arm) while cycle 1's frames are still LIVE on the native stack -- it nests
|
|
24
|
-
// ON TOP of cycle 1's depth rather than running after it -- so cycle 1 and
|
|
25
|
-
// cycle 3 share one stack budget and their depths ADD, not two independent
|
|
26
|
-
// caps. A depth guard that gives cycle 1 and cycle 3 each their own
|
|
27
|
-
// independent counter capped at the native-safe limit would still allow
|
|
28
|
-
// cap1 + cap3 frames live simultaneously and overflow; Task 2 needs ONE
|
|
29
|
-
// counter threaded through cycle 1 AND cycle 3 together, and a SEPARATE
|
|
30
|
-
// counter (reset to 0) for cycle 2 (substitute_labels), which only runs
|
|
31
|
-
// after cycle 1/3's frames are gone. Guard all of the functions listed
|
|
32
|
-
// below regardless of which cycle they're in -- checking depth in only one
|
|
33
|
-
// cycle's functions is the exact "Task 5 pattern" (a check added at only
|
|
34
|
-
// one of several recursive entry points) this task exists to avoid.
|
|
35
6
|
//
|
|
36
|
-
//
|
|
37
|
-
// AST's nesting depth (binary op chains, block/array/function-arg
|
|
38
|
-
// nesting, parenthesized sub-paths used as a path step's node, etc.):
|
|
39
|
-
// - transform_node (:558) -- recurses directly (Path -> transform_path_steps;
|
|
40
|
-
// Block -> transform_node per element, a loop, but each iteration's call
|
|
41
|
-
// itself recurses; Binary{FocusBind/IndexBind} -> transform_node(lhs));
|
|
42
|
-
// for every other node kind, delegates to transform_children (still the
|
|
43
|
-
// same cycle). This is the actual site hit by the confirmed arithmetic-
|
|
44
|
-
// chain repro (`1+1+1+...` has no Path/`%` at all -- it's pure nested
|
|
45
|
-
// Binary, handled by transform_node's `other => transform_children(...)`
|
|
46
|
-
// fallback).
|
|
47
|
-
// - transform_children (:653) -- recurses via transform_node on every
|
|
48
|
-
// child of every composite node type (Binary lhs/rhs, Unary operand,
|
|
49
|
-
// Array/Function/Call-args/Object/ObjectTransform/Sort/Transform/
|
|
50
|
-
// ArrayGroup elements, Conditional branches, Lambda body, Predicate/
|
|
51
|
-
// FunctionApplication inner). This is the other function actually hit
|
|
52
|
-
// by the arithmetic-chain repro (Binary's lhs/rhs recursion).
|
|
53
|
-
// - transform_path_steps (:933) -- does NOT recurse on the flat
|
|
54
|
-
// `Vec<PathStep>` itself (that's a `for` loop over the steps -- bounded
|
|
55
|
-
// iteration, not stack depth; confirmed empirically in Step 3 below: a
|
|
56
|
-
// 50,000-step flat dot-path parses fine). It DOES feed back into the
|
|
57
|
-
// cycle per-step: calls `migrate_binding_markers(step, ...)` for every
|
|
58
|
-
// step, and separately calls `transform_node` on each filter-stage
|
|
59
|
-
// expression. Depth here scales with how deeply a single step's OWN
|
|
60
|
-
// node is nested (e.g. a parenthesized sub-path `(Order.Product)` used
|
|
61
|
-
// as one step, itself containing another Path), not with the number of
|
|
62
|
-
// steps in the flat list.
|
|
63
|
-
// - migrate_binding_markers (:1224) -- not itself self-recursive (one
|
|
64
|
-
// match, each arm calls transform_node/splice_marker_steps once), but
|
|
65
|
-
// it's the edge that closes the transform_path_steps -> transform_node
|
|
66
|
-
// cycle, so it needs to participate in whatever depth-counter scheme
|
|
67
|
-
// Task 2 uses (thread it through, even if it never increments/checks
|
|
68
|
-
// independently of the transform_node call it makes).
|
|
7
|
+
// ── Recursion-depth safety (the invariant this file must preserve) ──────────
|
|
69
8
|
//
|
|
70
|
-
// (
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// resolve_ancestry), so it needs its own depth counter/reset -- reusing
|
|
76
|
-
// a counter left over (at whatever depth) from pass (1) would be wrong.
|
|
9
|
+
// `parser::parse()` pipes every parse through `resolve_ancestry`, so a deeply
|
|
10
|
+
// nested input can overflow the native stack HERE even when the Pratt parse
|
|
11
|
+
// succeeded (confirmed empirically with a 200,000-term `1+1+1+...` chain).
|
|
12
|
+
// See docs/superpowers/plans/2026-07-07-parser-depth-and-u16-truncation-fixes-plan.md
|
|
13
|
+
// for the full derivation. The rules, in short:
|
|
77
14
|
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// after it unwinds, (1) and (3) share ONE stack budget (see the note
|
|
86
|
-
// above the fold -- their depths add). Depth here scales with how many
|
|
87
|
-
// levels of parenthesized sub-path nesting (`(...)` wrapping another
|
|
88
|
-
// `(...)`) a `%` reference has to walk through, not with path step
|
|
89
|
-
// count or general AST depth:
|
|
90
|
-
// - walk_backward (:1056) -- its own "while level > 0" loop walking
|
|
91
|
-
// backward through one `&mut [PathStep]` is bounded iteration (not a
|
|
92
|
-
// stack risk regardless of the slice's length), but it calls
|
|
93
|
-
// seek_parent_step per candidate step, which can call back into
|
|
94
|
-
// walk_backward (via seek_parent_wrapped's Path case) -- indirect
|
|
95
|
-
// recursion.
|
|
96
|
-
// - seek_parent_step (:1121) -- recurses via seek_parent_wrapped for the
|
|
97
|
-
// FunctionApplication and Block step-node cases (a parenthesized
|
|
98
|
-
// sub-path used as a step).
|
|
99
|
-
// - seek_parent_wrapped (:1191) -- recurses via walk_backward (Path case)
|
|
100
|
-
// AND directly calls itself (Block case, recursing into the block's
|
|
101
|
-
// last expression) -- e.g. doubly (or N-ly) nested parens.
|
|
102
|
-
// - resolve_predicate_slot (:1028) -- NOT part of this cycle itself (no
|
|
103
|
-
// self-loop; called once per predicate slot from transform_path_steps's
|
|
104
|
-
// loop over a bounded number of stages), but forwards into it
|
|
105
|
-
// (seek_parent_step / walk_backward), so its own frame sits at the
|
|
106
|
-
// base of chain (3) each time -- no guard needed in this function
|
|
107
|
-
// itself, but Task 2 should not assume the chain "starts" at
|
|
108
|
-
// walk_backward/seek_parent_step without going through here first in
|
|
109
|
-
// the predicate case.
|
|
15
|
+
// 1. There are three recursive cycles but only TWO independent stack budgets.
|
|
16
|
+
// Cycle 1 (transform_node / transform_children / transform_path_steps /
|
|
17
|
+
// migrate_binding_markers) and cycle 3 (walk_backward / seek_parent_step /
|
|
18
|
+
// seek_parent_wrapped) nest on the SAME native stack — cycle 3 is reached
|
|
19
|
+
// while cycle 1's frames are live — so they must share ONE depth counter.
|
|
20
|
+
// Cycle 2 (substitute_labels) runs only after cycle 1 has fully unwound
|
|
21
|
+
// and gets its own counter.
|
|
110
22
|
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
// - AncestryState::canonical (:224) -- a `while let Some(...)` loop
|
|
118
|
-
// following an alias chain in a HashMap; iteration, not recursion, and
|
|
119
|
-
// the doc comment right above it already notes chains longer than one
|
|
120
|
-
// hop shouldn't arise in practice regardless.
|
|
121
|
-
// - apply_marker_to_step (:419), check_focus_bind_target (:454) -- single
|
|
122
|
-
// match/if-chain over already-computed values, no calls back into any
|
|
123
|
-
// tree-walking function.
|
|
124
|
-
// - splice_marker_steps (:486) -- loops over a `Vec<PathStep>` produced by
|
|
125
|
-
// an already-fully-transformed `Transformed` (its `steps`/`pending`
|
|
126
|
-
// inputs were recursed into by the CALLER before this runs), and over a
|
|
127
|
-
// small fixed-shape `while` popping trailing `Predicate` pseudo-steps;
|
|
128
|
-
// calls only check_focus_bind_target/apply_marker_to_step, never
|
|
129
|
-
// transform_node or itself.
|
|
130
|
-
// - wrap_marker_as_path (:545) -- calls splice_marker_steps once; no
|
|
131
|
-
// recursion, no self-loop.
|
|
132
|
-
// - resolve_ancestry (:252) -- the pass's entry point: calls
|
|
133
|
-
// transform_node exactly once, then substitute_labels exactly once.
|
|
134
|
-
// Not itself part of either cycle (never re-entered from within the
|
|
135
|
-
// tree walk it kicks off), so it doesn't need a depth CHECK, but Task 2
|
|
136
|
-
// should initialize/reset each of the three counters above here (one
|
|
137
|
-
// for cycle (1)+(shared edge into (3)), one for substitute_labels).
|
|
23
|
+
// 2. Dropping an abandoned deep subtree recurses in the Drop glue,
|
|
24
|
+
// independently of any depth counter. Every owning bail-out (an `Err`
|
|
25
|
+
// return that still holds a subtree) must dismantle it iteratively via
|
|
26
|
+
// `push_ast_node_children`, never let it fall out of scope. That function
|
|
27
|
+
// deliberately has no `_` wildcard arm so a new AST variant cannot
|
|
28
|
+
// silently opt out.
|
|
138
29
|
//
|
|
139
|
-
//
|
|
140
|
-
// 200,000-step flat dot-path (`a.a.a...a`) -- same N as the crashing
|
|
141
|
-
// arithmetic chain, for a clean apples-to-apples Ok-vs-crash comparison --
|
|
142
|
-
// parsed via the FULL `parser::parse()` entry point returns `Ok`
|
|
143
|
-
// immediately (iteration in transform_path_steps's `for step in steps`
|
|
144
|
-
// loop, not recursion), while the 200,000-term arithmetic chain (`1+1+1+
|
|
145
|
-
// ...`) still SIGABRTs ("stack overflow") via the same full `parser::
|
|
146
|
-
// parse()` entry point in the same run -- confirming the root cause
|
|
147
|
-
// identified in the prior session is still live in current code, and that
|
|
148
|
-
// it's specifically recursion-on-nesting-depth (transform_children's
|
|
149
|
-
// Binary arm), not merely "large input," that triggers it.
|
|
30
|
+
// Functions are referenced by NAME throughout this file — line numbers rot.
|
|
150
31
|
|
|
151
32
|
use crate::ast::{AstNode, BinaryOp, PathStep, Stage};
|
|
152
33
|
use std::collections::HashMap;
|
|
@@ -388,11 +269,27 @@ fn push_ast_node_children(node: AstNode, stack: &mut Vec<AstNode>) {
|
|
|
388
269
|
}
|
|
389
270
|
}
|
|
390
271
|
AstNode::FunctionApplication(inner) | AstNode::Predicate(inner) => stack.push(*inner),
|
|
391
|
-
// Leaf nodes (
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
|
|
272
|
+
// Leaf nodes: no nested AstNode -- whatever's left of `node` (a
|
|
273
|
+
// String, an f64, ...) is dropped here for free, in O(1), as this
|
|
274
|
+
// match arm ends. Listed EXPLICITLY (no `_` wildcard) on purpose:
|
|
275
|
+
// this function is the stack-overflow-on-drop guard for abandoned
|
|
276
|
+
// deep subtrees, and a future variant holding a `Box<AstNode>` that
|
|
277
|
+
// fell into a wildcard would silently reintroduce the recursive-drop
|
|
278
|
+
// crash. A new variant must fail compilation here and be classified.
|
|
279
|
+
AstNode::String(_)
|
|
280
|
+
| AstNode::Name(_)
|
|
281
|
+
| AstNode::Number(_)
|
|
282
|
+
| AstNode::Boolean(_)
|
|
283
|
+
| AstNode::Null
|
|
284
|
+
| AstNode::Undefined
|
|
285
|
+
| AstNode::Placeholder
|
|
286
|
+
| AstNode::Regex { .. }
|
|
287
|
+
| AstNode::Variable(_)
|
|
288
|
+
| AstNode::ParentVariable(_)
|
|
289
|
+
| AstNode::Wildcard
|
|
290
|
+
| AstNode::Descendant
|
|
291
|
+
| AstNode::KeepArray
|
|
292
|
+
| AstNode::Parent(_) => {}
|
|
396
293
|
}
|
|
397
294
|
}
|
|
398
295
|
|
|
@@ -9,21 +9,13 @@ use jsonata_core::evaluator::EvaluatorError;
|
|
|
9
9
|
/// callers use it directly.
|
|
10
10
|
pub fn format_evaluator_error(e: &EvaluatorError) -> String {
|
|
11
11
|
let msg = e.message();
|
|
12
|
-
if
|
|
12
|
+
if e.code().is_some() {
|
|
13
13
|
msg.to_string()
|
|
14
14
|
} else {
|
|
15
15
|
format!("error: {}", msg)
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
fn is_coded_error(message: &str) -> bool {
|
|
20
|
-
let bytes = message.as_bytes();
|
|
21
|
-
bytes.len() >= 6
|
|
22
|
-
&& matches!(bytes[0], b'T' | b'D' | b'U' | b'S')
|
|
23
|
-
&& bytes[1..5].iter().all(u8::is_ascii_digit)
|
|
24
|
-
&& bytes[5] == b':'
|
|
25
|
-
}
|
|
26
|
-
|
|
27
19
|
#[cfg(test)]
|
|
28
20
|
mod tests {
|
|
29
21
|
use super::*;
|